AshenDepths / rltrain.js
Quazim0t0's picture
Deploy Ashen Depths: player animated by skeleton_animator.pt
ee5ec19 verified
Raw
History Blame Contribute Delete
23.2 kB
import * as THREE from 'three';
/* RL gait/strike synthesis directly on a model's OWN skeleton.
*
* The failure of cross-skeleton retargeting is that we must guess each
* bone's flexion axis. Here we DON'T guess: each driven joint moves as a
* rotation about a LEARNABLE axis, and CEM (cross-entropy method, a simple
* evolution strategy) optimizes all joint axes/amplitudes/phases/biases
* against a physics rollout reward. The optimizer discovers whatever joint
* motion actually produces forward, balanced walking in the rig's native
* coordinates — so the result maps perfectly, no retarget.
*
* A joint's per-frame local quaternion:
* q = rest * axisAngle(axis, bias + amp*sin(2*pi*phase + phi))
* Params per joint: [ax, ay, az, amp, phi, bias] (axis auto-normalized)
*/
const _v = new THREE.Vector3(), _q = new THREE.Quaternion();
export class RigController {
// joints: [{key, bone}] ; feet: {L:boneL, R:boneR}; hips, head bones
constructor(rig, jointKeys, refs) {
this.rig = rig;
this.joints = jointKeys.map(k => ({ key: k, bone: rig.bones[refs.map[k]], rest: null }));
this.joints.forEach(j => { if (!j.bone) console.warn('RL: missing bone', refs.map[j.key], 'for', j.key); });
this.joints = this.joints.filter(j => j.bone);
this.joints.forEach(j => j.rest = j.bone.quaternion.clone());
this.refs = refs;
this.footL = rig.bones[refs.footL]; this.footR = rig.bones[refs.footR];
this.hips = rig.bones[refs.hips]; this.head = rig.bones[refs.head];
this.skinned = refs.skinnedRoot;
this.nJ = this.joints.length;
this.P = 6; // params per joint
}
randParams(rng) {
const p = new Float32Array(this.nJ * this.P);
for (let j = 0; j < this.nJ; j++) {
const o = j * this.P;
p[o] = rng() * 2 - 1; p[o + 1] = rng() * 2 - 1; p[o + 2] = rng() * 2 - 1; // axis
p[o + 3] = rng() * 0.6; // amp
p[o + 4] = rng() * Math.PI * 2; // phase
p[o + 5] = (rng() * 2 - 1) * 0.4; // bias
}
return p;
}
// set all joints to the pose at a given cycle phase (0..1)
pose(params, phase) {
const w = 2 * Math.PI * phase;
for (let j = 0; j < this.nJ; j++) {
const o = j * this.P;
let ax = params[o], ay = params[o + 1], az = params[o + 2];
const n = Math.hypot(ax, ay, az) || 1; ax /= n; ay /= n; az /= n;
const ang = params[o + 5] + params[o + 3] * Math.sin(w + params[o + 4]);
_v.set(ax, ay, az); _q.setFromAxisAngle(_v, ang);
this.joints[j].bone.quaternion.copy(this.joints[j].rest).multiply(_q);
}
}
restPose() { this.joints.forEach(j => j.bone.quaternion.copy(j.rest)); }
// physics rollout reward for WALK. Inverted-pendulum locomotion: the lower
// (stance) foot pins to the ground and its backward slide advances the COM.
walkReward(params, opt = {}) {
const T = opt.T || 46, dt = opt.dt || 1 / 30, f = opt.freq || 1.4;
const skinned = this.skinned;
let comZ = 0, comX = 0, prevStanceZ = null, prevStance = -1;
let upPen = 0, latPen = 0, clearL = 0, clearR = 0, fall = 0, footYmin = 1e9;
let energy = 0;
for (let j = 0; j < this.nJ; j++) energy += params[j * this.P + 3] ** 2;
// measure rest torso length
this.restPose(); skinned.updateWorldMatrix(true, true);
const restHead = this.head.getWorldPosition(new THREE.Vector3());
const restHip = this.hips.getWorldPosition(new THREE.Vector3());
const torso0 = restHead.y - restHip.y;
for (let t = 0; t < T; t++) {
const phase = (f * t * dt) % 1;
this.pose(params, phase);
skinned.updateWorldMatrix(true, true);
const fl = this.footL.getWorldPosition(new THREE.Vector3());
const fr = this.footR.getWorldPosition(new THREE.Vector3());
const hp = this.hips.getWorldPosition(new THREE.Vector3());
const hd = this.head.getWorldPosition(new THREE.Vector3());
// stance = lower foot (in world y)
const stanceL = fl.y <= fr.y;
const stanceZ = stanceL ? fl.z : fr.z;
const stance = stanceL ? 0 : 1;
if (prevStanceZ !== null && stance === prevStance) {
comZ += -(stanceZ - prevStanceZ); // foot slides back -> body forward
}
prevStanceZ = stanceZ; prevStance = stance;
// uprightness: torso should keep length & be vertical
const torso = hd.y - hp.y;
upPen += (torso0 - torso) ** 2 + (hd.x - hp.x) ** 2 * 0.5;
latPen += hp.x * hp.x;
// swing foot clearance
clearL += Math.max(0, fl.y); clearR += Math.max(0, fr.y);
footYmin = Math.min(footYmin, fl.y, fr.y);
if (torso < 0.45 * torso0) fall += 1;
}
this.restPose();
const speed = comZ / (T * dt);
// reward forward walking speed, balance, foot alternation clearance
const clearance = Math.min(clearL, clearR) / T; // both feet must lift
return 1.6 * speed
- 3.0 * upPen / T
- 2.0 * latPen / T
+ 1.2 * clearance
- 0.15 * energy
- 2.5 * fall / T
- 4.0 * Math.max(0, footYmin); // feet shouldn't float above ground
}
cem(rewardFn, opt = {}) {
const rng = mulberry32(opt.seed || 1);
const dim = this.nJ * this.P;
const pop = opt.pop || 64, elite = opt.elite || 10, iters = opt.iters || 40;
let mu = this.randParams(() => rng());
let sig = new Float32Array(dim).fill(0.5);
const hist = [];
let best = null, bestR = -1e9;
for (let it = 0; it < iters; it++) {
const cands = [], rewards = [];
for (let p = 0; p < pop; p++) {
const c = new Float32Array(dim);
for (let d = 0; d < dim; d++) c[d] = mu[d] + sig[d] * gauss(rng);
const r = rewardFn(c);
cands.push(c); rewards.push(r);
if (r > bestR) { bestR = r; best = c.slice(); }
}
const idx = rewards.map((r, i) => [r, i]).sort((a, b) => b[0] - a[0]).slice(0, elite).map(x => x[1]);
const nmu = new Float32Array(dim), nsig = new Float32Array(dim);
for (const i of idx) for (let d = 0; d < dim; d++) nmu[d] += cands[i][d] / elite;
for (const i of idx) for (let d = 0; d < dim; d++) nsig[d] += (cands[i][d] - nmu[d]) ** 2 / elite;
for (let d = 0; d < dim; d++) { mu[d] = nmu[d]; sig[d] = Math.sqrt(nsig[d]) + 0.02; }
hist.push(bestR);
if (opt.onIter) opt.onIter(it, bestR);
}
return { params: best, reward: bestR, hist };
}
}
/* ================= PHYSICS-BASED TRAINER =================
*
* DeepMimic-lite: the PhysRig's joint motors track the Mixamo clip
* (sampled via sampleClipTargets), and CEM learns a per-joint RESIDUAL
* rotation + motor gain that keeps the clip upright under real gravity,
* mass and contact. The trained weights ARE the animation controller:
* theta_j = bias + amp*sin(2πphase+phi) + K·s (s = feedback state)
* target_j(phase) = clip_j(phase) * axisAngle(axis_j, theta_j)
* Feedback state s (yaw-relative, so it generalizes across headings):
* [pelvis-up x, pelvis-up z, com-vel x, com-vel z - targetSpeed]
* Params per joint: [ax,ay,az, amp, phi, bias, gain, k0,k1,k2,k3] (P = 11)
*
* Cyclic clips (walk) loop phase; opt.oneshot clips (attack, roll) play
* phase 0..1 once and hold the final pose.
*/
const _tq = new THREE.Quaternion(), _tv = new THREE.Vector3();
const _com = new THREE.Vector3(), _cv = new THREE.Vector3();
const _aq = new THREE.Quaternion(); // actual qRel — MUST be separate from _tq
// (clipTarget returns _tq; aliasing zeroed imErr)
export class PhysTrainer {
/* rig: PhysRig ; ref: result of sampleClipTargets(walkClip, ...) */
constructor(rig, ref, opt = {}) {
this.rig = rig; this.ref = ref;
this.P = 11;
this.oneshot = !!opt.oneshot;
// joints the policy modulates (arms ride along on pure clip tracking)
this.driven = opt.driven ||
['thighL', 'shinL', 'footL', 'thighR', 'shinR', 'footR', 'torso'];
this.trackOnly = rig.joints.map(j => j.name).filter(n => !this.driven.includes(n));
this.nJ = this.driven.length;
this.dim = this.nJ * this.P;
this.freq = opt.freq || ref.freq;
this.dt = opt.dt || 1 / 30;
this.substeps = opt.substeps || 5;
this.T = opt.T || (this.oneshot
? Math.round(ref.duration / this.dt) + 8 // clip once + recovery
: Math.round(2.2 / this.dt)); // ~2 gait cycles
this.targetSpeed = opt.targetSpeed ?? (this.oneshot ? 0 : ref.speed);
this.assistScale = opt.assistScale ?? 1; // weaken balance assists (roll)
this.imWeight = opt.imWeight ?? 0.35; // imitation (Mixamo-match) reward weight
this.speedWeight = opt.speedWeight ?? 2.0; // forward-speed reward weight
this.distWeight = opt.distWeight ?? 0; // net forward-distance reward (traversal)
this.vertWeight = opt.vertWeight ?? 0; // peak-height reward (jumps leave the ground)
this.dirZ = opt.dirZ ?? 1; // clip forward sign (+1 fwd, -1 backward)
this.perturbMag = opt.perturbMag ?? 0; // combat knockback (m/s) injected mid-move
this.perturbK = opt.perturbK ?? 3; // fight scenarios averaged per candidate
this.contactWeight = opt.contactWeight ?? 0; // reward matching the clip's foot-contact timing
this._s = new Float32Array(4); // feedback state buffer
}
/* reference pelvis height at a phase (the clip's authored root bob). Falls
* back to rest height if the reference lacks it. */
refHeight(phase) {
const p = this.ref.pelvisY; if (!p) return this.rig.restPelvisY;
const n = p.length, x = (((phase % 1) + 1) % 1) * n;
const i0 = Math.floor(x) % n, i1 = (i0 + 1) % n;
return p[i0] + (p[i1] - p[i0]) * (x - i0);
}
/* yaw-relative balance state: pelvis tilt + com velocity deviation */
fbState() {
const s = this._s, p = this.rig.byName.pelvis;
_tv.set(0, 0, 1).applyQuaternion(p.quat);
const yaw = Math.atan2(_tv.x, _tv.z), sy = Math.sin(yaw), cy = Math.cos(yaw);
_tv.set(0, 1, 0).applyQuaternion(p.quat); // pelvis up
s[0] = cy * _tv.x - sy * _tv.z;
s[1] = sy * _tv.x + cy * _tv.z;
this.rig.comVel(_cv);
s[2] = cy * _cv.x - sy * _cv.z;
s[3] = (sy * _cv.x + cy * _cv.z) - this.targetSpeed; // forward vel error
return s;
}
zeroParams() {
const p = new Float32Array(this.dim);
for (let j = 0; j < this.nJ; j++) {
p[j * this.P] = 1; // axis x (sagittal flexion default)
p[j * this.P + 6] = 1; // gain
}
return p;
}
clipTarget(name, phase) { // interpolated clip target quat for a joint
const f = this.ref.frames, n = this.ref.nPhase;
if (this.oneshot) { // clamp: play once, hold last frame
const x = Math.min(Math.max(phase, 0), 1) * (n - 1);
const i0 = Math.floor(x), i1 = Math.min(i0 + 1, n - 1);
return _tq.copy(f[i0][name]).slerp(f[i1][name], x - i0);
}
const x = ((phase % 1) + 1) % 1 * n;
const i0 = Math.floor(x) % n, i1 = (i0 + 1) % n;
return _tq.copy(f[i0][name]).slerp(f[i1][name], x - i0);
}
/* Compute this gait's motor target per joint at `phase` WITHOUT applying
* them, so an AnimController can blend targets across several gaits. Writes
* {q, gain} into `out[jointName]` (out reused across calls). Reads the live
* rig state for the feedback term, so all blended gaits see the same pose. */
computeTargets(params, phase, out = {}) {
const s = this.fbState();
const w = 2 * Math.PI * phase;
for (const name of this.trackOnly) {
const e = out[name] || (out[name] = { q: new THREE.Quaternion(), gain: 1 });
e.q.copy(this.clipTarget(name, phase)); e.gain = 1;
}
for (let j = 0; j < this.nJ; j++) {
const o = j * this.P, name = this.driven[j];
let ax = params[o], ay = params[o + 1], az = params[o + 2];
const n = Math.hypot(ax, ay, az) || 1; ax /= n; ay /= n; az /= n;
let ang = params[o + 5] + params[o + 3] * Math.sin(w + params[o + 4])
+ params[o + 7] * s[0] + params[o + 8] * s[1]
+ params[o + 9] * s[2] + params[o + 10] * s[3];
ang = Math.max(-1.2, Math.min(1.2, ang)); // residual safety clamp
const gain = Math.max(0.2, Math.min(4, Math.abs(params[o + 6])));
_tv.set(ax, ay, az);
const e = out[name] || (out[name] = { q: new THREE.Quaternion(), gain: 1 });
e.q.copy(this.clipTarget(name, phase)).multiply(_aq.setFromAxisAngle(_tv, ang));
e.gain = gain;
}
return out;
}
/* set all motor targets for a phase given params (single-gait playback) */
drive(params, phase) {
const rig = this.rig;
rig.hoverTarget = this.vertWeight > 0 ? null : this.refHeight(phase); // track clip bob
for (const name of this.trackOnly)
rig.setTarget(name, this.clipTarget(name, phase).clone(), 1);
const s = this.fbState();
const w = 2 * Math.PI * phase;
for (let j = 0; j < this.nJ; j++) {
const o = j * this.P, name = this.driven[j];
let ax = params[o], ay = params[o + 1], az = params[o + 2];
const n = Math.hypot(ax, ay, az) || 1; ax /= n; ay /= n; az /= n;
let ang = params[o + 5] + params[o + 3] * Math.sin(w + params[o + 4])
+ params[o + 7] * s[0] + params[o + 8] * s[1]
+ params[o + 9] * s[2] + params[o + 10] * s[3];
ang = Math.max(-1.2, Math.min(1.2, ang)); // residual safety clamp
const gain = Math.max(0.2, Math.min(4, Math.abs(params[o + 6])));
_tv.set(ax, ay, az);
const t = this.clipTarget(name, phase).clone()
.multiply(new THREE.Quaternion().setFromAxisAngle(_tv, ang));
rig.setTarget(name, t, gain);
}
}
/* physics rollout; returns reward. opt.onFrame(rig, t) for playback/recording */
rollout(params, opt = {}) {
const rig = this.rig, T = opt.T || this.T;
rig.reset();
const up0 = rig.uprightK, hv0 = rig.hoverK;
rig.uprightK *= this.assistScale; rig.hoverK *= this.assistScale;
try { return this._rollout(params, T, opt); }
finally { rig.uprightK = up0; rig.hoverK = hv0; }
}
/* a fight scenario = list of knockback pokes {step, v:[x,y,z]} sampled to
* mimic getting hit mid-move (magnitude ~ combat knockback). Fixed per
* training so CEM compares candidates on identical disturbances. */
makeScenarios(rng) {
const K = this.perturbK, mag = this.perturbMag, out = [];
for (let k = 0; k < K; k++) {
const pokes = [];
const nP = 1 + Math.floor(rng() * 2);
for (let p = 0; p < nP; p++) {
const a = rng() * 2 * Math.PI, s = mag * (0.5 + rng());
pokes.push({ step: Math.floor(rng() * this.T * 0.7) + 2,
v: [Math.cos(a) * s, (rng() - 0.3) * s * 0.4, Math.sin(a) * s] });
}
out.push(pokes);
}
out[0] = []; // one clean scenario (no hit) always
return out;
}
_rollout(params, T, opt) {
const rig = this.rig;
const pelvis = rig.byName.pelvis;
const y0 = rig.restPelvisY;
const schedule = opt.schedule; // fight-context knockback pokes
let imErr = 0, upPen = 0, latPen = 0, fell = 0, contactPen = 0;
let energy = 0;
for (let j = 0; j < this.nJ; j++) energy += params[j * this.P + 3] ** 2;
const start = rig.comPos(new THREE.Vector3());
const vert = this.vertWeight > 0; // jump: reward leaving ground
let peakY = -1e9, steps = 0;
for (let t = 0; t < T; t++) {
let phase = this.freq * t * this.dt;
if (!this.oneshot) phase %= 1;
this.drive(params, phase);
rig.step(this.dt, this.substeps);
steps++;
if (schedule) for (const pk of schedule) if (pk.step === t)
for (const b of rig.bodies) { b.vel.x += pk.v[0]; b.vel.y += pk.v[1]; b.vel.z += pk.v[2]; }
// imitation error on driven joints (match the Mixamo clip)
for (const name of this.driven) {
const j = rig.jointByName[name];
_aq.copy(j.p.quat).invert().multiply(j.c.quat); // actual qRel
const tgt = this.clipTarget(name, phase); // returns _tq — keep separate
const d = Math.abs(_aq.dot(tgt));
imErr += 2 * Math.acos(Math.min(1, d));
}
// upright: body-up near world-up; height tracks the clip's authored root
// trajectory (refHeight) rather than a constant — this is the root-pose
// imitation term that was missing and caused the bounce.
_tv.set(0, 1, 0).applyQuaternion(pelvis.quat);
upPen += (1 - _tv.y) + (vert ? 0 : Math.abs(pelvis.pos.y - this.refHeight(phase)) * 3);
latPen += Math.abs(pelvis.pos.x - start.x);
peakY = Math.max(peakY, pelvis.pos.y);
// contact matching: foot should be planted (low, still) when the clip says
if (this.contactWeight > 0) {
const fc = this.ref.footContact, ci = Math.floor(((phase % 1) + 1) % 1 * fc.L.length) % fc.L.length;
// timing only: foot should be DOWN when the clip says down, UP when up.
// Do NOT penalize its horizontal velocity — in in-place tracking the
// stance foot slides backward and that slide IS the propulsion.
for (const [name, want] of [['footL', fc.L[ci]], ['footR', fc.R[ci]]]) {
const fb = rig.byName[name];
const grounded = fb.pos.y < fb.r * 1.7 ? 1 : 0;
contactPen += want ? (1 - grounded) : grounded * 0.25;
}
}
if (opt.onFrame) opt.onFrame(rig, t);
if (pelvis.pos.y < y0 * 0.55) { // fallen?
if (!this.oneshot || this.assistScale >= 1) { fell = 1; break; }
else if (t > T - 10) fell += 0.25; // tumbling is fine; ending down is not
}
}
const com = rig.comPos(new THREE.Vector3());
const dist = this.dirZ * (com.z - start.z); // signed by clip's forward dir
const dur = steps * this.dt;
const speed = dist / Math.max(dur, 1e-3);
const speedR = 1 - Math.min(1.5, Math.abs(speed - this.targetSpeed) / Math.max(this.targetSpeed, 0.3));
return this.speedWeight * speedR
+ this.distWeight * Math.max(0, Math.min(4, dist)) // net forward traversal
+ this.vertWeight * Math.max(0, peakY - y0) // jump apex above rest
+ 2.5 * (steps / T) // survival
- this.imWeight * imErr / (steps * this.nJ)
- 1.2 * upPen / steps
- 0.6 * latPen / steps
- this.contactWeight * contactPen / steps
- 0.08 * energy
- 2.0 * fell;
}
/* Imitation fidelity: fraction of the Mixamo clip's joint motion actually
* reproduced by the physics rollout. 1.0 = tracks the clip exactly; ~0 =
* ignores it (stands at rest). meanErr = actual-vs-clip angle; meanMotion =
* clip-vs-neutral angle (how much the clip itself moves). */
fidelity(params) {
const rig = this.rig, T = this.T;
rig.reset();
const up0 = rig.uprightK, hv0 = rig.hoverK;
rig.uprightK *= this.assistScale; rig.hoverK *= this.assistScale;
let err = 0, motion = 0, cnt = 0;
try {
for (let t = 0; t < T; t++) {
let phase = this.freq * t * this.dt;
if (!this.oneshot) phase %= 1;
this.drive(params, phase);
rig.step(this.dt, this.substeps);
for (const name of this.driven) {
const j = rig.jointByName[name];
_aq.copy(j.p.quat).invert().multiply(j.c.quat); // actual qRel
const tgt = this.clipTarget(name, phase); // clip target (_tq)
err += 2 * Math.acos(Math.min(1, Math.abs(_aq.dot(tgt))));
motion += 2 * Math.acos(Math.min(1, Math.abs(tgt.w))); // clip vs neutral
cnt++;
}
}
} finally { rig.uprightK = up0; rig.hoverK = hv0; rig.reset(); }
const meanErr = err / cnt, meanMotion = motion / cnt;
const fid = Math.max(0, Math.min(1, 1 - meanErr / Math.max(meanMotion, 1e-3)));
return { fidelity: fid, meanErrDeg: +(meanErr * 180 / Math.PI).toFixed(1),
meanMotionDeg: +(meanMotion * 180 / Math.PI).toFixed(1) };
}
/* CEM over residual params, seeded at zero-residual (= pure clip tracking).
* Async: yields to the event loop each iteration so a HUD can repaint. */
async cem(opt = {}) {
const rng = mulberry32(opt.seed || 1);
const pop = opt.pop || 40, elite = opt.elite || 8, iters = opt.iters || 30;
let mu = this.zeroParams();
const sig = new Float32Array(this.dim);
for (let j = 0; j < this.nJ; j++) {
const o = j * this.P;
sig[o] = sig[o + 1] = sig[o + 2] = 0.4; // axis
sig[o + 3] = 0.25; // amp
sig[o + 4] = 1.5; // phase
sig[o + 5] = 0.15; // bias
sig[o + 6] = 0.4; // gain
sig[o + 7] = sig[o + 8] = sig[o + 9] = sig[o + 10] = 0.12; // feedback gains
}
// fight-context: average each candidate over fixed knockback scenarios so
// the learned feedback gains reproduce the animation even while being hit
const scen = this.perturbMag > 0 ? this.makeScenarios(rng) : [null];
const evalR = c => { let s = 0; for (const sch of scen) s += this.rollout(c, { schedule: sch }); return s / scen.length; };
let best = mu.slice(), bestR = evalR(mu);
const hist = [bestR];
for (let it = 0; it < iters; it++) {
const cands = [], rewards = [];
for (let p = 0; p < pop; p++) {
const c = new Float32Array(this.dim);
for (let d = 0; d < this.dim; d++) c[d] = mu[d] + sig[d] * gauss(rng);
const r = evalR(c);
cands.push(c); rewards.push(r);
if (r > bestR) { bestR = r; best = c.slice(); }
}
const idx = rewards.map((r, i) => [r, i]).sort((a, b) => b[0] - a[0]).slice(0, elite).map(x => x[1]);
const nmu = new Float32Array(this.dim), nsig = new Float32Array(this.dim);
for (const i of idx) for (let d = 0; d < this.dim; d++) nmu[d] += cands[i][d] / elite;
for (const i of idx) for (let d = 0; d < this.dim; d++) nsig[d] += (cands[i][d] - nmu[d]) ** 2 / elite;
for (let d = 0; d < this.dim; d++) { mu[d] = nmu[d]; sig[d] = Math.sqrt(nsig[d]) + 0.01; }
hist.push(bestR);
if (opt.onIter) opt.onIter(it, bestR);
// MessageChannel yield: repaints HUD but isn't throttled in background tabs
await new Promise(r => { const c = new MessageChannel(); c.port1.onmessage = r; c.port2.postMessage(0); });
}
return { params: best, reward: bestR, hist };
}
/* portable trained-weights blob */
exportWeights(params, meta = {}) {
return {
kind: 'physgait-v1',
driven: this.driven, P: this.P, oneshot: this.oneshot, assistScale: this.assistScale,
params: Array.from(params).map(x => +x.toFixed(5)),
freq: this.freq, dt: this.dt, substeps: this.substeps,
targetSpeed: this.targetSpeed, ...meta,
};
}
}
function mulberry32(a) {
return function () {
a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function gauss(rng) {
let u = 0, v = 0; while (u === 0) u = rng(); while (v === 0) v = rng();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}