reze-engine 0.32.0 → 0.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ npm install reze-engine
15
15
  - **Anime-style rendering** — toon-ramp NPR over a Principled GGX BSDF, mixed per material
16
16
  - **Shader-graph materials** — every look is a Blender-style node graph compiled to WGSL; style groups bind any materials to any graph, fully customizable
17
17
  - **HDR pipeline** — bloom, Filmic tone mapping, ASC CDL colour grading, 4× MSAA, Apple-TBDR-friendly targets
18
- - **In-house TS physics** — sequential-impulse rigid bodies for PMX rigs with rest-stable implicit spring-dampers, zero dependencies
18
+ - **In-house TS physics** — sequential-impulse rigid bodies for PMX rigs with rest-stable implicit spring-dampers, zero dependencies; each model simulates on its own Web Worker (plain transferables — no SharedArrayBuffer, no special headers), so physics costs the main thread ~0.1 ms and a multi-model scene pays for its slowest world, not the sum
19
19
  - **Math library** — the shared Vec3/Quat/Mat4 layer of the Reze family: euler orders, swing-twist, shortest-arc, look-rotation, zero-alloc `*Into` variants
20
20
  - **VMD animation** — MMD IK with per-chain enable read from the motion, morphs on a GPU compute path, and VMD export
21
21
  - **Clip blending & locomotion** — weighted multi-clip pose blending, crossfades, and a game-style character controller (idle/run/sprint speed blend, authored stop skids, code-driven root motion) — the [demo](https://reze.one) is WASD-playable
@@ -88,6 +88,8 @@ engine/src/
88
88
 
89
89
  physics/ in-house rigid-body solver (~2.5k lines)
90
90
  physics.ts RezePhysics: bone↔body sync, fixed-step accumulator + interpolation
91
+ physics.worker.ts worker entry — one model's world per worker, pipelined one
92
+ worker-physics.ts frame deep over transferable buffers (main-thread facade)
91
93
  solver.ts sequential-impulse PGS (joint + contact rows)
92
94
  contact.ts narrowphase (analytical sphere/box/capsule pairs) + contact pool
93
95
  constraint.ts 6DOF spring joints · world.ts broadphase + step
@@ -90,6 +90,10 @@ export interface LocomotionOptions {
90
90
  turnTimeScale?: number;
91
91
  /** Playback rate for stop clips (default 1.25) — same reasoning. */
92
92
  stopTimeScale?: number;
93
+ /** Seconds a heading must be held before a release earns an authored stop
94
+ * (default 0.5). Below it, weaving direction changes blend to idle instead
95
+ * of skidding metres along the last-held heading. */
96
+ stopCommitTime?: number;
93
97
  /** Tank-mode steering rate, radians/second (default 2.5 ≈ 143°/s). */
94
98
  steerRate?: number;
95
99
  /** Backpedal speed as a fraction of run speed in tank mode (default 0.5). */
@@ -123,11 +127,15 @@ export declare class LocomotionController {
123
127
  private readonly turnClipMinAngle;
124
128
  private readonly turnTimeScale;
125
129
  private readonly stopTimeScale;
130
+ private readonly stopCommitTime;
126
131
  private turning;
127
132
  private runTurning;
128
133
  /** An interrupted authored clip fading out OVER resumed locomotion, so breaking
129
134
  * out of a stop is instantly responsive without a pose pop. */
130
135
  private exitGhost;
136
+ private headingHold;
137
+ private headingDirX;
138
+ private headingDirY;
131
139
  private stopping;
132
140
  private readonly turnEntries;
133
141
  private readonly yawOffset;
@@ -1 +1 @@
1
- {"version":3,"file":"locomotion.d.ts","sourceRoot":"","sources":["../src/locomotion.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"locomotion.d.ts","sourceRoot":"","sources":["../src/locomotion.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAO,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,kGAAkG;IAClG,KAAK,EAAE,MAAM,CAAA;IACb,kGAAkG;IAClG,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAA;IAChB;qFACiF;IACjF,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,KAAK,GAAG,QAAQ,CAAA;IACtB,IAAI,EAAE,GAAG,GAAG,GAAG,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAA;IAChB;8EAC0E;IAC1E,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,EAAE,KAAK,GAAG,QAAQ,CAAA;IACtB,IAAI,EAAE,GAAG,GAAG,GAAG,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;kEAC8D;IAC9D,SAAS,CAAC,EAAE,eAAe,EAAE,CAAA;IAC7B,YAAY,CAAC,EAAE,eAAe,EAAE,CAAA;IAChC;;mFAE+E;IAC/E,WAAW,CAAC,EAAE,aAAa,EAAE,CAAA;IAC7B;;;qCAGiC;IACjC,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B;;wEAEoE;IACpE,IAAI,CAAC,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC;;oEAEgE;IAChE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;qDAEiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;iFAC6E;IAC7E,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;2CACuC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;0DAEsD;IACtD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;qEACiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;iEAEiE;AACjE,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,IAAI,CAAA;IACd,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAA;IACX,uEAAuE;IACvE,QAAQ,EAAE,IAAI,CAAA;IACd,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAA;CACnB;AAaD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAO;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,OAAO,CAAsD;IACrE,OAAO,CAAC,UAAU,CAOH;IACf;oEACgE;IAChE,OAAO,CAAC,SAAS,CAAmE;IACpF,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,QAAQ,CAkBD;IACf,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAElC,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,WAAW,CAAQ;IAE3B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IAEvC,OAAO,CAAC,UAAU,CAAI;IACtB;;4EAEwE;IACxE,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,GAAG,CAAI;IAIf,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,IAAI,CAAI;IAChB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,SAAS,CAAI;IAErB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IAErC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAc;gBAEhC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,iBAAiB;IAmC7E;;yDAEqD;IACrD,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,IAAI;IAYnD;;;2EAGuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,IAAI;IAO9D,sDAAsD;IACtD,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,IAAI;IAKxD;;iEAE6D;IAC7D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAInC,WAAW,IAAI,IAAI;IAInB,4FAA4F;IAC5F,MAAM,IAAI,IAAI;IAKd;gFAC4E;IAC5E,eAAe,IAAI,UAAU,EAAE,GAAG,IAAI;IAItC,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,YAAY;IAKpB;+EAC2E;IAC3E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc;IAkPlC;+EAC2E;IAC3E,OAAO,CAAC,QAAQ;IAYhB;;mFAE+E;IAC/E,OAAO,CAAC,YAAY;IA2FpB;;;4EAGwE;IACxE,OAAO,CAAC,cAAc;IA6CtB;;wDAEoD;IACpD,OAAO,CAAC,UAAU;IA4ElB,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAC,SAAS;IAQxB;;;kEAG8D;IAC9D,OAAO,CAAC,aAAa;CAkDtB"}
@@ -6,6 +6,8 @@
6
6
  import { FPS } from "./animation";
7
7
  import { Quat, Vec3 } from "./math";
8
8
  const TWO_PI = Math.PI * 2;
9
+ /** Seconds an interrupted stop's ghost pose takes to dissolve over resumed locomotion. */
10
+ const GHOST_FADE = 0.25;
9
11
  function wrapAngle(a) {
10
12
  while (a > Math.PI)
11
13
  a -= TWO_PI;
@@ -21,6 +23,9 @@ export class LocomotionController {
21
23
  /** An interrupted authored clip fading out OVER resumed locomotion, so breaking
22
24
  * out of a stop is instantly responsive without a pose pop. */
23
25
  this.exitGhost = null;
26
+ this.headingHold = 0;
27
+ this.headingDirX = 0;
28
+ this.headingDirY = 0;
24
29
  this.stopping = null;
25
30
  this.inputX = 0;
26
31
  this.inputY = 0;
@@ -57,6 +62,7 @@ export class LocomotionController {
57
62
  this.turnClipMinAngle = options?.turnClipMinAngle ?? (100 * Math.PI) / 180;
58
63
  this.turnTimeScale = options?.turnTimeScale ?? 1.4;
59
64
  this.stopTimeScale = options?.stopTimeScale ?? 1.25;
65
+ this.stopCommitTime = options?.stopCommitTime ?? 0.5;
60
66
  this.turnEntries = [
61
67
  { name: clips.idle, time: 0, weight: 0 },
62
68
  { name: clips.idle, time: 0, weight: 1 },
@@ -174,12 +180,30 @@ export class LocomotionController {
174
180
  const m = Math.hypot(this.inputX, this.inputY);
175
181
  moving = m > 0.05;
176
182
  this.recentSpeed = Math.max(this.speedLevel, this.recentSpeed - dt * 1.5);
183
+ // How long the CURRENT heading has been held. An authored skid is the
184
+ // ending of a committed run; weaving through directions (D, then S+D,
185
+ // then S) rebuilds throttle in each new heading but earns no such
186
+ // ending — playing one there slides her metres along whichever way she
187
+ // happened to face last, which reads as drift.
188
+ if (moving) {
189
+ const dx = this.inputX / m;
190
+ const dy = this.inputY / m;
191
+ this.headingHold =
192
+ this.headingDirX * dx + this.headingDirY * dy > 0.94 ? this.headingHold + dt : 0;
193
+ this.headingDirX = dx;
194
+ this.headingDirY = dy;
195
+ }
177
196
  // Release at speed: play the authored stop instead of blending to idle.
178
197
  const stops = this.clips.stop;
179
198
  // Gate on the peak-hold speed: the release decay (throttle/analog) drains the
180
199
  // instantaneous level below any threshold before `moving` goes false. The
181
200
  // small floor on the actual level keeps standstill taps from faking a skid.
182
- if (!moving && stops && stops.length > 0 && this.recentSpeed >= 0.7 && this.speedLevel >= 0.35) {
201
+ if (!moving &&
202
+ stops &&
203
+ stops.length > 0 &&
204
+ this.recentSpeed >= 0.7 &&
205
+ this.speedLevel >= 0.35 &&
206
+ this.headingHold >= this.stopCommitTime) {
183
207
  const gear = this.recentSpeed > 1.4 ? "sprint" : "run";
184
208
  const foot = this.gaitPhase < 0.5 ? "L" : "R";
185
209
  // Gear must match exactly: supplying stops for only one gear means the
@@ -205,6 +229,13 @@ export class LocomotionController {
205
229
  dirX: this.dirX,
206
230
  dirZ: this.dirZ,
207
231
  startLevel: Math.min(this.recentSpeed, 2),
232
+ // How much of the authored skid this stop has earned. Each profile
233
+ // was captured at ITS gear's full speed (run = level 1, sprint =
234
+ // level 2), so compare like with like. Entering slower — or moments
235
+ // after a direction change, when the peak-hold still remembers the
236
+ // PREVIOUS heading's sprint — must not replay the full slide.
237
+ // Floored so a stop always still reads as a stop.
238
+ scale: Math.max(0.3, Math.min(1, this.speedLevel / (best.gear === "sprint" ? 2 : 1))),
208
239
  fromIdleW: Math.max(0, 1 - this.speedLevel),
209
240
  fromClip,
210
241
  fromTime: this.gaitPhase * this.clipDuration(fromClip),
@@ -281,12 +312,24 @@ export class LocomotionController {
281
312
  const maxStep = this.speedResponse * dt;
282
313
  const d = target - this.speedLevel;
283
314
  this.speedLevel += Math.abs(d) <= maxStep ? d : Math.sign(d) * maxStep;
315
+ // Breakout throttle: while an interrupted skid's ghost still owns part of
316
+ // the pose (planted feet), root motion and the gait clock scale by what's
317
+ // LEFT of it — she accelerates exactly as the skid dissolves, so the feet
318
+ // never slide (滑步) through the handoff.
319
+ let ghostW = 0;
320
+ if (this.exitGhost) {
321
+ this.exitGhost.elapsed += dt;
322
+ ghostW = Math.max(0, 1 - this.exitGhost.elapsed / GHOST_FADE);
323
+ if (ghostW <= 0)
324
+ this.exitGhost = null;
325
+ }
284
326
  // Root motion along the travel direction. In-place clips carry no horizontal root.
285
327
  const speed = (this.speedLevel <= 1
286
328
  ? this.runSpeed * this.speedLevel
287
329
  : this.runSpeed + (this.sprintSpeed - this.runSpeed) * (this.speedLevel - 1)) *
288
330
  align *
289
- speedScale;
331
+ speedScale *
332
+ (1 - ghostW);
290
333
  this.position.x += this.dirX * speed * dt;
291
334
  this.position.z += this.dirZ * speed * dt;
292
335
  // Clocks: idle free-runs; run/sprint share one normalized gait phase so a
@@ -296,7 +339,7 @@ export class LocomotionController {
296
339
  const sprintDur = hasSprint ? this.clipDuration(this.clips.sprint) : runDur;
297
340
  this.idleTime = (this.idleTime + dt) % idleDur;
298
341
  const gaitDur = this.speedLevel <= 1 ? runDur : runDur + (sprintDur - runDur) * (this.speedLevel - 1);
299
- this.gaitPhase = (this.gaitPhase + dt / gaitDur) % 1;
342
+ this.gaitPhase = (this.gaitPhase + (dt * (1 - ghostW)) / gaitDur) % 1;
300
343
  // Weights along the 1D speed axis.
301
344
  let wIdle, wRun, wSprint;
302
345
  if (this.speedLevel <= 1) {
@@ -315,23 +358,14 @@ export class LocomotionController {
315
358
  this.entries[1].weight = wRun;
316
359
  this.entries[2].time = this.gaitPhase * sprintDur;
317
360
  this.entries[2].weight = wSprint;
318
- const GHOST_FADE = 0.25;
319
- if (this.exitGhost) {
361
+ if (this.exitGhost !== null && ghostW > 0) {
320
362
  const g = this.exitGhost;
321
- g.elapsed += dt;
322
- const wGhost = Math.max(0, 1 - g.elapsed / GHOST_FADE);
323
- if (wGhost <= 0) {
324
- this.exitGhost = null;
325
- this.entries[3].weight = 0;
326
- }
327
- else {
328
- this.entries[0].weight *= 1 - wGhost;
329
- this.entries[1].weight *= 1 - wGhost;
330
- this.entries[2].weight *= 1 - wGhost;
331
- this.entries[3].name = g.clip;
332
- this.entries[3].time = g.clipTime + g.elapsed; // the clip's real tail keeps playing
333
- this.entries[3].weight = wGhost;
334
- }
363
+ this.entries[0].weight *= 1 - ghostW;
364
+ this.entries[1].weight *= 1 - ghostW;
365
+ this.entries[2].weight *= 1 - ghostW;
366
+ this.entries[3].name = g.clip;
367
+ this.entries[3].time = g.clipTime + g.elapsed; // the clip's real tail keeps playing
368
+ this.entries[3].weight = ghostW;
335
369
  }
336
370
  else {
337
371
  this.entries[3].weight = 0;
@@ -507,7 +541,7 @@ export class LocomotionController {
507
541
  }
508
542
  t.time += dt * this.stopTimeScale;
509
543
  const clipT = Math.min(t.time, entry.exitTime);
510
- const fwd = LocomotionController.profileAt(entry.forward, clipT / entry.exitTime);
544
+ const fwd = LocomotionController.profileAt(entry.forward, clipT / entry.exitTime) * t.scale;
511
545
  this.position.x = t.startX + t.dirX * fwd;
512
546
  this.position.z = t.startZ + t.dirZ * fwd;
513
547
  this.speedLevel = t.startLevel * Math.max(0, 1 - t.time / entry.exitTime);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.32.0",
3
+ "version": "0.32.2",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/locomotion.ts CHANGED
@@ -102,6 +102,10 @@ export interface LocomotionOptions {
102
102
  turnTimeScale?: number
103
103
  /** Playback rate for stop clips (default 1.25) — same reasoning. */
104
104
  stopTimeScale?: number
105
+ /** Seconds a heading must be held before a release earns an authored stop
106
+ * (default 0.5). Below it, weaving direction changes blend to idle instead
107
+ * of skidding metres along the last-held heading. */
108
+ stopCommitTime?: number
105
109
  /** Tank-mode steering rate, radians/second (default 2.5 ≈ 143°/s). */
106
110
  steerRate?: number
107
111
  /** Backpedal speed as a fraction of run speed in tank mode (default 0.5). */
@@ -126,6 +130,9 @@ export interface LocomotionPose {
126
130
 
127
131
  const TWO_PI = Math.PI * 2
128
132
 
133
+ /** Seconds an interrupted stop's ghost pose takes to dissolve over resumed locomotion. */
134
+ const GHOST_FADE = 0.25
135
+
129
136
  function wrapAngle(a: number): number {
130
137
  while (a > Math.PI) a -= TWO_PI
131
138
  while (a < -Math.PI) a += TWO_PI
@@ -145,6 +152,7 @@ export class LocomotionController {
145
152
  private readonly turnClipMinAngle: number
146
153
  private readonly turnTimeScale: number
147
154
  private readonly stopTimeScale: number
155
+ private readonly stopCommitTime: number
148
156
  private turning: { entry: TurnClipEntry; time: number } | null = null
149
157
  private runTurning: {
150
158
  entry: RunTurnClipEntry
@@ -157,6 +165,9 @@ export class LocomotionController {
157
165
  /** An interrupted authored clip fading out OVER resumed locomotion, so breaking
158
166
  * out of a stop is instantly responsive without a pose pop. */
159
167
  private exitGhost: { clip: string; clipTime: number; elapsed: number } | null = null
168
+ private headingHold = 0
169
+ private headingDirX = 0
170
+ private headingDirY = 0
160
171
  private stopping: {
161
172
  entry: StopClipEntry
162
173
  time: number
@@ -165,6 +176,8 @@ export class LocomotionController {
165
176
  dirX: number
166
177
  dirZ: number
167
178
  startLevel: number
179
+ /** Fraction of the authored deceleration distance this stop travels. */
180
+ scale: number
168
181
  /** Idle's share of the blend at release (level < 1 shows part idle) — the
169
182
  * crossfade source keeps this exact mix or the first stop frame snaps. */
170
183
  fromIdleW: number
@@ -223,6 +236,7 @@ export class LocomotionController {
223
236
  this.turnClipMinAngle = options?.turnClipMinAngle ?? (100 * Math.PI) / 180
224
237
  this.turnTimeScale = options?.turnTimeScale ?? 1.4
225
238
  this.stopTimeScale = options?.stopTimeScale ?? 1.25
239
+ this.stopCommitTime = options?.stopCommitTime ?? 0.5
226
240
  this.turnEntries = [
227
241
  { name: clips.idle, time: 0, weight: 0 },
228
242
  { name: clips.idle, time: 0, weight: 1 },
@@ -350,12 +364,32 @@ export class LocomotionController {
350
364
  const m = Math.hypot(this.inputX, this.inputY)
351
365
  moving = m > 0.05
352
366
  this.recentSpeed = Math.max(this.speedLevel, this.recentSpeed - dt * 1.5)
367
+ // How long the CURRENT heading has been held. An authored skid is the
368
+ // ending of a committed run; weaving through directions (D, then S+D,
369
+ // then S) rebuilds throttle in each new heading but earns no such
370
+ // ending — playing one there slides her metres along whichever way she
371
+ // happened to face last, which reads as drift.
372
+ if (moving) {
373
+ const dx = this.inputX / m
374
+ const dy = this.inputY / m
375
+ this.headingHold =
376
+ this.headingDirX * dx + this.headingDirY * dy > 0.94 ? this.headingHold + dt : 0
377
+ this.headingDirX = dx
378
+ this.headingDirY = dy
379
+ }
353
380
  // Release at speed: play the authored stop instead of blending to idle.
354
381
  const stops = this.clips.stop
355
382
  // Gate on the peak-hold speed: the release decay (throttle/analog) drains the
356
383
  // instantaneous level below any threshold before `moving` goes false. The
357
384
  // small floor on the actual level keeps standstill taps from faking a skid.
358
- if (!moving && stops && stops.length > 0 && this.recentSpeed >= 0.7 && this.speedLevel >= 0.35) {
385
+ if (
386
+ !moving &&
387
+ stops &&
388
+ stops.length > 0 &&
389
+ this.recentSpeed >= 0.7 &&
390
+ this.speedLevel >= 0.35 &&
391
+ this.headingHold >= this.stopCommitTime
392
+ ) {
359
393
  const gear = this.recentSpeed > 1.4 ? "sprint" : "run"
360
394
  const foot = this.gaitPhase < 0.5 ? "L" : "R"
361
395
  // Gear must match exactly: supplying stops for only one gear means the
@@ -380,6 +414,13 @@ export class LocomotionController {
380
414
  dirX: this.dirX,
381
415
  dirZ: this.dirZ,
382
416
  startLevel: Math.min(this.recentSpeed, 2),
417
+ // How much of the authored skid this stop has earned. Each profile
418
+ // was captured at ITS gear's full speed (run = level 1, sprint =
419
+ // level 2), so compare like with like. Entering slower — or moments
420
+ // after a direction change, when the peak-hold still remembers the
421
+ // PREVIOUS heading's sprint — must not replay the full slide.
422
+ // Floored so a stop always still reads as a stop.
423
+ scale: Math.max(0.3, Math.min(1, this.speedLevel / (best.gear === "sprint" ? 2 : 1))),
383
424
  fromIdleW: Math.max(0, 1 - this.speedLevel),
384
425
  fromClip,
385
426
  fromTime: this.gaitPhase * this.clipDuration(fromClip),
@@ -457,13 +498,25 @@ export class LocomotionController {
457
498
  const d = target - this.speedLevel
458
499
  this.speedLevel += Math.abs(d) <= maxStep ? d : Math.sign(d) * maxStep
459
500
 
501
+ // Breakout throttle: while an interrupted skid's ghost still owns part of
502
+ // the pose (planted feet), root motion and the gait clock scale by what's
503
+ // LEFT of it — she accelerates exactly as the skid dissolves, so the feet
504
+ // never slide (滑步) through the handoff.
505
+ let ghostW = 0
506
+ if (this.exitGhost) {
507
+ this.exitGhost.elapsed += dt
508
+ ghostW = Math.max(0, 1 - this.exitGhost.elapsed / GHOST_FADE)
509
+ if (ghostW <= 0) this.exitGhost = null
510
+ }
511
+
460
512
  // Root motion along the travel direction. In-place clips carry no horizontal root.
461
513
  const speed =
462
514
  (this.speedLevel <= 1
463
515
  ? this.runSpeed * this.speedLevel
464
516
  : this.runSpeed + (this.sprintSpeed - this.runSpeed) * (this.speedLevel - 1)) *
465
517
  align *
466
- speedScale
518
+ speedScale *
519
+ (1 - ghostW)
467
520
  this.position.x += this.dirX * speed * dt
468
521
  this.position.z += this.dirZ * speed * dt
469
522
 
@@ -474,7 +527,7 @@ export class LocomotionController {
474
527
  const sprintDur = hasSprint ? this.clipDuration(this.clips.sprint!) : runDur
475
528
  this.idleTime = (this.idleTime + dt) % idleDur
476
529
  const gaitDur = this.speedLevel <= 1 ? runDur : runDur + (sprintDur - runDur) * (this.speedLevel - 1)
477
- this.gaitPhase = (this.gaitPhase + dt / gaitDur) % 1
530
+ this.gaitPhase = (this.gaitPhase + (dt * (1 - ghostW)) / gaitDur) % 1
478
531
 
479
532
  // Weights along the 1D speed axis.
480
533
  let wIdle: number, wRun: number, wSprint: number
@@ -494,22 +547,14 @@ export class LocomotionController {
494
547
  this.entries[1].weight = wRun
495
548
  this.entries[2].time = this.gaitPhase * sprintDur
496
549
  this.entries[2].weight = wSprint
497
- const GHOST_FADE = 0.25
498
- if (this.exitGhost) {
550
+ if (this.exitGhost !== null && ghostW > 0) {
499
551
  const g = this.exitGhost
500
- g.elapsed += dt
501
- const wGhost = Math.max(0, 1 - g.elapsed / GHOST_FADE)
502
- if (wGhost <= 0) {
503
- this.exitGhost = null
504
- this.entries[3].weight = 0
505
- } else {
506
- this.entries[0].weight *= 1 - wGhost
507
- this.entries[1].weight *= 1 - wGhost
508
- this.entries[2].weight *= 1 - wGhost
509
- this.entries[3].name = g.clip
510
- this.entries[3].time = g.clipTime + g.elapsed // the clip's real tail keeps playing
511
- this.entries[3].weight = wGhost
512
- }
552
+ this.entries[0].weight *= 1 - ghostW
553
+ this.entries[1].weight *= 1 - ghostW
554
+ this.entries[2].weight *= 1 - ghostW
555
+ this.entries[3].name = g.clip
556
+ this.entries[3].time = g.clipTime + g.elapsed // the clip's real tail keeps playing
557
+ this.entries[3].weight = ghostW
513
558
  } else {
514
559
  this.entries[3].weight = 0
515
560
  }
@@ -700,7 +745,7 @@ export class LocomotionController {
700
745
 
701
746
  t.time += dt * this.stopTimeScale
702
747
  const clipT = Math.min(t.time, entry.exitTime)
703
- const fwd = LocomotionController.profileAt(entry.forward, clipT / entry.exitTime)
748
+ const fwd = LocomotionController.profileAt(entry.forward, clipT / entry.exitTime) * t.scale
704
749
  this.position.x = t.startX + t.dirX * fwd
705
750
  this.position.z = t.startZ + t.dirZ * fwd
706
751