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,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.2",
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
  }
@@ -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. 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
 
@@ -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
  }
@@ -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
- this.store = new RigidBodyStore(rigidbodies)
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
+ }