reze-engine 0.16.1 → 0.16.3
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/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -5
- package/dist/model.d.ts +4 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +14 -1
- package/dist/physics/body.d.ts +1 -0
- package/dist/physics/body.d.ts.map +1 -1
- package/dist/physics/body.js +2 -0
- package/dist/physics/constraint.d.ts +8 -1
- package/dist/physics/constraint.d.ts.map +1 -1
- package/dist/physics/constraint.js +13 -1
- package/dist/physics/physics.d.ts +15 -1
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +413 -48
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +223 -39
- package/dist/physics/types.d.ts +2 -1
- package/dist/physics/types.d.ts.map +1 -1
- package/dist/physics/types.js +5 -0
- package/dist/physics/world.d.ts +3 -0
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +31 -4
- 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/pmx-loader.d.ts +2 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +37 -20
- package/dist/shaders/materials/body.d.ts +1 -1
- package/dist/shaders/materials/body.d.ts.map +1 -1
- package/dist/shaders/materials/body.js +90 -85
- package/dist/shaders/materials/cloth_rough.d.ts +1 -1
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
- package/dist/shaders/materials/cloth_rough.js +1 -1
- package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
- package/dist/shaders/materials/cloth_smooth.js +2 -2
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +10 -0
- package/dist/shaders/materials/default.d.ts +1 -1
- package/dist/shaders/materials/default.d.ts.map +1 -1
- package/dist/shaders/materials/default.js +37 -32
- package/dist/shaders/materials/eye.d.ts +1 -1
- package/dist/shaders/materials/eye.d.ts.map +1 -1
- package/dist/shaders/materials/eye.js +40 -35
- package/dist/shaders/materials/face.d.ts +1 -1
- package/dist/shaders/materials/face.d.ts.map +1 -1
- package/dist/shaders/materials/face.js +90 -85
- package/dist/shaders/materials/hair.d.ts +1 -1
- package/dist/shaders/materials/hair.d.ts.map +1 -1
- package/dist/shaders/materials/hair.js +85 -75
- package/dist/shaders/materials/metal.d.ts +1 -1
- package/dist/shaders/materials/metal.d.ts.map +1 -1
- package/dist/shaders/materials/metal.js +1 -1
- package/dist/shaders/materials/stockings.d.ts +1 -1
- package/dist/shaders/materials/stockings.d.ts.map +1 -1
- package/dist/shaders/materials/stockings.js +1 -1
- 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/engine.ts +36 -21
- package/src/model.ts +18 -1
- package/src/physics/body.ts +5 -0
- package/src/physics/constraint.ts +35 -6
- package/src/physics/physics.ts +423 -56
- package/src/physics/solver.ts +213 -38
- package/src/physics/types.ts +10 -2
- package/src/physics/world.ts +33 -4
- package/src/pmx-loader.ts +38 -20
- package/src/shaders/materials/body.ts +97 -92
- package/src/shaders/materials/cloth_rough.ts +1 -1
- package/src/shaders/materials/cloth_smooth.ts +2 -2
- package/src/shaders/materials/common.ts +10 -0
- package/src/shaders/materials/default.ts +45 -40
- package/src/shaders/materials/eye.ts +48 -43
- package/src/shaders/materials/face.ts +97 -92
- package/src/shaders/materials/hair.ts +92 -82
- package/src/shaders/materials/metal.ts +1 -1
- package/src/shaders/materials/stockings.ts +1 -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,40 @@ 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
|
+
// A spring on a locked axis is redundant — the bilateral limit row
|
|
186
|
+
// already welds the DOF, and driving it twice overshoots every
|
|
187
|
+
// iteration (PMX rigs routinely put k=100000 springs on locked axes,
|
|
188
|
+
// which turned welded weight-bodies into energy pumps).
|
|
189
|
+
if (con.springEnabled[i] && denom > 0 && lo !== hi) {
|
|
190
|
+
// Clamp k to the deadbeat limit: an explicit spring with k·dt² > 1
|
|
191
|
+
// overshoots equilibrium every step and pumps energy — the classic
|
|
192
|
+
// pre-Spring2 Bullet 6dof instability this port inherited.
|
|
193
|
+
const k = Math.min(con.springStiffness[i], invDt * invDt)
|
|
194
|
+
const serr = curr - con.equilibriumPoint[i]
|
|
195
|
+
con.cacheLinSpringTarget[i] = -k * serr * dt
|
|
196
|
+
con.cacheLinSpringActive[i] = 1
|
|
197
|
+
} else {
|
|
198
|
+
con.cacheLinSpringActive[i] = 0
|
|
199
|
+
}
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
// Angular: TA^T · TB → Euler XYZ; axes from TA.col2 × TB.col0.
|
|
@@ -198,32 +235,93 @@ function setupConstraint(
|
|
|
198
235
|
const angDenom = iiA + iiB
|
|
199
236
|
con.cacheAngJacInv = angDenom > 0 ? 1 / angDenom : 0
|
|
200
237
|
|
|
238
|
+
// Per-axis rows carry only the springs. Sign flip vs linear:
|
|
239
|
+
// d(angDiff)/dt = −(ω_B − ω_A)·ax.
|
|
201
240
|
const angTgt = con.cacheAngTargetVel
|
|
202
241
|
const angAct = con.cacheAngActive
|
|
203
242
|
for (let i = 0; i < 3; i++) {
|
|
204
243
|
const idx = i + 3
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
target = err * STOP_ERP * invDt
|
|
217
|
-
active = 1
|
|
218
|
-
}
|
|
244
|
+
// Springs on locked axes are skipped — the limit row welds those, and
|
|
245
|
+
// double-driving a DOF overshoots every iteration (see the linear loop).
|
|
246
|
+
if (con.springEnabled[idx] && angDenom > 0 && con.angularMin[i] !== con.angularMax[i]) {
|
|
247
|
+
// Same deadbeat clamp as the linear springs.
|
|
248
|
+
const k = Math.min(con.springStiffness[idx], invDt * invDt)
|
|
249
|
+
const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
|
|
250
|
+
angTgt[i] = k * serr * dt
|
|
251
|
+
angAct[i] = 1
|
|
252
|
+
} else {
|
|
253
|
+
angTgt[i] = 0
|
|
254
|
+
angAct[i] = 0
|
|
219
255
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Angular limit handling is hybrid. Small violations (the resting-cloth
|
|
259
|
+
// regime) use per-axis euler rows — they converge cleanly and keep resting
|
|
260
|
+
// cloth dead still. Large violations switch to a single geodesic row toward
|
|
261
|
+
// the euler-clamped target: per-axis euler rows (the Bullet-2.7x approach
|
|
262
|
+
// this port used) become geometrically inconsistent for large errors — near
|
|
263
|
+
// the asin singularity they chase phantom errors and pump angular velocity
|
|
264
|
+
// into the chain instead of converging.
|
|
265
|
+
con.cacheAngLimActive = 0
|
|
266
|
+
if (angDenom > 0) {
|
|
267
|
+
const ex = _angDiffScratch[0], ey = _angDiffScratch[1], ez = _angDiffScratch[2]
|
|
268
|
+
// Free axes (min > max) follow the current angle, i.e. no correction.
|
|
269
|
+
let tx = ex, ty = ey, tz = ez
|
|
270
|
+
if (con.angularMin[0] <= con.angularMax[0]) tx = ex < con.angularMin[0] ? con.angularMin[0] : ex > con.angularMax[0] ? con.angularMax[0] : ex
|
|
271
|
+
if (con.angularMin[1] <= con.angularMax[1]) ty = ey < con.angularMin[1] ? con.angularMin[1] : ey > con.angularMax[1] ? con.angularMax[1] : ey
|
|
272
|
+
if (con.angularMin[2] <= con.angularMax[2]) tz = ez < con.angularMin[2] ? con.angularMin[2] : ez > con.angularMax[2] ? con.angularMax[2] : ez
|
|
273
|
+
const errX = ex - tx, errY = ey - ty, errZ = ez - tz
|
|
274
|
+
const maxErr = Math.max(Math.abs(errX), Math.abs(errY), Math.abs(errZ))
|
|
275
|
+
if (maxErr > 0 && maxErr < GEODESIC_THRESHOLD) {
|
|
276
|
+
// Per-axis euler limit rows, folded into the per-axis row set (the
|
|
277
|
+
// spring impulse clamp must not bound a limit row — lift it).
|
|
278
|
+
for (let i = 0; i < 3; i++) {
|
|
279
|
+
const err = i === 0 ? errX : i === 1 ? errY : errZ
|
|
280
|
+
if (err === 0) continue
|
|
281
|
+
let target = err * STOP_ERP * invDt
|
|
282
|
+
if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
|
|
283
|
+
else if (target < -MAX_ANGULAR_CORRECTION_VEL) target = -MAX_ANGULAR_CORRECTION_VEL
|
|
284
|
+
angTgt[i] += target
|
|
285
|
+
angAct[i] = 1
|
|
286
|
+
}
|
|
287
|
+
} else if (maxErr > 0) {
|
|
288
|
+
// Bilateral (equality) if any violated axis is locked — a locked axis
|
|
289
|
+
// is a joint, not a stop. Unilateral otherwise.
|
|
290
|
+
const bilateral =
|
|
291
|
+
(tx !== ex && con.angularMin[0] === con.angularMax[0]) ||
|
|
292
|
+
(ty !== ey && con.angularMin[1] === con.angularMax[1]) ||
|
|
293
|
+
(tz !== ez && con.angularMin[2] === con.angularMax[2])
|
|
294
|
+
// The decomposition above satisfies R_rel^T = Rx(x)·Ry(y)·Rz(z), so
|
|
295
|
+
// u = qx·qy·qz is conj(q_rel) and the error rotation (current →
|
|
296
|
+
// clamped target, expressed in TA's frame) is q_E = conj(u_t) ⊗ u.
|
|
297
|
+
eulerXYZQuatInto(ex, ey, ez, _quatScratchA)
|
|
298
|
+
eulerXYZQuatInto(tx, ty, tz, _quatScratchB)
|
|
299
|
+
const ux = _quatScratchA[0], uy = _quatScratchA[1], uz = _quatScratchA[2], uw = _quatScratchA[3]
|
|
300
|
+
const vx = _quatScratchB[0], vy = _quatScratchB[1], vz = _quatScratchB[2], vw = _quatScratchB[3]
|
|
301
|
+
// q_E = conj(v) ⊗ u
|
|
302
|
+
let qex = vw * ux - vx * uw - vy * uz + vz * uy
|
|
303
|
+
let qey = vw * uy + vx * uz - vy * uw - vz * ux
|
|
304
|
+
let qez = vw * uz - vx * uy + vy * ux - vz * uw
|
|
305
|
+
let qew = vw * uw + vx * ux + vy * uy + vz * uz
|
|
306
|
+
if (qew < 0) { qex = -qex; qey = -qey; qez = -qez; qew = -qew }
|
|
307
|
+
const sinHalf = Math.sqrt(qex * qex + qey * qey + qez * qez)
|
|
308
|
+
if (sinHalf > 1e-6) {
|
|
309
|
+
const angle = 2 * Math.atan2(sinHalf, qew)
|
|
310
|
+
const invS = 1 / sinHalf
|
|
311
|
+
const axx = qex * invS, axy = qey * invS, axz = qez * invS
|
|
312
|
+
// Axis lives in TA's frame; TA's basis columns map it to world.
|
|
313
|
+
const lim = con.cacheAngLimAxis
|
|
314
|
+
lim[0] = _TA[0] * axx + _TA[4] * axy + _TA[8] * axz
|
|
315
|
+
lim[1] = _TA[1] * axx + _TA[5] * axy + _TA[9] * axz
|
|
316
|
+
lim[2] = _TA[2] * axx + _TA[6] * axy + _TA[10] * axz
|
|
317
|
+
let target = angle * STOP_ERP * invDt
|
|
318
|
+
if (target > MAX_ANGULAR_CORRECTION_VEL) target = MAX_ANGULAR_CORRECTION_VEL
|
|
319
|
+
con.cacheAngLimTarget = target
|
|
320
|
+
con.cacheAngLimActive = bilateral ? 1 : 2
|
|
321
|
+
}
|
|
223
322
|
}
|
|
224
|
-
angTgt[i] = target
|
|
225
|
-
angAct[i] = angDenom > 0 ? active : 0
|
|
226
323
|
}
|
|
324
|
+
con.cacheAngLimImp = 0
|
|
227
325
|
}
|
|
228
326
|
|
|
229
327
|
// ITER: read cache, compute relVel from current lv/av, apply impulse.
|
|
@@ -266,12 +364,43 @@ function iterateConstraint(
|
|
|
266
364
|
const dvy = vBy - vAy
|
|
267
365
|
const dvz = vBz - vAz
|
|
268
366
|
|
|
367
|
+
const sprAct = con.cacheLinSpringActive
|
|
368
|
+
const sprTgt = con.cacheLinSpringTarget
|
|
369
|
+
const limImp = con.cacheLinLimitImp
|
|
269
370
|
for (let i = 0; i < 3; i++) {
|
|
270
|
-
if (!act[i]) continue
|
|
371
|
+
if (!act[i] && !sprAct[i]) continue
|
|
271
372
|
const o = i * 3
|
|
272
373
|
const axx = axes[o + 0], axy = axes[o + 1], axz = axes[o + 2]
|
|
273
374
|
const relVel = dvx * axx + dvy * axy + dvz * axz
|
|
274
|
-
|
|
375
|
+
let j = 0
|
|
376
|
+
|
|
377
|
+
// Limit row. Locked axes (act 1) are bilateral equality joints; ranged
|
|
378
|
+
// axes in violation (act 2) are unilateral stops — accumulated impulse
|
|
379
|
+
// clamped to the corrective sign, so the stop pushes back into range but
|
|
380
|
+
// never pulls deeper or brakes natural recovery (a bilateral stop acts
|
|
381
|
+
// as a motor and pumps energy into swinging cloth).
|
|
382
|
+
if (act[i]) {
|
|
383
|
+
const target = tgt[i]
|
|
384
|
+
let dImp = LIMIT_SOFTNESS_LINEAR * (target - relVel) * jac[i]
|
|
385
|
+
if (act[i] === 2) {
|
|
386
|
+
const old = limImp[i]
|
|
387
|
+
let next = old + dImp
|
|
388
|
+
if (target > 0 ? next < 0 : next > 0) next = 0
|
|
389
|
+
dImp = next - old
|
|
390
|
+
limImp[i] = next
|
|
391
|
+
}
|
|
392
|
+
j += dImp
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Spring row: velocity-target drive; the deadbeat k-clamp at setup
|
|
396
|
+
// bounds its aggression. relVel is refreshed with the limit impulse
|
|
397
|
+
// applied just above (j·denom = j / jac) — driving the spring off the
|
|
398
|
+
// stale value double-corrects the DOF and overshoots every iteration.
|
|
399
|
+
if (sprAct[i]) {
|
|
400
|
+
const relVelNow = j !== 0 ? relVel + j / jac[i] : relVel
|
|
401
|
+
j += (sprTgt[i] - relVelNow) * jac[i]
|
|
402
|
+
}
|
|
403
|
+
|
|
275
404
|
if (j === 0) continue
|
|
276
405
|
if (imA > 0) {
|
|
277
406
|
lv[ai + 0] -= j * imA * axx
|
|
@@ -305,6 +434,8 @@ function iterateConstraint(
|
|
|
305
434
|
const o = i * 3
|
|
306
435
|
const axx = angAxes[o + 0], axy = angAxes[o + 1], axz = angAxes[o + 2]
|
|
307
436
|
const relAv = dax * axx + day * axy + daz * axz
|
|
437
|
+
// Spring drive (deadbeat-clamped at setup) plus, for small violations,
|
|
438
|
+
// the folded per-axis limit correction.
|
|
308
439
|
const j = (angTgt[i] - relAv) * angJacInv
|
|
309
440
|
if (j === 0) continue
|
|
310
441
|
if (iiA > 0) {
|
|
@@ -318,6 +449,39 @@ function iterateConstraint(
|
|
|
318
449
|
av[bi + 2] += j * iiB * axz
|
|
319
450
|
}
|
|
320
451
|
}
|
|
452
|
+
|
|
453
|
+
// Geodesic limit row: drive (ω_B − ω_A)·axis toward the correction target.
|
|
454
|
+
// Unilateral — the accumulated impulse can only push toward the legal
|
|
455
|
+
// region (target is always ≥ 0 along the corrective axis).
|
|
456
|
+
if (con.cacheAngLimActive) {
|
|
457
|
+
const lim = con.cacheAngLimAxis
|
|
458
|
+
const nx = lim[0], ny = lim[1], nz = lim[2]
|
|
459
|
+
// Re-read relAv — the spring rows above may have changed av.
|
|
460
|
+
const relAv =
|
|
461
|
+
(av[bi + 0] - av[ai + 0]) * nx +
|
|
462
|
+
(av[bi + 1] - av[ai + 1]) * ny +
|
|
463
|
+
(av[bi + 2] - av[ai + 2]) * nz
|
|
464
|
+
let j = LIMIT_SOFTNESS_ANGULAR * (con.cacheAngLimTarget - relAv) * angJacInv
|
|
465
|
+
if (con.cacheAngLimActive === 2) {
|
|
466
|
+
const old = con.cacheAngLimImp
|
|
467
|
+
let next = old + j
|
|
468
|
+
if (next < 0) next = 0
|
|
469
|
+
j = next - old
|
|
470
|
+
con.cacheAngLimImp = next
|
|
471
|
+
}
|
|
472
|
+
if (j !== 0) {
|
|
473
|
+
if (iiA > 0) {
|
|
474
|
+
av[ai + 0] -= j * iiA * nx
|
|
475
|
+
av[ai + 1] -= j * iiA * ny
|
|
476
|
+
av[ai + 2] -= j * iiA * nz
|
|
477
|
+
}
|
|
478
|
+
if (iiB > 0) {
|
|
479
|
+
av[bi + 0] += j * iiB * nx
|
|
480
|
+
av[bi + 1] += j * iiB * ny
|
|
481
|
+
av[bi + 2] += j * iiB * nz
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
321
485
|
}
|
|
322
486
|
|
|
323
487
|
// SETUP: pre-compute Jacobians, friction basis, and the bounce reference
|
|
@@ -552,6 +716,17 @@ function buildBodyMat(store: RigidBodyStore, i: number, out: Float32Array): void
|
|
|
552
716
|
)
|
|
553
717
|
}
|
|
554
718
|
|
|
719
|
+
// Quaternion of qx(x) ⊗ qy(y) ⊗ qz(z) (three.js 'XYZ' order).
|
|
720
|
+
function eulerXYZQuatInto(x: number, y: number, z: number, out: Float32Array): void {
|
|
721
|
+
const sx = Math.sin(x * 0.5), cx = Math.cos(x * 0.5)
|
|
722
|
+
const sy = Math.sin(y * 0.5), cy = Math.cos(y * 0.5)
|
|
723
|
+
const sz = Math.sin(z * 0.5), cz = Math.cos(z * 0.5)
|
|
724
|
+
out[0] = sx * cy * cz + cx * sy * sz
|
|
725
|
+
out[1] = cx * sy * cz - sx * cy * sz
|
|
726
|
+
out[2] = cx * cy * sz + sx * sy * cz
|
|
727
|
+
out[3] = cx * cy * cz - sx * sy * sz
|
|
728
|
+
}
|
|
729
|
+
|
|
555
730
|
// Euler XYZ from a 3×3 rotation matrix (row-major elements).
|
|
556
731
|
function matrixToEulerXYZ(
|
|
557
732
|
r00: number, r01: number,
|
package/src/physics/types.ts
CHANGED
|
@@ -6,10 +6,15 @@ export enum RigidbodyShape {
|
|
|
6
6
|
Capsule = 2,
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// PMX physics mode: 0 = follow bone, 1 = physics, 2 = physics + bone-position
|
|
10
|
+
// alignment. Mode 2 is DYNAMIC — the loader maps it to Dynamic with
|
|
11
|
+
// `aligned: true` (bone gets rotation only, body position re-pins to the
|
|
12
|
+
// animated bone each frame). Mapping the raw byte 1:1 onto this enum froze
|
|
13
|
+
// mode-2 bodies (most modern cloth rigs, and every 胸_回転 breast body).
|
|
9
14
|
export enum RigidbodyType {
|
|
10
|
-
Static = 0,
|
|
15
|
+
Static = 0, // follows bone (anchor)
|
|
11
16
|
Dynamic = 1,
|
|
12
|
-
Kinematic = 2,
|
|
17
|
+
Kinematic = 2, // follows bone (legacy alias; loader no longer emits it)
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export interface Rigidbody {
|
|
@@ -28,6 +33,9 @@ export interface Rigidbody {
|
|
|
28
33
|
restitution: number
|
|
29
34
|
friction: number
|
|
30
35
|
type: RigidbodyType
|
|
36
|
+
// PMX mode 2: dynamic body whose bone takes rotation only; the body's
|
|
37
|
+
// position re-pins to the animated bone each frame.
|
|
38
|
+
aligned?: boolean
|
|
31
39
|
bodyOffsetMatrixInverse: Mat4 // Inverse of body offset matrix, used to sync rigidbody to bone
|
|
32
40
|
bodyOffsetMatrix?: Mat4 // Cached non-inverse for performance (computed once during initialization)
|
|
33
41
|
}
|
package/src/physics/world.ts
CHANGED
|
@@ -14,6 +14,12 @@ export class World {
|
|
|
14
14
|
readonly gravity: Vec3
|
|
15
15
|
solverIterations = 10
|
|
16
16
|
|
|
17
|
+
// Per-body damping factors pow(1−damping, dt), cached because damping and
|
|
18
|
+
// the fixed dt never change — two Math.pow per body per substep otherwise.
|
|
19
|
+
private dampCacheDt = -1
|
|
20
|
+
private linDampFactor: Float32Array | null = null
|
|
21
|
+
private angDampFactor: Float32Array | null = null
|
|
22
|
+
|
|
17
23
|
constructor(gravity: Vec3) {
|
|
18
24
|
this.gravity = new Vec3(gravity.x, gravity.y, gravity.z)
|
|
19
25
|
}
|
|
@@ -43,15 +49,26 @@ export class World {
|
|
|
43
49
|
|
|
44
50
|
// 1. Predict — gravity + damping. The pow form (vs the linear
|
|
45
51
|
// 1−damping·dt approximation) stays stable at high PMX damping
|
|
46
|
-
// values like 0.99.
|
|
52
|
+
// values like 0.99. Factors are cached (damping and dt are constant).
|
|
53
|
+
if (this.dampCacheDt !== dt || !this.linDampFactor || this.linDampFactor.length !== N) {
|
|
54
|
+
this.dampCacheDt = dt
|
|
55
|
+
this.linDampFactor = new Float32Array(N)
|
|
56
|
+
this.angDampFactor = new Float32Array(N)
|
|
57
|
+
for (let i = 0; i < N; i++) {
|
|
58
|
+
this.linDampFactor[i] = Math.pow(Math.max(0, 1 - ldamp[i]), dt)
|
|
59
|
+
this.angDampFactor[i] = Math.pow(Math.max(0, 1 - adamp[i]), dt)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const linDamp = this.linDampFactor
|
|
63
|
+
const angDamp = this.angDampFactor!
|
|
47
64
|
for (let i = 0; i < N; i++) {
|
|
48
65
|
if (types[i] !== RigidbodyType.Dynamic || invMass[i] <= 0) continue
|
|
49
66
|
const i3 = i * 3
|
|
50
67
|
lv[i3 + 0] += gx * dt
|
|
51
68
|
lv[i3 + 1] += gy * dt
|
|
52
69
|
lv[i3 + 2] += gz * dt
|
|
53
|
-
const ld =
|
|
54
|
-
const ad =
|
|
70
|
+
const ld = linDamp[i]
|
|
71
|
+
const ad = angDamp[i]
|
|
55
72
|
lv[i3 + 0] *= ld; lv[i3 + 1] *= ld; lv[i3 + 2] *= ld
|
|
56
73
|
av[i3 + 0] *= ad; av[i3 + 1] *= ad; av[i3 + 2] *= ad
|
|
57
74
|
}
|
|
@@ -101,13 +118,25 @@ export class World {
|
|
|
101
118
|
|
|
102
119
|
// 5. Integrate. Cap angular velocity at π/2 per step — a high-impulse
|
|
103
120
|
// contact spike on a low-inertia body would otherwise spin past π
|
|
104
|
-
// in one step and trash the quaternion integration.
|
|
121
|
+
// in one step and trash the quaternion integration. Linear velocity
|
|
122
|
+
// is capped at 5 units per step for the same reason: an explosion
|
|
123
|
+
// guard far above what legit cloth motion ever reaches.
|
|
105
124
|
const MAX_ANGVEL_DT = Math.PI * 0.5
|
|
125
|
+
const MAX_LINVEL_DT = 5
|
|
106
126
|
for (let i = 0; i < N; i++) {
|
|
107
127
|
if (types[i] !== RigidbodyType.Dynamic || invMass[i] <= 0) continue
|
|
108
128
|
const i3 = i * 3
|
|
109
129
|
const i4 = i * 4
|
|
110
130
|
|
|
131
|
+
const vx = lv[i3 + 0], vy = lv[i3 + 1], vz = lv[i3 + 2]
|
|
132
|
+
const vmag = Math.sqrt(vx * vx + vy * vy + vz * vz)
|
|
133
|
+
if (vmag * dt > MAX_LINVEL_DT) {
|
|
134
|
+
const scale = MAX_LINVEL_DT / (vmag * dt)
|
|
135
|
+
lv[i3 + 0] = vx * scale
|
|
136
|
+
lv[i3 + 1] = vy * scale
|
|
137
|
+
lv[i3 + 2] = vz * scale
|
|
138
|
+
}
|
|
139
|
+
|
|
111
140
|
pos[i3 + 0] += lv[i3 + 0] * dt
|
|
112
141
|
pos[i3 + 1] += lv[i3 + 1] * dt
|
|
113
142
|
pos[i3 + 2] += lv[i3 + 2] * dt
|
package/src/pmx-loader.ts
CHANGED
|
@@ -37,11 +37,22 @@ export class PmxLoader {
|
|
|
37
37
|
private vertexCount: number = 0
|
|
38
38
|
private rigidbodies: Rigidbody[] = []
|
|
39
39
|
private joints: Joint[] = []
|
|
40
|
+
private warnings: string[] = []
|
|
40
41
|
|
|
41
42
|
private constructor(buffer: ArrayBuffer) {
|
|
42
43
|
this.view = new DataView(buffer)
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Parse problems are non-fatal (the mesh may still render), but they must
|
|
47
|
+
// be queryable afterwards — a rig whose rigidbody section failed to parse
|
|
48
|
+
// silently loses physics, and console.warn alone drowns in browser log
|
|
49
|
+
// limits. Collected warnings ship on the Model (getLoadWarnings()).
|
|
50
|
+
private warn(message: string, error?: unknown): void {
|
|
51
|
+
const full = error instanceof Error ? `${message}: ${error.message}` : message
|
|
52
|
+
this.warnings.push(full)
|
|
53
|
+
console.warn(message, error ?? "")
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
static async load(url: string): Promise<Model> {
|
|
46
57
|
return PmxLoader.loadFromReader(createFetchAssetReader(), url)
|
|
47
58
|
}
|
|
@@ -82,7 +93,7 @@ export class PmxLoader {
|
|
|
82
93
|
const version = this.getFloat32()
|
|
83
94
|
if (version < 2.0 || version > 2.2) {
|
|
84
95
|
// Continue, but warn for unexpected version
|
|
85
|
-
|
|
96
|
+
this.warn(`PMX version ${version} may not be fully supported`)
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
// PMX: globals count (uint8) followed by that many bytes describing encoding and index sizes
|
|
@@ -472,7 +483,7 @@ export class PmxLoader {
|
|
|
472
483
|
}
|
|
473
484
|
this.bones = bones
|
|
474
485
|
} catch (e) {
|
|
475
|
-
|
|
486
|
+
this.warn("Error parsing bones:", e)
|
|
476
487
|
this.bones = []
|
|
477
488
|
}
|
|
478
489
|
}
|
|
@@ -487,7 +498,7 @@ export class PmxLoader {
|
|
|
487
498
|
const count = this.getInt32()
|
|
488
499
|
if (count < 0 || count > 100000) {
|
|
489
500
|
// Suspicious count, likely corrupted
|
|
490
|
-
|
|
501
|
+
this.warn(`Suspicious morph count: ${count}`)
|
|
491
502
|
this.morphs = []
|
|
492
503
|
return
|
|
493
504
|
}
|
|
@@ -623,11 +634,11 @@ export class PmxLoader {
|
|
|
623
634
|
this.morphs.push(morph)
|
|
624
635
|
} catch (e) {
|
|
625
636
|
// If we fail to read a morph, skip it
|
|
626
|
-
|
|
637
|
+
this.warn(`Error reading morph ${i}:`, e)
|
|
627
638
|
}
|
|
628
639
|
}
|
|
629
640
|
} catch (e) {
|
|
630
|
-
|
|
641
|
+
this.warn("Error parsing morphs:", e)
|
|
631
642
|
this.morphs = []
|
|
632
643
|
}
|
|
633
644
|
}
|
|
@@ -669,13 +680,13 @@ export class PmxLoader {
|
|
|
669
680
|
}
|
|
670
681
|
} catch (e) {
|
|
671
682
|
// If we fail to read a frame, stop skipping
|
|
672
|
-
|
|
683
|
+
this.warn(`Error reading display frame ${i}:`, e)
|
|
673
684
|
return false
|
|
674
685
|
}
|
|
675
686
|
}
|
|
676
687
|
return true
|
|
677
688
|
} catch (e) {
|
|
678
|
-
|
|
689
|
+
this.warn("Error skipping display frames:", e)
|
|
679
690
|
return false
|
|
680
691
|
}
|
|
681
692
|
}
|
|
@@ -685,7 +696,7 @@ export class PmxLoader {
|
|
|
685
696
|
// Note: morphs and display frames are already skipped in parse() before calling this
|
|
686
697
|
// Check bounds before reading rigidbody count
|
|
687
698
|
if (this.offset + 4 > this.view.buffer.byteLength) {
|
|
688
|
-
|
|
699
|
+
this.warn("Not enough bytes for rigidbody count")
|
|
689
700
|
this.rigidbodies = []
|
|
690
701
|
return
|
|
691
702
|
}
|
|
@@ -693,7 +704,7 @@ export class PmxLoader {
|
|
|
693
704
|
const count = this.getInt32()
|
|
694
705
|
if (count < 0 || count > 10000) {
|
|
695
706
|
// Suspicious count
|
|
696
|
-
|
|
707
|
+
this.warn(`Suspicious rigidbody count: ${count}`)
|
|
697
708
|
this.rigidbodies = []
|
|
698
709
|
return
|
|
699
710
|
}
|
|
@@ -704,7 +715,7 @@ export class PmxLoader {
|
|
|
704
715
|
try {
|
|
705
716
|
// Check bounds before reading each rigidbody
|
|
706
717
|
if (this.offset >= this.view.buffer.byteLength) {
|
|
707
|
-
|
|
718
|
+
this.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`)
|
|
708
719
|
break
|
|
709
720
|
}
|
|
710
721
|
|
|
@@ -737,7 +748,12 @@ export class PmxLoader {
|
|
|
737
748
|
const angularDamping = this.getFloat32()
|
|
738
749
|
const restitution = this.getFloat32()
|
|
739
750
|
const friction = this.getFloat32()
|
|
740
|
-
|
|
751
|
+
// PMX physics mode: 0 = follow bone, 1 = physics,
|
|
752
|
+
// 2 = physics + bone-position alignment. Mode 2 is dynamic — it
|
|
753
|
+
// must NOT be cast onto RigidbodyType (2 = Kinematic there, which
|
|
754
|
+
// freezes the body to its bone and kills physics for most modern
|
|
755
|
+
// cloth rigs and every 胸_回転 breast body).
|
|
756
|
+
const mode = this.getUint8()
|
|
741
757
|
|
|
742
758
|
this.rigidbodies.push({
|
|
743
759
|
name,
|
|
@@ -754,17 +770,18 @@ export class PmxLoader {
|
|
|
754
770
|
angularDamping,
|
|
755
771
|
restitution,
|
|
756
772
|
friction,
|
|
757
|
-
type:
|
|
773
|
+
type: mode === 0 ? RigidbodyType.Static : RigidbodyType.Dynamic,
|
|
774
|
+
aligned: mode === 2,
|
|
758
775
|
bodyOffsetMatrixInverse: Mat4.identity(),
|
|
759
776
|
})
|
|
760
777
|
} catch (e) {
|
|
761
|
-
|
|
778
|
+
this.warn(`Error reading rigidbody ${i} of ${count}:`, e)
|
|
762
779
|
// Stop parsing if we encounter an error
|
|
763
780
|
break
|
|
764
781
|
}
|
|
765
782
|
}
|
|
766
783
|
} catch (e) {
|
|
767
|
-
|
|
784
|
+
this.warn("Error parsing rigidbodies:", e)
|
|
768
785
|
this.rigidbodies = []
|
|
769
786
|
}
|
|
770
787
|
}
|
|
@@ -773,14 +790,14 @@ export class PmxLoader {
|
|
|
773
790
|
try {
|
|
774
791
|
// Check bounds before reading joint count
|
|
775
792
|
if (this.offset + 4 > this.view.buffer.byteLength) {
|
|
776
|
-
|
|
793
|
+
this.warn("Not enough bytes for joint count")
|
|
777
794
|
this.joints = []
|
|
778
795
|
return
|
|
779
796
|
}
|
|
780
797
|
|
|
781
798
|
const count = this.getInt32()
|
|
782
799
|
if (count < 0 || count > 10000) {
|
|
783
|
-
|
|
800
|
+
this.warn(`Suspicious joint count: ${count}`)
|
|
784
801
|
this.joints = []
|
|
785
802
|
return
|
|
786
803
|
}
|
|
@@ -791,7 +808,7 @@ export class PmxLoader {
|
|
|
791
808
|
try {
|
|
792
809
|
// Check bounds before reading each joint
|
|
793
810
|
if (this.offset >= this.view.buffer.byteLength) {
|
|
794
|
-
|
|
811
|
+
this.warn(`Reached end of buffer while reading joint ${i} of ${count}`)
|
|
795
812
|
break
|
|
796
813
|
}
|
|
797
814
|
|
|
@@ -857,13 +874,13 @@ export class PmxLoader {
|
|
|
857
874
|
springRotation: new Vec3(springRotX, springRotY, springRotZ), // Store as Vec3 (stiffness values)
|
|
858
875
|
})
|
|
859
876
|
} catch (e) {
|
|
860
|
-
|
|
877
|
+
this.warn(`Error reading joint ${i} of ${count}:`, e)
|
|
861
878
|
// Stop parsing if we encounter an error
|
|
862
879
|
break
|
|
863
880
|
}
|
|
864
881
|
}
|
|
865
882
|
} catch (e) {
|
|
866
|
-
|
|
883
|
+
this.warn("Error parsing joints:", e)
|
|
867
884
|
this.joints = []
|
|
868
885
|
}
|
|
869
886
|
}
|
|
@@ -1044,7 +1061,8 @@ export class PmxLoader {
|
|
|
1044
1061
|
skinning,
|
|
1045
1062
|
morphing,
|
|
1046
1063
|
this.rigidbodies,
|
|
1047
|
-
this.joints
|
|
1064
|
+
this.joints,
|
|
1065
|
+
this.warnings
|
|
1048
1066
|
)
|
|
1049
1067
|
}
|
|
1050
1068
|
|