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.
@@ -0,0 +1,126 @@
1
+ // Main-thread facade over a physics worker, presenting RezePhysics' synchronous
2
+ // step()/reset() surface so the engine's frame loop doesn't change shape.
3
+ //
4
+ // PIPELINED ONE FRAME DEEP: step(N) applies the worker's result for frame N−1
5
+ // onto the current pose (dynamic bones only — cloth runs one frame behind its
6
+ // anchors, invisible at 60Hz) and posts frame N without waiting. Main-thread
7
+ // cost collapses to two ~20KB copies; the simulation itself runs on another
8
+ // core, and with one worker per model the wall cost of a multi-model scene is
9
+ // the SLOWEST model instead of the sum. If a frame arrives while the worker is
10
+ // still busy, its dt accumulates and the next post carries it — the worker's
11
+ // own fixed-step accumulator and load-shedding handle catch-up exactly as the
12
+ // main-thread path would.
13
+ import type { Rigidbody, Joint } from "./types"
14
+ import type { Mat4 } from "../math"
15
+
16
+ interface ReadyMsg {
17
+ cmd: "ready"
18
+ dynamicBones: number[]
19
+ }
20
+ interface SteppedMsg {
21
+ cmd: "stepped"
22
+ bones: ArrayBuffer
23
+ stepMs: number
24
+ }
25
+
26
+ export class WorkerPhysics {
27
+ private readonly worker: Worker
28
+ private dynamicBones: number[] = []
29
+ private readonly boneCount: number
30
+ /** Transfer buffer when idle; null while a step is in flight. */
31
+ private buf: ArrayBuffer | null
32
+ /** Latest completed pose from the worker (copied out of the transfer buffer). */
33
+ private readonly result: Float32Array
34
+ private hasResult = false
35
+ private pendingDt = 0
36
+ private queuedReset = false
37
+ /** Worker-side cost of the last completed step — for engine stats. */
38
+ stepMs = 0
39
+
40
+ private constructor(worker: Worker, boneCount: number) {
41
+ this.worker = worker
42
+ this.boneCount = boneCount
43
+ this.buf = new ArrayBuffer(boneCount * 64)
44
+ this.result = new Float32Array(boneCount * 16)
45
+ }
46
+
47
+ static supported(): boolean {
48
+ return typeof Worker !== "undefined"
49
+ }
50
+
51
+ static create(rigidbodies: Rigidbody[], joints: Joint[], inverseBind: Float32Array): Promise<WorkerPhysics> {
52
+ return new Promise((resolve, reject) => {
53
+ let worker: Worker
54
+ try {
55
+ worker = new Worker(new URL("./physics.worker.js", import.meta.url), { type: "module" })
56
+ } catch (e) {
57
+ reject(e instanceof Error ? e : new Error(String(e)))
58
+ return
59
+ }
60
+ const wp = new WorkerPhysics(worker, inverseBind.length / 16)
61
+ const fail = (message: string) => {
62
+ worker.terminate()
63
+ reject(new Error(message))
64
+ }
65
+ worker.onerror = (e) => fail(`physics worker failed to boot: ${e.message || "worker error"}`)
66
+ worker.onmessage = (ev: MessageEvent<ReadyMsg>) => {
67
+ if (ev.data?.cmd !== "ready") return
68
+ wp.dynamicBones = ev.data.dynamicBones
69
+ worker.onmessage = (m: MessageEvent<SteppedMsg>) => wp.onStepped(m)
70
+ worker.onerror = null
71
+ resolve(wp)
72
+ }
73
+ // Rigidbody/Joint carry only data (Vec3/Mat4 fields clone as plain
74
+ // objects with the same fields — the physics constructor reads fields,
75
+ // never methods), so structuredClone is a faithful serializer.
76
+ worker.postMessage({ cmd: "init", rigidbodies, joints, inverseBind: inverseBind.slice() })
77
+ })
78
+ }
79
+
80
+ /** Same signature as RezePhysics.step — the engine cannot tell them apart.
81
+ * (inverseBind was shipped to the worker at init; the param is unused.) */
82
+ step(dt: number, boneWorldMatrices: Mat4[], _inverseBind: Float32Array): void {
83
+ this.pendingDt += dt
84
+ // Apply the newest completed simulation onto this frame's pose. Dynamic
85
+ // bones only: kinematic bones must keep the LIVE animation pose.
86
+ if (this.hasResult) {
87
+ const r = this.result
88
+ for (const bi of this.dynamicBones) {
89
+ boneWorldMatrices[bi].values.set(r.subarray(bi * 16, bi * 16 + 16))
90
+ }
91
+ }
92
+ if (this.buf === null) return // worker mid-step: dt accumulated for the next post
93
+ const flat = new Float32Array(this.buf)
94
+ const n = Math.min(this.boneCount, boneWorldMatrices.length)
95
+ for (let i = 0; i < n; i++) flat.set(boneWorldMatrices[i].values, i * 16)
96
+ this.worker.postMessage(
97
+ { cmd: this.queuedReset ? "reset" : "step", dt: this.pendingDt, bones: this.buf },
98
+ [this.buf],
99
+ )
100
+ this.buf = null
101
+ this.pendingDt = 0
102
+ this.queuedReset = false
103
+ }
104
+
105
+ /** Reset rides the same pipeline: the next posted frame carries a reset
106
+ * command instead of a step, and stale results stop applying immediately. */
107
+ reset(boneWorldMatrices: Mat4[]): void {
108
+ this.queuedReset = true
109
+ this.hasResult = false
110
+ // Post right away if idle — reuse step's snapshot/post path with dt 0.
111
+ if (this.buf !== null) this.step(0, boneWorldMatrices, undefined as unknown as Float32Array)
112
+ }
113
+
114
+ dispose(): void {
115
+ this.worker.terminate()
116
+ }
117
+
118
+ private onStepped(ev: MessageEvent<SteppedMsg>): void {
119
+ const d = ev.data
120
+ if (d?.cmd !== "stepped") return
121
+ this.buf = d.bones
122
+ this.stepMs = d.stepMs
123
+ this.result.set(new Float32Array(this.buf))
124
+ this.hasResult = true
125
+ }
126
+ }
@@ -2,7 +2,7 @@ import { Vec3 } from "../math"
2
2
  import type { RigidBodyStore } from "./body"
3
3
  import { RigidbodyType } from "./types"
4
4
  import type { SixDofSpringConstraint } from "./constraint"
5
- import { solveConstraints } from "./solver"
5
+ import { solveConstraints, type SolverCache } from "./solver"
6
6
  import { findContacts, type ContactPool } from "./contact"
7
7
 
8
8
  // World step: predict velocities → collide → solve → position correction →
@@ -30,7 +30,13 @@ export class World {
30
30
  this.gravity.z = g.z
31
31
  }
32
32
 
33
- step(store: RigidBodyStore, constraints: SixDofSpringConstraint[], contacts: ContactPool, dt: number): void {
33
+ step(
34
+ store: RigidBodyStore,
35
+ constraints: SixDofSpringConstraint[],
36
+ cache: SolverCache,
37
+ contacts: ContactPool,
38
+ dt: number,
39
+ ): void {
34
40
  if (dt <= 0) return
35
41
 
36
42
  const N = store.count
@@ -79,7 +85,7 @@ export class World {
79
85
 
80
86
  // 3. Solve joint + contact constraints (velocity-only).
81
87
  if (constraints.length > 0 || contacts.count > 0) {
82
- solveConstraints(store, constraints, contacts, dt, this.solverIterations)
88
+ solveConstraints(store, constraints, cache, contacts, dt, this.solverIterations)
83
89
  }
84
90
 
85
91
  // 4. Position correction (split impulse). Direct translation along the