Spaces:
Running
Running
| <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> | |