reze-engine 0.32.1 → 0.33.0
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 +1 -3
- package/dist/engine.d.ts +1 -11
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -28
- package/dist/locomotion.d.ts +8 -0
- package/dist/locomotion.d.ts.map +1 -1
- package/dist/locomotion.js +31 -2
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +9 -2
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +29 -11
- package/package.json +1 -1
- package/src/engine.ts +3 -31
- package/src/locomotion.ts +40 -2
- package/src/physics/world.ts +9 -2
- package/src/pmx-loader.ts +28 -12
- package/dist/gpu-profile.d.ts +0 -19
- package/dist/gpu-profile.d.ts.map +0 -1
- package/dist/gpu-profile.js +0 -120
- package/dist/physics/physics.worker.d.ts +0 -2
- package/dist/physics/physics.worker.d.ts.map +0 -1
- package/dist/physics/physics.worker.js +0 -52
- package/dist/physics/profile.d.ts +0 -18
- package/dist/physics/profile.d.ts.map +0 -1
- package/dist/physics/profile.js +0 -44
- package/dist/physics/worker-physics.d.ts +0 -28
- package/dist/physics/worker-physics.d.ts.map +0 -1
- package/dist/physics/worker-physics.js +0 -91
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/src/physics/physics.worker.ts +0 -66
- package/src/physics/worker-physics.ts +0 -126
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). */
|
|
@@ -148,6 +152,7 @@ export class LocomotionController {
|
|
|
148
152
|
private readonly turnClipMinAngle: number
|
|
149
153
|
private readonly turnTimeScale: number
|
|
150
154
|
private readonly stopTimeScale: number
|
|
155
|
+
private readonly stopCommitTime: number
|
|
151
156
|
private turning: { entry: TurnClipEntry; time: number } | null = null
|
|
152
157
|
private runTurning: {
|
|
153
158
|
entry: RunTurnClipEntry
|
|
@@ -160,6 +165,9 @@ export class LocomotionController {
|
|
|
160
165
|
/** An interrupted authored clip fading out OVER resumed locomotion, so breaking
|
|
161
166
|
* out of a stop is instantly responsive without a pose pop. */
|
|
162
167
|
private exitGhost: { clip: string; clipTime: number; elapsed: number } | null = null
|
|
168
|
+
private headingHold = 0
|
|
169
|
+
private headingDirX = 0
|
|
170
|
+
private headingDirY = 0
|
|
163
171
|
private stopping: {
|
|
164
172
|
entry: StopClipEntry
|
|
165
173
|
time: number
|
|
@@ -168,6 +176,8 @@ export class LocomotionController {
|
|
|
168
176
|
dirX: number
|
|
169
177
|
dirZ: number
|
|
170
178
|
startLevel: number
|
|
179
|
+
/** Fraction of the authored deceleration distance this stop travels. */
|
|
180
|
+
scale: number
|
|
171
181
|
/** Idle's share of the blend at release (level < 1 shows part idle) — the
|
|
172
182
|
* crossfade source keeps this exact mix or the first stop frame snaps. */
|
|
173
183
|
fromIdleW: number
|
|
@@ -226,6 +236,7 @@ export class LocomotionController {
|
|
|
226
236
|
this.turnClipMinAngle = options?.turnClipMinAngle ?? (100 * Math.PI) / 180
|
|
227
237
|
this.turnTimeScale = options?.turnTimeScale ?? 1.4
|
|
228
238
|
this.stopTimeScale = options?.stopTimeScale ?? 1.25
|
|
239
|
+
this.stopCommitTime = options?.stopCommitTime ?? 0.5
|
|
229
240
|
this.turnEntries = [
|
|
230
241
|
{ name: clips.idle, time: 0, weight: 0 },
|
|
231
242
|
{ name: clips.idle, time: 0, weight: 1 },
|
|
@@ -353,12 +364,32 @@ export class LocomotionController {
|
|
|
353
364
|
const m = Math.hypot(this.inputX, this.inputY)
|
|
354
365
|
moving = m > 0.05
|
|
355
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
|
+
}
|
|
356
380
|
// Release at speed: play the authored stop instead of blending to idle.
|
|
357
381
|
const stops = this.clips.stop
|
|
358
382
|
// Gate on the peak-hold speed: the release decay (throttle/analog) drains the
|
|
359
383
|
// instantaneous level below any threshold before `moving` goes false. The
|
|
360
384
|
// small floor on the actual level keeps standstill taps from faking a skid.
|
|
361
|
-
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
|
+
) {
|
|
362
393
|
const gear = this.recentSpeed > 1.4 ? "sprint" : "run"
|
|
363
394
|
const foot = this.gaitPhase < 0.5 ? "L" : "R"
|
|
364
395
|
// Gear must match exactly: supplying stops for only one gear means the
|
|
@@ -383,6 +414,13 @@ export class LocomotionController {
|
|
|
383
414
|
dirX: this.dirX,
|
|
384
415
|
dirZ: this.dirZ,
|
|
385
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))),
|
|
386
424
|
fromIdleW: Math.max(0, 1 - this.speedLevel),
|
|
387
425
|
fromClip,
|
|
388
426
|
fromTime: this.gaitPhase * this.clipDuration(fromClip),
|
|
@@ -707,7 +745,7 @@ export class LocomotionController {
|
|
|
707
745
|
|
|
708
746
|
t.time += dt * this.stopTimeScale
|
|
709
747
|
const clipT = Math.min(t.time, entry.exitTime)
|
|
710
|
-
const fwd = LocomotionController.profileAt(entry.forward, clipT / entry.exitTime)
|
|
748
|
+
const fwd = LocomotionController.profileAt(entry.forward, clipT / entry.exitTime) * t.scale
|
|
711
749
|
this.position.x = t.startX + t.dirX * fwd
|
|
712
750
|
this.position.z = t.startZ + t.dirZ * fwd
|
|
713
751
|
|
package/src/physics/world.ts
CHANGED
|
@@ -93,8 +93,15 @@ export class World {
|
|
|
93
93
|
// it because it doesn't go through the velocity channel. Inverse-mass
|
|
94
94
|
// weighted so a kinematic body stays put and only the dynamic one
|
|
95
95
|
// translates.
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Softer and later than a rigid-body engine would use, deliberately. MMD
|
|
97
|
+
// cloth is a dense chain of small bodies resting on the character, and
|
|
98
|
+
// aggressive penetration recovery feeds those contacts energy that the
|
|
99
|
+
// joint springs hand straight back — the tips never settle. Measured on a
|
|
100
|
+
// 688-body model: resting peak velocity 1.79 → 1.10, and 12.9 → 6.6 under
|
|
101
|
+
// an idle animation, for a slop of two hundredths of a unit (under 2 mm at
|
|
102
|
+
// MMD scale) that no eye finds.
|
|
103
|
+
const POS_CORRECTION_FACTOR = 0.15
|
|
104
|
+
const POS_SLOP = 0.02
|
|
98
105
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
99
106
|
const c = contacts.get(ci)
|
|
100
107
|
if (c.depth <= POS_SLOP) continue
|
package/src/pmx-loader.ts
CHANGED
|
@@ -84,10 +84,19 @@ export class PmxLoader {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
private parseHeader() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
87
|
+
// The signature is four bytes. PMX 2.x writes "PMX ", but exporters in the
|
|
88
|
+
// wild also write "Pmx " — and a strict uppercase compare rejected those
|
|
89
|
+
// models outright. Compare case-insensitively and name what was actually
|
|
90
|
+
// found when it is something else, so a PMD (the older format, signature
|
|
91
|
+
// "Pmd") says so instead of "not a PMX file".
|
|
92
|
+
const signature = this.getString(4)
|
|
93
|
+
if (signature.slice(0, 3).toUpperCase() !== "PMX") {
|
|
94
|
+
const head = signature.slice(0, 3).toUpperCase()
|
|
95
|
+
if (head === "PMD") {
|
|
96
|
+
throw new Error("This is a PMD file (MMD's older format). Convert it to PMX in PMXEditor first.")
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Not a PMX file (signature "${signature.replace(/[^\x20-\x7e]/g, "?")}")`)
|
|
99
|
+
}
|
|
91
100
|
|
|
92
101
|
// PMX: version (float32)
|
|
93
102
|
const version = this.getFloat32()
|
|
@@ -1128,7 +1137,9 @@ export class PmxLoader {
|
|
|
1128
1137
|
}
|
|
1129
1138
|
|
|
1130
1139
|
private getString(len: number) {
|
|
1131
|
-
|
|
1140
|
+
// byteOffset matters: a DataView built over a slice of a larger buffer has
|
|
1141
|
+
// its own origin, and reading the raw buffer without it lands elsewhere.
|
|
1142
|
+
const bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len)
|
|
1132
1143
|
this.offset += len
|
|
1133
1144
|
return String.fromCharCode(...bytes)
|
|
1134
1145
|
}
|
|
@@ -1137,18 +1148,23 @@ export class PmxLoader {
|
|
|
1137
1148
|
const len = this.getInt32()
|
|
1138
1149
|
if (len <= 0) return ""
|
|
1139
1150
|
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1151
|
+
// No arbitrary length ceiling. A model's comment field carries credits and
|
|
1152
|
+
// terms of use and routinely runs past a thousand bytes — roughly five
|
|
1153
|
+
// hundred characters in UTF-16 — so a "this looks too long" heuristic
|
|
1154
|
+
// rejected perfectly ordinary models from several distributors. The bounds
|
|
1155
|
+
// check below is the real guard: a length that would read past the end of
|
|
1156
|
+
// the file is corrupt, and any length that fits is the author's business.
|
|
1145
1157
|
// Ensure we don't read beyond buffer bounds
|
|
1146
|
-
if (this.offset + len > this.view.
|
|
1158
|
+
if (this.offset + len > this.view.byteLength) {
|
|
1147
1159
|
throw new RangeError(`String length ${len} exceeds buffer bounds at offset ${this.offset - 4}`)
|
|
1148
1160
|
}
|
|
1149
1161
|
|
|
1150
|
-
|
|
1162
|
+
let bytes = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len)
|
|
1151
1163
|
this.offset += len
|
|
1164
|
+
// UTF-16 needs whole code units. A stray odd length (seen from a few
|
|
1165
|
+
// exporters) made TextDecoder emit a replacement char and, worse, shifted
|
|
1166
|
+
// nothing — better to drop the orphan byte than to carry it into the name.
|
|
1167
|
+
if (this.encoding === 0 && len % 2 === 1) bytes = bytes.subarray(0, len - 1)
|
|
1152
1168
|
return this.decoder.decode(bytes)
|
|
1153
1169
|
}
|
|
1154
1170
|
|
package/dist/gpu-profile.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export declare class GpuTimestamp {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
readonly supported: boolean;
|
|
4
|
-
private device;
|
|
5
|
-
private querySet;
|
|
6
|
-
private resolveBuffer;
|
|
7
|
-
private readbackBuffer;
|
|
8
|
-
private nextSlot;
|
|
9
|
-
private frameLabels;
|
|
10
|
-
private inFlight;
|
|
11
|
-
private pendingLabels;
|
|
12
|
-
constructor(device: GPUDevice);
|
|
13
|
-
private ensureCreated;
|
|
14
|
-
beginFrame(): void;
|
|
15
|
-
wrap(desc: GPURenderPassDescriptor, label: string): GPURenderPassDescriptor;
|
|
16
|
-
endFrame(encoder: GPUCommandEncoder): void;
|
|
17
|
-
afterSubmit(): void;
|
|
18
|
-
}
|
|
19
|
-
//# sourceMappingURL=gpu-profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gpu-profile.d.ts","sourceRoot":"","sources":["../src/gpu-profile.ts"],"names":[],"mappings":"AAoBA,qBAAa,YAAY;IACvB,OAAO,UAAQ;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAQ;IAKxB,OAAO,CAAC,aAAa,CAAwB;gBAEjC,MAAM,EAAE,SAAS;IAK7B,OAAO,CAAC,aAAa;IAiBrB,UAAU,IAAI,IAAI;IAWlB,IAAI,CAAC,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAqB3E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAW1C,WAAW,IAAI,IAAI;CAwBpB"}
|
package/dist/gpu-profile.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// WebGPU timestamp-query profiling. Records pass durations into the same
|
|
2
|
-
// profiler module as CPU phases (so the snapshot table mixes both with a
|
|
3
|
-
// "gpu." prefix). Read-only — no effect on rendering output.
|
|
4
|
-
//
|
|
5
|
-
// Pattern (per spec):
|
|
6
|
-
// 1. Each render pass declares timestampWrites { begin, end } slot indices.
|
|
7
|
-
// 2. After all passes, encoder.resolveQuerySet writes uint64 ns timestamps
|
|
8
|
-
// into a QUERY_RESOLVE buffer.
|
|
9
|
-
// 3. We copy the resolve buffer into a MAP_READ readback buffer.
|
|
10
|
-
// 4. Submit. After the GPU finishes, mapAsync resolves; we read durations
|
|
11
|
-
// and record them.
|
|
12
|
-
//
|
|
13
|
-
// One readback buffer with an in-flight gate: if mapAsync hasn't resolved
|
|
14
|
-
// from a previous frame, we skip the copy this frame (no contention, just
|
|
15
|
-
// one frame of dropped sampling).
|
|
16
|
-
import { profiler } from "./physics/profile";
|
|
17
|
-
const MAX_PASSES_PER_FRAME = 64;
|
|
18
|
-
export class GpuTimestamp {
|
|
19
|
-
constructor(device) {
|
|
20
|
-
this.enabled = false;
|
|
21
|
-
this.querySet = null;
|
|
22
|
-
this.resolveBuffer = null;
|
|
23
|
-
this.readbackBuffer = null;
|
|
24
|
-
this.nextSlot = 0;
|
|
25
|
-
this.frameLabels = [];
|
|
26
|
-
this.inFlight = false;
|
|
27
|
-
// Set inside endFrame() when a copy was queued this frame; consumed by
|
|
28
|
-
// afterSubmit() to kick mapAsync. mapAsync must run AFTER queue.submit —
|
|
29
|
-
// calling it before submit puts the buffer into "mapping pending" state,
|
|
30
|
-
// which makes the copy in the encoder illegal.
|
|
31
|
-
this.pendingLabels = null;
|
|
32
|
-
this.device = device;
|
|
33
|
-
this.supported = device.features.has("timestamp-query");
|
|
34
|
-
}
|
|
35
|
-
ensureCreated() {
|
|
36
|
-
if (this.querySet || !this.supported)
|
|
37
|
-
return;
|
|
38
|
-
const slots = MAX_PASSES_PER_FRAME * 2;
|
|
39
|
-
this.querySet = this.device.createQuerySet({ type: "timestamp", count: slots });
|
|
40
|
-
this.resolveBuffer = this.device.createBuffer({
|
|
41
|
-
label: "gpu-timestamp-resolve",
|
|
42
|
-
size: slots * 8,
|
|
43
|
-
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
44
|
-
});
|
|
45
|
-
this.readbackBuffer = this.device.createBuffer({
|
|
46
|
-
label: "gpu-timestamp-readback",
|
|
47
|
-
size: slots * 8,
|
|
48
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
// Called once at the top of each render() before any pass wrap.
|
|
52
|
-
beginFrame() {
|
|
53
|
-
if (!this.enabled || !this.supported)
|
|
54
|
-
return;
|
|
55
|
-
this.ensureCreated();
|
|
56
|
-
this.nextSlot = 0;
|
|
57
|
-
this.frameLabels = [];
|
|
58
|
-
}
|
|
59
|
-
// Wrap a render-pass descriptor with timestampWrites for `label`. If
|
|
60
|
-
// profiling is off / unsupported / out of slots, returns desc unchanged.
|
|
61
|
-
// The same label called multiple times (e.g. each bloom mip) is fine —
|
|
62
|
-
// the profiler accumulates by label.
|
|
63
|
-
wrap(desc, label) {
|
|
64
|
-
if (!this.enabled || !this.querySet)
|
|
65
|
-
return desc;
|
|
66
|
-
if (this.nextSlot + 2 > MAX_PASSES_PER_FRAME * 2)
|
|
67
|
-
return desc;
|
|
68
|
-
const begin = this.nextSlot;
|
|
69
|
-
const end = this.nextSlot + 1;
|
|
70
|
-
this.nextSlot += 2;
|
|
71
|
-
this.frameLabels.push(label);
|
|
72
|
-
return {
|
|
73
|
-
...desc,
|
|
74
|
-
timestampWrites: {
|
|
75
|
-
querySet: this.querySet,
|
|
76
|
-
beginningOfPassWriteIndex: begin,
|
|
77
|
-
endOfPassWriteIndex: end,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// Call on the encoder after all passes, BEFORE encoder.finish(). Adds
|
|
82
|
-
// resolveQuerySet + copyBufferToBuffer commands. mapAsync is deferred to
|
|
83
|
-
// afterSubmit() — calling it before submit transitions the readback buffer
|
|
84
|
-
// into "mapping pending" state, which makes the queued copy illegal.
|
|
85
|
-
endFrame(encoder) {
|
|
86
|
-
if (!this.enabled || !this.querySet || this.frameLabels.length === 0)
|
|
87
|
-
return;
|
|
88
|
-
const count = this.frameLabels.length * 2;
|
|
89
|
-
encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
|
|
90
|
-
if (this.inFlight)
|
|
91
|
-
return;
|
|
92
|
-
encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, count * 8);
|
|
93
|
-
this.pendingLabels = this.frameLabels.slice();
|
|
94
|
-
}
|
|
95
|
-
// Call AFTER device.queue.submit(). Kicks mapAsync if a copy was queued
|
|
96
|
-
// this frame; the .then() reads durations and unmaps.
|
|
97
|
-
afterSubmit() {
|
|
98
|
-
const labels = this.pendingLabels;
|
|
99
|
-
if (!labels)
|
|
100
|
-
return;
|
|
101
|
-
this.pendingLabels = null;
|
|
102
|
-
this.inFlight = true;
|
|
103
|
-
const readback = this.readbackBuffer;
|
|
104
|
-
void readback.mapAsync(GPUMapMode.READ).then(() => {
|
|
105
|
-
const arr = new BigInt64Array(readback.getMappedRange());
|
|
106
|
-
for (let i = 0; i < labels.length; i++) {
|
|
107
|
-
const start = arr[i * 2];
|
|
108
|
-
const end = arr[i * 2 + 1];
|
|
109
|
-
// Per spec, timestamps are nanoseconds.
|
|
110
|
-
const ms = Number(end - start) / 1e6;
|
|
111
|
-
if (ms >= 0 && ms < 1000)
|
|
112
|
-
profiler.record("gpu." + labels[i], ms);
|
|
113
|
-
}
|
|
114
|
-
readback.unmap();
|
|
115
|
-
this.inFlight = false;
|
|
116
|
-
}, () => {
|
|
117
|
-
this.inFlight = false;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"physics.worker.d.ts","sourceRoot":"","sources":["../../src/physics/physics.worker.ts"],"names":[],"mappings":""}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Physics worker: owns ONE model's RezePhysics world. The host posts the
|
|
2
|
-
// frame's bone world matrices as a transferable buffer; the worker steps the
|
|
3
|
-
// simulation (identical math to the main-thread path — same class, same code)
|
|
4
|
-
// and returns the buffer with the dynamic bones' matrices written back, plus
|
|
5
|
-
// its own step cost for the host's stats.
|
|
6
|
-
//
|
|
7
|
-
// The incoming buffer is COPIED into a persistent local pose (and back out)
|
|
8
|
-
// rather than wrapped: transferred ArrayBuffers get a fresh identity on every
|
|
9
|
-
// hop, so wrappers could never be cached against them — and two ~20KB copies
|
|
10
|
-
// per frame are microseconds.
|
|
11
|
-
import { RezePhysics } from "./physics";
|
|
12
|
-
import { Mat4 } from "../math";
|
|
13
|
-
import { RigidbodyType } from "./types";
|
|
14
|
-
const ctx = self;
|
|
15
|
-
let physics = null;
|
|
16
|
-
let inverseBind = new Float32Array(0);
|
|
17
|
-
let pose = new Float32Array(0);
|
|
18
|
-
let mats = [];
|
|
19
|
-
ctx.onmessage = (e) => {
|
|
20
|
-
const msg = e.data;
|
|
21
|
-
if (msg.cmd === "init") {
|
|
22
|
-
physics = new RezePhysics(msg.rigidbodies, msg.joints);
|
|
23
|
-
inverseBind = msg.inverseBind;
|
|
24
|
-
const boneCount = inverseBind.length / 16;
|
|
25
|
-
pose = new Float32Array(boneCount * 16);
|
|
26
|
-
mats = new Array(boneCount);
|
|
27
|
-
for (let i = 0; i < boneCount; i++)
|
|
28
|
-
mats[i] = new Mat4(pose.subarray(i * 16, i * 16 + 16));
|
|
29
|
-
// The host copies back only the bones physics can write — the bones of
|
|
30
|
-
// dynamic bodies. Kinematic bones must NOT round-trip: in the pipelined
|
|
31
|
-
// protocol they would drag a stale (frame-old) pose over the live one.
|
|
32
|
-
const dynamicBones = [];
|
|
33
|
-
for (const rb of msg.rigidbodies) {
|
|
34
|
-
if (rb.type === RigidbodyType.Dynamic && rb.mass > 0 && rb.boneIndex >= 0)
|
|
35
|
-
dynamicBones.push(rb.boneIndex);
|
|
36
|
-
}
|
|
37
|
-
ctx.postMessage({ cmd: "ready", dynamicBones });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (!physics)
|
|
41
|
-
return;
|
|
42
|
-
const incoming = new Float32Array(msg.bones);
|
|
43
|
-
pose.set(incoming);
|
|
44
|
-
const t0 = performance.now();
|
|
45
|
-
if (msg.cmd === "step")
|
|
46
|
-
physics.step(msg.dt, mats, inverseBind);
|
|
47
|
-
else
|
|
48
|
-
physics.reset(mats);
|
|
49
|
-
const stepMs = performance.now() - t0;
|
|
50
|
-
incoming.set(pose);
|
|
51
|
-
ctx.postMessage({ cmd: "stepped", bones: msg.bones, stepMs }, [msg.bones]);
|
|
52
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
declare class PhysicsProfiler {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
private sums;
|
|
4
|
-
private counts;
|
|
5
|
-
begin(): number;
|
|
6
|
-
end(label: string, t0: number): void;
|
|
7
|
-
record(label: string, ms: number): void;
|
|
8
|
-
snapshot(): Record<string, {
|
|
9
|
-
avgMs: number;
|
|
10
|
-
calls: number;
|
|
11
|
-
totalMs: number;
|
|
12
|
-
}>;
|
|
13
|
-
reset(): void;
|
|
14
|
-
}
|
|
15
|
-
export declare const profiler: PhysicsProfiler;
|
|
16
|
-
export type ProfileSnapshot = ReturnType<PhysicsProfiler["snapshot"]>;
|
|
17
|
-
export {};
|
|
18
|
-
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/physics/profile.ts"],"names":[],"mappings":"AAIA,cAAM,eAAe;IACnB,OAAO,UAAQ;IACf,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,MAAM,CAA8C;IAE5D,KAAK,IAAI,MAAM;IAIf,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAU7E,KAAK,IAAI,IAAI;CAId;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA;AAC7C,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA"}
|
package/dist/physics/profile.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Per-phase wall-clock accumulator. Off by default — when `enabled` is false,
|
|
2
|
-
// begin()/end() short-circuit so production has zero overhead. Toggle via
|
|
3
|
-
// `RezePhysics.setProfileEnabled(true)` and read with `getProfile()`.
|
|
4
|
-
class PhysicsProfiler {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.enabled = false;
|
|
7
|
-
this.sums = Object.create(null);
|
|
8
|
-
this.counts = Object.create(null);
|
|
9
|
-
}
|
|
10
|
-
begin() {
|
|
11
|
-
return this.enabled ? performance.now() : 0;
|
|
12
|
-
}
|
|
13
|
-
end(label, t0) {
|
|
14
|
-
if (!this.enabled)
|
|
15
|
-
return;
|
|
16
|
-
const dt = performance.now() - t0;
|
|
17
|
-
this.sums[label] = (this.sums[label] ?? 0) + dt;
|
|
18
|
-
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
19
|
-
}
|
|
20
|
-
// Direct accumulator for durations not measured via begin()/end() — used by
|
|
21
|
-
// GPU timestamp readback, which arrives async after the frame has ended.
|
|
22
|
-
record(label, ms) {
|
|
23
|
-
if (!this.enabled)
|
|
24
|
-
return;
|
|
25
|
-
this.sums[label] = (this.sums[label] ?? 0) + ms;
|
|
26
|
-
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
27
|
-
}
|
|
28
|
-
// Snapshot since last reset(). avgMs is per call; totalMs is the cumulative
|
|
29
|
-
// window cost — divide by elapsed wall time to get a "% of frame budget".
|
|
30
|
-
snapshot() {
|
|
31
|
-
const out = {};
|
|
32
|
-
for (const k in this.sums) {
|
|
33
|
-
const total = this.sums[k];
|
|
34
|
-
const calls = this.counts[k];
|
|
35
|
-
out[k] = { avgMs: calls > 0 ? total / calls : 0, calls, totalMs: total };
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
reset() {
|
|
40
|
-
this.sums = Object.create(null);
|
|
41
|
-
this.counts = Object.create(null);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export const profiler = new PhysicsProfiler();
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Rigidbody, Joint } from "./types";
|
|
2
|
-
import type { Mat4 } from "../math";
|
|
3
|
-
export declare class WorkerPhysics {
|
|
4
|
-
private readonly worker;
|
|
5
|
-
private dynamicBones;
|
|
6
|
-
private readonly boneCount;
|
|
7
|
-
/** Transfer buffer when idle; null while a step is in flight. */
|
|
8
|
-
private buf;
|
|
9
|
-
/** Latest completed pose from the worker (copied out of the transfer buffer). */
|
|
10
|
-
private readonly result;
|
|
11
|
-
private hasResult;
|
|
12
|
-
private pendingDt;
|
|
13
|
-
private queuedReset;
|
|
14
|
-
/** Worker-side cost of the last completed step — for engine stats. */
|
|
15
|
-
stepMs: number;
|
|
16
|
-
private constructor();
|
|
17
|
-
static supported(): boolean;
|
|
18
|
-
static create(rigidbodies: Rigidbody[], joints: Joint[], inverseBind: Float32Array): Promise<WorkerPhysics>;
|
|
19
|
-
/** Same signature as RezePhysics.step — the engine cannot tell them apart.
|
|
20
|
-
* (inverseBind was shipped to the worker at init; the param is unused.) */
|
|
21
|
-
step(dt: number, boneWorldMatrices: Mat4[], _inverseBind: Float32Array): void;
|
|
22
|
-
/** Reset rides the same pipeline: the next posted frame carries a reset
|
|
23
|
-
* command instead of a step, and stale results stop applying immediately. */
|
|
24
|
-
reset(boneWorldMatrices: Mat4[]): void;
|
|
25
|
-
dispose(): void;
|
|
26
|
-
private onStepped;
|
|
27
|
-
}
|
|
28
|
-
//# sourceMappingURL=worker-physics.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"worker-physics.d.ts","sourceRoot":"","sources":["../../src/physics/worker-physics.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAYnC,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,iEAAiE;IACjE,OAAO,CAAC,GAAG,CAAoB;IAC/B,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,WAAW,CAAQ;IAC3B,sEAAsE;IACtE,MAAM,SAAI;IAEV,OAAO;IAOP,MAAM,CAAC,SAAS,IAAI,OAAO;IAI3B,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IA6B3G;gFAC4E;IAC5E,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI;IAuB7E;kFAC8E;IAC9E,KAAK,CAAC,iBAAiB,EAAE,IAAI,EAAE,GAAG,IAAI;IAOtC,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,SAAS;CAQlB"}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
export class WorkerPhysics {
|
|
2
|
-
constructor(worker, boneCount) {
|
|
3
|
-
this.dynamicBones = [];
|
|
4
|
-
this.hasResult = false;
|
|
5
|
-
this.pendingDt = 0;
|
|
6
|
-
this.queuedReset = false;
|
|
7
|
-
/** Worker-side cost of the last completed step — for engine stats. */
|
|
8
|
-
this.stepMs = 0;
|
|
9
|
-
this.worker = worker;
|
|
10
|
-
this.boneCount = boneCount;
|
|
11
|
-
this.buf = new ArrayBuffer(boneCount * 64);
|
|
12
|
-
this.result = new Float32Array(boneCount * 16);
|
|
13
|
-
}
|
|
14
|
-
static supported() {
|
|
15
|
-
return typeof Worker !== "undefined";
|
|
16
|
-
}
|
|
17
|
-
static create(rigidbodies, joints, inverseBind) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
let worker;
|
|
20
|
-
try {
|
|
21
|
-
worker = new Worker(new URL("./physics.worker.js", import.meta.url), { type: "module" });
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const wp = new WorkerPhysics(worker, inverseBind.length / 16);
|
|
28
|
-
const fail = (message) => {
|
|
29
|
-
worker.terminate();
|
|
30
|
-
reject(new Error(message));
|
|
31
|
-
};
|
|
32
|
-
worker.onerror = (e) => fail(`physics worker failed to boot: ${e.message || "worker error"}`);
|
|
33
|
-
worker.onmessage = (ev) => {
|
|
34
|
-
if (ev.data?.cmd !== "ready")
|
|
35
|
-
return;
|
|
36
|
-
wp.dynamicBones = ev.data.dynamicBones;
|
|
37
|
-
worker.onmessage = (m) => wp.onStepped(m);
|
|
38
|
-
worker.onerror = null;
|
|
39
|
-
resolve(wp);
|
|
40
|
-
};
|
|
41
|
-
// Rigidbody/Joint carry only data (Vec3/Mat4 fields clone as plain
|
|
42
|
-
// objects with the same fields — the physics constructor reads fields,
|
|
43
|
-
// never methods), so structuredClone is a faithful serializer.
|
|
44
|
-
worker.postMessage({ cmd: "init", rigidbodies, joints, inverseBind: inverseBind.slice() });
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
/** Same signature as RezePhysics.step — the engine cannot tell them apart.
|
|
48
|
-
* (inverseBind was shipped to the worker at init; the param is unused.) */
|
|
49
|
-
step(dt, boneWorldMatrices, _inverseBind) {
|
|
50
|
-
this.pendingDt += dt;
|
|
51
|
-
// Apply the newest completed simulation onto this frame's pose. Dynamic
|
|
52
|
-
// bones only: kinematic bones must keep the LIVE animation pose.
|
|
53
|
-
if (this.hasResult) {
|
|
54
|
-
const r = this.result;
|
|
55
|
-
for (const bi of this.dynamicBones) {
|
|
56
|
-
boneWorldMatrices[bi].values.set(r.subarray(bi * 16, bi * 16 + 16));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (this.buf === null)
|
|
60
|
-
return; // worker mid-step: dt accumulated for the next post
|
|
61
|
-
const flat = new Float32Array(this.buf);
|
|
62
|
-
const n = Math.min(this.boneCount, boneWorldMatrices.length);
|
|
63
|
-
for (let i = 0; i < n; i++)
|
|
64
|
-
flat.set(boneWorldMatrices[i].values, i * 16);
|
|
65
|
-
this.worker.postMessage({ cmd: this.queuedReset ? "reset" : "step", dt: this.pendingDt, bones: this.buf }, [this.buf]);
|
|
66
|
-
this.buf = null;
|
|
67
|
-
this.pendingDt = 0;
|
|
68
|
-
this.queuedReset = false;
|
|
69
|
-
}
|
|
70
|
-
/** Reset rides the same pipeline: the next posted frame carries a reset
|
|
71
|
-
* command instead of a step, and stale results stop applying immediately. */
|
|
72
|
-
reset(boneWorldMatrices) {
|
|
73
|
-
this.queuedReset = true;
|
|
74
|
-
this.hasResult = false;
|
|
75
|
-
// Post right away if idle — reuse step's snapshot/post path with dt 0.
|
|
76
|
-
if (this.buf !== null)
|
|
77
|
-
this.step(0, boneWorldMatrices, undefined);
|
|
78
|
-
}
|
|
79
|
-
dispose() {
|
|
80
|
-
this.worker.terminate();
|
|
81
|
-
}
|
|
82
|
-
onStepped(ev) {
|
|
83
|
-
const d = ev.data;
|
|
84
|
-
if (d?.cmd !== "stepped")
|
|
85
|
-
return;
|
|
86
|
-
this.buf = d.bones;
|
|
87
|
-
this.stepMs = d.stepMs;
|
|
88
|
-
this.result.set(new Float32Array(this.buf));
|
|
89
|
-
this.hasResult = true;
|
|
90
|
-
}
|
|
91
|
-
}
|