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,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
|
+
}
|
package/src/physics/world.ts
CHANGED
|
@@ -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(
|
|
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
|