ZeyuLing commited on
Commit
0b11fcf
·
verified ·
1 Parent(s): d5a8986

Precompute and cache FBX exports in viewers

Browse files
Files changed (1) hide show
  1. cases/index.html +3 -3
cases/index.html CHANGED
@@ -54,7 +54,7 @@
54
  const $=selector=>document.querySelector(selector);
55
  const canvas=$("#canvas"),gallery=$("#gallery"),main=$("#main"),loading=$("#loading"),audio=$("#audio");
56
  const renderer=new THREE.WebGLRenderer({canvas,antialias:true,alpha:false});renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));renderer.shadowMap.enabled=true;renderer.outputColorSpace=THREE.SRGBColorSpace;
57
- const assetCache=new Map(),descriptorChunkCache=new Map(),fbxCache=new Map(),FBX_EXPORTER_URL="https://cdn.jsdelivr.net/npm/@comfyorg/fbx-exporter-three@1.0.1/dist/index.js",fbxExporterPromise=import(FBX_EXPORTER_URL),fbxWarmQueue=[],MAX_FETCHES=1,MIN_FETCH_INTERVAL_MS=350,SMPL_NAMES=["Pelvis","L_Hip","R_Hip","Spine1","L_Knee","R_Knee","Spine2","L_Ankle","R_Ankle","Spine3","L_Foot","R_Foot","Neck","L_Collar","R_Collar","Head","L_Shoulder","R_Shoulder","L_Elbow","R_Elbow","L_Wrist","R_Wrist","L_Hand","R_Hand"],AISTPP_PARENTS=[-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19,20,21],Y_AXIS=new THREE.Vector3(0,1,0);let manifest,body,filtered=[],selected=0,views=[],frame=0,playing=true,lastTime=0,maxFrames=1,loadGeneration=0,currentItem=null,audioStart=0,audioEnd=Infinity,viewObserver=null,activeFetches=0,lastFetchStartedAt=0,fbxWarmActive=false;const fetchQueue=[];
58
 
59
  function url(path,base){return new URL(path,base?new URL(base,location.href):location.href).href}
60
  function wait(ms){return new Promise(resolve=>setTimeout(resolve,ms))}
@@ -101,10 +101,10 @@
101
  function fbxKey(view,item){return`${item.case_key||item.sample_id||item.case_id}:${view.method.key}:${view.motion?.descriptor.display_frames||0}`}
102
  async function buildFbx(view,item){const{FBXExporter}=await fbxExporterPromise,frames=view.motion.descriptor.display_frames,fps=view.motion.descriptor.fps||30,times=new Float32Array(frames),positions=new Float32Array(frames*3),quaternions=Array.from({length:22},()=>new Float32Array(frames*4)),quaternion=new THREE.Quaternion();for(let index=0;index<frames;index++){const values=decodeFrame(view,index);times[index]=index/fps;positions.set([body.joints[0]+values[0],body.joints[1]+values[1],body.joints[2]+values[2]],index*3);for(let joint=0;joint<22;joint++){rot6dQuaternion(values,3+joint*6,quaternion);quaternions[joint].set(quaternion.toArray(),index*4)}}const material=new THREE.MeshStandardMaterial({color:view.method.accent||"#2e78ad",roughness:.58,metalness:.02}),skinned=buildSkinnedMesh(material),scene=new THREE.Scene(),tracks=[new THREE.VectorKeyframeTrack(`${skinned.bones[0].name}.position`,times,positions)];for(let joint=0;joint<22;joint++)tracks.push(new THREE.QuaternionKeyframeTrack(`${skinned.bones[joint].name}.quaternion`,times,quaternions[joint]));const clip=new THREE.AnimationClip(`${safeName(item.sample_id||item.case_id)}_${safeName(view.method.key)}`,frames/fps,tracks);skinned.mesh.userData={case_id:String(item.case_id),sample_id:String(item.sample_id||item.case_id),method:view.method.key,fps,ground_offset:view.groundOffset};scene.add(skinned.mesh);scene.animations=[clip];return new FBXExporter().parseSync(scene,{preset:"blender",fps,animations:[clip],creator:"Motius SMPL Gallery"})}
103
  function preparedFbx(view,item){const key=fbxKey(view,item);if(fbxCache.has(key))return fbxCache.get(key);const pending=buildFbx(view,item).catch(error=>{if(fbxCache.get(key)===pending)fbxCache.delete(key);throw error});fbxCache.set(key,pending);while(fbxCache.size>12)fbxCache.delete(fbxCache.keys().next().value);return pending}
104
- function scheduleFbxWarm(view,item,generation){if(!view.motion)return;const key=fbxKey(view,item);if(fbxCache.has(key)||fbxWarmQueue.some(entry=>entry.key===key))return;fbxWarmQueue.push({view,item,generation,key});if(fbxWarmActive)return;fbxWarmActive=true;(async()=>{while(fbxWarmQueue.length){const entry=fbxWarmQueue.shift();await idle();if(entry.generation!==loadGeneration||entry.item!==currentItem||!entry.view.motion)continue;try{await preparedFbx(entry.view,entry.item)}catch(error){console.warn("FBX warm-up failed",error)}}fbxWarmActive=false})()}
105
  async function exportFbx(view,item){const bytes=await preparedFbx(view,item);downloadBlob(bytes,`${safeName(item.sample_id||item.case_id)}_${safeName(view.method.key)}.fbx`)}
106
  async function downloadView(view,item,format,button){if(!view.motion)return;const old=button.textContent;button.disabled=true;button.textContent="…";try{await new Promise(resolve=>requestAnimationFrame(resolve));if(format==="npz")await exportNpz(view,item);else await exportFbx(view,item)}catch(error){console.error(error);alert(`Export failed: ${error.message}`)}finally{button.disabled=false;button.textContent=old}}
107
- function renderTiles(item){gallery.querySelectorAll(".tile").forEach(node=>node.remove());views=manifest.motion_methods.map((method,index)=>makeView(method,index));gallery.classList.remove("comparison-four");for(const view of views){const tile=document.createElement("article");tile.className="tile";tile.dataset.loadState="pending";const exports=`<button class="download" data-format="npz" disabled>NPZ</button><button class="download" data-format="fbx" disabled>FBX</button>`;tile.innerHTML=`<div class="tile-head"><div class="method"><span class="swatch" style="background:${view.method.accent}"></span>${view.method.label}</div><div class="tile-actions"><span class="motion-meta">Waiting</span><button class="download retry-load" type="button" hidden>Retry</button><button class="download camera-reset" type="button" title="Reset 3D view" aria-label="Reset 3D view">↺</button>${exports}</div></div><div class="viewport" aria-label="Interactive 3D motion view"></div>`;for(const button of tile.querySelectorAll("[data-format]")){button.addEventListener("click",event=>{event.stopPropagation();downloadView(view,item,button.dataset.format,button)});if(button.dataset.format==="fbx")button.addEventListener("pointerenter",()=>scheduleFbxWarm(view,item,loadGeneration))}tile.querySelector(".camera-reset").addEventListener("click",()=>{resetCamera(view);refreshCamera(view)});const output=item.outputs?.[view.method.key];if(output){const text=document.createElement("div");text.className="output-text";text.textContent=output;tile.append(text)}gallery.append(tile);view.viewport=tile.querySelector(".viewport");view.tile=tile;view.retryButton=tile.querySelector(".retry-load");attachCameraControls(view)}for(const method of manifest.output_methods||[]){const tile=document.createElement("article");tile.className="tile text-tile";tile.innerHTML=`<div class="tile-head"><div class="method"><span class="swatch" style="background:${method.accent}"></span>${method.label}</div></div><div class="text-result"></div>`;tile.querySelector(".text-result").textContent=item.outputs?.[method.key]||"No output";gallery.append(tile)}}
108
  function referenceValues(item){return item.segments||item.references||[]}
109
  function renderCaseMeta(item,index){$("#position").textContent=`Case ${index+1} of ${manifest.population}`;$("#case-id").textContent=item.sample_id||item.case_id;const refs=referenceValues(item);$("#captions").replaceChildren(...refs.map(value=>{const span=document.createElement("span");span.className="caption";span.textContent=typeof value==="string"?value:value.caption;if(typeof value!=="string"){span.dataset.start=value.start_frame;span.dataset.end=value.end_frame}return span}));const legend=$("#condition-legend");if(item.condition_intervals?.length){const condition=manifest.condition_legend?.conditioned||{label:"Condition frame",color:"#d95f02"},generated=manifest.condition_legend?.generated||{label:"Generated frame"};legend.replaceChildren(...[[condition.label,condition.color],[generated.label,"#66736d"]].map(([label,color])=>{const chip=document.createElement("span");chip.className="legend-chip";chip.innerHTML=`<span class="legend-dot" style="background:${color}"></span>`;chip.append(document.createTextNode(label));return chip}))}else legend.replaceChildren()}
110
  function idle(){return new Promise(resolve=>"requestIdleCallback"in globalThis?requestIdleCallback(()=>resolve(),{timeout:500}):setTimeout(resolve,0))}
 
54
  const $=selector=>document.querySelector(selector);
55
  const canvas=$("#canvas"),gallery=$("#gallery"),main=$("#main"),loading=$("#loading"),audio=$("#audio");
56
  const renderer=new THREE.WebGLRenderer({canvas,antialias:true,alpha:false});renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));renderer.shadowMap.enabled=true;renderer.outputColorSpace=THREE.SRGBColorSpace;
57
+ const assetCache=new Map(),descriptorChunkCache=new Map(),fbxCache=new Map(),FBX_EXPORTER_URL="https://cdn.jsdelivr.net/npm/@comfyorg/fbx-exporter-three@1.0.1/dist/index.js",fbxExporterPromise=import(FBX_EXPORTER_URL),fbxWarmQueue=[],fbxWarmAttempted=new Set(),MAX_FETCHES=1,MIN_FETCH_INTERVAL_MS=350,SMPL_NAMES=["Pelvis","L_Hip","R_Hip","Spine1","L_Knee","R_Knee","Spine2","L_Ankle","R_Ankle","Spine3","L_Foot","R_Foot","Neck","L_Collar","R_Collar","Head","L_Shoulder","R_Shoulder","L_Elbow","R_Elbow","L_Wrist","R_Wrist","L_Hand","R_Hand"],AISTPP_PARENTS=[-1,0,0,0,1,2,3,4,5,6,7,8,9,9,9,12,13,14,16,17,18,19,20,21],Y_AXIS=new THREE.Vector3(0,1,0);let manifest,body,filtered=[],selected=0,views=[],frame=0,playing=true,lastTime=0,maxFrames=1,loadGeneration=0,currentItem=null,audioStart=0,audioEnd=Infinity,viewObserver=null,activeFetches=0,lastFetchStartedAt=0,fbxWarmActive=false;const fetchQueue=[];
58
 
59
  function url(path,base){return new URL(path,base?new URL(base,location.href):location.href).href}
60
  function wait(ms){return new Promise(resolve=>setTimeout(resolve,ms))}
 
101
  function fbxKey(view,item){return`${item.case_key||item.sample_id||item.case_id}:${view.method.key}:${view.motion?.descriptor.display_frames||0}`}
102
  async function buildFbx(view,item){const{FBXExporter}=await fbxExporterPromise,frames=view.motion.descriptor.display_frames,fps=view.motion.descriptor.fps||30,times=new Float32Array(frames),positions=new Float32Array(frames*3),quaternions=Array.from({length:22},()=>new Float32Array(frames*4)),quaternion=new THREE.Quaternion();for(let index=0;index<frames;index++){const values=decodeFrame(view,index);times[index]=index/fps;positions.set([body.joints[0]+values[0],body.joints[1]+values[1],body.joints[2]+values[2]],index*3);for(let joint=0;joint<22;joint++){rot6dQuaternion(values,3+joint*6,quaternion);quaternions[joint].set(quaternion.toArray(),index*4)}}const material=new THREE.MeshStandardMaterial({color:view.method.accent||"#2e78ad",roughness:.58,metalness:.02}),skinned=buildSkinnedMesh(material),scene=new THREE.Scene(),tracks=[new THREE.VectorKeyframeTrack(`${skinned.bones[0].name}.position`,times,positions)];for(let joint=0;joint<22;joint++)tracks.push(new THREE.QuaternionKeyframeTrack(`${skinned.bones[joint].name}.quaternion`,times,quaternions[joint]));const clip=new THREE.AnimationClip(`${safeName(item.sample_id||item.case_id)}_${safeName(view.method.key)}`,frames/fps,tracks);skinned.mesh.userData={case_id:String(item.case_id),sample_id:String(item.sample_id||item.case_id),method:view.method.key,fps,ground_offset:view.groundOffset};scene.add(skinned.mesh);scene.animations=[clip];return new FBXExporter().parseSync(scene,{preset:"blender",fps,animations:[clip],creator:"Motius SMPL Gallery"})}
103
  function preparedFbx(view,item){const key=fbxKey(view,item);if(fbxCache.has(key))return fbxCache.get(key);const pending=buildFbx(view,item).catch(error=>{if(fbxCache.get(key)===pending)fbxCache.delete(key);throw error});fbxCache.set(key,pending);while(fbxCache.size>12)fbxCache.delete(fbxCache.keys().next().value);return pending}
104
+ function scheduleFbxWarm(view,item,generation){if(!view.motion)return;const key=fbxKey(view,item);if(fbxCache.has(key)||fbxWarmAttempted.has(key)||fbxWarmQueue.some(entry=>entry.key===key))return;fbxWarmAttempted.add(key);fbxWarmQueue.push({view,item,generation,key});if(fbxWarmActive)return;fbxWarmActive=true;(async()=>{while(fbxWarmQueue.length){const entry=fbxWarmQueue.shift();await idle();if(entry.generation!==loadGeneration||entry.item!==currentItem||!entry.view.motion)continue;try{await preparedFbx(entry.view,entry.item)}catch(error){console.warn("FBX warm-up failed",error)}}fbxWarmActive=false})()}
105
  async function exportFbx(view,item){const bytes=await preparedFbx(view,item);downloadBlob(bytes,`${safeName(item.sample_id||item.case_id)}_${safeName(view.method.key)}.fbx`)}
106
  async function downloadView(view,item,format,button){if(!view.motion)return;const old=button.textContent;button.disabled=true;button.textContent="…";try{await new Promise(resolve=>requestAnimationFrame(resolve));if(format==="npz")await exportNpz(view,item);else await exportFbx(view,item)}catch(error){console.error(error);alert(`Export failed: ${error.message}`)}finally{button.disabled=false;button.textContent=old}}
107
+ function renderTiles(item){gallery.querySelectorAll(".tile").forEach(node=>node.remove());views=manifest.motion_methods.map((method,index)=>makeView(method,index));gallery.classList.remove("comparison-four");for(const view of views){const tile=document.createElement("article");tile.className="tile";tile.dataset.loadState="pending";const exports=`<button class="download" data-format="npz" disabled>NPZ</button><button class="download" data-format="fbx" disabled>FBX</button>`;tile.innerHTML=`<div class="tile-head"><div class="method"><span class="swatch" style="background:${view.method.accent}"></span>${view.method.label}</div><div class="tile-actions"><span class="motion-meta">Waiting</span><button class="download retry-load" type="button" hidden>Retry</button><button class="download camera-reset" type="button" title="Reset 3D view" aria-label="Reset 3D view">↺</button>${exports}</div></div><div class="viewport" aria-label="Interactive 3D motion view"></div>`;for(const button of tile.querySelectorAll("[data-format]")){button.addEventListener("click",event=>{event.stopPropagation();downloadView(view,item,button.dataset.format,button)});if(button.dataset.format==="fbx")button.addEventListener("pointerenter",()=>{if(view.motion&&!fbxCache.has(fbxKey(view,item)))void preparedFbx(view,item).catch(error=>console.warn("FBX pointer warm-up failed",error))})}tile.querySelector(".camera-reset").addEventListener("click",()=>{resetCamera(view);refreshCamera(view)});const output=item.outputs?.[view.method.key];if(output){const text=document.createElement("div");text.className="output-text";text.textContent=output;tile.append(text)}gallery.append(tile);view.viewport=tile.querySelector(".viewport");view.tile=tile;view.retryButton=tile.querySelector(".retry-load");attachCameraControls(view)}for(const method of manifest.output_methods||[]){const tile=document.createElement("article");tile.className="tile text-tile";tile.innerHTML=`<div class="tile-head"><div class="method"><span class="swatch" style="background:${method.accent}"></span>${method.label}</div></div><div class="text-result"></div>`;tile.querySelector(".text-result").textContent=item.outputs?.[method.key]||"No output";gallery.append(tile)}}
108
  function referenceValues(item){return item.segments||item.references||[]}
109
  function renderCaseMeta(item,index){$("#position").textContent=`Case ${index+1} of ${manifest.population}`;$("#case-id").textContent=item.sample_id||item.case_id;const refs=referenceValues(item);$("#captions").replaceChildren(...refs.map(value=>{const span=document.createElement("span");span.className="caption";span.textContent=typeof value==="string"?value:value.caption;if(typeof value!=="string"){span.dataset.start=value.start_frame;span.dataset.end=value.end_frame}return span}));const legend=$("#condition-legend");if(item.condition_intervals?.length){const condition=manifest.condition_legend?.conditioned||{label:"Condition frame",color:"#d95f02"},generated=manifest.condition_legend?.generated||{label:"Generated frame"};legend.replaceChildren(...[[condition.label,condition.color],[generated.label,"#66736d"]].map(([label,color])=>{const chip=document.createElement("span");chip.className="legend-chip";chip.innerHTML=`<span class="legend-dot" style="background:${color}"></span>`;chip.append(document.createTextNode(label));return chip}))}else legend.replaceChildren()}
110
  function idle(){return new Promise(resolve=>"requestIdleCallback"in globalThis?requestIdleCallback(()=>resolve(),{timeout:500}):setTimeout(resolve,0))}