AshenDepths / rlphys.html
Quazim0t0's picture
Deploy Ashen Depths: player animated by skeleton_animator.pt
ee5ec19 verified
Raw
History Blame Contribute Delete
11.8 kB
<!DOCTYPE html><html><head><meta charset="utf-8"><title>Physics moves trainer</title>
<style>html,body{margin:0;height:100%;background:#202028;overflow:hidden}
#hud{position:fixed;top:0;left:0;color:#0f0;font:12px monospace;z-index:9;padding:6px;background:#000a;white-space:pre}
#ui{position:fixed;top:6px;right:6px;z-index:9;font:12px monospace;display:none}
#ui button{display:block;margin:2px;width:110px}</style>
<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><div id="hud">loading…</div>
<div id="ui"><button id="save">save weights</button></div>
<script type="module">
import * as THREE from 'three';
import {FBXLoader} from 'three/addons/loaders/FBXLoader.js';
import {PhysRig, sampleClipTargets} from './physbody.js';
import {PhysTrainer} from './rltrain.js';
const hud=document.getElementById('hud');const L=m=>hud.textContent=m;
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;document.body.appendChild(renderer.domElement);
const scene=new THREE.Scene();scene.background=new THREE.Color(0x202028);
scene.add(new THREE.HemisphereLight(0xffffff,0x445,1.2));
const dl=new THREE.DirectionalLight(0xffffff,2.2);dl.position.set(3,8,6);dl.castShadow=true;scene.add(dl);
const ground=new THREE.Mesh(new THREE.PlaneGeometry(60,60),new THREE.MeshStandardMaterial({color:0x33343e}));
ground.rotation.x=-Math.PI/2;ground.receiveShadow=true;scene.add(ground);
const grid=new THREE.GridHelper(60,60,0x555,0x3a3a44);grid.position.y=0.002;scene.add(grid);
const cam=new THREE.PerspectiveCamera(45,innerWidth/innerHeight,0.05,100);
cam.position.set(2.8,1.4,3.2);cam.lookAt(0,1.0,0);
const fbx=new FBXLoader();
const load=p=>new Promise((res,rej)=>fbx.load(p,res,undefined,rej));
function makeDebug(rig){
const g=new THREE.Group();scene.add(g);
const mat=new THREE.MeshStandardMaterial({color:0x50c0ff,transparent:true,opacity:0.45});
rig.bodies.forEach(b=>{
const m=new THREE.Mesh(new THREE.CapsuleGeometry(b.r,Math.max(b.len-2*b.r,0.02),4,8),mat);
g.add(m);b._dbg=m;
const axis=b.endB.clone().sub(b.endA).normalize();
b._dbgQ=new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0),axis);
});
return {group:g,update(){rig.bodies.forEach(b=>{
b._dbg.position.copy(b.endA).add(b.endB).multiplyScalar(0.5).applyQuaternion(b.quat).add(b.pos);
b._dbg.quaternion.copy(b.quat).multiply(b._dbgQ);});}};
}
// FIGHT-IMITATION training: the physics ragdoll (student) learns to reproduce
// the REAL Mixamo animation (teacher) while withstanding combat knockback.
// perturbMag = knockback speed (m/s) injected mid-move, sized from the game's
// actual hit knock values (boss hits ~4-9 -> impulse ~3-6 m/s). Locomotion &
// block get hit hardest; fragile jumps train clean.
const LEGS11=['thighL','shinL','footL','thighR','shinR','footR','torso','uarmL','farmL','uarmR','farmR'];
const _q=new URLSearchParams(location.search);const _itOv=+_q.get('iters')||0;
const MOVES=(_q.get('only')==='walk')?[
['walk', {oneshot:false, iters:_itOv||4000, pop:56, imWeight:1.3, speedWeight:2.6, distWeight:0.8, driven:LEGS11, perturbMag:4, clip:_q.get('clip')||'walk'}],
]:[
// trained on the Male Locomotion Pack (cleaner set) + boxing/uppercut/roll.
// cfg.clip = source fbx; move key = how the game refers to it.
['idle', {oneshot:false, iters:30, pop:32, imWeight:1.6, speedWeight:0, targetSpeed:0, perturbMag:3.5, clip:'lp_idle'}],
['walk', {oneshot:false, iters:110, pop:56, imWeight:1.3, speedWeight:2.6, distWeight:0.8, driven:LEGS11, perturbMag:4, clip:'lp_walk'}],
['walkback',{oneshot:false, iters:110, pop:56, imWeight:1.3, speedWeight:2.6, distWeight:0.8, dirZ:-1, driven:LEGS11, perturbMag:4, clip:'walkback'}],
['run', {oneshot:false, iters:110, pop:56, imWeight:1.2, speedWeight:2.6, distWeight:0.8, driven:LEGS11, perturbMag:4, clip:'lp_run'}],
['runback', {oneshot:false, iters:110, pop:56, imWeight:1.2, speedWeight:2.6, distWeight:0.8, dirZ:-1, driven:LEGS11, perturbMag:4, clip:'runback'}],
['turnL', {oneshot:true, iters:50, pop:36, imWeight:2.5, speedWeight:0, targetSpeed:0, perturbMag:2.5, clip:'lp_turnL'}],
['turnR', {oneshot:true, iters:50, pop:36, imWeight:2.5, speedWeight:0, targetSpeed:0, perturbMag:2.5, clip:'lp_turnR'}],
['jump', {oneshot:true, iters:70, pop:40, imWeight:1.0, speedWeight:0, targetSpeed:0, vertWeight:3.0, assistScale:0.15, clip:'lp_jump'}],
['attack1', {oneshot:true, iters:60, pop:36, imWeight:2.0, perturbMag:3, clip:'boxing'}],
['hit', {oneshot:true, iters:50, pop:36, imWeight:1.5, perturbMag:5, assistScale:0.6, clip:'uppercut'}],
['block', {oneshot:false, iters:60, pop:36, imWeight:2.0, perturbMag:5.5, clip:'block'}],
['roll', {oneshot:true, iters:64, pop:36, assistScale:0.75, imWeight:1.2, perturbMag:2, clip:'roll2'}],
];
let rig,obj,dbg,trained={},cur='walk',phase=0,playing=false,showMesh=true;
window.__mesh=v=>{showMesh=v;obj.visible=v;return v;};
window.__poke=(x=0,y=0,z=2)=>{rig.bodies.forEach(b=>b.vel.add(new THREE.Vector3(x,y,z)));return 'poked';};
window.__move=n=>{if(trained[n]){cur=n;phase=0;}return cur;};
window.__weights=()=>{const moves={};for(const[n,t] of Object.entries(trained))
moves[n]=t.trainer.exportWeights(t.params,{clip:t.clip||n,character:'player'});
return {kind:'physmoves-v1',character:'player',moves};};
(async()=>{
L('loading player…');
obj=await load('assets/chars/player.fbx');
const box=new THREE.Box3().setFromObject(obj);const s=new THREE.Vector3();box.getSize(s);
const sc=1.8/s.y;obj.scale.setScalar(sc);obj.position.y=-box.min.y*sc;
obj.traverse(o=>{if(o.isMesh){o.castShadow=true;o.frustumCulled=false;}});
scene.add(obj);
const bones={};obj.traverse(o=>{if(o.isBone)bones[o.name]=o;});
obj.updateWorldMatrix(true,true);
rig=new PhysRig(bones,obj,{mass:78});
dbg=makeDebug(rig);
window.__rig=rig;
const t0=performance.now();
for(const[name,cfg] of MOVES){
L(`loading clip ${name}…`);
let clip;
try{clip=(await load(`assets/anim/player/${cfg.clip||name}.fbx`)).animations[0];}
catch(e){console.warn('no clip',name,e);continue;}
rig.reset();obj.updateWorldMatrix(true,true);
const ref=sampleClipTargets(clip,obj,rig,30);
const trainer=new PhysTrainer(rig,ref,{oneshot:cfg.oneshot,T:cfg.oneshot?undefined:170,assistScale:cfg.assistScale,imWeight:cfg.imWeight,speedWeight:cfg.speedWeight,distWeight:cfg.distWeight,driven:cfg.driven,dirZ:cfg.dirZ,vertWeight:cfg.vertWeight,targetSpeed:cfg.targetSpeed,perturbMag:cfg.perturbMag,perturbK:2,contactWeight:cfg.contactWeight});
const res=await trainer.cem({pop:cfg.pop,elite:9,iters:cfg.iters,seed:7,
onIter:(it,r)=>L(`training ${name}… iter ${it+1}/${cfg.iters} reward ${r.toFixed(3)} (${((performance.now()-t0)/1000).toFixed(0)}s total)`)});
const fid=trainer.fidelity(res.params);
trained[name]={trainer,params:res.params,reward:res.reward,oneshot:cfg.oneshot,fid,clip:cfg.clip||name};
// ui button per move
const b=document.createElement('button');b.textContent=`${name} (${res.reward.toFixed(2)})`;
b.onclick=()=>window.__move(name);document.getElementById('ui').appendChild(b);
window.__T=window.__T||trainer;
}
window.__fid=()=>Object.fromEntries(Object.entries(trained).map(([n,t])=>[n,{fidelity:+(t.fid.fidelity*100).toFixed(0)+'%',errDeg:t.fid.meanErrDeg,motionDeg:t.fid.meanMotionDeg}]));
// robustness: fraction of hit-directions the move survives without falling
window.__robust=(mag=4.5)=>{const V=rig.byName.pelvis.pos.constructor;const out={};
for(const[n,t] of Object.entries(trained)){const T=t.trainer;let ok=0,tot=0;
for(let a=0;a<8;a++){const ang=a/8*2*Math.PI;rig.reset();rig.heading=null;
const up0=rig.uprightK,hv0=rig.hoverK;rig.uprightK*=(T.assistScale||1);rig.hoverK*=(T.assistScale||1);
let fell=false;const N=t.oneshot?T.T:Math.round(2.2/T.dt);
for(let i=0;i<N;i++){let ph=T.freq*i*T.dt;if(!t.oneshot)ph%=1;T.drive(t.params,ph);rig.step(T.dt,T.substeps);
if(i===Math.floor(N*0.3))rig.bodies.forEach(b=>{b.vel.x+=Math.cos(ang)*mag;b.vel.z+=Math.sin(ang)*mag;});
if(rig.byName.pelvis.pos.y<rig.restPelvisY*0.5)fell=true;}
rig.uprightK=up0;rig.hoverK=hv0;if(!fell)ok++;tot++;}
out[n]=Math.round(ok/tot*100)+'%';}
rig.reset();rig.heading=0;return out;};
// measure actual physical motion each trained move produces
window.__behavior=()=>{const V=rig.byName.pelvis.pos.constructor;const out={};
for(const[n,t] of Object.entries(trained)){const T=t.trainer;rig.reset();rig.heading=null;
const up0=rig.uprightK,hv0=rig.hoverK;rig.uprightK*=(t.trainer.assistScale||1);rig.hoverK*=(t.trainer.assistScale||1);
const p0=rig.byName.pelvis.pos.clone();const f0=new V(0,0,1).applyQuaternion(rig.byName.pelvis.quat);
const y0v=rig.restPelvisY;let peak=-9,minY=9;const N=t.oneshot?T.T:Math.round(2.5/T.dt);
for(let i=0;i<N;i++){let ph=T.freq*i*T.dt;if(!t.oneshot)ph%=1;T.drive(t.params,ph);rig.step(T.dt,T.substeps);
peak=Math.max(peak,rig.byName.pelvis.pos.y);minY=Math.min(minY,rig.byName.pelvis.pos.y);}
const p1=rig.byName.pelvis.pos,f1=new V(0,0,1).applyQuaternion(rig.byName.pelvis.quat);
const yaw0=Math.atan2(f0.x,f0.z),yaw1=Math.atan2(f1.x,f1.z);
let dyaw=(yaw1-yaw0)*180/Math.PI;while(dyaw>180)dyaw-=360;while(dyaw<-180)dyaw+=360;
out[n]={dz:+(p1.z-p0.z).toFixed(2),dx:+(p1.x-p0.x).toFixed(2),dyawDeg:+dyaw.toFixed(0),
peakAboveRest:+(peak-y0v).toFixed(2),minY:+minY.toFixed(2),fell:minY<y0v*0.55};
rig.uprightK=up0;rig.hoverK=hv0;}
rig.reset();rig.heading=0;return out;};
L(`all trained in ${((performance.now()-t0)/1000).toFixed(1)}s\n`+
Object.entries(trained).map(([n,t])=>`${n}: R${t.reward.toFixed(2)} fid${(t.fid.fidelity*100).toFixed(0)}%`).join(' ')+
`\nkeys: 1 walk 2 attack1 3 block 4 roll __poke(x,y,z)`);
document.getElementById('ui').style.display='block';
window.__trained=trained;
rig.reset();rig.heading=0;playing=true;
let last=performance.now(),acc=0;
(function loop(now){requestAnimationFrame(loop);
const dt=Math.min((now-last)/1000,0.05);last=now;
if(playing&&trained[cur]){acc+=dt;
const t=trained[cur],T=t.trainer;
while(acc>=T.dt){acc-=T.dt;
phase+=T.dt*T.freq;
if(t.oneshot&&phase>=1){cur='walk';phase=0;}
else if(!t.oneshot)phase%=1;
const tt=trained[cur];
tt.trainer.drive(tt.params,phase);
rig.step(tt.trainer.dt,tt.trainer.substeps);
const p=rig.byName.pelvis;
if(p.pos.z>12||p.pos.z<-12||Math.abs(p.pos.x)>12){rig.reset();rig.heading=0;phase=0;}
if(p.pos.y<rig.restPelvisY*0.35){rig.reset();rig.heading=0;phase=0;cur='walk';}
}
if(showMesh)rig.applyToBones();
dbg.update();
const p=rig.byName.pelvis.pos;
cam.position.set(p.x+2.8,1.5,p.z+3.2);cam.lookAt(p.x,1.0,p.z);
}
renderer.render(scene,cam);})(performance.now());
})().catch(e=>L('ERR '+e.message+'\n'+(e.stack||'')));
addEventListener('keydown',e=>{
const k={Digit1:'walk',Digit2:'attack1',Digit3:'block',Digit4:'roll'}[e.code];
if(k)window.__move(k);});
document.getElementById('save').onclick=()=>{
const blob=new Blob([JSON.stringify(window.__weights())],{type:'application/json'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='player_moves.json';a.click();
localStorage.setItem('player_moves',JSON.stringify(window.__weights()));
};
</script></body></html>