Spaces:
Running
Running
Download src/viewer-engine.ts from fffiloni/openusd-explorer: direct link, hf CLI and curl.
- Browser
- Download file 5.96 kB
-
https://huggingface.co/spaces/fffiloni/openusd-explorer/resolve/main/src/viewer-engine.ts
- Command line
-
hf download hf://spaces/fffiloni/openusd-explorer/src/viewer-engine.ts
-
curl -L -o viewer-engine.ts https://huggingface.co/spaces/fffiloni/openusd-explorer/resolve/main/src/viewer-engine.ts
5.96 kB
| import * as THREE from 'three'; | |
| import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; | |
| import {KTX2Loader} from 'three/addons/loaders/KTX2Loader.js'; | |
| import {loadWebUsd,setWebUsdReader} from './vendor/web-usd.js'; | |
| import {bodyMatrices,controllableJoints,homeProgress} from './vendor/kinematics.js'; | |
| import {createStudio,objectBounds,STUDIO_DIRECTION} from './studio'; | |
| import {affectedBodies,selectedPose,type MotionJoint} from './motion'; | |
| const base=import.meta.env.BASE_URL; | |
| setWebUsdReader({workerUrl:new URL(base+'vendor/usd-reader/esm/worker.js',location.href).href,wasmUrl:new URL(base+'vendor/usd-reader/wasm/usd_wasm_bg.wasm',location.href).href}); | |
| export type ModelAsset={url:string;compact?:number[];name?:string}; | |
| export type Joint=MotionJoint; | |
| export function createEngine(host:HTMLElement,asset:ModelAsset,onStatus:(s:string)=>void,onReady:(j:Joint[],info:Record<string,unknown>)=>void,onFailure:(e:Error)=>void,onMotion?:(id:string,progress:number)=>void){ | |
| const controller=new AbortController(); | |
| let disposed=false,model:any=null,raf=0,playing=false,start=0,lastMotion=0,activeJoint:string|null=null,highlightEnabled=true; | |
| let values:Record<string,number>={}; | |
| const highlights:{mesh:THREE.Mesh;original:THREE.Material|THREE.Material[];copies:THREE.Material[]}[]=[]; | |
| const clearHighlight=()=>{for(const item of highlights){item.mesh.material=item.original;item.copies.forEach(material=>material.dispose());}highlights.length=0;}; | |
| const highlight=()=>{ | |
| clearHighlight();if(!model||!activeJoint||!highlightEnabled)return; | |
| const bodies=affectedBodies(model.articulation,activeJoint,values),moving=new Set<THREE.Object3D>(); | |
| for(const id of bodies)model.bodies.get(id)?.traverse((object:THREE.Object3D)=>moving.add(object)); | |
| if(!moving.size)return; | |
| model.scene.traverse((object:THREE.Object3D)=>{ | |
| if(!(object instanceof THREE.Mesh))return; | |
| const original=object.material,materials=Array.isArray(original)?original:[original]; | |
| const copies=materials.map(material=>{ | |
| const copy=material.clone() as THREE.MeshStandardMaterial; | |
| if(moving.has(object)){if(copy.emissive){copy.emissive.set('#9573e8');copy.emissiveIntensity=.3;}} | |
| else{copy.transparent=true;copy.opacity=Math.min(copy.opacity,.14);copy.depthWrite=false;} | |
| return copy; | |
| }); | |
| object.material=Array.isArray(original)?copies:copies[0];highlights.push({mesh:object,original,copies}); | |
| }); | |
| }; | |
| const renderer=new THREE.WebGLRenderer({alpha:true,antialias:true,powerPreference:'high-performance'}); | |
| renderer.setPixelRatio(Math.min(devicePixelRatio,1.75)); | |
| host.appendChild(renderer.domElement);renderer.domElement.setAttribute('aria-label','Interactive 3D object'); | |
| const studio=createStudio(renderer),scene=studio.scene,camera=new THREE.PerspectiveCamera(35,1,0.001,10000); | |
| const controls=new OrbitControls(camera,renderer.domElement);controls.enableDamping=true;controls.enablePan=false;controls.enableZoom=false; | |
| const ktx=new KTX2Loader().setTranscoderPath(base+'vendor/basis/').setWorkerLimit(2);ktx.detectSupport(renderer); | |
| const update=()=>{if(!model?.articulation)return;for(const [id,matrix] of Object.entries(bodyMatrices(model.articulation,values))){const body=model.bodies.get(id);if(body){body.matrixAutoUpdate=false;body.matrix.set(...matrix as number[]);body.matrixWorldNeedsUpdate=true;}}}; | |
| const resize=()=>{const {width,height}=host.getBoundingClientRect();renderer.setSize(Math.max(width,1),Math.max(height,1),false);camera.aspect=Math.max(width,1)/Math.max(height,1);camera.updateProjectionMatrix();}; | |
| const ro=new ResizeObserver(resize);ro.observe(host);resize(); | |
| const frame=(now:number)=>{if(disposed)return;if(playing&&model&&activeJoint){const progress=(1-Math.cos((now-start)/1600))/2;values=selectedPose(values,activeJoint,progress);update();if(now-lastMotion>65){onMotion?.(activeJoint,progress);lastMotion=now;}}controls.update();renderer.render(scene,camera);raf=requestAnimationFrame(frame);};raf=requestAnimationFrame(frame); | |
| onStatus('Loading 3D…'); | |
| loadWebUsd(asset.url,{ktx2:ktx,signal:controller.signal,physics:true,colliders:false,compact:asset.compact,onStage:(stage:string)=>{if(!disposed)onStatus(stage==='geometry'?'Loading materials…':'Loading 3D…');}}).then((loaded:any)=>{ | |
| if(disposed){loaded.dispose();return;}model=loaded;scene.add(model.scene); | |
| for(const j of controllableJoints(model.articulation))values[j.id]=homeProgress(j);update(); | |
| activeJoint=controllableJoints(model.articulation)[0]?.id??null;highlight(); | |
| const {center,radius}=objectBounds(model.scene),distance=radius/Math.sin(THREE.MathUtils.degToRad(camera.fov/2))*1.08; | |
| camera.near=Math.max(radius/1000,0.00001);camera.far=radius*100;camera.updateProjectionMatrix(); | |
| camera.position.copy(center).add(new THREE.Vector3(...STUDIO_DIRECTION).normalize().multiplyScalar(distance));controls.target.copy(center);controls.update(); | |
| onStatus('');onReady(controllableJoints(model.articulation),model.info); | |
| }).catch((error:Error)=>{if(!disposed)onFailure(error);}); | |
| return { | |
| setJoint:(id:string,value:number)=>{playing=false;values=selectedPose(values,id,value);update();}, | |
| selectJoint:(id:string)=>{playing=false;activeJoint=id;highlight();}, | |
| setHighlight:(enabled:boolean)=>{highlightEnabled=enabled;highlight();}, | |
| play:(enabled:boolean,id?:string)=>{if(!enabled&&activeJoint)onMotion?.(activeJoint,values[activeJoint]??0);if(id&&id!==activeJoint){activeJoint=id;highlight();}playing=enabled&&!!activeJoint;const position=activeJoint?(values[activeJoint]??0):0;start=performance.now()-Math.acos(1-2*Math.min(1,Math.max(0,position)))*1600;}, | |
| dispose:()=>{disposed=true;controller.abort();cancelAnimationFrame(raf);ro.disconnect();controls.dispose();clearHighlight();model?.dispose();ktx.dispose();studio.dispose();renderer.dispose();renderer.forceContextLoss();renderer.domElement.remove();} | |
| }; | |
| } | |