Spaces:
Running
Running
Download assets/progressive-gltf.js from SimWorld-AI/Code4Scene: direct link, hf CLI and curl.
- Browser
- Download file 8.04 kB
-
https://huggingface.co/spaces/SimWorld-AI/Code4Scene/resolve/main/assets/progressive-gltf.js
- Command line
-
hf download hf://spaces/SimWorld-AI/Code4Scene/assets/progressive-gltf.js
-
curl -L -o progressive-gltf.js https://huggingface.co/spaces/SimWorld-AI/Code4Scene/resolve/main/assets/progressive-gltf.js
8.04 kB
| import {Texture,Source} from 'three'; | |
| import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'; | |
| import {MeshoptDecoder} from 'three/addons/libs/meshopt_decoder.module.js'; | |
| import {fetchSceneBytes} from './scene-assets.js?v=20261002a'; | |
| const KEY=Uint8Array.from('5c2f9e71d3a84b06e1f7c29a38d05b64a7193ce8f2046db95e81c73a2df6904b'.match(/../g),h=>parseInt(h,16)); | |
| export function unpack(bytes){for(let i=0;i<bytes.length;i++)bytes[i]^=KEY[i&31]^(((i>>>5)*131)&255);return bytes;} | |
| let manifestPromise; | |
| function manifest(){return manifestPromise??=fetch(new URL('../data/progressive-scenes.json?v=1',import.meta.url)).then(r=>r.ok?r.json():{}).catch(()=>({}));} | |
| // Two texture jobs across the entire page, separate from the geometry queue. | |
| let running=0;const waiting=[]; | |
| function textureJob(work,signal){return new Promise((resolve,reject)=>{waiting.push({work,signal,resolve,reject});pump();});} | |
| function pump(){while(running<2&&waiting.length){const job=waiting.shift();if(job.signal.aborted){job.reject(new DOMException('Aborted','AbortError'));continue;}running++;Promise.resolve().then(job.work).then(job.resolve,job.reject).finally(()=>{running--;pump();});}} | |
| class Materials { | |
| constructor(parser,maxSize){this.parser=parser;this.maxSize=maxSize;this.sources=new Map();this.bundles=new Map();this.failedBundles=new Set();this.active=false;this.disposed=false;this.epoch=0;this.onUpdate=()=>{};} | |
| placeholder(index){ | |
| let entry=this.sources.get(index); | |
| if(!entry){const canvas=document.createElement('canvas');canvas.width=canvas.height=1;const ctx=canvas.getContext('2d');ctx.fillStyle='#ffffff';ctx.fillRect(0,0,1,1);const texture=new Texture(canvas);texture.needsUpdate=true;entry={index,texture,state:'pending',refs:new Set([texture])};this.sources.set(index,entry);} | |
| return Promise.resolve(entry.texture.clone()); | |
| } | |
| bind(root){ | |
| root.traverse(node=>{for(const material of [].concat(node.material||[])){for(const [name,texture] of Object.entries(material)){if(!texture?.isTexture)continue;const entry=[...this.sources.values()].find(e=>e.texture.source===texture.source);if(!entry)continue;entry.refs.add(texture);const colors={map:'#b0b0b0',normalMap:'#8080ff',clearcoatNormalMap:'#8080ff',emissiveMap:'#000000',roughnessMap:'#ffff00',metalnessMap:'#ffff00',transmissionMap:'#000000'};if(colors[name]){const ctx=texture.image.getContext('2d');ctx.fillStyle=colors[name];ctx.fillRect(0,0,1,1);texture.needsUpdate=true;}}}}); | |
| } | |
| get status(){const entries=[...this.sources.values()];return {loaded:entries.filter(e=>e.state==='loaded').length,failed:entries.filter(e=>e.state==='failed').length,total:entries.length,active:this.active};} | |
| async imageBytes(entry,signal){ | |
| const image=this.parser.json.images[entry.index],part=image.extras?.c4sTexture; | |
| if(part){ | |
| if(!this.bundles.has(part.bundle)){const promise=(async()=>{for(let attempt=0;attempt<2;attempt++){try{return unpack(await fetchSceneBytes(new URL(part.bundle,this.parser.options.path),signal,()=>{}));}catch(error){if(signal.aborted||attempt===1)throw error;}}})().catch(error=>{if(signal.aborted)this.bundles.delete(part.bundle);else this.failedBundles.add(part.bundle);throw error;});this.bundles.set(part.bundle,promise);} | |
| const bytes=await this.bundles.get(part.bundle);return new Blob([bytes.subarray(part.offset,part.offset+part.length)],{type:image.mimeType}); | |
| } | |
| if(image.bufferView!==undefined)return new Blob([await this.parser.getDependency('bufferView',image.bufferView)],{type:image.mimeType}); | |
| const bytes=await fetchSceneBytes(new URL(image.uri,this.parser.options.path),signal,()=>{});return new Blob([bytes],{type:image.mimeType}); | |
| } | |
| async load(entry,signal){ | |
| let bitmap; | |
| for(let attempt=0;attempt<2;attempt++){ | |
| try{const blob=await this.imageBytes(entry,signal);signal.throwIfAborted();bitmap=await createImageBitmap(blob,{colorSpaceConversion:'none',premultiplyAlpha:'none'});break;} | |
| catch(error){if(signal.aborted||attempt===1)throw error;} | |
| } | |
| if(signal.aborted){bitmap.close();signal.throwIfAborted();} | |
| if(this.maxSize&&Math.max(bitmap.width,bitmap.height)>this.maxSize){const scale=this.maxSize/Math.max(bitmap.width,bitmap.height);const small=await createImageBitmap(bitmap,{resizeWidth:Math.max(1,Math.round(bitmap.width*scale)),resizeHeight:Math.max(1,Math.round(bitmap.height*scale)),resizeQuality:'high',colorSpaceConversion:'none',premultiplyAlpha:'none'});bitmap.close();bitmap=small;} | |
| if(signal.aborted||this.disposed){bitmap.close();throw new DOMException('Aborted','AbortError');} | |
| // WebGL2 texture storage cannot grow from a 1px placeholder in place. | |
| // Retain each glTF texture object's sampler/transform, but allocate fresh storage. | |
| const source=new Source(bitmap);for(const texture of entry.refs){texture.dispose();texture.source=source;texture.needsUpdate=true;}entry.state='loaded';this.onUpdate(this.status); | |
| // Yield between uploads so orbiting and the first geometry frame remain responsive. | |
| await new Promise(resolve=>setTimeout(resolve,0)); | |
| } | |
| start(onUpdate){ | |
| this.onUpdate=onUpdate||this.onUpdate; | |
| if(this.active||this.disposed||![...this.sources.values()].some(e=>e.state==='pending'))return; | |
| this.active=true;const epoch=++this.epoch;this.controller=new AbortController();const signal=this.controller.signal; | |
| const worker=async()=>{while(this.active&&!signal.aborted){const entry=[...this.sources.values()].find(e=>e.state==='pending');if(!entry)break;entry.state='loading';try{await textureJob(()=>this.load(entry,signal),signal);}catch(error){entry.state=signal.aborted?'pending':'failed';if(!signal.aborted){entry.error=error;this.onUpdate(this.status);}}}}; | |
| Promise.all([worker(),worker()]).finally(()=>{if(this.epoch===epoch){this.active=false;this.onUpdate(this.status);}}); | |
| } | |
| pause(){this.active=false;this.epoch++;this.controller?.abort();} | |
| retry(){for(const name of this.failedBundles)this.bundles.delete(name);this.failedBundles.clear();for(const e of this.sources.values())if(e.state==='failed')e.state='pending';this.start();} | |
| dispose(){this.disposed=true;this.pause();this.bundles.clear();this.sources.clear();this.parser=null;} | |
| } | |
| export async function loadProgressive(url,{signal,onProgress=()=>{},derivative=true,maxTextureSize=512}={}){ | |
| const original=new URL(url,document.baseURI),name=original.pathname.split('/').pop(); | |
| const entry=derivative?(await manifest())[name]:null; | |
| const geometry=entry?new URL('../'+entry.geometry,import.meta.url):original; | |
| onProgress('downloading',{received:0,total:0}); | |
| let bytes; | |
| try{bytes=await fetchSceneBytes(geometry.href,signal,p=>onProgress('downloading',p));} | |
| catch(error){if(!entry||signal.aborted)throw error;bytes=await fetchSceneBytes(original.href,signal,p=>onProgress('downloading',p));} | |
| unpack(bytes);signal.throwIfAborted();onProgress('decoding');let materials; | |
| const loader=new GLTFLoader().setMeshoptDecoder(MeshoptDecoder); | |
| loader.register(parser=>{materials=new Materials(parser,maxTextureSize);parser.loadImageSource=index=>materials.placeholder(index);return {name:'C4S_progressive_materials'};}); | |
| const gltf=await new Promise((resolve,reject)=>{ | |
| let settled=false; | |
| const fail=error=>{if(settled)return;settled=true;clearTimeout(timer);signal.removeEventListener('abort',abort);materials?.dispose();reject(error);}; | |
| const abort=()=>fail(new DOMException('Aborted','AbortError')); | |
| const timer=setTimeout(()=>fail(new Error('Scene geometry decoding timed out.')),30000); | |
| signal.addEventListener('abort',abort,{once:true}); | |
| loader.parseAsync(bytes.buffer,new URL('.',geometry).href).then(result=>{ | |
| if(settled||signal.aborted){result.scene.traverse(n=>{n.geometry?.dispose();for(const m of [].concat(n.material||[]))m.dispose();});materials?.dispose();return;} | |
| settled=true;clearTimeout(timer);signal.removeEventListener('abort',abort);resolve(result); | |
| },fail); | |
| }); | |
| materials.bind(gltf.scene);gltf.materials=materials; | |
| return gltf; | |
| } | |