diff --git a/README.md b/README.md index 73648db..f554b90 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,19 @@ npm run build # static build in dist/ — host anywhere, still fully local Dev test hook: `?test=.glb&run=1` auto-loads and optimizes. +## CLI (batch / farm use) + +Same pipeline, headless — sharp for textures (faster than the browser canvas path): + +```bash +node bin/cli.js model.glb # -> model.opt.glb (balanced) +node bin/cli.js --preset crunch *.glb --outdir out/ +node bin/cli.js model.glb --ratio 0.15 --max-tex 512 -o small.glb +node bin/cli.js --help # all flags +``` + +`npm link` (or install from the repo) puts a `shrinkgod` command on PATH. Rig protection applies identically. + ## Pipeline dedup → flatten+join (static models only) → weld → per-primitive meshopt simplify → animation resample → prune → texture resize/re-encode → quantize → (optional) meshopt compression. @@ -34,8 +47,10 @@ dedup → flatten+join (static models only) → weld → per-primitive meshopt s | Balanced | 25% | 1% | 1024 | yes | | Crunch | 10% | 5% | 512 | yes | +## Draco input +Draco-compressed GLBs are decoded on load (decoder WASM is vendored in `public/draco/` — still fully local, no CDN) and written back out as standard uncompressed GLB. Draco *output* is intentionally not supported; use meshopt compression for web targets instead. + ## Known limits -- Draco-compressed input not supported yet (friendly error; re-export without Draco). - Meshopt compression output (`EXT_meshopt_compression`) reads fine in three.js but **not in Blender** — leave it off for assets going back into a DCC. - KTX2/basis textures pass through untouched. diff --git a/bin/cli.js b/bin/cli.js new file mode 100644 index 0000000..8f63d9f --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,190 @@ +#!/usr/bin/env node +// SHRINKGOD CLI — same pipeline as the web UI, headless. +// shrinkgod model.glb -> model.opt.glb (balanced) +// shrinkgod --preset crunch *.glb --outdir out/ +// shrinkgod model.glb --ratio 0.15 --max-tex 512 --webp -o small.glb +import { parseArgs } from 'node:util'; +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import path from 'node:path'; +import { NodeIO } from '@gltf-transform/core'; +import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; +import { listTextureSlots } from '@gltf-transform/functions'; +import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer'; +import draco3d from 'draco3dgltf'; +import { runPipeline, reportDoc, isColorSlot, PRESETS } from '../src/pipeline.js'; + +const HELP = `SHRINKGOD — local GLB optimizer (CLI) + +Usage: shrinkgod [options] [more.glb ...] + +Options: + --preset base settings (default: balanced) + --ratio <0..1> fraction of geometry to keep (overrides preset) + --error <0..1> simplify error tolerance, fraction of mesh radius + --max-tex longest texture side (0 = leave textures alone) + --webp / --no-webp convert color maps to WebP + --quality <0..100> lossy texture quality (default 85) + --no-join don't merge static meshes + --no-quantize skip vertex quantization + --meshopt EXT_meshopt_compression (web loaders only; Blender can't read it) + -o, --out output path (single input only) + --outdir output directory for batch runs + --suffix output suffix (default ".opt") + -h, --help + +Rig protection is automatic: skinned meshes keep >=50% geometry, morph-target +meshes are untouched, joining is disabled for animated models.`; + +function fmtBytes(n) { + if (n >= 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + 'MB'; + if (n >= 1024) return (n / 1024).toFixed(0) + 'KB'; + return n + 'B'; +} + +async function nodeTextureStep(doc, opts, notes) { + const textures = doc.getRoot().listTextures(); + if (!textures.length) return; + let sharp; + try { + sharp = (await import('sharp')).default; + } catch { + notes.push('sharp not installed — textures left untouched (npm install sharp)'); + return; + } + for (const tex of textures) { + const image = tex.getImage(); + if (!image) continue; + const mime = tex.getMimeType(); + if (!/^image\/(png|jpeg|webp)$/.test(mime)) continue; + + let img, meta; + try { + img = sharp(Buffer.from(image.buffer, image.byteOffset, image.byteLength)); + meta = await img.metadata(); + } catch { + notes.push(`could not decode texture "${tex.getName() || mime}" — left as-is`); + continue; + } + const longest = Math.max(meta.width, meta.height); + const scale = opts.maxTex > 0 ? Math.min(1, opts.maxTex / longest) : 1; + const needsResize = scale < 1; + + const slots = listTextureSlots(tex); + const color = slots.length === 0 || slots.some(isColorSlot); + let target; // sharp format name + if (!color) target = 'png'; + else if (opts.webp) target = 'webp'; + else target = mime === 'image/webp' ? 'webp' : mime.slice(6); // png|jpeg + + if (!needsResize && target === 'png' && mime === 'image/png') continue; + + if (needsResize) { + img = img.resize( + Math.max(1, Math.round(meta.width * scale)), + Math.max(1, Math.round(meta.height * scale)), + { fit: 'fill' } + ); + } + const q = Math.round(opts.texQuality * 100); + if (target === 'webp') img = img.webp({ quality: q }); + else if (target === 'jpeg') img = img.jpeg({ quality: q }); + else img = img.png(); + + const out = new Uint8Array(await img.toBuffer()); + if (!needsResize && out.byteLength >= image.byteLength) continue; + tex.setImage(out).setMimeType('image/' + target); + } +} + +async function main() { + const { values: v, positionals } = parseArgs({ + allowPositionals: true, + options: { + preset: { type: 'string', default: 'balanced' }, + ratio: { type: 'string' }, + error: { type: 'string' }, + 'max-tex': { type: 'string' }, + webp: { type: 'boolean' }, + 'no-webp': { type: 'boolean' }, + quality: { type: 'string', default: '85' }, + 'no-join': { type: 'boolean' }, + 'no-quantize': { type: 'boolean' }, + meshopt: { type: 'boolean' }, + out: { type: 'string', short: 'o' }, + outdir: { type: 'string' }, + suffix: { type: 'string', default: '.opt' }, + help: { type: 'boolean', short: 'h' }, + }, + }); + + if (v.help || positionals.length === 0) { + console.log(HELP); + process.exit(v.help ? 0 : 1); + } + const preset = PRESETS[v.preset]; + if (!preset) { + console.error(`unknown preset "${v.preset}" (light|balanced|crunch)`); + process.exit(1); + } + if (v.out && positionals.length > 1) { + console.error('--out only works with a single input; use --outdir for batches'); + process.exit(1); + } + + const opts = { + ratio: v.ratio != null ? parseFloat(v.ratio) : preset.ratio, + error: v.error != null ? parseFloat(v.error) : preset.error, + maxTex: v['max-tex'] != null ? parseInt(v['max-tex'], 10) : preset.maxTex, + webp: v['no-webp'] ? false : v.webp != null ? v.webp : preset.webp, + texQuality: parseInt(v.quality, 10) / 100, + join: !v['no-join'], + quantize: !v['no-quantize'], + meshopt: !!v.meshopt, + }; + + await Promise.all([MeshoptDecoder.ready, MeshoptEncoder.ready]); + const io = new NodeIO() + .registerExtensions(ALL_EXTENSIONS) + .registerDependencies({ + 'meshopt.decoder': MeshoptDecoder, + 'meshopt.encoder': MeshoptEncoder, + 'draco3d.decoder': await draco3d.createDecoderModule(), + }); + + if (v.outdir) await mkdir(v.outdir, { recursive: true }); + + let failed = 0; + for (const input of positionals) { + const t0 = performance.now(); + try { + const inBytes = await readFile(input); + const doc = await io.readBinary(new Uint8Array(inBytes)); + const before = reportDoc(doc, inBytes.byteLength); + + const { notes } = await runPipeline(doc, opts, { textureStep: nodeTextureStep }); + const outBytes = await io.writeBinary(doc); + const after = reportDoc(doc, outBytes.byteLength); + + const base = path.basename(input).replace(/\.glb$/i, ''); + const outPath = + v.out ?? path.join(v.outdir ?? path.dirname(input), `${base}${v.suffix}.glb`); + await writeFile(outPath, outBytes); + + const secs = ((performance.now() - t0) / 1000).toFixed(1); + const saved = (1 - after.bytes / before.bytes) * 100; + console.log( + `${input} -> ${outPath}\n` + + ` ${fmtBytes(before.bytes)} -> ${fmtBytes(after.bytes)} (-${saved.toFixed(0)}%) ` + + `tris ${before.tris.toLocaleString()} -> ${after.tris.toLocaleString()} ` + + `tex ${fmtBytes(before.texBytes)} -> ${fmtBytes(after.texBytes)} [${secs}s]` + ); + for (const n of notes) console.log(` · ${n}`); + } catch (e) { + failed++; + console.error(`${input}: FAILED — ${e.message || e}`); + } + } + process.exit(failed ? 1 : 0); +} + +main(); diff --git a/package-lock.json b/package-lock.json index 8ff68de..ac34ce7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@gltf-transform/core": "^4.4.2", "@gltf-transform/extensions": "^4.4.2", "@gltf-transform/functions": "^4.4.2", + "draco3dgltf": "^1.5.7", "meshoptimizer": "^1.2.0", + "sharp": "^0.35.4", "three": "^0.185.1" }, "devDependencies": { @@ -933,6 +935,12 @@ "node": ">=8" } }, + "node_modules/draco3dgltf": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3dgltf/-/draco3dgltf-1.5.7.tgz", + "integrity": "sha512-LeqcpmoHIyYUi0z70/H3tMkGj8QhqVxq6FJGPjlzR24BNkQ6jyMheMvFKJBI0dzGZrEOUyQEmZ8axM1xRrbRiw==", + "license": "Apache-2.0" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", diff --git a/package.json b/package.json index 65dab40..f192673 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,20 @@ "keywords": [], "author": "", "license": "ISC", - "type": "commonjs", + "type": "module", "dependencies": { "@gltf-transform/core": "^4.4.2", "@gltf-transform/extensions": "^4.4.2", "@gltf-transform/functions": "^4.4.2", + "draco3dgltf": "^1.5.7", "meshoptimizer": "^1.2.0", + "sharp": "^0.35.4", "three": "^0.185.1" }, "devDependencies": { "vite": "^8.2.2" + }, + "bin": { + "shrinkgod": "bin/cli.js" } } \ No newline at end of file diff --git a/public/draco/draco_decoder.wasm b/public/draco/draco_decoder.wasm new file mode 100644 index 0000000..469904e Binary files /dev/null and b/public/draco/draco_decoder.wasm differ diff --git a/public/draco/draco_wasm_wrapper.js b/public/draco/draco_wasm_wrapper.js new file mode 100644 index 0000000..43f1556 --- /dev/null +++ b/public/draco/draco_wasm_wrapper.js @@ -0,0 +1,116 @@ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE? +$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))}; +$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)}; +var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q=y}},"es6","es3"); +$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(kp;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3"); +$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5"); +$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5"); +var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e); +e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)}); +if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+ +12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+ +16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0=d?b++:2047>=d?b+=2:55296<=d&&57343>= +d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"=== +typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]= +this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr= +Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3=e[1]?!0:0!=e[0]||10< +e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e, +b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)} +function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")|| +P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0= +function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0= +a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0= +a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0= +function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0= +a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0= +function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1= +function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb= +a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0= +a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb= +a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null, +arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0= +a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0= +a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0= +a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib= +a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0= +function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1= +a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1= +function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0= +a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0= +function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0= +a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0= +function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2= +a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null, +arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0= +function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2= +a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)}, +oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3= +function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc= +a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3= +a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3= +a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3= +a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1= +a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM= +function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM= +a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION= +a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD= +a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD= +a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc= +a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32= +a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32= +function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT= +a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER= +function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)}; +a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0=r.size?(0>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;gb.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule); diff --git a/src/main.js b/src/main.js index 7167ba0..98cec31 100644 --- a/src/main.js +++ b/src/main.js @@ -95,11 +95,7 @@ async function loadFile(file) { try { report = await analyzeBytes(bytes); } catch (e) { - if (/draco/i.test(String(e))) { - alert('This GLB is Draco-compressed — not supported yet. Re-export without Draco (it will still come out smaller here).'); - } else { - alert('Could not read this file as a GLB:\n' + (e.message || e)); - } + alert('Could not read this file as a GLB:\n' + (e.message || e)); setStatus(''); return; } diff --git a/src/optimize.js b/src/optimize.js index 8e87500..c6d5965 100644 --- a/src/optimize.js +++ b/src/optimize.js @@ -1,26 +1,14 @@ import { WebIO } from '@gltf-transform/core'; -import { - ALL_EXTENSIONS, - EXTTextureWebP, - EXTMeshoptCompression, -} from '@gltf-transform/extensions'; -import { - dedup, - prune, - weld, - weldPrimitive, - simplifyPrimitive, - resample, - flatten, - join, - quantize, - listTextureSlots, -} from '@gltf-transform/functions'; -import { MeshoptDecoder, MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer'; +import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; +import { listTextureSlots } from '@gltf-transform/functions'; +import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer'; +import { runPipeline, reportDoc, isColorSlot, PRESETS } from './pipeline.js'; -const TRIANGLES = 4; +export { PRESETS }; let _io = null; +let _dracoRegistered = false; + async function getIO() { if (!_io) { await Promise.all([MeshoptDecoder.ready, MeshoptEncoder.ready]); @@ -34,88 +22,53 @@ async function getIO() { return _io; } -export const PRESETS = { - light: { ratio: 0.5, error: 0.005, maxTex: 2048, webp: false }, - balanced: { ratio: 0.25, error: 0.01, maxTex: 1024, webp: true }, - crunch: { ratio: 0.1, error: 0.05, maxTex: 512, webp: true }, -}; - -// Skinned meshes never go below this keep-ratio, and error is capped — -// collapsing verts near joints wrecks skin weights and silhouettes in motion. -const SKINNED_MIN_RATIO = 0.5; -const SKINNED_MAX_ERROR = 0.005; - -function skinnedMeshSet(root) { - const set = new Set(); - for (const node of root.listNodes()) { - if (node.getSkin() && node.getMesh()) set.add(node.getMesh()); - } - return set; +// Draco decoder is ~250KB of WASM — load it only when a file actually needs it. +// Vendored in /public/draco (same files three's DRACOLoader uses), so this +// stays fully local. +async function registerDraco(io) { + if (_dracoRegistered) return; + const [wrapperSrc, wasmBinary] = await Promise.all([ + fetch('/draco/draco_wasm_wrapper.js').then((r) => r.text()), + fetch('/draco/draco_decoder.wasm').then((r) => r.arrayBuffer()), + ]); + const factory = new Function(`${wrapperSrc}; return DracoDecoderModule;`)(); + // Resolve with a wrapper: the emscripten module is a thenable, and + // resolving a Promise with it directly makes `await` chain into it. + const { module } = await new Promise((resolve) => { + factory({ wasmBinary, onModuleLoaded: (m) => resolve({ module: m }) }); + }); + io.registerDependencies({ 'draco3d.decoder': module }); + _dracoRegistered = true; } -export function reportDoc(doc, byteLength) { - const root = doc.getRoot(); - let tris = 0; - let verts = 0; - let prims = 0; - let morphPrims = 0; - for (const mesh of root.listMeshes()) { - for (const prim of mesh.listPrimitives()) { - prims++; - if (prim.listTargets().length > 0) morphPrims++; - const pos = prim.getAttribute('POSITION'); - if (!pos) continue; - verts += pos.getCount(); - const idx = prim.getIndices(); - const count = idx ? idx.getCount() : pos.getCount(); - if (prim.getMode() === TRIANGLES) tris += Math.floor(count / 3); - } +// Peek the GLB's JSON chunk: gltf-transform needs the draco decoder registered +// BEFORE reading a draco file (a missing dependency fails mid-read with a +// cryptic error, not a catchable "please install"). +function needsDraco(bytes) { + try { + const view = new DataView(bytes); + if (view.getUint32(0, true) !== 0x46546c67) return false; // 'glTF' + const jsonLen = view.getUint32(12, true); + const json = new TextDecoder().decode(new Uint8Array(bytes, 20, jsonLen)); + return json.includes('KHR_draco_mesh_compression'); + } catch { + return false; } - let drawCalls = 0; - for (const node of root.listNodes()) { - const mesh = node.getMesh(); - if (mesh) drawCalls += mesh.listPrimitives().length; - } - const textures = root.listTextures().map((tex) => { - let size = null; - try { size = tex.getSize(); } catch { /* unknown mime */ } - return { - name: tex.getName() || '', - mime: tex.getMimeType(), - bytes: tex.getImage() ? tex.getImage().byteLength : 0, - size, - }; - }); - return { - bytes: byteLength, - tris, - verts, - prims, - morphPrims, - drawCalls, - meshes: root.listMeshes().length, - materials: root.listMaterials().length, - textures, - texBytes: textures.reduce((s, t) => s + t.bytes, 0), - animations: root.listAnimations().map((a) => a.getName() || 'clip'), - skins: root.listSkins().length, - }; +} + +async function readGLB(bytes) { + const io = await getIO(); + if (needsDraco(bytes)) await registerDraco(io); + return await io.readBinary(new Uint8Array(bytes.slice(0))); } export async function analyzeBytes(bytes) { - const io = await getIO(); - const doc = await io.readBinary(new Uint8Array(bytes)); + const doc = await readGLB(bytes); return reportDoc(doc, bytes.byteLength); } -function isColorSlot(slot) { - return /base|diffuse|emissive|sheenColor|specularColor/i.test(slot); -} - -async function processTextures(doc, opts, notes) { - const textures = doc.getRoot().listTextures(); - let touched = 0; - for (const tex of textures) { +async function browserTextureStep(doc, opts, notes) { + for (const tex of doc.getRoot().listTextures()) { const image = tex.getImage(); if (!image) continue; const mime = tex.getMimeType(); @@ -165,12 +118,7 @@ async function processTextures(doc, opts, notes) { // Keep the original if we somehow made it bigger without shrinking dims. if (!needsResize && out.byteLength >= image.byteLength) continue; tex.setImage(out).setMimeType(blob.type); - touched++; } - if (doc.getRoot().listTextures().some((t) => t.getMimeType() === 'image/webp')) { - doc.createExtension(EXTTextureWebP).setRequired(true); - } - return touched; } /** @@ -178,96 +126,14 @@ async function processTextures(doc, opts, notes) { * opts: { ratio, error, maxTex, webp, texQuality, join, quantize, meshopt } */ export async function optimize(bytes, opts, onStatus = () => {}) { - const io = await getIO(); - await MeshoptSimplifier.ready; - const notes = []; - onStatus('parsing'); - const doc = await io.readBinary(new Uint8Array(bytes.slice(0))); - const root = doc.getRoot(); - const rigged = root.listSkins().length > 0 || root.listAnimations().length > 0; - - onStatus('deduplicating'); - await doc.transform(dedup()); - - if (opts.join && !rigged) { - onStatus('joining meshes'); - try { - await doc.transform(flatten(), join()); - } catch (e) { - notes.push(`join skipped: ${e.message}`); - } - } - - onStatus('welding'); - await doc.transform(weld()); - - onStatus('simplifying geometry'); - const skinned = skinnedMeshSet(root); - let skippedMorph = 0; - let clampedSkinned = 0; - for (const mesh of root.listMeshes()) { - const isSkinned = skinned.has(mesh); - for (const prim of mesh.listPrimitives()) { - if (prim.getMode() !== TRIANGLES) continue; - if (prim.listTargets().length > 0) { - skippedMorph++; - continue; - } - let { ratio, error } = opts; - let lockBorder = false; - if (isSkinned) { - if (ratio < SKINNED_MIN_RATIO) { - ratio = SKINNED_MIN_RATIO; - clampedSkinned++; - } - error = Math.min(error, SKINNED_MAX_ERROR); - lockBorder = true; - } - try { - weldPrimitive(prim); - simplifyPrimitive(prim, { simplifier: MeshoptSimplifier, ratio, error, lockBorder }); - } catch (e) { - notes.push(`simplify skipped on "${mesh.getName() || 'mesh'}": ${e.message}`); - } - } - } - - if (root.listAnimations().length > 0) { - onStatus('resampling animations'); - try { - await doc.transform(resample()); - } catch (e) { - notes.push(`animation resample skipped: ${e.message}`); - } - } - - onStatus('pruning'); - await doc.transform(prune()); - - onStatus('processing textures'); - await processTextures(doc, opts, notes); - - if (opts.quantize || opts.meshopt) { - onStatus('quantizing'); - try { - await doc.transform(quantize()); - } catch (e) { - notes.push(`quantize skipped: ${e.message}`); - } - } - - if (opts.meshopt) { - doc - .createExtension(EXTMeshoptCompression) - .setRequired(true) - .setEncoderOptions({ method: EXTMeshoptCompression.EncoderMethod.FILTER }); - } - - if (skippedMorph) notes.push(`${skippedMorph} morph-target primitive(s) left untouched (blendshape protection)`); - if (clampedSkinned) notes.push(`${clampedSkinned} skinned primitive(s) clamped to keep ≥${SKINNED_MIN_RATIO * 100}% (rig protection)`); - + const doc = await readGLB(bytes); + const { notes, rigged } = await runPipeline(doc, opts, { + textureStep: browserTextureStep, + onStatus, + }); onStatus('writing glb'); + const io = await getIO(); const out = await io.writeBinary(doc); const report = reportDoc(doc, out.byteLength); return { bytes: out, report, notes, rigged }; diff --git a/src/pipeline.js b/src/pipeline.js new file mode 100644 index 0000000..5b426ff --- /dev/null +++ b/src/pipeline.js @@ -0,0 +1,198 @@ +// Shared optimization pipeline — environment-agnostic (browser + node CLI). +// The texture step is injected because image encode/decode differs +// (OffscreenCanvas in the browser, sharp in node). +import { + dedup, + prune, + weld, + weldPrimitive, + simplifyPrimitive, + resample, + flatten, + join, + quantize, +} from '@gltf-transform/functions'; +import { EXTTextureWebP, EXTMeshoptCompression } from '@gltf-transform/extensions'; +import { MeshoptSimplifier } from 'meshoptimizer'; + +export const TRIANGLES = 4; + +export const PRESETS = { + light: { ratio: 0.5, error: 0.005, maxTex: 2048, webp: false }, + balanced: { ratio: 0.25, error: 0.01, maxTex: 1024, webp: true }, + crunch: { ratio: 0.1, error: 0.05, maxTex: 512, webp: true }, +}; + +// Skinned meshes never go below this keep-ratio, and error is capped — +// collapsing verts near joints wrecks skin weights and silhouettes in motion. +export const SKINNED_MIN_RATIO = 0.5; +export const SKINNED_MAX_ERROR = 0.005; + +export function isColorSlot(slot) { + return /base|diffuse|emissive|sheenColor|specularColor/i.test(slot); +} + +function skinnedMeshSet(root) { + const set = new Set(); + for (const node of root.listNodes()) { + if (node.getSkin() && node.getMesh()) set.add(node.getMesh()); + } + return set; +} + +export function reportDoc(doc, byteLength) { + const root = doc.getRoot(); + let tris = 0; + let verts = 0; + let prims = 0; + let morphPrims = 0; + for (const mesh of root.listMeshes()) { + for (const prim of mesh.listPrimitives()) { + prims++; + if (prim.listTargets().length > 0) morphPrims++; + const pos = prim.getAttribute('POSITION'); + if (!pos) continue; + verts += pos.getCount(); + const idx = prim.getIndices(); + const count = idx ? idx.getCount() : pos.getCount(); + if (prim.getMode() === TRIANGLES) tris += Math.floor(count / 3); + } + } + let drawCalls = 0; + for (const node of root.listNodes()) { + const mesh = node.getMesh(); + if (mesh) drawCalls += mesh.listPrimitives().length; + } + const textures = root.listTextures().map((tex) => { + let size = null; + try { size = tex.getSize(); } catch { /* unknown mime */ } + return { + name: tex.getName() || '', + mime: tex.getMimeType(), + bytes: tex.getImage() ? tex.getImage().byteLength : 0, + size, + }; + }); + return { + bytes: byteLength, + tris, + verts, + prims, + morphPrims, + drawCalls, + meshes: root.listMeshes().length, + materials: root.listMaterials().length, + textures, + texBytes: textures.reduce((s, t) => s + t.bytes, 0), + animations: root.listAnimations().map((a) => a.getName() || 'clip'), + skins: root.listSkins().length, + }; +} + +/** + * Mutates doc in place. opts: { ratio, error, maxTex, webp, texQuality, + * join, quantize, meshopt }. textureStep(doc, opts, notes) handles + * decode/resize/encode; pipeline handles everything else. + */ +export async function runPipeline(doc, opts, { textureStep, onStatus = () => {} } = {}) { + await MeshoptSimplifier.ready; + const notes = []; + const root = doc.getRoot(); + const rigged = root.listSkins().length > 0 || root.listAnimations().length > 0; + + // Draco inputs were decompressed on read; drop the extension so the writer + // emits a plain GLB instead of demanding a draco encoder. + for (const ext of root.listExtensionsUsed()) { + if (ext.extensionName === 'KHR_draco_mesh_compression') { + ext.dispose(); + notes.push('Draco input decompressed — output is a standard GLB'); + } + } + + onStatus('deduplicating'); + await doc.transform(dedup()); + + if (opts.join && !rigged) { + onStatus('joining meshes'); + try { + await doc.transform(flatten(), join()); + } catch (e) { + notes.push(`join skipped: ${e.message}`); + } + } + + onStatus('welding'); + await doc.transform(weld()); + + onStatus('simplifying geometry'); + const skinned = skinnedMeshSet(root); + let skippedMorph = 0; + let clampedSkinned = 0; + for (const mesh of root.listMeshes()) { + const isSkinned = skinned.has(mesh); + for (const prim of mesh.listPrimitives()) { + if (prim.getMode() !== TRIANGLES) continue; + if (prim.listTargets().length > 0) { + skippedMorph++; + continue; + } + let { ratio, error } = opts; + let lockBorder = false; + if (isSkinned) { + if (ratio < SKINNED_MIN_RATIO) { + ratio = SKINNED_MIN_RATIO; + clampedSkinned++; + } + error = Math.min(error, SKINNED_MAX_ERROR); + lockBorder = true; + } + try { + weldPrimitive(prim); + simplifyPrimitive(prim, { simplifier: MeshoptSimplifier, ratio, error, lockBorder }); + } catch (e) { + notes.push(`simplify skipped on "${mesh.getName() || 'mesh'}": ${e.message}`); + } + } + } + + if (root.listAnimations().length > 0) { + onStatus('resampling animations'); + try { + await doc.transform(resample()); + } catch (e) { + notes.push(`animation resample skipped: ${e.message}`); + } + } + + onStatus('pruning'); + await doc.transform(prune()); + + if (textureStep) { + onStatus('processing textures'); + await textureStep(doc, opts, notes); + if (root.listTextures().some((t) => t.getMimeType() === 'image/webp')) { + doc.createExtension(EXTTextureWebP).setRequired(true); + } + } + + if (opts.quantize || opts.meshopt) { + onStatus('quantizing'); + try { + await doc.transform(quantize()); + } catch (e) { + notes.push(`quantize skipped: ${e.message}`); + } + } + + if (opts.meshopt) { + doc + .createExtension(EXTMeshoptCompression) + .setRequired(true) + .setEncoderOptions({ method: EXTMeshoptCompression.EncoderMethod.FILTER }); + } + + if (skippedMorph) notes.push(`${skippedMorph} morph-target primitive(s) left untouched (blendshape protection)`); + if (clampedSkinned) notes.push(`${clampedSkinned} skinned primitive(s) clamped to keep ≥${SKINNED_MIN_RATIO * 100}% (rig protection)`); + + return { notes, rigged }; +} diff --git a/src/viewer.js b/src/viewer.js index 3edfb27..380fbc4 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -1,5 +1,6 @@ import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; import { MeshoptDecoder } from 'meshoptimizer'; @@ -76,6 +77,9 @@ export class CompareViewer { this.loader = new GLTFLoader(); this.loader.setMeshoptDecoder(MeshoptDecoder); + const draco = new DRACOLoader(); + draco.setDecoderPath('/draco/'); // vendored; fetched only when a file needs it + this.loader.setDRACOLoader(draco); this.clock = new THREE.Clock(); this.playing = true;