reze-engine 0.31.3 → 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,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
+ }
@@ -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;AAE1D,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,CAAC,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,sBAAsB,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;CAoJ5G"}
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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.31.3",
3
+ "version": "0.32.0",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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) => inst.model.stopAnimation())
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
- const physics = rbs.length > 0 ? new RezePhysics(rbs, model.getJoints()) : null
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
  }
@@ -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. Filled by solver's setup pass once before SI iters,
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
 
@@ -4,6 +4,7 @@ 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
@@ -100,6 +102,7 @@ export class RezePhysics {
100
102
  this.store.groundIndex = gi
101
103
  this.world = new World(new Vec3(0, -98, 0))
102
104
  this.constraints = buildConstraints(rigidbodies, joints)
105
+ this.solverCache = new SolverCache(this.constraints)
103
106
  this.contacts = new ContactPool()
104
107
  this.prevPositions = new Float32Array(this.store.count * 3)
105
108
  this.prevOrientations = new Float32Array(this.store.count * 4)
@@ -256,7 +259,7 @@ export class RezePhysics {
256
259
  this.savePrevState()
257
260
  this.advanceKinematicToTargets(1 / (nSub - k))
258
261
  const tStep = performance.now()
259
- 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)
260
263
  this.stepCostEmaMs += (performance.now() - tStep - this.stepCostEmaMs) * 0.2
261
264
  this.restoreNonFiniteBodies()
262
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
+ }