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 +3 -1
- package/dist/locomotion.d.ts +8 -0
- package/dist/locomotion.d.ts.map +1 -1
- package/dist/locomotion.js +54 -20
- package/package.json +1 -1
- package/src/locomotion.ts +64 -19
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
|
package/dist/locomotion.d.ts
CHANGED
|
@@ -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;
|
package/dist/locomotion.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,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;
|
|
1
|
+
{"version":3,"file":"locomotion.d.ts","sourceRoot":"","sources":["../src/locomotion.ts"],"names":[],"mappings":"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"}
|
package/dist/locomotion.js
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
319
|
-
if (this.exitGhost) {
|
|
361
|
+
if (this.exitGhost !== null && ghostW > 0) {
|
|
320
362
|
const g = this.exitGhost;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
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 (
|
|
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
|
-
|
|
498
|
-
if (this.exitGhost) {
|
|
550
|
+
if (this.exitGhost !== null && ghostW > 0) {
|
|
499
551
|
const g = this.exitGhost
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|