reze-engine 0.16.1 → 0.16.2
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/README.md +139 -284
- package/dist/physics/constraint.d.ts +7 -0
- package/dist/physics/constraint.d.ts.map +1 -1
- package/dist/physics/constraint.js +7 -0
- package/dist/physics/physics.d.ts +12 -1
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +344 -47
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +215 -39
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +12 -1
- package/dist/physics-debug.d.ts +30 -0
- package/dist/physics-debug.d.ts.map +1 -0
- package/dist/physics-debug.js +526 -0
- package/dist/shaders/passes/physics-debug.d.ts +2 -0
- package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
- package/dist/shaders/passes/physics-debug.js +69 -0
- package/package.json +1 -1
- package/src/physics/constraint.ts +29 -5
- package/src/physics/physics.ts +355 -55
- package/src/physics/solver.ts +205 -38
- package/src/physics/world.ts +13 -1
- package/dist/gpu-profile.d.ts +0 -19
- package/dist/gpu-profile.d.ts.map +0 -1
- package/dist/gpu-profile.js +0 -120
- package/dist/physics/profile.d.ts +0 -18
- package/dist/physics/profile.d.ts.map +0 -1
- package/dist/physics/profile.js +0 -44
package/src/physics/solver.ts
CHANGED
|
@@ -19,12 +19,30 @@ import type { Contact, ContactPool } from "./contact"
|
|
|
19
19
|
|
|
20
20
|
const BOUNCE_THRESHOLD = 2.0
|
|
21
21
|
|
|
22
|
+
// Ceilings on limit-correction velocity. In normal operation limit errors are
|
|
23
|
+
// tiny; a large error only appears after a discontinuity (teleport, stall,
|
|
24
|
+
// deep penetration), and feeding err·ERP/dt to the solver unclamped then
|
|
25
|
+
// injects explosion-scale impulses into the chain.
|
|
26
|
+
const MAX_LINEAR_CORRECTION_VEL = 120 // units/s
|
|
27
|
+
const MAX_ANGULAR_CORRECTION_VEL = 30 // rad/s
|
|
28
|
+
|
|
29
|
+
// Bullet's limit-motor softness defaults (0.7 translational, 0.5 rotational):
|
|
30
|
+
// scale each iteration's limit impulse so the stop engages progressively
|
|
31
|
+
// instead of as a hard velocity snap.
|
|
32
|
+
const LIMIT_SOFTNESS_LINEAR = 0.7
|
|
33
|
+
const LIMIT_SOFTNESS_ANGULAR = 0.5
|
|
34
|
+
// Angular limit violations below this switch to per-axis euler rows; above
|
|
35
|
+
// it, the single geodesic row takes over (see setupConstraint).
|
|
36
|
+
const GEODESIC_THRESHOLD = 0.5 // rad
|
|
37
|
+
|
|
22
38
|
// Module-level scratch (no per-iter allocations).
|
|
23
39
|
const _TA = new Float32Array(16)
|
|
24
40
|
const _TB = new Float32Array(16)
|
|
25
41
|
const _bodyMatA = new Float32Array(16)
|
|
26
42
|
const _bodyMatB = new Float32Array(16)
|
|
27
43
|
const _angDiffScratch = new Float32Array(3)
|
|
44
|
+
const _quatScratchA = new Float32Array(4)
|
|
45
|
+
const _quatScratchB = new Float32Array(4)
|
|
28
46
|
|
|
29
47
|
export function solveConstraints(
|
|
30
48
|
store: RigidBodyStore,
|
|
@@ -82,21 +100,23 @@ function setupConstraint(
|
|
|
82
100
|
Mat4.multiplyArrays(_bodyMatA, 0, con.frameA, 0, _TA, 0)
|
|
83
101
|
Mat4.multiplyArrays(_bodyMatB, 0, con.frameB, 0, _TB, 0)
|
|
84
102
|
|
|
85
|
-
//
|
|
103
|
+
// Per-body pivots at each body's own joint-frame origin (Spring2-style).
|
|
104
|
+
// Bullet 2.7x's shared mass-weighted anchor (m_AnchorPos) degenerates when
|
|
105
|
+
// the joint is violated by a large distance: the midpoint sits far from
|
|
106
|
+
// both bodies, the lever arms grow with the separation, the Jacobian
|
|
107
|
+
// denominator blows up as err²·invInertia, and the row applies torque
|
|
108
|
+
// instead of closing velocity — the joint "breaks" and the error runs
|
|
109
|
+
// away. Per-body pivots keep the levers bounded by the frame offsets, so
|
|
110
|
+
// the row stays effective no matter how large the violation is.
|
|
86
111
|
const pos = store.positions
|
|
87
112
|
const ai = a * 3
|
|
88
113
|
const bi = b * 3
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const rAy = anchorY - pos[ai + 1]
|
|
96
|
-
const rAz = anchorZ - pos[ai + 2]
|
|
97
|
-
const rBx = anchorX - pos[bi + 0]
|
|
98
|
-
const rBy = anchorY - pos[bi + 1]
|
|
99
|
-
const rBz = anchorZ - pos[bi + 2]
|
|
114
|
+
const rAx = _TA[12] - pos[ai + 0]
|
|
115
|
+
const rAy = _TA[13] - pos[ai + 1]
|
|
116
|
+
const rAz = _TA[14] - pos[ai + 2]
|
|
117
|
+
const rBx = _TB[12] - pos[bi + 0]
|
|
118
|
+
const rBy = _TB[13] - pos[bi + 1]
|
|
119
|
+
const rBz = _TB[14] - pos[bi + 2]
|
|
100
120
|
const lA = con.cacheLeverA
|
|
101
121
|
const lB = con.cacheLeverB
|
|
102
122
|
lA[0] = rAx; lA[1] = rAy; lA[2] = rAz
|
|
@@ -143,23 +163,37 @@ function setupConstraint(
|
|
|
143
163
|
const lo = con.linearMin[i]
|
|
144
164
|
const hi = con.linearMax[i]
|
|
145
165
|
const curr = i === 0 ? linDiff0 : i === 1 ? linDiff1 : linDiff2
|
|
166
|
+
// active: 1 = bilateral equality (locked axis — a joint, always on),
|
|
167
|
+
// 2 = unilateral stop (ranged axis in violation).
|
|
146
168
|
let target = 0
|
|
147
169
|
let active = 0
|
|
148
170
|
if (lo <= hi) {
|
|
149
171
|
let err = 0
|
|
150
172
|
if (curr < lo) err = curr - lo
|
|
151
173
|
else if (curr > hi) err = curr - hi
|
|
174
|
+
if (lo === hi) active = 1
|
|
175
|
+
else if (err !== 0) active = 2
|
|
152
176
|
if (err !== 0) {
|
|
153
177
|
target = -err * STOP_ERP * invDt
|
|
154
|
-
|
|
178
|
+
if (target > MAX_LINEAR_CORRECTION_VEL) target = MAX_LINEAR_CORRECTION_VEL
|
|
179
|
+
else if (target < -MAX_LINEAR_CORRECTION_VEL) target = -MAX_LINEAR_CORRECTION_VEL
|
|
155
180
|
}
|
|
156
181
|
}
|
|
157
|
-
if (con.springEnabled[i]) {
|
|
158
|
-
target += -con.springStiffness[i] * (curr - con.equilibriumPoint[i]) * dt
|
|
159
|
-
active = 1
|
|
160
|
-
}
|
|
161
182
|
tgt[i] = target
|
|
162
183
|
act[i] = denom > 0 ? active : 0
|
|
184
|
+
con.cacheLinLimitImp[i] = 0
|
|
185
|
+
if (con.springEnabled[i] && denom > 0) {
|
|
186
|
+
// Clamp k to the deadbeat limit: an explicit spring with k·dt² > 1
|
|
187
|
+
// overshoots equilibrium every step and pumps energy — the classic
|
|
188
|
+
// pre-Spring2 Bullet 6dof instability this port inherited. (¼ margin
|
|
189
|
+
// for Gauss-Seidel coupling in chains.)
|
|
190
|
+
const k = Math.min(con.springStiffness[i], 0.25 * invDt * invDt)
|
|
191
|
+
const serr = curr - con.equilibriumPoint[i]
|
|
192
|
+
con.cacheLinSpringTarget[i] = -k * serr * dt
|
|
193
|
+
con.cacheLinSpringActive[i] = 1
|
|
194
|
+
} else {
|
|
195
|
+
con.cacheLinSpringActive[i] = 0
|
|
196
|
+
}
|
|
163
197
|
}
|
|
164
198
|
|
|
165
199
|
// Angular: TA^T · TB → Euler XYZ; axes from TA.col2 × TB.col0.
|
|
@@ -198,32 +232,91 @@ function setupConstraint(
|
|
|
198
232
|
const angDenom = iiA + iiB
|
|
199
233
|
con.cacheAngJacInv = angDenom > 0 ? 1 / angDenom : 0
|
|
200
234
|
|
|
235
|
+
// Per-axis rows carry only the springs. Sign flip vs linear:
|
|
236
|
+
// d(angDiff)/dt = −(ω_B − ω_A)·ax.
|
|
201
237
|
const angTgt = con.cacheAngTargetVel
|
|
202
238
|
const angAct = con.cacheAngActive
|
|
203
239
|
for (let i = 0; i < 3; i++) {
|
|
204
240
|
const idx = i + 3
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// Sign flip vs linear: d(angDiff)/dt = −(ω_B − ω_A)·ax.
|
|
215
|
-
if (err !== 0) {
|
|
216
|
-
target = err * STOP_ERP * invDt
|
|
217
|
-
active = 1
|
|
218
|
-
}
|
|
241
|
+
if (con.springEnabled[idx] && angDenom > 0) {
|
|
242
|
+
// Same deadbeat clamp as the linear springs.
|
|
243
|
+
const k = Math.min(con.springStiffness[idx], 0.25 * invDt * invDt)
|
|
244
|
+
const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
|
|
245
|
+
angTgt[i] = k * serr * dt
|
|
246
|
+
angAct[i] = 1
|
|
247
|
+
} else {
|
|
248
|
+
angTgt[i] = 0
|
|
249
|
+
angAct[i] = 0
|
|
219
250
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Angular limit handling is hybrid. Small violations (the resting-cloth
|
|
254
|
+
// regime) use per-axis euler rows — they converge cleanly and keep resting
|
|
255
|
+
// cloth dead still. Large violations switch to a single geodesic row toward
|
|
256
|
+
// the euler-clamped target: per-axis euler rows (the Bullet-2.7x approach
|
|
257
|
+
// this port used) become geometrically inconsistent for large errors — near
|
|
258
|
+
// the asin singularity they chase phantom errors and pump angular velocity
|
|
259
|
+
// into the chain instead of converging.
|
|
260
|
+
con.cacheAngLimActive = 0
|
|
261
|
+
if (angDenom > 0) {
|
|
262
|
+
const ex = _angDiffScratch[0], ey = _angDiffScratch[1], ez = _angDiffScratch[2]
|
|
263
|
+
// Free axes (min > max) follow the current angle, i.e. no correction.
|
|
264
|
+
let tx = ex, ty = ey, tz = ez
|
|
265
|
+
if (con.angularMin[0] <= con.angularMax[0]) tx = ex < con.angularMin[0] ? con.angularMin[0] : ex > con.angularMax[0] ? con.angularMax[0] : ex
|
|
266
|
+
if (con.angularMin[1] <= con.angularMax[1]) ty = ey < con.angularMin[1] ? con.angularMin[1] : ey > con.angularMax[1] ? con.angularMax[1] : ey
|
|
267
|
+
if (con.angularMin[2] <= con.angularMax[2]) tz = ez < con.angularMin[2] ? con.angularMin[2] : ez > con.angularMax[2] ? con.angularMax[2] : ez
|
|
268
|
+
const errX = ex - tx, errY = ey - ty, errZ = ez - tz
|
|
269
|
+
const maxErr = Math.max(Math.abs(errX), Math.abs(errY), Math.abs(errZ))
|
|
270
|
+
if (maxErr > 0 && maxErr < GEODESIC_THRESHOLD) {
|
|
271
|
+
// Per-axis euler limit rows, folded into the per-axis row set (the
|
|
272
|
+
// spring impulse clamp must not bound a limit row — lift it).
|
|
273
|
+
for (let i = 0; i < 3; i++) {
|
|
274
|
+
const err = i === 0 ? errX : i === 1 ? errY : errZ
|
|
275
|
+
if (err === 0) continue
|
|
276
|
+
let target = err * STOP_ERP * invDt
|
|
277
|
+
if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
|
|
278
|
+
else if (target < -MAX_ANGULAR_CORRECTION_VEL) target = -MAX_ANGULAR_CORRECTION_VEL
|
|
279
|
+
angTgt[i] += target
|
|
280
|
+
angAct[i] = 1
|
|
281
|
+
}
|
|
282
|
+
} else if (maxErr > 0) {
|
|
283
|
+
// Bilateral (equality) if any violated axis is locked — a locked axis
|
|
284
|
+
// is a joint, not a stop. Unilateral otherwise.
|
|
285
|
+
const bilateral =
|
|
286
|
+
(tx !== ex && con.angularMin[0] === con.angularMax[0]) ||
|
|
287
|
+
(ty !== ey && con.angularMin[1] === con.angularMax[1]) ||
|
|
288
|
+
(tz !== ez && con.angularMin[2] === con.angularMax[2])
|
|
289
|
+
// The decomposition above satisfies R_rel^T = Rx(x)·Ry(y)·Rz(z), so
|
|
290
|
+
// u = qx·qy·qz is conj(q_rel) and the error rotation (current →
|
|
291
|
+
// clamped target, expressed in TA's frame) is q_E = conj(u_t) ⊗ u.
|
|
292
|
+
eulerXYZQuatInto(ex, ey, ez, _quatScratchA)
|
|
293
|
+
eulerXYZQuatInto(tx, ty, tz, _quatScratchB)
|
|
294
|
+
const ux = _quatScratchA[0], uy = _quatScratchA[1], uz = _quatScratchA[2], uw = _quatScratchA[3]
|
|
295
|
+
const vx = _quatScratchB[0], vy = _quatScratchB[1], vz = _quatScratchB[2], vw = _quatScratchB[3]
|
|
296
|
+
// q_E = conj(v) ⊗ u
|
|
297
|
+
let qex = vw * ux - vx * uw - vy * uz + vz * uy
|
|
298
|
+
let qey = vw * uy + vx * uz - vy * uw - vz * ux
|
|
299
|
+
let qez = vw * uz - vx * uy + vy * ux - vz * uw
|
|
300
|
+
let qew = vw * uw + vx * ux + vy * uy + vz * uz
|
|
301
|
+
if (qew < 0) { qex = -qex; qey = -qey; qez = -qez; qew = -qew }
|
|
302
|
+
const sinHalf = Math.sqrt(qex * qex + qey * qey + qez * qez)
|
|
303
|
+
if (sinHalf > 1e-6) {
|
|
304
|
+
const angle = 2 * Math.atan2(sinHalf, qew)
|
|
305
|
+
const invS = 1 / sinHalf
|
|
306
|
+
const axx = qex * invS, axy = qey * invS, axz = qez * invS
|
|
307
|
+
// Axis lives in TA's frame; TA's basis columns map it to world.
|
|
308
|
+
const lim = con.cacheAngLimAxis
|
|
309
|
+
lim[0] = _TA[0] * axx + _TA[4] * axy + _TA[8] * axz
|
|
310
|
+
lim[1] = _TA[1] * axx + _TA[5] * axy + _TA[9] * axz
|
|
311
|
+
lim[2] = _TA[2] * axx + _TA[6] * axy + _TA[10] * axz
|
|
312
|
+
let target = angle * STOP_ERP * invDt
|
|
313
|
+
if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
|
|
314
|
+
con.cacheAngLimTarget = target
|
|
315
|
+
con.cacheAngLimActive = bilateral ? 1 : 2
|
|
316
|
+
}
|
|
223
317
|
}
|
|
224
|
-
angTgt[i] = target
|
|
225
|
-
angAct[i] = angDenom > 0 ? active : 0
|
|
226
318
|
}
|
|
319
|
+
con.cacheAngLimImp = 0
|
|
227
320
|
}
|
|
228
321
|
|
|
229
322
|
// ITER: read cache, compute relVel from current lv/av, apply impulse.
|
|
@@ -266,12 +359,40 @@ function iterateConstraint(
|
|
|
266
359
|
const dvy = vBy - vAy
|
|
267
360
|
const dvz = vBz - vAz
|
|
268
361
|
|
|
362
|
+
const sprAct = con.cacheLinSpringActive
|
|
363
|
+
const sprTgt = con.cacheLinSpringTarget
|
|
364
|
+
const limImp = con.cacheLinLimitImp
|
|
269
365
|
for (let i = 0; i < 3; i++) {
|
|
270
|
-
if (!act[i]) continue
|
|
366
|
+
if (!act[i] && !sprAct[i]) continue
|
|
271
367
|
const o = i * 3
|
|
272
368
|
const axx = axes[o + 0], axy = axes[o + 1], axz = axes[o + 2]
|
|
273
369
|
const relVel = dvx * axx + dvy * axy + dvz * axz
|
|
274
|
-
|
|
370
|
+
let j = 0
|
|
371
|
+
|
|
372
|
+
// Limit row. Locked axes (act 1) are bilateral equality joints; ranged
|
|
373
|
+
// axes in violation (act 2) are unilateral stops — accumulated impulse
|
|
374
|
+
// clamped to the corrective sign, so the stop pushes back into range but
|
|
375
|
+
// never pulls deeper or brakes natural recovery (a bilateral stop acts
|
|
376
|
+
// as a motor and pumps energy into swinging cloth).
|
|
377
|
+
if (act[i]) {
|
|
378
|
+
const target = tgt[i]
|
|
379
|
+
let dImp = LIMIT_SOFTNESS_LINEAR * (target - relVel) * jac[i]
|
|
380
|
+
if (act[i] === 2) {
|
|
381
|
+
const old = limImp[i]
|
|
382
|
+
let next = old + dImp
|
|
383
|
+
if (target > 0 ? next < 0 : next > 0) next = 0
|
|
384
|
+
dImp = next - old
|
|
385
|
+
limImp[i] = next
|
|
386
|
+
}
|
|
387
|
+
j += dImp
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Spring row: velocity-target drive; the deadbeat k-clamp at setup
|
|
391
|
+
// bounds its aggression.
|
|
392
|
+
if (sprAct[i]) {
|
|
393
|
+
j += (sprTgt[i] - relVel) * jac[i]
|
|
394
|
+
}
|
|
395
|
+
|
|
275
396
|
if (j === 0) continue
|
|
276
397
|
if (imA > 0) {
|
|
277
398
|
lv[ai + 0] -= j * imA * axx
|
|
@@ -305,6 +426,8 @@ function iterateConstraint(
|
|
|
305
426
|
const o = i * 3
|
|
306
427
|
const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2]
|
|
307
428
|
const relAv = dax * axx + day * axy + daz * axz
|
|
429
|
+
// Spring drive (deadbeat-clamped at setup) plus, for small violations,
|
|
430
|
+
// the folded per-axis limit correction.
|
|
308
431
|
const j = (angTgt[i] - relAv) * angJacInv
|
|
309
432
|
if (j === 0) continue
|
|
310
433
|
if (iiA > 0) {
|
|
@@ -318,6 +441,39 @@ function iterateConstraint(
|
|
|
318
441
|
av[bi + 2] += j * iiB * axz
|
|
319
442
|
}
|
|
320
443
|
}
|
|
444
|
+
|
|
445
|
+
// Geodesic limit row: drive (ω_B − ω_A)·axis toward the correction target.
|
|
446
|
+
// Unilateral — the accumulated impulse can only push toward the legal
|
|
447
|
+
// region (target is always ≥ 0 along the corrective axis).
|
|
448
|
+
if (con.cacheAngLimActive) {
|
|
449
|
+
const lim = con.cacheAngLimAxis
|
|
450
|
+
const nx = lim[0], ny = lim[1], nz = lim[2]
|
|
451
|
+
// Re-read relAv — the spring rows above may have changed av.
|
|
452
|
+
const relAv =
|
|
453
|
+
(av[bi + 0] - av[ai + 0]) * nx +
|
|
454
|
+
(av[bi + 1] - av[ai + 1]) * ny +
|
|
455
|
+
(av[bi + 2] - av[ai + 2]) * nz
|
|
456
|
+
let j = LIMIT_SOFTNESS_ANGULAR * (con.cacheAngLimTarget - relAv) * angJacInv
|
|
457
|
+
if (con.cacheAngLimActive === 2) {
|
|
458
|
+
const old = con.cacheAngLimImp
|
|
459
|
+
let next = old + j
|
|
460
|
+
if (next < 0) next = 0
|
|
461
|
+
j = next - old
|
|
462
|
+
con.cacheAngLimImp = next
|
|
463
|
+
}
|
|
464
|
+
if (j !== 0) {
|
|
465
|
+
if (iiA > 0) {
|
|
466
|
+
av[ai + 0] -= j * iiA * nx
|
|
467
|
+
av[ai + 1] -= j * iiA * ny
|
|
468
|
+
av[ai + 2] -= j * iiA * nz
|
|
469
|
+
}
|
|
470
|
+
if (iiB > 0) {
|
|
471
|
+
av[bi + 0] += j * iiB * nx
|
|
472
|
+
av[bi + 1] += j * iiB * ny
|
|
473
|
+
av[bi + 2] += j * iiB * nz
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
321
477
|
}
|
|
322
478
|
|
|
323
479
|
// SETUP: pre-compute Jacobians, friction basis, and the bounce reference
|
|
@@ -552,6 +708,17 @@ function buildBodyMat(store: RigidBodyStore, i: number, out: Float32Array): void
|
|
|
552
708
|
)
|
|
553
709
|
}
|
|
554
710
|
|
|
711
|
+
// Quaternion of qx(x) ⊗ qy(y) ⊗ qz(z) (three.js 'XYZ' order).
|
|
712
|
+
function eulerXYZQuatInto(x: number, y: number, z: number, out: Float32Array): void {
|
|
713
|
+
const sx = Math.sin(x * 0.5), cx = Math.cos(x * 0.5)
|
|
714
|
+
const sy = Math.sin(y * 0.5), cy = Math.cos(y * 0.5)
|
|
715
|
+
const sz = Math.sin(z * 0.5), cz = Math.cos(z * 0.5)
|
|
716
|
+
out[0] = sx * cy * cz + cx * sy * sz
|
|
717
|
+
out[1] = cx * sy * cz - sx * cy * sz
|
|
718
|
+
out[2] = cx * cy * sz + sx * sy * cz
|
|
719
|
+
out[3] = cx * cy * cz - sx * sy * sz
|
|
720
|
+
}
|
|
721
|
+
|
|
555
722
|
// Euler XYZ from a 3×3 rotation matrix (row-major elements).
|
|
556
723
|
function matrixToEulerXYZ(
|
|
557
724
|
r00: number, r01: number,
|
package/src/physics/world.ts
CHANGED
|
@@ -101,13 +101,25 @@ export class World {
|
|
|
101
101
|
|
|
102
102
|
// 5. Integrate. Cap angular velocity at π/2 per step — a high-impulse
|
|
103
103
|
// contact spike on a low-inertia body would otherwise spin past π
|
|
104
|
-
// in one step and trash the quaternion integration.
|
|
104
|
+
// in one step and trash the quaternion integration. Linear velocity
|
|
105
|
+
// is capped at 5 units per step for the same reason: an explosion
|
|
106
|
+
// guard far above what legit cloth motion ever reaches.
|
|
105
107
|
const MAX_ANGVEL_DT = Math.PI * 0.5
|
|
108
|
+
const MAX_LINVEL_DT = 5
|
|
106
109
|
for (let i = 0; i < N; i++) {
|
|
107
110
|
if (types[i] !== RigidbodyType.Dynamic || invMass[i] <= 0) continue
|
|
108
111
|
const i3 = i * 3
|
|
109
112
|
const i4 = i * 4
|
|
110
113
|
|
|
114
|
+
const vx = lv[i3 + 0], vy = lv[i3 + 1], vz = lv[i3 + 2]
|
|
115
|
+
const vmag = Math.sqrt(vx * vx + vy * vy + vz * vz)
|
|
116
|
+
if (vmag * dt > MAX_LINVEL_DT) {
|
|
117
|
+
const scale = MAX_LINVEL_DT / (vmag * dt)
|
|
118
|
+
lv[i3 + 0] = vx * scale
|
|
119
|
+
lv[i3 + 1] = vy * scale
|
|
120
|
+
lv[i3 + 2] = vz * scale
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
pos[i3 + 0] += lv[i3 + 0] * dt
|
|
112
124
|
pos[i3 + 1] += lv[i3 + 1] * dt
|
|
113
125
|
pos[i3 + 2] += lv[i3 + 2] * dt
|
package/dist/gpu-profile.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export declare class GpuTimestamp {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
readonly supported: boolean;
|
|
4
|
-
private device;
|
|
5
|
-
private querySet;
|
|
6
|
-
private resolveBuffer;
|
|
7
|
-
private readbackBuffer;
|
|
8
|
-
private nextSlot;
|
|
9
|
-
private frameLabels;
|
|
10
|
-
private inFlight;
|
|
11
|
-
private pendingLabels;
|
|
12
|
-
constructor(device: GPUDevice);
|
|
13
|
-
private ensureCreated;
|
|
14
|
-
beginFrame(): void;
|
|
15
|
-
wrap(desc: GPURenderPassDescriptor, label: string): GPURenderPassDescriptor;
|
|
16
|
-
endFrame(encoder: GPUCommandEncoder): void;
|
|
17
|
-
afterSubmit(): void;
|
|
18
|
-
}
|
|
19
|
-
//# sourceMappingURL=gpu-profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gpu-profile.d.ts","sourceRoot":"","sources":["../src/gpu-profile.ts"],"names":[],"mappings":"AAoBA,qBAAa,YAAY;IACvB,OAAO,UAAQ;IACf,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAQ;IAKxB,OAAO,CAAC,aAAa,CAAwB;gBAEjC,MAAM,EAAE,SAAS;IAK7B,OAAO,CAAC,aAAa;IAiBrB,UAAU,IAAI,IAAI;IAWlB,IAAI,CAAC,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB;IAqB3E,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAW1C,WAAW,IAAI,IAAI;CAwBpB"}
|
package/dist/gpu-profile.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// WebGPU timestamp-query profiling. Records pass durations into the same
|
|
2
|
-
// profiler module as CPU phases (so the snapshot table mixes both with a
|
|
3
|
-
// "gpu." prefix). Read-only — no effect on rendering output.
|
|
4
|
-
//
|
|
5
|
-
// Pattern (per spec):
|
|
6
|
-
// 1. Each render pass declares timestampWrites { begin, end } slot indices.
|
|
7
|
-
// 2. After all passes, encoder.resolveQuerySet writes uint64 ns timestamps
|
|
8
|
-
// into a QUERY_RESOLVE buffer.
|
|
9
|
-
// 3. We copy the resolve buffer into a MAP_READ readback buffer.
|
|
10
|
-
// 4. Submit. After the GPU finishes, mapAsync resolves; we read durations
|
|
11
|
-
// and record them.
|
|
12
|
-
//
|
|
13
|
-
// One readback buffer with an in-flight gate: if mapAsync hasn't resolved
|
|
14
|
-
// from a previous frame, we skip the copy this frame (no contention, just
|
|
15
|
-
// one frame of dropped sampling).
|
|
16
|
-
import { profiler } from "./physics/profile";
|
|
17
|
-
const MAX_PASSES_PER_FRAME = 64;
|
|
18
|
-
export class GpuTimestamp {
|
|
19
|
-
constructor(device) {
|
|
20
|
-
this.enabled = false;
|
|
21
|
-
this.querySet = null;
|
|
22
|
-
this.resolveBuffer = null;
|
|
23
|
-
this.readbackBuffer = null;
|
|
24
|
-
this.nextSlot = 0;
|
|
25
|
-
this.frameLabels = [];
|
|
26
|
-
this.inFlight = false;
|
|
27
|
-
// Set inside endFrame() when a copy was queued this frame; consumed by
|
|
28
|
-
// afterSubmit() to kick mapAsync. mapAsync must run AFTER queue.submit —
|
|
29
|
-
// calling it before submit puts the buffer into "mapping pending" state,
|
|
30
|
-
// which makes the copy in the encoder illegal.
|
|
31
|
-
this.pendingLabels = null;
|
|
32
|
-
this.device = device;
|
|
33
|
-
this.supported = device.features.has("timestamp-query");
|
|
34
|
-
}
|
|
35
|
-
ensureCreated() {
|
|
36
|
-
if (this.querySet || !this.supported)
|
|
37
|
-
return;
|
|
38
|
-
const slots = MAX_PASSES_PER_FRAME * 2;
|
|
39
|
-
this.querySet = this.device.createQuerySet({ type: "timestamp", count: slots });
|
|
40
|
-
this.resolveBuffer = this.device.createBuffer({
|
|
41
|
-
label: "gpu-timestamp-resolve",
|
|
42
|
-
size: slots * 8,
|
|
43
|
-
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
44
|
-
});
|
|
45
|
-
this.readbackBuffer = this.device.createBuffer({
|
|
46
|
-
label: "gpu-timestamp-readback",
|
|
47
|
-
size: slots * 8,
|
|
48
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
// Called once at the top of each render() before any pass wrap.
|
|
52
|
-
beginFrame() {
|
|
53
|
-
if (!this.enabled || !this.supported)
|
|
54
|
-
return;
|
|
55
|
-
this.ensureCreated();
|
|
56
|
-
this.nextSlot = 0;
|
|
57
|
-
this.frameLabels = [];
|
|
58
|
-
}
|
|
59
|
-
// Wrap a render-pass descriptor with timestampWrites for `label`. If
|
|
60
|
-
// profiling is off / unsupported / out of slots, returns desc unchanged.
|
|
61
|
-
// The same label called multiple times (e.g. each bloom mip) is fine —
|
|
62
|
-
// the profiler accumulates by label.
|
|
63
|
-
wrap(desc, label) {
|
|
64
|
-
if (!this.enabled || !this.querySet)
|
|
65
|
-
return desc;
|
|
66
|
-
if (this.nextSlot + 2 > MAX_PASSES_PER_FRAME * 2)
|
|
67
|
-
return desc;
|
|
68
|
-
const begin = this.nextSlot;
|
|
69
|
-
const end = this.nextSlot + 1;
|
|
70
|
-
this.nextSlot += 2;
|
|
71
|
-
this.frameLabels.push(label);
|
|
72
|
-
return {
|
|
73
|
-
...desc,
|
|
74
|
-
timestampWrites: {
|
|
75
|
-
querySet: this.querySet,
|
|
76
|
-
beginningOfPassWriteIndex: begin,
|
|
77
|
-
endOfPassWriteIndex: end,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
// Call on the encoder after all passes, BEFORE encoder.finish(). Adds
|
|
82
|
-
// resolveQuerySet + copyBufferToBuffer commands. mapAsync is deferred to
|
|
83
|
-
// afterSubmit() — calling it before submit transitions the readback buffer
|
|
84
|
-
// into "mapping pending" state, which makes the queued copy illegal.
|
|
85
|
-
endFrame(encoder) {
|
|
86
|
-
if (!this.enabled || !this.querySet || this.frameLabels.length === 0)
|
|
87
|
-
return;
|
|
88
|
-
const count = this.frameLabels.length * 2;
|
|
89
|
-
encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
|
|
90
|
-
if (this.inFlight)
|
|
91
|
-
return;
|
|
92
|
-
encoder.copyBufferToBuffer(this.resolveBuffer, 0, this.readbackBuffer, 0, count * 8);
|
|
93
|
-
this.pendingLabels = this.frameLabels.slice();
|
|
94
|
-
}
|
|
95
|
-
// Call AFTER device.queue.submit(). Kicks mapAsync if a copy was queued
|
|
96
|
-
// this frame; the .then() reads durations and unmaps.
|
|
97
|
-
afterSubmit() {
|
|
98
|
-
const labels = this.pendingLabels;
|
|
99
|
-
if (!labels)
|
|
100
|
-
return;
|
|
101
|
-
this.pendingLabels = null;
|
|
102
|
-
this.inFlight = true;
|
|
103
|
-
const readback = this.readbackBuffer;
|
|
104
|
-
void readback.mapAsync(GPUMapMode.READ).then(() => {
|
|
105
|
-
const arr = new BigInt64Array(readback.getMappedRange());
|
|
106
|
-
for (let i = 0; i < labels.length; i++) {
|
|
107
|
-
const start = arr[i * 2];
|
|
108
|
-
const end = arr[i * 2 + 1];
|
|
109
|
-
// Per spec, timestamps are nanoseconds.
|
|
110
|
-
const ms = Number(end - start) / 1e6;
|
|
111
|
-
if (ms >= 0 && ms < 1000)
|
|
112
|
-
profiler.record("gpu." + labels[i], ms);
|
|
113
|
-
}
|
|
114
|
-
readback.unmap();
|
|
115
|
-
this.inFlight = false;
|
|
116
|
-
}, () => {
|
|
117
|
-
this.inFlight = false;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
declare class PhysicsProfiler {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
private sums;
|
|
4
|
-
private counts;
|
|
5
|
-
begin(): number;
|
|
6
|
-
end(label: string, t0: number): void;
|
|
7
|
-
record(label: string, ms: number): void;
|
|
8
|
-
snapshot(): Record<string, {
|
|
9
|
-
avgMs: number;
|
|
10
|
-
calls: number;
|
|
11
|
-
totalMs: number;
|
|
12
|
-
}>;
|
|
13
|
-
reset(): void;
|
|
14
|
-
}
|
|
15
|
-
export declare const profiler: PhysicsProfiler;
|
|
16
|
-
export type ProfileSnapshot = ReturnType<PhysicsProfiler["snapshot"]>;
|
|
17
|
-
export {};
|
|
18
|
-
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/physics/profile.ts"],"names":[],"mappings":"AAIA,cAAM,eAAe;IACnB,OAAO,UAAQ;IACf,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,MAAM,CAA8C;IAE5D,KAAK,IAAI,MAAM;IAIf,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IASpC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAQvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAU7E,KAAK,IAAI,IAAI;CAId;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA;AAC7C,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA"}
|
package/dist/physics/profile.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Per-phase wall-clock accumulator. Off by default — when `enabled` is false,
|
|
2
|
-
// begin()/end() short-circuit so production has zero overhead. Toggle via
|
|
3
|
-
// `RezePhysics.setProfileEnabled(true)` and read with `getProfile()`.
|
|
4
|
-
class PhysicsProfiler {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.enabled = false;
|
|
7
|
-
this.sums = Object.create(null);
|
|
8
|
-
this.counts = Object.create(null);
|
|
9
|
-
}
|
|
10
|
-
begin() {
|
|
11
|
-
return this.enabled ? performance.now() : 0;
|
|
12
|
-
}
|
|
13
|
-
end(label, t0) {
|
|
14
|
-
if (!this.enabled)
|
|
15
|
-
return;
|
|
16
|
-
const dt = performance.now() - t0;
|
|
17
|
-
this.sums[label] = (this.sums[label] ?? 0) + dt;
|
|
18
|
-
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
19
|
-
}
|
|
20
|
-
// Direct accumulator for durations not measured via begin()/end() — used by
|
|
21
|
-
// GPU timestamp readback, which arrives async after the frame has ended.
|
|
22
|
-
record(label, ms) {
|
|
23
|
-
if (!this.enabled)
|
|
24
|
-
return;
|
|
25
|
-
this.sums[label] = (this.sums[label] ?? 0) + ms;
|
|
26
|
-
this.counts[label] = (this.counts[label] ?? 0) + 1;
|
|
27
|
-
}
|
|
28
|
-
// Snapshot since last reset(). avgMs is per call; totalMs is the cumulative
|
|
29
|
-
// window cost — divide by elapsed wall time to get a "% of frame budget".
|
|
30
|
-
snapshot() {
|
|
31
|
-
const out = {};
|
|
32
|
-
for (const k in this.sums) {
|
|
33
|
-
const total = this.sums[k];
|
|
34
|
-
const calls = this.counts[k];
|
|
35
|
-
out[k] = { avgMs: calls > 0 ? total / calls : 0, calls, totalMs: total };
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
reset() {
|
|
40
|
-
this.sums = Object.create(null);
|
|
41
|
-
this.counts = Object.create(null);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export const profiler = new PhysicsProfiler();
|