AshenDepths / animcompat.html
Quazim0t0's picture
Deploy Ashen Depths: player animated by skeleton_animator.pt
ee5ec19 verified
Raw
History Blame Contribute Delete
3.89 kB
<!DOCTYPE html><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>