Spaces:
Running
Running
File size: 11,828 Bytes
ee5ec19 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | <!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>
|