Quellcode durchsuchen

Add @threepipe/gltf-transform with GLTFDracoExportPlugin and examples.

master
Palash Bansal vor 1 Jahr
Ursprung
Commit
8185214297
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden

+ 37
- 0
README.md Datei anzeigen

@@ -141,6 +141,7 @@ To make changes and run the example, click on the CodePen button on the top righ
- [@threepipe/plugin-blueprintjs](#threepipeplugin-blueprintjs) BlueprintJs UI Plugin
- [@threepipe/plugin-tweakpane-editor](#threepipeplugin-tweakpane-editor) - Tweakpane Editor Plugin
- [@threepipe/plugin-configurator](#threepipeplugin-configurator) - Provides Material Configurator and Switch Node Plugin to allow users to select variations
- [@threepipe/plugin-gltf-transform](#threepipeplugin-gltf-transform) - Plugin to transform gltf models (draco compression)
- [@threepipe/plugins-extra-importers](#threepipeplugins-extra-importers) - Plugin for loading more file types supported by loaders in three.js
- [@threepipe/plugin-blend-importer](#threepipeplugin-blend-importer) - Blender to add support for loading .blend file
- [@threepipe/plugin-geometry-generator](#threepipeplugin-geometry-generator) - Generate parametric geometry types that can be re-generated from UI/API.
@@ -3753,6 +3754,41 @@ To create a custom configurator UI, use the `SwitchNodeBasePlugin` directly and

[//]: # (TODO Add Example for custom UI)

## @threepipe/plugin-gltf-transform

Exports [GLTFDracoExportPlugin](https://threepipe.org/plugins/gltf-transform/docs/classes/GLTFDracoExportPlugin.html) that extends the default gltf exporter to compress the file after export.

[Example](https://threepipe.org/examples/#glb-draco-export/) —
[Source Code](plugins/gltf-transform/src/index.ts) —
[API Reference](https://threepipe.org/plugins/gltf-transform/docs)

NPM: `npm install @threepipe/plugin-gltf-transform`

To use, simply add the plugin to the viewer and export using the `viewer.export` or `viewer.exportScene` functions. This also adds UI options to `AssetExporterPlugin` which are used when exporting using the plugin or using `viewer.exportScene`

The plugin overloads the default gltf exporter in the asset manager with `GLTFDracoExporter`. Using the [gltf-transform](https://gltf-transform.donmccurdy.com/) library, it compresses the exported gltf file using the [khr_draco_mesh_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_draco_mesh_compression/README.md) extension.

Note - Only `glb` export supported right now.

Sample Usage:

```typescript
import {ThreeViewer, downloadBlob} from 'threepipe'
import {GLTFDracoExportPlugin} from '@threepipe/plugin-gltf-transform'

const viewer = new ThreeViewer({...})
viewer.addPluginSync(GLTFDracoExportPlugin)

await viewer.load('file.glb')

const blob = await viewer.exportScene({
compress: true, // this must be specified, by default it's false.
viewerConfig: true, // to export with viewer, scene and plugin settings
})
// download the file
downloadBlob(blob, 'scene.glb')
```

## @threepipe/plugin-network

Network/Cloud related plugin implementations for Threepipe.
@@ -3941,6 +3977,7 @@ const model1 = await viewer.load<IObject3D>('data:application/x-blender;base64,.

[//]: # ( TODO: The plugin should parse and references to other assets and find them relative to the .blend file or the current location.)


## @threepipe/plugin-geometry-generator

Exports [GeometryGeneratorPlugin](https://threepipe.org/plugins/geometry-generator/docs/classes/BlendLoadPlugin.html) with several Geometry generators to create parametric and updatable geometries like plane, circle, sphere, box, torus, cylinder, cone etc.

+ 1
- 0
examples/asset-exporter-plugin/index.html Datei anzeigen

@@ -12,6 +12,7 @@
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-gltf-transform": "./../../plugins/gltf-transform/dist/index.mjs",
"@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs"
}
}

+ 2
- 1
examples/asset-exporter-plugin/script.ts Datei anzeigen

@@ -7,13 +7,14 @@ import {
ThreeViewer,
} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
import {GLTFDracoExportPlugin} from '@threepipe/plugin-gltf-transform'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
plugins: [LoadingScreenPlugin, AssetExporterPlugin, SceneUiConfigPlugin],
plugins: [LoadingScreenPlugin, AssetExporterPlugin, SceneUiConfigPlugin, GLTFDracoExportPlugin],
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

+ 36
- 0
examples/glb-draco-export/index.html Datei anzeigen

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GLB Draco Export</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-gltf-transform": "./../../plugins/gltf-transform/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 63
- 0
examples/glb-draco-export/script.ts Datei anzeigen

@@ -0,0 +1,63 @@
import {_testFinish, downloadBlob, IObject3D, LoadingScreenPlugin, ThreeViewer} from 'threepipe'
import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js'
import {GLTFDracoExportPlugin} from '@threepipe/plugin-gltf-transform'

const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas') as HTMLCanvasElement, msaa: true})

async function init() {

viewer.addPluginSync(LoadingScreenPlugin)
viewer.addPluginSync(GLTFDracoExportPlugin)

// Note: see asset-exporter-plugin example as well

// load obj + mtl
await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
const helmet = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})
if (!helmet) {
console.error('Unable to load model')
return
}
const mesh = helmet.getObjectByName('node_damagedHelmet_-6514')!

// const blob = await viewer.export(helmetObject, {exportExt: 'glb'})
// const blob = await viewer.exportScene({viewerConfig: false}) // export scene without viewer config
// const blob = await viewer.exportScene() // export scene with viewer config and default settings.

createSimpleButtons({
['Download Helmet Object GLB + DRACO']: async() => {
const blob = await viewer.export(mesh, {
exportExt: 'glb',
embedUrlImages: true, // embed images in glb even when url is available.
compress: true,
})
if (!blob) {
alert('Unable to export helmet object')
return
}
downloadBlob(blob, 'helmet.' + blob.ext)
},
['Download Scene GLB (Without Viewer Config) + DRACO']: async() => {
const blob = await viewer.exportScene({viewerConfig: false, compress: true})
if (!blob || blob.ext !== 'glb') {
alert('Unable to export scene')
return
}
downloadBlob(blob, 'scene.glb')
},
['Download Scene GLB (With Viewer Config) + DRACO']: async() => {
const blob = await viewer.exportScene({viewerConfig: true, compress: true})
if (!blob || blob.ext !== 'glb') {
alert('Unable to export scene')
return
}
downloadBlob(blob, 'scene_with_config.glb')
},
})

}

init().finally(_testFinish)

+ 2
- 0
examples/index.html Datei anzeigen

@@ -383,6 +383,7 @@
<li><a href="./image-snapshot-export/">PNG, JPEG, WEBP Export<br/>(Image Snapshot) </a></li>
<li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li>
<li><a href="./glb-export/">GLB Export </a></li>
<li><a href="./glb-draco-export/">GLB (+DRACO) Export </a></li>
<li><a href="./pmat-material-export/">PMAT Material Export </a></li>
<li><a href="./transfr-share-plugin/">Transfr.one Share Plugin<br/>(Upload, share link) </a></li>
<li><a href="./svg-geometry-playground/">SVG Geometry Playground </a></li>
@@ -468,6 +469,7 @@
</ul>
</div>
<div class="iframe-container">
<!-- TODO: allow only threepipe and localhost domains -->
<iframe id="example-iframe" src="./tweakpane-editor/" frameborder="0" allowfullscreen="allowfullscreen"
allow="accelerometer *; ambient-light-sensor *; autoplay *; camera *; clipboard-read *; clipboard-write *; encrypted-media *; fullscreen *; geolocation *; gyroscope *; magnetometer *; microphone *; midi *; payment *; picture-in-picture *; screen-wake-lock *; speaker *; sync-xhr *; usb *; web-share *; vibrate *; vr *">
</iframe>

+ 1
- 0
examples/tweakpane-editor/index.html Datei anzeigen

@@ -24,6 +24,7 @@
"@threepipe/plugin-geometry-generator": "./../../plugins/geometry-generator/dist/index.mjs",
"@threepipe/plugin-configurator": "./../../plugins/configurator/dist/index.mjs",
"@threepipe/plugin-network": "./../../plugins/network/dist/index.mjs",
"@threepipe/plugin-gltf-transform": "./../../plugins/gltf-transform/dist/index.mjs"
"@threepipe/plugin-gaussian-splatting": "./../../plugins/gaussian-splatting/dist/index.mjs"
}
}

+ 2
- 0
examples/tweakpane-editor/script.ts Datei anzeigen

@@ -61,6 +61,7 @@ import {GeometryGeneratorPlugin} from '@threepipe/plugin-geometry-generator'
import {GaussianSplattingPlugin} from '@threepipe/plugin-gaussian-splatting'
import {MaterialConfiguratorPlugin, SwitchNodePlugin} from '@threepipe/plugin-configurator'
import {AWSClientPlugin, TransfrSharePlugin} from '@threepipe/plugin-network'
import {GLTFDracoExportPlugin} from '@threepipe/plugin-gltf-transform'

async function init() {

@@ -84,6 +85,7 @@ async function init() {
await viewer.addPlugins([
LoadingScreenPlugin,
AssetExporterPlugin,
GLTFDracoExportPlugin,
new ProgressivePlugin(),
new SSAAPlugin(),
GLTFAnimationPlugin,

+ 3
- 3
package-lock.json Datei anzeigen

@@ -2244,9 +2244,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001583",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz",
"integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==",
"version": "1.0.30001642",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz",
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==",
"dev": true,
"funding": [
{

+ 2
- 2
package.json Datei anzeigen

@@ -1,6 +1,6 @@
{
"name": "threepipe",
"version": "0.0.31",
"version": "0.0.32",
"description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.",
"main": "dist/index.js",
"module": "dist/index.mjs",
@@ -27,7 +27,7 @@
"docs-all": "npm run docs && npm run docs-plugins",
"build-plugins": "node scripts/each-plugin.mjs install",
"prepare": "npm run build && npm run build-plugins && npm run build-examples",
"update-version": "node scripts/update-version.mjs"
"update-version": "node scripts/update-version.mjs"
},
"clean-package": {
"remove": [

+ 59
- 0
plugins/gltf-transform/package-lock.json Datei anzeigen

@@ -0,0 +1,59 @@
{
"name": "@threepipe/plugin-gltf-transform",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@threepipe/plugin-gltf-transform",
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"threepipe": "file:./../../src/"
},
"devDependencies": {
"@gltf-transform/core": "3.2.1",
"@gltf-transform/extensions": "3.2.1"
}
},
"../../src": {},
"../tweakpane/src": {
"extraneous": true
},
"node_modules/@gltf-transform/core": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@gltf-transform/core/-/core-3.2.1.tgz",
"integrity": "sha512-EE4AXJsu1jsSvcTnzk+mCu/VgLldPsb0gGhOV7onRlHM4DYh8m9aCCjGdLJf1uNJi+KUP5hPmOrUteXuopFq2A==",
"dev": true,
"dependencies": {
"property-graph": "^1.2.0"
}
},
"node_modules/@gltf-transform/extensions": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@gltf-transform/extensions/-/extensions-3.2.1.tgz",
"integrity": "sha512-7uiCXDV7/o2pnuXM9z/URdP9RR3baOaVo3f3eo65WtoUBIRKO6I/WHJzwGrRdWIWKQQWfxwZLTzYYaAgprnxDg==",
"dev": true,
"dependencies": {
"@gltf-transform/core": "^3.2.1",
"ktx-parse": "^0.5.0"
}
},
"node_modules/ktx-parse": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.5.0.tgz",
"integrity": "sha512-5IZrv5s1byUeDTIee1jjJQBiD5LPDB0w9pJJ0oT9BCKKJf16Tuj123vm1Ps0GOHSHmeWPgKM0zuViCVuTRpqaA==",
"dev": true
},
"node_modules/property-graph": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/property-graph/-/property-graph-1.3.1.tgz",
"integrity": "sha512-gei3N/bHWJdCItJ4blnlGWd9iauEZI+JZYj/A0D177XSI01+QhiJGAVscYBhe3Yywow3A2QJzVtsO2P+UgrRRQ==",
"dev": true
},
"node_modules/threepipe": {
"resolved": "../../src",
"link": true
}
}
}

+ 60
- 0
plugins/gltf-transform/package.json Datei anzeigen

@@ -0,0 +1,60 @@
{
"name": "@threepipe/plugin-gltf-transform",
"description": "Utility plugins for threepipe using gltf-transform to optimize/compress glTF files.",
"version": "0.1.0",
"devDependencies": {
"@gltf-transform/core": "3.2.1",
"@gltf-transform/extensions": "3.2.1"
},
"dependencies": {
"threepipe": "file:./../../src/"
},
"clean-package": {
"remove": [
"clean-package",
"scripts",
"devDependencies",
"//",
"markdown-to-html"
],
"replace": {
"dependencies": {},
"peerDependencies": {
"threepipe": "^0.0.32"
}
}
},
"type": "module",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"new:pack": "npm run prepare && clean-package && npm pack && clean-package restore",
"new:publish": "npm run prepare && clean-package && npm publish --access public && clean-package restore",
"prepare": "npm run build && npm run docs",
"build": "rimraf dist && vite build",
"dev": "NODE_ENV=development vite build --watch",
"docs": "rimraf docs && npx typedoc"
},
"author": "repalash <palash@shaders.app>",
"license": "Apache-2.0",
"keywords": [
"three",
"three.js",
"threepipe",
"vite",
"plugin"
],
"bugs": {
"url": "https://github.com/repalash/threepipe/issues"
},
"homepage": "https://github.com/repalash/threepipe#readme",
"repository": {
"type": "git",
"url": "git://github.com/repalash/threepipe.git"
}
}

+ 161
- 0
plugins/gltf-transform/src/GLTFDracoExportPlugin.ts Datei anzeigen

@@ -0,0 +1,161 @@
import {
AssetExporterPlugin,
AViewerPluginSync,
ClearcoatTintPlugin,
CustomBumpMapPlugin,
DRACOLoader2,
generateUUID,
GLTFLightExtrasExtension,
GLTFMaterialExtrasExtension,
GLTFMaterialsAlphaMapExtension,
GLTFMaterialsBumpMapExtension,
GLTFMaterialsDisplacementMapExtension,
GLTFMaterialsLightMapExtension,
GLTFObject3DExtrasExtension,
NoiseBumpMaterialPlugin,
ThreeViewer,
} from 'threepipe'
import {GLTFDracoExporter} from './GLTFDracoExporter'
import {UiObjectConfig} from 'uiconfig.js'
import {EncoderOptions} from '@gltf-transform/extensions/dist/khr-draco-mesh-compression/encoder'

export enum EncoderMethod {
EDGEBREAKER = 1,
SEQUENTIAL = 0
}

/**
* GLTF Draco Export Plugin
*
* Overloads the default gltf exporter in the asset manager with GLTFDracoExporter. When exporting with compress = true, the output will be compressed.
* Note - Only `glb` supported right now.
*
* @category Plugins
*/
export class GLTFDracoExportPlugin extends AViewerPluginSync<''> {
public static readonly PluginType = 'GLTFDracoExportPlugin'
enabled = true

onAdded(viewer: ThreeViewer): void {
super.onAdded(viewer)
const importer = viewer.assetManager.importer
const exporter = viewer.assetManager.exporter

const glbExporter = exporter.getExporter('glb')
if (glbExporter) exporter.removeExporter(glbExporter)

// todo remove exporter and add back the old one on plugin remove.
exporter.addExporter({
...glbExporter || {
ext: ['glb', 'gltf'],
extensions: [],
}, // for extensions
ctor: (_, _exporter) => {
const tempFile = generateUUID() + '.drc' // dummy
const ex = new GLTFDracoExporter({},
// todo unregister on dispose
importer.registerFile(tempFile) as DRACOLoader2)
ex.setup(viewer, _exporter.extensions)
ex.createAndAddExtension(GLTFMaterialsBumpMapExtension.WebGiMaterialsBumpMapExtension, {
bumpTexture: 'R',
})
ex.createAndAddExtension(GLTFMaterialsLightMapExtension.WebGiMaterialsLightMapExtension, {
lightMapTexture: 'RGB',
})
ex.createAndAddExtension(GLTFMaterialsAlphaMapExtension.WebGiMaterialsAlphaMapExtension, {
alphaTexture: 'G',
})
ex.createAndAddExtension(GLTFMaterialsDisplacementMapExtension.WebGiMaterialsDisplacementMapExtension, {
displacementTexture: 'R',
})
ex.createAndAddExtension(CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION, {
customBumpMap: 'RGB',
})
ex.createAndAddExtension(GLTFLightExtrasExtension.WebGiLightExtrasExtension)
ex.createAndAddExtension(GLTFObject3DExtrasExtension.WebGiObject3DExtrasExtension)
ex.createAndAddExtension(GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension)
ex.createAndAddExtension(ClearcoatTintPlugin.CLEARCOAT_TINT_GLTF_EXTENSION)
ex.createAndAddExtension(NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION)
// todo port
// DiamondMaterialExtension
// AnimationMarkersExtension
// AnisotropyMaterialExtension
// ThinFilmLayerMaterialExtension
// TriplanarMappingMaterialExtension
// SSBevelMaterialExtension
return ex
},
})

// for ui
const exportPlugin = viewer.getPlugin(AssetExporterPlugin)
if (exportPlugin) {
Object.assign(exportPlugin.exportOptions, {
compress: false,
dracoOptions: {
encodeSpeed: 5,
method: EncoderMethod.EDGEBREAKER,
quantizationVolume: 'mesh',
quantizationBits: {
['POSITION']: 14,
['NORMAL']: 10,
['COLOR']: 8,
['TEX_COORD']: 12,
['GENERIC']: 12,
},
} as EncoderOptions,
})
const exportOptions = exportPlugin.uiConfig.children?.find(c => (c as UiObjectConfig).label === 'GLB Export') as UiObjectConfig
if (exportOptions) {
exportOptions.children = [this._makeUi(exportPlugin), ...exportOptions.children || []]
} else {
console.warn('GLTFDracoExportPlugin: Unable to setup UI')
}
}
}

protected _makeUi = (exporter: AssetExporterPlugin)=>[
{
type: 'checkbox',
label: 'DRACO Compress',
property: [exporter.exportOptions, 'compress'],
onChange: ()=>exporter.uiConfig.uiRefresh?.(true),
},
{
type: 'folder',
hidden: ()=>!exporter.exportOptions.compress,
label: 'DRACO Options',
children: [
{
type: 'slider',
label: 'Encode Speed',
bounds: [1, 10],
property: [exporter.exportOptions.dracoOptions, 'encodeSpeed'],
},
{
type: 'dropdown',
label: 'Encoder Method',
property: [exporter.exportOptions.dracoOptions, 'method'],
children: Object.entries(EncoderMethod).map(([k, v]) => ({label: k, value: v})),
},
{
type: 'dropdown',
label: 'Quantization Volume',
property: [exporter.exportOptions.dracoOptions, 'quantizationVolume'],
children: ['mesh', 'scene', 'bbox'].map(v => ({label: v})),
},
{
type: 'folder',
label: 'Quantization Bits',
children: Object.keys(exporter.exportOptions.dracoOptions?.quantizationBits || {}).map(k => ({
type: 'slider',
label: k,
bounds: [1, 16],
stepSize: 1,
property: [exporter.exportOptions.dracoOptions?.quantizationBits, k],
})),
},
],
},
]
}

+ 418
- 0
plugins/gltf-transform/src/GLTFDracoExporter.ts Datei anzeigen

@@ -0,0 +1,418 @@
import {
Extension,
ExtensionProperty,
GLTF,
Graph,
Property,
PropertyType,
ReaderContext,
Texture,
TextureChannel,
TextureInfo,
WebIO,
WriterContext,
} from '@gltf-transform/core'
import {EncoderOptions} from '@gltf-transform/extensions/dist/khr-draco-mesh-compression/encoder'
import {ALL_EXTENSIONS, KHRDracoMeshCompression} from '@gltf-transform/extensions'
import {DRACOLoader2, GLTFExporter2, GLTFExporter2Options, GLTFViewerConfigExtension, IExportParser} from 'threepipe'

/**
* GLTF Draco Exporter
*
* Extension of GLTFExporter2 that runs the output through gltf-transform for draco compression.
*/
export class GLTFDracoExporter extends GLTFExporter2 implements IExportParser {
public loader?: DRACOLoader2 // required for loading draco libs.
private _io: WebIO
private _loadedLibs = false
private _encoderOptions: EncoderOptions

constructor(encoderOptions?: EncoderOptions, loader?: DRACOLoader2) {
super()
encoderOptions = encoderOptions || {
method: KHRDracoMeshCompression.EncoderMethod.EDGEBREAKER,
encodeSpeed: 5,
}
this._io = new WebIO().registerExtensions(ALL_EXTENSIONS)
.registerExtensions([
GLTFViewerConfigExtensionGP,
])
this._encoderOptions = encoderOptions

if (loader) {
this.loader = loader
this.loader.setDecoderConfig({type: 'js'}) // todo: hack for now.
this.loader.preload(true, true)
}

}

preload(): this {
this._loadLibs()
return this
}

private async _loadLibs() {
if (this._loadedLibs || !this.loader) return

const libs = await Promise.all([
this.loader.initEncoder(),
this.loader.initDecoder(),
])
this._io.registerDependencies({
['draco3d.encoder']: libs[0],
['draco3d.decoder']: libs[1], // only required if we are loading a draco compressed gltf
})
this._loadedLibs = true

}

async parseAsync(obj: any, {compress = false, dracoOptions, ...options}: {compress: boolean, dracoOptions?: EncoderOptions} & GLTFExporter2Options): Promise<Blob> {
if (!this.loader) {
console.error('GLTFDracoExporter: No DRACOLoader2 instance provided')
return super.parseAsync(obj, options)
}
await this._loadLibs()

const ops = {...options}
if (compress) {
// externalImagesInExtras: this is required because gltf-transform doesn't support external images in glb
// see https://github.com/donmccurdy/glTF-Transform/discussions/644
ops.externalImagesInExtras = true
}

const uncompressed = await new Promise((resolve, reject) => this.parse(obj, resolve, reject, ops)) as any

const uncompressedBlob = await super.parseAsync(uncompressed, ops)
if (!compress) return uncompressedBlob

if (!uncompressed) throw new Error('GLTFDracoExporter: gltf is null')

let gltf = uncompressed

const bytes = (gltf as ArrayBuffer).byteLength || Infinity

const iDocument = await (typeof gltf === 'object' && !(gltf as any).byteLength ? this._io.readJSON({
json: gltf as GLTF.IGLTF,
resources: {},
}) : this._io.readBinary(new Uint8Array(gltf as ArrayBuffer)))

// iDocument.createExtension(GLTFViewerConfigExtensionGP)
iDocument.createExtension(KHRDracoMeshCompression)
.setRequired(true)
.setEncoderOptions({...this._encoderOptions, ...dracoOptions ?? {}})

if (ops.exportExt === 'glb') {
gltf = await this._io.writeBinary(iDocument)
if (isFinite(bytes)) {
console.log('DRACO Compression ratio: ' + ((gltf as ArrayBuffer).byteLength / bytes).toFixed(5))
}
} else {
const jDoc = await this._io.writeJSON(iDocument)
gltf = jDoc.json
if (Object.values(jDoc.resources).filter(v => v).length > 0) {
console.warn('DRACOExporter: extra resources in resources not supported properly')
;(gltf as any).resources = jDoc.resources
}
}

gltf.__isGLTFOutput = true
const blob = await super.parseAsync(gltf, ops) as any // this will just convert it to blob because __isGLTFOutput is set (checked in GLTFExporter2)
if (!blob) throw new Error('GLTFDracoExporter: blob is null')
blob.ext = 'glb'
;(blob as any).__uncompressed = uncompressedBlob
return blob
}

addExtension(extension: typeof Extension): this {
this._io.registerExtensions([extension])
return this
}
createAndAddExtension(name: string, textures?: Record<string, string|number>): this {
return this.addExtension(createGenericExtensionClass(name, textures))
}
}

declare module 'threepipe'{
interface GLTFExporter2Options {
compress?: boolean
dracoOptions?: EncoderOptions
}
}

// for @gltf-transform/core
class ViewerJSONExtensionProperty extends ExtensionProperty {
readonly extensionName: string = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
readonly parentTypes: string[] = [PropertyType.SCENE]
readonly propertyType: string = 'ViewerJSON'

// eslint-disable-next-line @typescript-eslint/naming-convention
protected init(): void {return}

}
class GLTFViewerConfigExtensionGP extends Extension {
public readonly extensionName = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
public static readonly EXTENSION_NAME = GLTFViewerConfigExtension.ViewerConfigGLTFExtension
private _viewerConfig: any = {}
// private _texturesRef: [any, Texture][] = []

read(context: ReaderContext): this {
this._viewerConfig = {}
context.jsonDoc.json.scenes?.forEach((sceneDef, sceneIndex)=>{
if (sceneDef.extensions && sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension]) {
const prop = new ViewerJSONExtensionProperty(this.document.getGraph())
context.scenes[sceneIndex].setExtension(GLTFViewerConfigExtension.ViewerConfigGLTFExtension, prop)
this._viewerConfig = sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension] as any
// prop.setExtras()

/*
const buffers = [] as any[]

Object.values(viewerConfig.resources).forEach((res: any) => {
Object.values(res).forEach((item: any) => {
if (!item.url) return
if (item.url.data?.image !== null) {
buffers.push(item.url)
}
})
})
const jsonDoc = context.jsonDoc
console.log(buffers)
for (const buffer of buffers) {
const img = buffer.data.image as number
const imageDef = jsonDoc.json.images![img]
const bufferViewDef = jsonDoc.json.bufferViews![imageDef.bufferView!]
const bufferDef = jsonDoc.json.buffers![bufferViewDef.buffer]
const bufferData = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER]
const byteOffset = bufferViewDef.byteOffset || 0
const byteLength = bufferViewDef.byteLength
const imageData = bufferData.slice(byteOffset, byteOffset + byteLength)
const texture = this.document.createTexture(imageDef.name)
texture.setImage(imageData)
this._texturesRef.push([buffer, texture])
}
*/


}
})
return this
}

write(context: WriterContext): this {
this.document.getRoot().listScenes().forEach((scene)=>{
const prop = scene.getExtension(GLTFViewerConfigExtension.ViewerConfigGLTFExtension)
if (prop) {
const sceneDef = context.jsonDoc.json.scenes?.[context.jsonDoc.json.scene || 0] // todo: get proper scene index, if working with multiple scenes
if (sceneDef && Object.keys(this._viewerConfig).length > 0) {
sceneDef.extensions = sceneDef.extensions || {}

/*
console.log(context.jsonDoc.json.images)
for (const [buffer, texture] of this._texturesRef) {
const imageDef = context.createPropertyDef(texture) as GLTF.IImage
context.createImageData(imageDef, texture.getImage()!, texture)
buffer.data.image = context.jsonDoc.json.images!.push(imageDef) - 1
context.imageIndexMap.set(texture, buffer.data.image)
}
console.log(context.jsonDoc.json)
*/

sceneDef.extensions[GLTFViewerConfigExtension.ViewerConfigGLTFExtension] = this._viewerConfig

// this._texturesRef = []
this._viewerConfig = {}

}
}
})

return this
}

required = true
}

class GenericExtensionProperty extends ExtensionProperty<any> {
readonly extensionName: string
readonly parentTypes: string[] = [PropertyType.MATERIAL, PropertyType.MESH, PropertyType.NODE, PropertyType.SCENE]
readonly propertyType: string = 'GenericExtension'
textures: Record<string, [TextureInfo, Texture|null]> = {}

addTexture(key: string, texInfo: TextureInfo, texture: Texture | null, channels = 0x1111) {
this.setRef(key, texture, {channels})
this.textures[key] = [texInfo, texture]
}

constructor(graph: Graph<Property>, name: string, extensionName: string) {
super(graph, name)
this.extensionName = extensionName
}

// eslint-disable-next-line @typescript-eslint/naming-convention
protected init(): void {return}
}

// see transmission extension for reference
abstract class GenericExtension extends Extension {
abstract readonly extensionName: string

textureChannels: Record<string, number> = {}

read(context: ReaderContext): this {
const jsonDoc = context.jsonDoc
// console.log(jsonDoc)
const materialDefs = jsonDoc.json.materials || []
const textureDefs = jsonDoc.json.textures || []
materialDefs.forEach((materialDef, materialIndex) => {
if (materialDef.extensions && materialDef.extensions[this.extensionName]) {
const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
context.materials[materialIndex].setExtension(this.extensionName, paramsExt)
const paramsExtDef = materialDef.extensions[this.extensionName] as Record<string, any>
const paramsExtDef2 = {...paramsExtDef}

for (const [key, value] of Object.entries(paramsExtDef2)) {
if (typeof value?.index === 'number') { // this is a texture...
const textureInfoDef = value
const source = textureDefs[textureInfoDef.index]?.source
if (typeof source !== 'number') {
console.warn('GLTF Pipeline: source texture not found for texture info', textureInfoDef)
continue
}
const texture = context.textures[source]
const texInfo = new TextureInfo(this.document.getGraph())
const channels = this.textureChannels[key] ?? 0x1111
paramsExt.addTexture(key, texInfo, texture, channels)
context.setTextureInfo(texInfo, textureInfoDef)
delete paramsExtDef2[key]
}
}

paramsExt.setExtras(paramsExtDef2)
// console.log({...paramsExtDef})
}
})
const meshDefs = jsonDoc.json.meshes || []
meshDefs.forEach((meshDef, meshIndex) => {
if (meshDef.extensions && meshDef.extensions[this.extensionName]) {
const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
context.meshes[meshIndex].setExtension(this.extensionName, paramsExt)
const paramsExtDef = meshDef.extensions[this.extensionName] as Record<string, any>
paramsExt.setExtras(paramsExtDef)
}
})
const nodeDefs = jsonDoc.json.nodes || []
nodeDefs.forEach((nodeDef, nodeIndex) => {
if (nodeDef.extensions && nodeDef.extensions[this.extensionName]) {
const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
context.nodes[nodeIndex].setExtension(this.extensionName, paramsExt)
const paramsExtDef = nodeDef.extensions[this.extensionName] as Record<string, any>
paramsExt.setExtras(paramsExtDef)
// console.log(paramsExtDef)
}
})
const sceneDefs = jsonDoc.json.scenes || []
sceneDefs.forEach((sceneDef, sceneIndex) => {
if (sceneDef.extensions && sceneDef.extensions[this.extensionName]) {
const paramsExt = new GenericExtensionProperty(this.document.getGraph(), '', this.extensionName)
context.scenes[sceneIndex].setExtension(this.extensionName, paramsExt)
const paramsExtDef = sceneDef.extensions[this.extensionName] as Record<string, any>
paramsExt.setExtras(paramsExtDef)
// console.log(paramsExtDef)
}
})

return this
}

write(context: WriterContext): this {
const jsonDoc = context.jsonDoc
this.document.getRoot()
.listMaterials()
.forEach((material) => {
const paramsExt = material.getExtension<GenericExtensionProperty>(this.extensionName)
// console.log(paramsExt)
if (paramsExt) {
const materialIndex = context.materialIndexMap.get(material)!
const materialDef = jsonDoc.json.materials![materialIndex]
materialDef.extensions = materialDef.extensions || {}
const extensionDef = paramsExt.getExtras()
const extensionDef2 = {...extensionDef}

// console.log(paramsExt.textures)
for (const [key, value] of Object.entries(paramsExt.textures)) {
const textureInfo = value[0]
const textureLink = value[1]
const texture = textureLink

if (texture)
extensionDef2[key] = context.createTextureInfoDef(texture, textureInfo)

// console.log(texture)

}
// console.log(extensionDef2)

materialDef.extensions[this.extensionName] = extensionDef2
}
})
this.document.getRoot()
.listMeshes()
.forEach((mesh) => {
const paramsExt = mesh.getExtension<GenericExtensionProperty>(this.extensionName)
if (paramsExt) {
const meshIndex = context.meshIndexMap.get(mesh)!
const meshDef = jsonDoc.json.meshes![meshIndex]
meshDef.extensions = meshDef.extensions || {}
meshDef.extensions[this.extensionName] = paramsExt.getExtras()
}
})
this.document.getRoot()
.listNodes()
.forEach((node) => {
const paramsExt = node.getExtension<GenericExtensionProperty>(this.extensionName)
if (paramsExt) {
const nodeIndex = context.nodeIndexMap.get(node)!
const nodeDef = jsonDoc.json.nodes![nodeIndex]
nodeDef.extensions = nodeDef.extensions || {}
nodeDef.extensions[this.extensionName] = paramsExt.getExtras()
}
})
this.document.getRoot()
.listScenes()
.forEach((scene) => {
const paramsExt = scene.getExtension<GenericExtensionProperty>(this.extensionName)
if (paramsExt) {
const sceneIndex = context.jsonDoc.json.scene || 0 // todo: get proper scene index, if working with multiple scenes, this will do the default one.
const sceneDef = jsonDoc.json.scenes![sceneIndex]
if (!sceneDef) return
sceneDef.extensions = sceneDef.extensions || {}
sceneDef.extensions[this.extensionName] = paramsExt.getExtras()
}
})

return this
}

}

function stringToChannel(s: string) {
let r = 0
if (s.includes('R')) r |= TextureChannel.R
if (s.includes('G')) r |= TextureChannel.G
if (s.includes('B')) r |= TextureChannel.B
if (s.includes('A')) r |= TextureChannel.A
return r
}

export function createGenericExtensionClass(name: string, textures?: Record<string, string|number>): typeof GenericExtension {
return class extends GenericExtension {
public static readonly EXTENSION_NAME = name
readonly extensionName = name
textureChannels: Record<string, number> = !textures ? {} : Object.fromEntries(
Object.entries(textures)
.map(([k, v])=>
[k, typeof v === 'number' ? v : stringToChannel(v)])
)
}
}

+ 40
- 0
plugins/gltf-transform/src/global.d.ts Datei anzeigen

@@ -0,0 +1,40 @@
declare module '*.txt' {
const content: string
export default content
}
declare module '*.glsl' {
const content: string
export default content
}
declare module '*.vert' {
const content: string
export default content
}
declare module '*.frag' {
const content: string
export default content
}
declare module '*.module.scss' {
const content: any
export default content
export const stylesheet: string
}
declare module '*.module.css' {
const content: any
export default content
export const stylesheet: string
}
declare module '*.css' {
const content: string
export default content
}
declare module '*.css?inline' { // for vite
const content: string
export default content
}

// export {}

// hack for typedoc
// eslint-disable-next-line @typescript-eslint/naming-convention
// declare type OffscreenCanvas = HTMLCanvasElement

+ 2
- 0
plugins/gltf-transform/src/index.ts Datei anzeigen

@@ -0,0 +1,2 @@
export {GLTFDracoExporter, createGenericExtensionClass} from './GLTFDracoExporter'
export {GLTFDracoExportPlugin} from './GLTFDracoExportPlugin'

+ 41
- 0
plugins/gltf-transform/tsconfig.json Datei anzeigen

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"baseUrl": "./src",
"rootDir": "./src",
"allowJs": true,
"checkJs": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"isolatedModules": true,
"module": "es2020",
"noImplicitAny": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "dist",
"outDir": "dist",
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"preserveConstEnums": true,
"moduleResolution": "node",
"emitDecoratorMetadata": false,
"sourceMap": true,
"target": "ES2021",
"strictNullChecks": true,
"lib": [
"es2020",
"esnext",
"dom"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts",
"dist"
]
}

+ 10
- 0
plugins/gltf-transform/typedoc.json Datei anzeigen

@@ -0,0 +1,10 @@
{
"extends": [
"../../typedoc.json"
],
"entryPoints": [
"src/index.ts"
],
"name": "Threepipe gltf-transform Plugins",
"readme": "none"
}

+ 90
- 0
plugins/gltf-transform/vite.config.js Datei anzeigen

@@ -0,0 +1,90 @@
import {defineConfig} from 'vite'
import json from '@rollup/plugin-json';
import dts from 'vite-plugin-dts'
import packageJson from './package.json';
import license from 'rollup-plugin-license';
import replace from '@rollup/plugin-replace';
import glsl from 'rollup-plugin-glsl';
import path from 'node:path';

const isProd = process.env.NODE_ENV === 'production'
const { name, version, author } = packageJson
const {main, module, browser} = packageJson

const globals = {
'three': 'threepipe', // just incase someone uses three
'threepipe': 'threepipe',
}

export default defineConfig({
optimizeDeps: {
exclude: ['uiconfig.js', 'ts-browser-helpers'],
},
base: '',
// define: {
// 'process.env': process.env
// },
build: {
sourcemap: true,
minify: false,
cssMinify: isProd,
cssCodeSplit: false,
watch: !isProd ? {
buildDelay: 1000,
} : null,
lib: {
entry: 'src/index.ts',
formats: isProd ? ['es', 'umd'] : ['es'],
name: name,
fileName: (format) => (format === 'umd' ? main : module).replace('dist/', ''),
},
outDir: 'dist',
emptyOutDir: isProd,
commonjsOptions: {
exclude: [/uiconfig.js/, /ts-browser-helpers/],
},
rollupOptions: {
output: {
// inlineDynamicImports: false,
globals,
},
external: Object.keys(globals),

},
},
plugins: [
isProd ? dts({tsconfigPath: './tsconfig.json'}) : null,
replace({
'from \'three\'': 'from \'threepipe\'',
delimiters: ['', ''],
}),
replace({
'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'),
preventAssignment: true,
}),
glsl({ // todo: minify glsl.
include: 'src/**/*.glsl',
}),
json(),
// postcss({
// modules: false,
// autoModules: true, // todo; issues with typescript import css, because inject is false
// inject: false,
// minimize: isProduction,
// // Or with custom options for `postcss-modules`
// }),
license({
banner: `
@license
${name} v${version}
Copyright 2022<%= moment().format('YYYY') > 2022 ? '-' + moment().format('YYYY') : null %> ${author}
${packageJson.license} License
See ./dependencies.txt for any bundled third-party dependencies and licenses.
`,
thirdParty: {
output: path.join(__dirname, 'dist', 'dependencies.txt'),
includePrivate: true, // Default is false.
},
}),
],
})

Laden…
Abbrechen
Speichern