Spaces:
Running
Running
| <html><head><meta charset="utf-8"><title>anim compat</title> | |
| <script type="importmap">{"imports":{ | |
| "three":"https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/", | |
| "fflate":"https://unpkg.com/fflate@0.8.2/esm/browser.js" | |
| }}</script></head><body><pre id="o">testing…</pre> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import {FBXLoader} from 'three/addons/loaders/FBXLoader.js'; | |
| const fbx=new FBXLoader(); | |
| const load=p=>new Promise((res,rej)=>fbx.load(p,res,undefined,rej)); | |
| window.__compat=async(clips)=>{ | |
| const base=await load('assets/chars/player.fbx'); | |
| const bones=new Set();base.traverse(o=>{if(o.isBone)bones.add(o.name);}); | |
| const out={baseBones:bones.size}; | |
| for(const name of clips){ | |
| try{ | |
| const a=await load(`assets/anim/player/${name}.fbx`); | |
| const clip=a.animations&&a.animations[0]; | |
| if(!clip){out[name]={err:'no clip'};continue;} | |
| const trackBones=new Set(clip.tracks.map(t=>t.name.split('.')[0])); | |
| let matched=0,missing=[]; | |
| for(const tb of trackBones){if(bones.has(tb))matched++;else missing.push(tb);} | |
| // hips root translation delta -> speed | |
| const hipsPos=clip.tracks.find(t=>/Hips\.position$/.test(t.name)); | |
| let speed=0; | |
| if(hipsPos){const v=hipsPos.values;const n=v.length/3; | |
| const dx=v[(n-1)*3]-v[0],dz=v[(n-1)*3+2]-v[2]; | |
| speed=Math.hypot(dx,dz)/clip.duration/100;} // mixamo cm -> m rough | |
| out[name]={dur:+clip.duration.toFixed(2),tracks:clip.tracks.length, | |
| matched,missingCount:missing.length,missingSample:missing.slice(0,3),rawSpeed:+speed.toFixed(2)}; | |
| }catch(e){out[name]={err:e.message};} | |
| } | |
| return out; | |
| }; | |
| /* foot-skate: while a foot is planted (near its lowest Y this cycle) it should | |
| * stay world-stationary. Sliding = the clip is bad for physics imitation. | |
| * Returns per-clip mean stance-foot slide speed (m/s) + vertical bob. */ | |
| window.__skate=async(clips)=>{ | |
| const base=await load('assets/chars/player.fbx'); | |
| const box=new THREE.Box3().setFromObject(base);const s=new THREE.Vector3();box.getSize(s); | |
| const sc=1.8/s.y;base.scale.setScalar(sc);base.updateWorldMatrix(true,true); | |
| const bones={};base.traverse(o=>{if(o.isBone)bones[o.name]=o;}); | |
| const P='mixamorig'; | |
| const footL=bones[P+'LeftFoot']||bones[P+'LeftToeBase'], footR=bones[P+'RightFoot']||bones[P+'RightToeBase']; | |
| const hips=bones[P+'Hips']; | |
| const out={}; | |
| for(const name of clips){ | |
| try{ | |
| const a=await load(`assets/anim/player/${name}.fbx`);const clip=a.animations[0]; | |
| const mixer=new THREE.AnimationMixer(base);const act=mixer.clipAction(clip);act.play(); | |
| const N=48,dt=clip.duration/N;const V=THREE.Vector3; | |
| const posL=[],posR=[],hipY=[]; | |
| for(let k=0;k<N;k++){mixer.setTime(k*dt);base.updateWorldMatrix(true,true); | |
| posL.push(footL.getWorldPosition(new V()));posR.push(footR.getWorldPosition(new V())); | |
| hipY.push(hips.getWorldPosition(new V()).y);} | |
| const minLY=Math.min(...posL.map(p=>p.y)),minRY=Math.min(...posR.map(p=>p.y)); | |
| const rng=posL.map(p=>p.y).reduce((a,b)=>Math.max(a,b),0)-minLY; | |
| const thr=0.35*rng; // "planted" = within 35% of lowest | |
| let slide=0,cnt=0; | |
| for(let k=0;k<N;k++){const k2=(k+1)%N; | |
| for(const[pos,mnY] of [[posL,minLY],[posR,minRY]]){ | |
| if(pos[k].y-mnY<thr){const dx=pos[k2].x-pos[k].x,dz=pos[k2].z-pos[k].z; | |
| slide+=Math.hypot(dx,dz)/dt;cnt++;}}} | |
| act.stop();mixer.uncacheClip(clip); | |
| out[name]={skate_mps:+(slide/Math.max(cnt,1)).toFixed(2),bob_m:+(Math.max(...hipY)-Math.min(...hipY)).toFixed(3),dur:+clip.duration.toFixed(2)}; | |
| }catch(e){out[name]={err:e.message};} | |
| base.traverse(o=>{if(o.isBone){}}); | |
| } | |
| return out; | |
| }; | |
| document.getElementById('o').textContent='ready — __compat([...]) / __skate([...])'; | |
| </script></body></html> | |