Spaces:
Running
Running
File size: 9,453 Bytes
b120323 1ed29dd b120323 | 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 | import { DenseNetwork } from './network.js';
const DT = 1 / 60;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const wrap = a => Math.atan2(Math.sin(a), Math.cos(a));
const finiteVector = (v, n) => (Array.isArray(v) || ArrayBuffer.isView(v)) && v.length === n && Array.from(v).every(Number.isFinite);
const yaw = (v, a) => [Math.cos(a)*v[0]+Math.sin(a)*v[2], v[1], -Math.sin(a)*v[0]+Math.cos(a)*v[2]];
function quaternion(columns) {
const [a,b,c] = columns, m00=a[0],m11=b[1],m22=c[2], trace=m00+m11+m22;
let q;
if (trace>0) { const s=2*Math.sqrt(trace+1);q=[(b[2]-c[1])/s,(c[0]-a[2])/s,(a[1]-b[0])/s,s/4]; }
else if(m00>m11&&m00>m22) {const s=2*Math.sqrt(1+m00-m11-m22);q=[s/4,(b[0]+a[1])/s,(c[0]+a[2])/s,(b[2]-c[1])/s];}
else if(m11>m22) {const s=2*Math.sqrt(1+m11-m00-m22);q=[(b[0]+a[1])/s,s/4,(c[1]+b[2])/s,(c[0]-a[2])/s];}
else {const s=2*Math.sqrt(1+m22-m00-m11);q=[(c[0]+a[2])/s,(c[1]+b[2])/s,s/4,(a[1]-b[0])/s];}
const length=Math.hypot(...q);return q.map(v=>v/length);
}
function rotation(value, offset) {
const x=Array.from(value.subarray(offset,offset+3)), b=Array.from(value.subarray(offset+3,offset+6));
const nx=Math.hypot(...x);if(nx<1e-6)throw Error('Degenerate predicted rotation.');
for(let i=0;i<3;i++)x[i]/=nx;
const dot=x.reduce((s,v,i)=>s+v*b[i],0),y=b.map((v,i)=>v-dot*x[i]),ny=Math.hypot(...y);
if(ny<1e-6)throw Error('Degenerate predicted rotation.');
for(let i=0;i<3;i++)y[i]/=ny;
return [x,y,[x[1]*y[2]-x[2]*y[1],x[2]*y[0]-x[0]*y[2],x[0]*y[1]-x[1]*y[0]]];
}
/** Decode an absolute 143-value pose using the canonical rig. */
export function decodeMotionPose(output,rig,position=[0,0,0],facing=0) {
if(!finiteVector(output,143)||!finiteVector(position,3)||!Number.isFinite(facing))throw Error('Invalid predicted motion pose.');
const localQuaternions=[];let articulated=0;
for(const end of rig.end_sites) {
if(end){localQuaternions.push([0,0,0,1]);continue;}
let columns=rotation(output,articulated*6);
if(articulated===0)columns=columns.map(v=>yaw(v,facing));
localQuaternions.push(quaternion(columns));articulated++;
}
const offset=yaw(Array.from(output.subarray(138,141)),facing);
return {rootPosition:position.map((v,i)=>v+offset[i]),localQuaternions,
contacts:Array.from(output.subarray(141),v=>1/(1+Math.exp(-v)))};
}
/** Fixed-step, engine-independent locomotion. Units: metres, seconds, radians. */
export class MotionController {
constructor(manifest, weights, config) {
if(config.format!=='superhq-motion-controller-v1'||config.dt!==DT||
manifest.input_contract!=='trajectory22_causal_phase3_v1'||
manifest.layers[0]?.inputs!==25||manifest.layers.at(-1)?.outputs!==143||
manifest.original_feature_indices?.some((v,i)=>v!==843+i)||manifest.original_feature_indices?.length!==25||
!finiteVector(config.cadence,2)||!Array.isArray(config.rig?.end_sites)||
config.rig.end_sites.filter(v=>!v).length!==23||config.rig.names?.length!==config.rig.end_sites.length||
![config.max_speed_mps,config.acceleration_mps2,config.yaw_rate_radps].every(v=>Number.isFinite(v)&&v>0)) {
throw Error('Incompatible Motion controller contract.');
}
this.network=new DenseNetwork(manifest,weights);this.config=config;this.rig=config.rig;
this.reset();
}
static async load(baseURL, {fetch: fetcher=globalThis.fetch}={}) {
const base=baseURL instanceof URL?baseURL:new URL(baseURL,globalThis.location?.href);
async function request(path, binary=false) {
const r=await fetcher(new URL(path,base));if(!r.ok)throw Error(`Cannot load ${path}: HTTP ${r.status}`);
return binary?r.arrayBuffer():r.json();
}
const [manifest,config]=await Promise.all([request('network.json'),request('controller.json')]);
if(manifest.weights!=='weights.bin')throw Error('Unexpected weight filename.');
const weights=await request(manifest.weights,true);
if(!globalThis.crypto?.subtle)throw Error('Weight verification requires Web Crypto (HTTPS or localhost).');
const digest=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',weights)),v=>v.toString(16).padStart(2,'0')).join('');
if(digest!==manifest.sha256)throw Error('Motion weight checksum mismatch.');
return new MotionController(manifest,weights,config);
}
reset({position=[0,0,0],facing=0,velocity=[0,0],phase=0}={}) {
if(!finiteVector(position,3)||Math.abs(position[1])>1e-8||!finiteVector(velocity,2)||
!Number.isFinite(facing)||!Number.isFinite(phase)||Math.hypot(...velocity)>this.config.max_speed_mps+1e-6) {
throw Error('Expected a finite flat-ground reset transform and bounded velocity.');
}
this.position=Array.from(position);this.velocity=[velocity[0],0,velocity[1]];
this.facing=facing;this.phase=phase;this.frame=0;
}
_intent(intent) {
if(!finiteVector(intent?.velocity,2)||!Number.isFinite(intent.facing??this.facing))throw Error('Expected velocity [x,z] and finite facing radians.');
const speed=Math.hypot(...intent.velocity),scale=Math.min(1,this.config.max_speed_mps/Math.max(speed,1e-8));
return {desired:[intent.velocity[0]*scale,0,intent.velocity[1]*scale],facing:intent.facing??this.facing,clamped:speed>this.config.max_speed_mps};
}
/** Call once per 1/60 s. Optional resolver supplies a collision-resolved root. */
step(intent, {resolveMovement}={}) {
const {desired,facing,clamped}=this._intent(intent),oldPosition=this.position.slice(),oldFacing=this.facing;
const change=desired.map((v,i)=>v-this.velocity[i]),distance=Math.hypot(...change),increment=this.config.acceleration_mps2*DT;
const direction=change.map(v=>v/Math.max(distance,1e-8)),turn=wrap(facing-oldFacing);
const points=[],velocities=[],angles=[];
for(const n of [12,24,36]) {
const accelerating=Math.min(n,Math.trunc(distance/increment));
const sum=increment*accelerating*(accelerating+1)/2+(n-accelerating)*distance;
points.push(oldPosition.map((v,i)=>v+DT*(n*this.velocity[i]+direction[i]*sum)));
velocities.push(this.velocity.map((v,i)=>v+direction[i]*Math.min(n*increment,distance)));
angles.push(oldFacing+clamp(turn,-n*this.config.yaw_rate_radps*DT,n*this.config.yaw_rate_radps*DT));
}
let nextVelocity=this.velocity.map((v,i)=>v+change[i]*Math.min(1,increment/Math.max(distance,1e-8)));
let nextPosition=oldPosition.map((v,i)=>v+nextVelocity[i]*DT);
let nextFacing=oldFacing+clamp(turn,-this.config.yaw_rate_radps*DT,this.config.yaw_rate_radps*DT);
if(resolveMovement!==undefined) {
if(typeof resolveMovement!=='function')throw Error('resolveMovement must be a function.');
const resolved=resolveMovement({position:oldPosition.slice(),facing:oldFacing},{position:nextPosition.slice(),facing:nextFacing});
if(!finiteVector(resolved?.position,3)||Math.abs(resolved.position[1])>1e-8||!Number.isFinite(resolved.facing??nextFacing))throw Error('Invalid resolved flat-ground transform.');
nextPosition=Array.from(resolved.position);nextFacing=resolved.facing??nextFacing;
nextVelocity=nextPosition.map((v,i)=>(v-oldPosition[i])/DT);
if(Math.hypot(...nextVelocity)>this.config.max_speed_mps+1e-5)throw Error('Collision resolution exceeds bounded movement; use reset for teleports.');
// Forecast actual movement when a host resolves collisions. No contact or
// physics prediction is implied by this constant-velocity preview.
for(let j=0;j<3;j++){const n=[12,24,36][j];points[j]=oldPosition.map((v,i)=>v+nextVelocity[i]*n*DT);velocities[j]=nextVelocity.slice();angles[j]=nextFacing;}
}
const speed=Math.hypot(...(resolveMovement?nextVelocity:this.velocity));
const activity=clamp((speed-.06)/.19,0,1),frequency=clamp(this.config.cadence[0]+this.config.cadence[1]*speed,.4,1.8);
const nextPhase=(this.phase+2*Math.PI*frequency*activity*DT)%(2*Math.PI);
const input=new Float32Array(25),firstVelocity=yaw(velocities[0],-oldFacing),firstAngle=angles[0]-oldFacing;
input.set([firstVelocity[0],firstVelocity[2],Math.sin(firstAngle),Math.cos(firstAngle)]);
for(let j=0;j<3;j++) {
const p=yaw(points[j].map((v,i)=>v-oldPosition[i]),-oldFacing),v=yaw(velocities[j],-oldFacing),a=angles[j]-oldFacing;
input.set([p[0],p[2],v[0],v[2],Math.sin(a),Math.cos(a)],4+j*6);
}
input.set([Math.sin(nextPhase)*activity,Math.cos(nextPhase)*activity,frequency*activity],22);
const output=this.network.forward(input),localQuaternions=[];let articulated=0;
for(const end of this.rig.end_sites) {
if(end){localQuaternions.push([0,0,0,1]);continue;}
let columns=rotation(output,articulated*6);
if(articulated===0)columns=columns.map(v=>yaw(v,nextFacing));
localQuaternions.push(quaternion(columns));articulated++;
}
const offset=yaw(Array.from(output.subarray(138,141)),nextFacing),rootPosition=nextPosition.map((v,i)=>v+offset[i]);
if(!rootPosition.every(Number.isFinite)||rootPosition[1]<.25||rootPosition[1]>1.8)throw Error('Predicted pelvis outside valid range.');
this.position=nextPosition;this.velocity=nextVelocity;this.facing=nextFacing;this.phase=nextPhase;this.frame++;
return {rootPosition,localQuaternions,contacts:Array.from(output.subarray(141),v=>1/(1+Math.exp(-v))),
simulationPosition:nextPosition.slice(),facing:nextFacing,phase:nextPhase,frame:this.frame,
speed:Math.hypot(...nextVelocity),inputClamped:clamped};
}
}
|