motion / src /profile-clock.js
harshthedev's picture
SuperHQ Motion experiment: locomotion and action checkpoints, runtime, clips, evaluation
6adbbb1 verified
Raw History Blame Contribute Delete
3.33 kB
const DT = 1 / 60;
const smooth = t => {t=Math.max(0,Math.min(1,t));return t*t*(3-2*t);};
const vector = (a,n) => (Array.isArray(a)||ArrayBuffer.isView(a))&&a.length===n&&Array.from(a).every(Number.isFinite);
function profile(value,rate) {
if(!Number.isFinite(rate)||rate<.75||rate>1.25||!value||
!vector(value.flags,9)||value.flags.some(v=>v!==0&&v!==1)||value.flags[0]!==1||
value.flags.slice(1,5).reduce((a,b)=>a+b,0)!==1||
!Number.isFinite(value.frequency_hz)||value.frequency_hz<=0||value.frequency_hz>2||
!Number.isFinite(value.speed_mps)||value.speed_mps<0||value.speed_mps>12)throw Error('Invalid Motion profile or playback rate.');
const velocity=value.velocity??[0,0,value.speed_mps];
if(!vector(velocity,3)||velocity[1]!==0||Math.abs(Math.hypot(...velocity)-value.speed_mps)>1e-4||
(value.flags[1]===1&&value.speed_mps!==0))throw Error('Invalid flat-ground profile velocity.');
return {flags:Array.from(value.flags),velocity:velocity.map(v=>v*rate),frequency:value.frequency_hz*rate};
}
/** Causal 34-feature clock for the game repair checkpoint. Phase is in cycles.
* Call step once per 1/60 second. The host owns movement and collision handling.
* Supported profile pairs are listed in the checkpoint study protocol.
*/
export class MotionProfileClock {
constructor(initial,{rate=1,transitionSeconds=.75}={}) {
if(!Number.isFinite(transitionSeconds)||transitionSeconds<.5||transitionSeconds>1)throw Error('Transition duration must be .5 to 1 second.');
this.transitionSeconds=transitionSeconds;this.reset(initial,{rate});
}
reset(initial,{rate=1,phase=0,position=[0,0,0]}={}) {
const p=profile(initial,rate);
if(!Number.isFinite(phase)||!vector(position,3)||position[1]!==0)throw Error('Invalid flat-ground reset.');
this.from=p;this.to=p;this.start=-this.transitionSeconds;this.time=0;
this.phase=((phase%1)+1)%1;this.position=Array.from(position);this.frame=0;
}
at(time) {
const u=smooth((time-this.start)/this.transitionSeconds),mix=(a,b)=>a*(1-u)+b*u;
return {flags:this.from.flags.map((v,i)=>mix(v,this.to.flags[i])),
velocity:this.from.velocity.map((v,i)=>mix(v,this.to.velocity[i])),frequency:mix(this.from.frequency,this.to.frequency)};
}
setProfile(value,{rate=1}={}) {
const target=profile(value,rate),current=this.at(this.time);
this.from=current;this.to=target;this.start=this.time;
}
step() {
const p=this.at(this.time),input=new Float32Array(34),delta=[0,0,0];
// Same fixed-step trajectory integration used in the training targets.
for(let n=1;n<=36;n++) {
const previous=this.at(this.time+(n-1)*DT);
for(let j=0;j<3;j++)delta[j]+=previous.velocity[j]*DT;
if(n%12===0){const future=this.at(this.time+n*DT);input.set([delta[0],delta[2],future.velocity[0],future.velocity[2],0,1],4+6*(n/12-1));}
}
input.set(input.subarray(6,10),0);
input.set([Math.sin(this.phase*2*Math.PI),Math.cos(this.phase*2*Math.PI),p.frequency],22);input.set(p.flags,25);
this.phase=(this.phase+p.frequency*DT)%1;
for(let j=0;j<3;j++)this.position[j]+=p.velocity[j]*DT;
this.time+=DT;this.frame++;
return {input,phase:this.phase,simulationPosition:this.position.slice(),frequency:p.frequency,
speed:Math.hypot(...p.velocity),flags:p.flags,frame:this.frame};
}
}