reze-engine 0.31.2 → 0.32.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/dist/engine.d.ts +11 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +31 -2
- package/dist/physics/body.d.ts +3 -0
- package/dist/physics/body.d.ts.map +1 -1
- package/dist/physics/body.js +3 -0
- package/dist/physics/constraint.d.ts +0 -32
- package/dist/physics/constraint.d.ts.map +1 -1
- package/dist/physics/constraint.js +0 -32
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +84 -0
- package/dist/physics/physics.d.ts +1 -0
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +35 -3
- package/dist/physics/physics.worker.d.ts +2 -0
- package/dist/physics/physics.worker.d.ts.map +1 -0
- package/dist/physics/physics.worker.js +52 -0
- package/dist/physics/solver.d.ts +9 -1
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +248 -185
- package/dist/physics/worker-physics.d.ts +28 -0
- package/dist/physics/worker-physics.d.ts.map +1 -0
- package/dist/physics/worker-physics.js +91 -0
- package/dist/physics/world.d.ts +2 -1
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +2 -2
- package/package.json +1 -1
- package/src/engine.ts +33 -3
- package/src/physics/body.ts +4 -0
- package/src/physics/constraint.ts +2 -65
- package/src/physics/contact.ts +80 -0
- package/src/physics/physics.ts +36 -3
- package/src/physics/physics.worker.ts +66 -0
- package/src/physics/solver.ts +979 -905
- package/src/physics/worker-physics.ts +126 -0
- package/src/physics/world.ts +9 -3
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
package/dist/physics/world.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Vec3 } from "../math";
|
|
2
2
|
import type { RigidBodyStore } from "./body";
|
|
3
3
|
import type { SixDofSpringConstraint } from "./constraint";
|
|
4
|
+
import { type SolverCache } from "./solver";
|
|
4
5
|
import { type ContactPool } from "./contact";
|
|
5
6
|
export declare class World {
|
|
6
7
|
readonly gravity: Vec3;
|
|
@@ -10,6 +11,6 @@ export declare class World {
|
|
|
10
11
|
private angDampFactor;
|
|
11
12
|
constructor(gravity: Vec3);
|
|
12
13
|
setGravity(g: Vec3): void;
|
|
13
|
-
step(store: RigidBodyStore, constraints: SixDofSpringConstraint[], contacts: ContactPool, dt: number): void;
|
|
14
|
+
step(store: RigidBodyStore, constraints: SixDofSpringConstraint[], cache: SolverCache, contacts: ContactPool, dt: number): void;
|
|
14
15
|
}
|
|
15
16
|
//# sourceMappingURL=world.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/physics/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/physics/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC1D,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAO1D,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,gBAAgB,SAAK;IAIrB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,aAAa,CAA4B;gBAErC,OAAO,EAAE,IAAI;IAIzB,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI;IAMzB,IAAI,CACF,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,GACT,IAAI;CAoJR"}
|
package/dist/physics/world.js
CHANGED
|
@@ -22,7 +22,7 @@ export class World {
|
|
|
22
22
|
this.gravity.y = g.y;
|
|
23
23
|
this.gravity.z = g.z;
|
|
24
24
|
}
|
|
25
|
-
step(store, constraints, contacts, dt) {
|
|
25
|
+
step(store, constraints, cache, contacts, dt) {
|
|
26
26
|
if (dt <= 0)
|
|
27
27
|
return;
|
|
28
28
|
const N = store.count;
|
|
@@ -72,7 +72,7 @@ export class World {
|
|
|
72
72
|
findContacts(store, contacts);
|
|
73
73
|
// 3. Solve joint + contact constraints (velocity-only).
|
|
74
74
|
if (constraints.length > 0 || contacts.count > 0) {
|
|
75
|
-
solveConstraints(store, constraints, contacts, dt, this.solverIterations);
|
|
75
|
+
solveConstraints(store, constraints, cache, contacts, dt, this.solverIterations);
|
|
76
76
|
}
|
|
77
77
|
// 4. Position correction (split impulse). Direct translation along the
|
|
78
78
|
// contact normal — joint constraints in the same SI loop can't undo
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { VMDLoader } from "./vmd-loader"
|
|
|
7
7
|
import { CameraAnimation } from "./camera-animation"
|
|
8
8
|
import { PmxLoader } from "./pmx-loader"
|
|
9
9
|
import { RezePhysics } from "./physics"
|
|
10
|
+
import { WorkerPhysics } from "./physics/worker-physics"
|
|
10
11
|
import {
|
|
11
12
|
createFetchAssetReader,
|
|
12
13
|
createFileMapAssetReader,
|
|
@@ -346,6 +347,11 @@ export interface GizmoDragEvent {
|
|
|
346
347
|
export type GizmoDragCallback = (event: GizmoDragEvent) => void
|
|
347
348
|
|
|
348
349
|
export type EngineOptions = {
|
|
350
|
+
/** Run each model's physics on a dedicated Web Worker (default true where
|
|
351
|
+
* Workers exist). Pipelined one frame deep: main-thread physics cost drops
|
|
352
|
+
* to two small copies and multi-model scenes pay for the SLOWEST model
|
|
353
|
+
* instead of the sum. false = classic main-thread stepping. */
|
|
354
|
+
physicsWorkers?: boolean
|
|
349
355
|
world?: WorldOptions
|
|
350
356
|
sun?: SunOptions
|
|
351
357
|
camera?: CameraOptions
|
|
@@ -376,6 +382,7 @@ export interface EngineStats {
|
|
|
376
382
|
jitter: number // ms — stddev of frame intervals (pacing evenness; high = janky at any mean fps)
|
|
377
383
|
cpuAnimMs: number // ms/frame (EMA) — model updates: blending, IK, world matrices
|
|
378
384
|
cpuPhysicsMs: number // ms/frame (EMA) — physics stepping across all instances
|
|
385
|
+
cpuPhysicsWorkerMs: number // ms/frame (EMA) — the SLOWEST worker's step cost. Background-thread time (often an efficiency core), parallel to the frame: it only matters if it exceeds the frame budget.
|
|
379
386
|
cpuRenderMs: number // ms/frame (EMA) — the rest of the render thread: uniforms, encoding, submit
|
|
380
387
|
}
|
|
381
388
|
|
|
@@ -429,7 +436,7 @@ interface ModelInstance {
|
|
|
429
436
|
pickPerInstanceBindGroup: GPUBindGroup
|
|
430
437
|
pickDrawCalls: PickDrawCall[]
|
|
431
438
|
hiddenMaterials: Set<string>
|
|
432
|
-
physics: RezePhysics | null
|
|
439
|
+
physics: RezePhysics | WorkerPhysics | null
|
|
433
440
|
vertexBufferNeedsUpdate: boolean
|
|
434
441
|
gpuMorph: GpuMorph | null
|
|
435
442
|
// Style groups applied to this model: group id → compiled install.
|
|
@@ -830,6 +837,7 @@ export class Engine {
|
|
|
830
837
|
fps1PercentLow: 0,
|
|
831
838
|
cpuAnimMs: 0,
|
|
832
839
|
cpuPhysicsMs: 0,
|
|
840
|
+
cpuPhysicsWorkerMs: 0,
|
|
833
841
|
cpuRenderMs: 0,
|
|
834
842
|
jitter: 0,
|
|
835
843
|
}
|
|
@@ -839,6 +847,7 @@ export class Engine {
|
|
|
839
847
|
private viewTransform!: ViewTransformOptions
|
|
840
848
|
|
|
841
849
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
850
|
+
this.physicsWorkersEnabled = options?.physicsWorkers ?? true
|
|
842
851
|
this.canvas = canvas
|
|
843
852
|
const d = DEFAULT_ENGINE_OPTIONS
|
|
844
853
|
this.world = {
|
|
@@ -3031,7 +3040,10 @@ export class Engine {
|
|
|
3031
3040
|
|
|
3032
3041
|
dispose() {
|
|
3033
3042
|
this.stopRenderLoop()
|
|
3034
|
-
this.forEachInstance((inst) =>
|
|
3043
|
+
this.forEachInstance((inst) => {
|
|
3044
|
+
inst.model.stopAnimation()
|
|
3045
|
+
if (inst.physics instanceof WorkerPhysics) inst.physics.dispose()
|
|
3046
|
+
})
|
|
3035
3047
|
if (Engine.instance === this) Engine.instance = null
|
|
3036
3048
|
if (this.camera) this.camera.detachControl()
|
|
3037
3049
|
|
|
@@ -3108,6 +3120,7 @@ export class Engine {
|
|
|
3108
3120
|
const inst = this.modelInstances.get(name)
|
|
3109
3121
|
if (!inst) return
|
|
3110
3122
|
inst.model.stopAnimation()
|
|
3123
|
+
if (inst.physics instanceof WorkerPhysics) inst.physics.dispose()
|
|
3111
3124
|
for (const path of inst.textureCacheKeys) {
|
|
3112
3125
|
const tex = this.textureCache.get(path)
|
|
3113
3126
|
if (!tex) continue
|
|
@@ -3281,6 +3294,9 @@ export class Engine {
|
|
|
3281
3294
|
// else on the render thread. The first question of any perf report.
|
|
3282
3295
|
private cpuAnimMs = 0
|
|
3283
3296
|
private cpuPhysicsMs = 0
|
|
3297
|
+
/** EMA of worker-side step cost (all instances) — the off-thread physics bill. */
|
|
3298
|
+
private cpuPhysicsWorkerMs = 0
|
|
3299
|
+
private physicsWorkersEnabled = true
|
|
3284
3300
|
private cpuRenderMs = 0
|
|
3285
3301
|
private frameAnimMsRaw = 0
|
|
3286
3302
|
private framePhysicsMsRaw = 0
|
|
@@ -3288,6 +3304,7 @@ export class Engine {
|
|
|
3288
3304
|
private updateInstances(deltaTime: number): void {
|
|
3289
3305
|
let animMs = 0
|
|
3290
3306
|
let physicsMs = 0
|
|
3307
|
+
let workerMs = 0
|
|
3291
3308
|
this.forEachInstance((inst) => {
|
|
3292
3309
|
const tAnim = performance.now()
|
|
3293
3310
|
const verticesChanged = inst.model.update(deltaTime, this.ikEnabled)
|
|
@@ -3313,6 +3330,7 @@ export class Engine {
|
|
|
3313
3330
|
const tPhys = performance.now()
|
|
3314
3331
|
inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
|
|
3315
3332
|
physicsMs += performance.now() - tPhys
|
|
3333
|
+
if (inst.physics instanceof WorkerPhysics) workerMs = Math.max(workerMs, inst.physics.stepMs)
|
|
3316
3334
|
}
|
|
3317
3335
|
if (inst.vertexBufferNeedsUpdate) this.updateVertexBuffer(inst)
|
|
3318
3336
|
})
|
|
@@ -3321,6 +3339,7 @@ export class Engine {
|
|
|
3321
3339
|
const EMA = 0.1
|
|
3322
3340
|
this.cpuAnimMs += (animMs - this.cpuAnimMs) * EMA
|
|
3323
3341
|
this.cpuPhysicsMs += (physicsMs - this.cpuPhysicsMs) * EMA
|
|
3342
|
+
this.cpuPhysicsWorkerMs += (workerMs - this.cpuPhysicsWorkerMs) * EMA
|
|
3324
3343
|
}
|
|
3325
3344
|
|
|
3326
3345
|
private updateVertexBuffer(inst: ModelInstance): void {
|
|
@@ -3432,7 +3451,17 @@ export class Engine {
|
|
|
3432
3451
|
this.device.queue.writeBuffer(indexBuffer, 0, indices)
|
|
3433
3452
|
|
|
3434
3453
|
const rbs = model.getRigidbodies()
|
|
3435
|
-
|
|
3454
|
+
let physics: RezePhysics | WorkerPhysics | null = null
|
|
3455
|
+
if (rbs.length > 0) {
|
|
3456
|
+
if (this.physicsWorkersEnabled && WorkerPhysics.supported()) {
|
|
3457
|
+
try {
|
|
3458
|
+
physics = await WorkerPhysics.create(rbs, model.getJoints(), model.getBoneInverseBindMatrices())
|
|
3459
|
+
} catch (e) {
|
|
3460
|
+
console.warn("[reze] physics worker unavailable — falling back to main-thread physics:", e)
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
if (!physics) physics = new RezePhysics(rbs, model.getJoints())
|
|
3464
|
+
}
|
|
3436
3465
|
|
|
3437
3466
|
const shadowBindGroup = this.device.createBindGroup({
|
|
3438
3467
|
label: `${name}: shadow bind`,
|
|
@@ -5486,6 +5515,7 @@ export class Engine {
|
|
|
5486
5515
|
this.stats.jitter = Math.round(stddev * 100) / 100
|
|
5487
5516
|
this.stats.cpuAnimMs = Math.round(this.cpuAnimMs * 100) / 100
|
|
5488
5517
|
this.stats.cpuPhysicsMs = Math.round(this.cpuPhysicsMs * 100) / 100
|
|
5518
|
+
this.stats.cpuPhysicsWorkerMs = Math.round(this.cpuPhysicsWorkerMs * 100) / 100
|
|
5489
5519
|
this.stats.cpuRenderMs = Math.round(this.cpuRenderMs * 100) / 100
|
|
5490
5520
|
}
|
|
5491
5521
|
}
|
package/src/physics/body.ts
CHANGED
|
@@ -53,6 +53,10 @@ export class RigidBodyStore {
|
|
|
53
53
|
// Built lazily on first access.
|
|
54
54
|
private collisionPairs: Uint16Array | null = null
|
|
55
55
|
|
|
56
|
+
/** Index of the built-in floor body (see RezePhysics constructor), -1 if none.
|
|
57
|
+
* Excluded from the pair list; findContacts gives it a dedicated plane pass. */
|
|
58
|
+
groundIndex = -1
|
|
59
|
+
|
|
56
60
|
constructor(rigidbodies: Rigidbody[]) {
|
|
57
61
|
const N = rigidbodies.length
|
|
58
62
|
this.count = N
|
|
@@ -33,64 +33,33 @@ export interface SixDofSpringConstraint {
|
|
|
33
33
|
// positions and full-rate corrections chatter (see LOOP_ERP_SCALE).
|
|
34
34
|
isLoop: boolean
|
|
35
35
|
|
|
36
|
-
// Per-substep cache
|
|
36
|
+
// Per-substep solver cache lives in solver.ts' SolverCache (flat SoA typed
|
|
37
|
+
// arrays indexed by constraint slot) — see F_STRIDE layout there.
|
|
37
38
|
// read by the velocity-only iter loop. None of these depend on lv/av — only
|
|
38
39
|
// on pos/ori/inertia which are constant during solve.
|
|
39
|
-
cacheSkip: boolean // both bodies static — skip entirely
|
|
40
|
-
cacheLeverA: Float32Array // 3: rA = anchor − posA (world-space)
|
|
41
|
-
cacheLeverB: Float32Array // 3
|
|
42
|
-
cacheLinAxes: Float32Array // 9: 3 linear axes × xyz, world-space
|
|
43
|
-
cacheLinCrossA: Float32Array // 9: I⁻¹A·(rA × ax) per axis
|
|
44
|
-
cacheLinCrossB: Float32Array // 9: I⁻¹B·(rB × ax) per axis
|
|
45
|
-
cacheLinJacInv: Float32Array // 3: 1/(im+im+cA²·ii+cB²·ii) per axis
|
|
46
40
|
// Limit rows are unilateral (Bullet-style): the per-substep accumulated
|
|
47
41
|
// impulse is clamped to the corrective sign, so a limit can push a body
|
|
48
42
|
// back into range but never pull it deeper / brake its natural recovery —
|
|
49
43
|
// bilateral limit rows act as motors and pump energy into swinging cloth.
|
|
50
|
-
cacheLinTargetVel: Float32Array // 3: limit ERP target, signed
|
|
51
|
-
cacheLinActive: Uint8Array // 3
|
|
52
|
-
cacheLinLimitImp: Float32Array // 3: accumulated limit impulse (per substep)
|
|
53
44
|
// Spring rows are velocity-target drives; setup clamps k to the deadbeat
|
|
54
45
|
// stability bound so they cannot pump energy. maxImp bounds the per-substep
|
|
55
46
|
// accumulated impulse by the real spring force (k·|err|·dt) for rows that
|
|
56
47
|
// must stay force-limited (loop-edge welds); Infinity for authored springs.
|
|
57
|
-
cacheLinSpringTarget: Float32Array // 3
|
|
58
|
-
cacheLinSpringMaxImp: Float32Array // 3
|
|
59
|
-
cacheLinSpringImp: Float32Array // 3
|
|
60
|
-
cacheLinSpringActive: Uint8Array // 3
|
|
61
|
-
cacheAngAxes: Float32Array // 9 (spring rows)
|
|
62
|
-
cacheAngTargetVel: Float32Array // 3 (spring rows)
|
|
63
|
-
cacheAngActive: Uint8Array // 3 (spring rows)
|
|
64
48
|
// Per-axis angular Jacobians and tensor-multiplied axes: with full inertia
|
|
65
49
|
// tensors the effective mass differs per axis, and impulse application
|
|
66
50
|
// needs I⁻¹·axis per body.
|
|
67
|
-
cacheAngJacInv: Float32Array // 3: 1/(axᵀ(I⁻¹A+I⁻¹B)ax) per axis
|
|
68
51
|
// Angular spring force clamp (|k·err|·dt) + per-substep accumulator —
|
|
69
52
|
// unclamped spring velocity-drives pump energy slowly (the restoring
|
|
70
53
|
// magnitude on derived euler axes is orientation-dependent by up to ~25%,
|
|
71
54
|
// and a mis-scaled oscillator restoring force injects per cycle).
|
|
72
|
-
cacheAngSpringMaxImp: Float32Array // 3
|
|
73
|
-
cacheAngSpringImp: Float32Array // 3
|
|
74
|
-
cacheAngWA: Float32Array // 9: I⁻¹A·axis per axis
|
|
75
|
-
cacheAngWB: Float32Array // 9
|
|
76
55
|
// Single geodesic limit row: shortest rotation from the current relative
|
|
77
56
|
// orientation to the euler-clamped target. Per-axis euler limit rows are
|
|
78
57
|
// geometrically inconsistent for large violations (asin singularity) and
|
|
79
58
|
// pump energy instead of converging. Unilateral like the linear limits.
|
|
80
|
-
cacheAngLimAxis: Float32Array // 3, world-space unit axis
|
|
81
|
-
cacheAngLimWA: Float32Array // 3: I⁻¹A·axis
|
|
82
|
-
cacheAngLimWB: Float32Array // 3
|
|
83
|
-
cacheAngLimJacInv: number
|
|
84
|
-
cacheAngLimTarget: number // target relative angular velocity along axis
|
|
85
|
-
cacheAngLimActive: number
|
|
86
|
-
cacheAngLimImp: number // accumulated impulse (per substep)
|
|
87
59
|
// Per-axis angular limit rows for the small-violation regime. act:
|
|
88
60
|
// 1 = bilateral (locked axis), 2 = unilateral stop (ranged axis) — a
|
|
89
61
|
// bilateral row on a ranged axis brakes natural recovery and pumps
|
|
90
62
|
// energy into swinging cloth.
|
|
91
|
-
cacheAngPATarget: Float32Array // 3
|
|
92
|
-
cacheAngPAActive: Uint8Array // 3
|
|
93
|
-
cacheAngPAImp: Float32Array // 3
|
|
94
63
|
}
|
|
95
64
|
|
|
96
65
|
// Stop-limit ERP. PMX rigs are tuned against MMD's stiff limit response;
|
|
@@ -173,38 +142,6 @@ export function buildConstraints(
|
|
|
173
142
|
springStiffness,
|
|
174
143
|
equilibriumPoint: new Float32Array(6),
|
|
175
144
|
isLoop: false,
|
|
176
|
-
cacheSkip: false,
|
|
177
|
-
cacheLeverA: new Float32Array(3),
|
|
178
|
-
cacheLeverB: new Float32Array(3),
|
|
179
|
-
cacheLinAxes: new Float32Array(9),
|
|
180
|
-
cacheLinCrossA: new Float32Array(9),
|
|
181
|
-
cacheLinCrossB: new Float32Array(9),
|
|
182
|
-
cacheLinJacInv: new Float32Array(3),
|
|
183
|
-
cacheLinTargetVel: new Float32Array(3),
|
|
184
|
-
cacheLinActive: new Uint8Array(3),
|
|
185
|
-
cacheLinLimitImp: new Float32Array(3),
|
|
186
|
-
cacheLinSpringTarget: new Float32Array(3),
|
|
187
|
-
cacheLinSpringMaxImp: new Float32Array(3),
|
|
188
|
-
cacheLinSpringImp: new Float32Array(3),
|
|
189
|
-
cacheLinSpringActive: new Uint8Array(3),
|
|
190
|
-
cacheAngAxes: new Float32Array(9),
|
|
191
|
-
cacheAngTargetVel: new Float32Array(3),
|
|
192
|
-
cacheAngActive: new Uint8Array(3),
|
|
193
|
-
cacheAngJacInv: new Float32Array(3),
|
|
194
|
-
cacheAngSpringMaxImp: new Float32Array(3),
|
|
195
|
-
cacheAngSpringImp: new Float32Array(3),
|
|
196
|
-
cacheAngWA: new Float32Array(9),
|
|
197
|
-
cacheAngWB: new Float32Array(9),
|
|
198
|
-
cacheAngLimAxis: new Float32Array(3),
|
|
199
|
-
cacheAngLimWA: new Float32Array(3),
|
|
200
|
-
cacheAngLimWB: new Float32Array(3),
|
|
201
|
-
cacheAngLimJacInv: 0,
|
|
202
|
-
cacheAngLimTarget: 0,
|
|
203
|
-
cacheAngLimActive: 0,
|
|
204
|
-
cacheAngLimImp: 0,
|
|
205
|
-
cacheAngPATarget: new Float32Array(3),
|
|
206
|
-
cacheAngPAActive: new Uint8Array(3),
|
|
207
|
-
cacheAngPAImp: new Float32Array(3),
|
|
208
145
|
})
|
|
209
146
|
}
|
|
210
147
|
|
package/src/physics/contact.ts
CHANGED
|
@@ -993,4 +993,84 @@ export function findContacts(store: RigidBodyStore, pool: ContactPool): void {
|
|
|
993
993
|
if (!aabbOverlap(store, i, j)) continue
|
|
994
994
|
generateContacts(store, i, j, pool)
|
|
995
995
|
}
|
|
996
|
+
// Built-in floor: a plane pass against every dynamic body. Cheaper and more
|
|
997
|
+
// complete than routing the huge ground box through the pair machinery —
|
|
998
|
+
// one y-test per body, and box hems collide too (there is no generic
|
|
999
|
+
// box-box narrowphase to lean on).
|
|
1000
|
+
const g = store.groundIndex
|
|
1001
|
+
if (g >= 0) {
|
|
1002
|
+
const minA = store.aabbMin
|
|
1003
|
+
for (let i = 0; i < store.count; i++) {
|
|
1004
|
+
if (store.invMass[i] <= 0) continue
|
|
1005
|
+
if (minA[i * 3 + 1] > CONTACT_MARGIN) continue
|
|
1006
|
+
detectFloor(store, i, g, pool)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// The floor's top face is the model-space plane y = 0; its body (`g`) exists so
|
|
1012
|
+
// contact rows have a static B side. Normal convention matches the detectors:
|
|
1013
|
+
// A→B, so pointing DOWN into the floor; depth > 0 = penetrating.
|
|
1014
|
+
function detectFloor(store: RigidBodyStore, a: number, g: number, pool: ContactPool): void {
|
|
1015
|
+
const ai = a * 3
|
|
1016
|
+
const pos = store.positions
|
|
1017
|
+
const gx = pos[g * 3 + 0]
|
|
1018
|
+
const gy = pos[g * 3 + 1]
|
|
1019
|
+
const gz = pos[g * 3 + 2]
|
|
1020
|
+
const emit = (px: number, py: number, pz: number, depth: number) => {
|
|
1021
|
+
const c = pool.acquire()
|
|
1022
|
+
c.bodyA = a
|
|
1023
|
+
c.bodyB = g
|
|
1024
|
+
c.nx = 0
|
|
1025
|
+
c.ny = -1
|
|
1026
|
+
c.nz = 0
|
|
1027
|
+
c.depth = depth
|
|
1028
|
+
c.rAx = px - pos[ai + 0]
|
|
1029
|
+
c.rAy = py - pos[ai + 1]
|
|
1030
|
+
c.rAz = pz - pos[ai + 2]
|
|
1031
|
+
c.rBx = px - gx
|
|
1032
|
+
c.rBy = py - gy
|
|
1033
|
+
c.rBz = pz - gz
|
|
1034
|
+
combineMaterials(store, a, g, c)
|
|
1035
|
+
}
|
|
1036
|
+
const cx = pos[ai + 0]
|
|
1037
|
+
const cy = pos[ai + 1]
|
|
1038
|
+
const cz = pos[ai + 2]
|
|
1039
|
+
switch (store.shape[a]) {
|
|
1040
|
+
case RigidbodyShape.Sphere: {
|
|
1041
|
+
const r = store.size[ai + 0]
|
|
1042
|
+
const low = cy - r
|
|
1043
|
+
if (low <= CONTACT_MARGIN) emit(cx, low, cz, -low)
|
|
1044
|
+
break
|
|
1045
|
+
}
|
|
1046
|
+
case RigidbodyShape.Capsule: {
|
|
1047
|
+
const r = store.size[ai + 0]
|
|
1048
|
+
const h = store.size[ai + 1] * 0.5
|
|
1049
|
+
const ax = _capPoint
|
|
1050
|
+
capsuleAxis(store, a, ax)
|
|
1051
|
+
for (const sgn of [-1, 1]) {
|
|
1052
|
+
const ex = cx + ax[0] * h * sgn
|
|
1053
|
+
const ey = cy + ax[1] * h * sgn
|
|
1054
|
+
const ez = cz + ax[2] * h * sgn
|
|
1055
|
+
const low = ey - r
|
|
1056
|
+
if (low <= CONTACT_MARGIN) emit(ex, low, ez, -low)
|
|
1057
|
+
}
|
|
1058
|
+
break
|
|
1059
|
+
}
|
|
1060
|
+
case RigidbodyShape.Box: {
|
|
1061
|
+
const hx = store.size[ai + 0]
|
|
1062
|
+
const hy = store.size[ai + 1]
|
|
1063
|
+
const hz = store.size[ai + 2]
|
|
1064
|
+
const out = _capPointB
|
|
1065
|
+
for (let k = 0; k < 8; k++) {
|
|
1066
|
+
const lx = k & 1 ? hx : -hx
|
|
1067
|
+
const ly = k & 2 ? hy : -hy
|
|
1068
|
+
const lz = k & 4 ? hz : -hz
|
|
1069
|
+
bodyLocalToWorldDir(store, a, lx, ly, lz, out)
|
|
1070
|
+
const wy = cy + out[1]
|
|
1071
|
+
if (wy <= CONTACT_MARGIN) emit(cx + out[0], wy, cz + out[2], -wy)
|
|
1072
|
+
}
|
|
1073
|
+
break
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
996
1076
|
}
|
package/src/physics/physics.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Vec3, Quat, Mat4 } from "../math"
|
|
2
2
|
import type { Rigidbody, Joint } from "./types"
|
|
3
|
-
import { RigidbodyType } from "./types"
|
|
3
|
+
import { RigidbodyType, RigidbodyShape } from "./types"
|
|
4
4
|
import { RigidBodyStore } from "./body"
|
|
5
5
|
import { World } from "./world"
|
|
6
6
|
import { buildConstraints, type SixDofSpringConstraint } from "./constraint"
|
|
7
|
+
import { SolverCache } from "./solver"
|
|
7
8
|
import { ContactPool } from "./contact"
|
|
8
9
|
|
|
9
10
|
const _bodyMat = new Float32Array(16)
|
|
@@ -19,6 +20,7 @@ export class RezePhysics {
|
|
|
19
20
|
private store: RigidBodyStore
|
|
20
21
|
private world: World
|
|
21
22
|
private constraints: SixDofSpringConstraint[]
|
|
23
|
+
private solverCache: SolverCache
|
|
22
24
|
private contacts: ContactPool
|
|
23
25
|
private firstFrame = true
|
|
24
26
|
private timeAccum = 0
|
|
@@ -67,9 +69,40 @@ export class RezePhysics {
|
|
|
67
69
|
constructor(rigidbodies: Rigidbody[], joints: Joint[] = []) {
|
|
68
70
|
this.rigidbodies = rigidbodies
|
|
69
71
|
this.joints = joints
|
|
70
|
-
|
|
72
|
+
// The floor is part of every world: a huge static box whose top face is
|
|
73
|
+
// model-space y = 0, so hair and hems rest on the ground instead of
|
|
74
|
+
// clipping through when a pose reaches it. Boneless (every bone-sync loop
|
|
75
|
+
// skips boneIndex < 0); collides with EVERY dynamic body regardless of
|
|
76
|
+
// group masks via findContacts' dedicated plane pass (spheres, capsules
|
|
77
|
+
// and boxes — box hems included).
|
|
78
|
+
const ground: Rigidbody = {
|
|
79
|
+
name: "__ground__",
|
|
80
|
+
englishName: "__ground__",
|
|
81
|
+
boneIndex: -1,
|
|
82
|
+
group: 0,
|
|
83
|
+
collisionMask: 0xffff,
|
|
84
|
+
shape: RigidbodyShape.Box,
|
|
85
|
+
size: new Vec3(500, 1, 500),
|
|
86
|
+
shapePosition: new Vec3(0, -1, 0),
|
|
87
|
+
shapeRotation: new Vec3(0, 0, 0),
|
|
88
|
+
mass: 0,
|
|
89
|
+
linearDamping: 0,
|
|
90
|
+
angularDamping: 0,
|
|
91
|
+
restitution: 0,
|
|
92
|
+
friction: 0.6,
|
|
93
|
+
type: RigidbodyType.Static,
|
|
94
|
+
bodyOffsetMatrixInverse: Mat4.identity(),
|
|
95
|
+
}
|
|
96
|
+
this.store = new RigidBodyStore([...rigidbodies, ground])
|
|
97
|
+
const gi = this.store.count - 1
|
|
98
|
+
// Mask 0 keeps the floor OUT of the generic pair list — findContacts runs a
|
|
99
|
+
// dedicated plane pass against every dynamic body (all shapes, boxes too).
|
|
100
|
+
this.store.collisionGroup[gi] = 0
|
|
101
|
+
this.store.willCollideMask[gi] = 0
|
|
102
|
+
this.store.groundIndex = gi
|
|
71
103
|
this.world = new World(new Vec3(0, -98, 0))
|
|
72
104
|
this.constraints = buildConstraints(rigidbodies, joints)
|
|
105
|
+
this.solverCache = new SolverCache(this.constraints)
|
|
73
106
|
this.contacts = new ContactPool()
|
|
74
107
|
this.prevPositions = new Float32Array(this.store.count * 3)
|
|
75
108
|
this.prevOrientations = new Float32Array(this.store.count * 4)
|
|
@@ -226,7 +259,7 @@ export class RezePhysics {
|
|
|
226
259
|
this.savePrevState()
|
|
227
260
|
this.advanceKinematicToTargets(1 / (nSub - k))
|
|
228
261
|
const tStep = performance.now()
|
|
229
|
-
this.world.step(this.store, this.constraints, this.contacts, this.fixedTimeStep)
|
|
262
|
+
this.world.step(this.store, this.constraints, this.solverCache, this.contacts, this.fixedTimeStep)
|
|
230
263
|
this.stepCostEmaMs += (performance.now() - tStep - this.stepCostEmaMs) * 0.2
|
|
231
264
|
this.restoreNonFiniteBodies()
|
|
232
265
|
this.timeAccum -= this.fixedTimeStep
|
|
@@ -0,0 +1,66 @@
|
|
|
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 type { Rigidbody, Joint } from "./types"
|
|
14
|
+
import { RigidbodyType } from "./types"
|
|
15
|
+
|
|
16
|
+
interface InitMsg {
|
|
17
|
+
cmd: "init"
|
|
18
|
+
rigidbodies: Rigidbody[]
|
|
19
|
+
joints: Joint[]
|
|
20
|
+
inverseBind: Float32Array
|
|
21
|
+
}
|
|
22
|
+
interface PoseMsg {
|
|
23
|
+
cmd: "step" | "reset"
|
|
24
|
+
dt: number
|
|
25
|
+
bones: ArrayBuffer
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ctx = self as unknown as {
|
|
29
|
+
onmessage: ((e: MessageEvent<InitMsg | PoseMsg>) => void) | null
|
|
30
|
+
postMessage(msg: unknown, transfer?: Transferable[]): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let physics: RezePhysics | null = null
|
|
34
|
+
let inverseBind: Float32Array<ArrayBufferLike> = new Float32Array(0)
|
|
35
|
+
let pose = new Float32Array(0)
|
|
36
|
+
let mats: Mat4[] = []
|
|
37
|
+
|
|
38
|
+
ctx.onmessage = (e) => {
|
|
39
|
+
const msg = e.data
|
|
40
|
+
if (msg.cmd === "init") {
|
|
41
|
+
physics = new RezePhysics(msg.rigidbodies, msg.joints)
|
|
42
|
+
inverseBind = msg.inverseBind
|
|
43
|
+
const boneCount = inverseBind.length / 16
|
|
44
|
+
pose = new Float32Array(boneCount * 16)
|
|
45
|
+
mats = new Array(boneCount)
|
|
46
|
+
for (let i = 0; i < boneCount; i++) mats[i] = new Mat4(pose.subarray(i * 16, i * 16 + 16) as Float32Array)
|
|
47
|
+
// The host copies back only the bones physics can write — the bones of
|
|
48
|
+
// dynamic bodies. Kinematic bones must NOT round-trip: in the pipelined
|
|
49
|
+
// protocol they would drag a stale (frame-old) pose over the live one.
|
|
50
|
+
const dynamicBones: number[] = []
|
|
51
|
+
for (const rb of msg.rigidbodies) {
|
|
52
|
+
if (rb.type === RigidbodyType.Dynamic && rb.mass > 0 && rb.boneIndex >= 0) dynamicBones.push(rb.boneIndex)
|
|
53
|
+
}
|
|
54
|
+
ctx.postMessage({ cmd: "ready", dynamicBones })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (!physics) return
|
|
58
|
+
const incoming = new Float32Array(msg.bones)
|
|
59
|
+
pose.set(incoming)
|
|
60
|
+
const t0 = performance.now()
|
|
61
|
+
if (msg.cmd === "step") physics.step(msg.dt, mats, inverseBind)
|
|
62
|
+
else physics.reset(mats)
|
|
63
|
+
const stepMs = performance.now() - t0
|
|
64
|
+
incoming.set(pose)
|
|
65
|
+
ctx.postMessage({ cmd: "stepped", bones: msg.bones, stepMs }, [msg.bones])
|
|
66
|
+
}
|