reze-engine 0.14.0 → 0.15.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.
Files changed (93) hide show
  1. package/README.md +81 -108
  2. package/dist/engine.d.ts +1 -7
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +4 -7
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/physics/body.d.ts +30 -0
  9. package/dist/physics/body.d.ts.map +1 -0
  10. package/dist/physics/body.js +215 -0
  11. package/dist/physics/constraint.d.ts +17 -0
  12. package/dist/physics/constraint.d.ts.map +1 -0
  13. package/dist/physics/constraint.js +102 -0
  14. package/dist/physics/contact.d.ts +32 -0
  15. package/dist/physics/contact.d.ts.map +1 -0
  16. package/dist/physics/contact.js +728 -0
  17. package/dist/physics/index.d.ts +4 -0
  18. package/dist/physics/index.d.ts.map +1 -0
  19. package/dist/physics/index.js +3 -0
  20. package/dist/physics/physics.d.ts +31 -0
  21. package/dist/physics/physics.d.ts.map +1 -0
  22. package/dist/physics/physics.js +211 -0
  23. package/dist/physics/solver.d.ts +5 -0
  24. package/dist/physics/solver.d.ts.map +1 -0
  25. package/dist/physics/solver.js +416 -0
  26. package/dist/physics/types.d.ts +46 -0
  27. package/dist/physics/types.d.ts.map +1 -0
  28. package/dist/physics/types.js +12 -0
  29. package/dist/physics/world.d.ts +12 -0
  30. package/dist/physics/world.d.ts.map +1 -0
  31. package/dist/physics/world.js +146 -0
  32. package/dist/physics-debug.d.ts +30 -0
  33. package/dist/physics-debug.d.ts.map +1 -0
  34. package/dist/physics-debug.js +526 -0
  35. package/dist/shaders/materials/hair.d.ts +1 -1
  36. package/dist/shaders/materials/hair.d.ts.map +1 -1
  37. package/dist/shaders/materials/hair.js +2 -2
  38. package/dist/shaders/passes/physics-debug.d.ts +2 -0
  39. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  40. package/dist/shaders/passes/physics-debug.js +69 -0
  41. package/package.json +3 -6
  42. package/src/engine.ts +5 -9
  43. package/src/index.ts +1 -1
  44. package/src/physics/body.ts +305 -0
  45. package/src/physics/constraint.ts +151 -0
  46. package/src/physics/contact.ts +983 -0
  47. package/src/physics/index.ts +8 -0
  48. package/src/physics/physics.ts +255 -0
  49. package/src/physics/solver.ts +430 -0
  50. package/src/physics/types.ts +50 -0
  51. package/src/physics/world.ts +152 -0
  52. package/src/shaders/materials/hair.ts +2 -2
  53. package/dist/ammo-loader.d.ts +0 -3
  54. package/dist/ammo-loader.d.ts.map +0 -1
  55. package/dist/ammo-loader.js +0 -26
  56. package/dist/physics.d.ts +0 -86
  57. package/dist/physics.d.ts.map +0 -1
  58. package/dist/physics.js +0 -527
  59. package/dist/shaders/body.d.ts +0 -2
  60. package/dist/shaders/body.d.ts.map +0 -1
  61. package/dist/shaders/body.js +0 -199
  62. package/dist/shaders/classify.d.ts +0 -4
  63. package/dist/shaders/classify.d.ts.map +0 -1
  64. package/dist/shaders/classify.js +0 -12
  65. package/dist/shaders/cloth_rough.d.ts +0 -2
  66. package/dist/shaders/cloth_rough.d.ts.map +0 -1
  67. package/dist/shaders/cloth_rough.js +0 -178
  68. package/dist/shaders/cloth_smooth.d.ts +0 -2
  69. package/dist/shaders/cloth_smooth.d.ts.map +0 -1
  70. package/dist/shaders/cloth_smooth.js +0 -174
  71. package/dist/shaders/default.d.ts +0 -2
  72. package/dist/shaders/default.d.ts.map +0 -1
  73. package/dist/shaders/default.js +0 -171
  74. package/dist/shaders/eye.d.ts +0 -2
  75. package/dist/shaders/eye.d.ts.map +0 -1
  76. package/dist/shaders/eye.js +0 -146
  77. package/dist/shaders/face.d.ts +0 -2
  78. package/dist/shaders/face.d.ts.map +0 -1
  79. package/dist/shaders/face.js +0 -199
  80. package/dist/shaders/hair.d.ts +0 -2
  81. package/dist/shaders/hair.d.ts.map +0 -1
  82. package/dist/shaders/hair.js +0 -176
  83. package/dist/shaders/metal.d.ts +0 -2
  84. package/dist/shaders/metal.d.ts.map +0 -1
  85. package/dist/shaders/metal.js +0 -174
  86. package/dist/shaders/nodes.d.ts +0 -2
  87. package/dist/shaders/nodes.d.ts.map +0 -1
  88. package/dist/shaders/nodes.js +0 -456
  89. package/dist/shaders/stockings.d.ts +0 -2
  90. package/dist/shaders/stockings.d.ts.map +0 -1
  91. package/dist/shaders/stockings.js +0 -244
  92. package/src/ammo-loader.ts +0 -31
  93. package/src/physics.ts +0 -706
package/src/physics.ts DELETED
@@ -1,706 +0,0 @@
1
- import { Quat, Vec3, Mat4 } from "./math"
2
-
3
- // Physics-local scratch pool for per-frame sync (syncFromBones, applyAmmoRigidbodiesToBones).
4
- // Each method uses only these slots and completes synchronously before the next is called.
5
- const _physMat: Float32Array[] = [
6
- new Float32Array(16), new Float32Array(16), new Float32Array(16),
7
- ]
8
- const _physQuat = new Quat(0, 0, 0, 1)
9
- import { loadAmmo } from "./ammo-loader"
10
- import type { AmmoInstance } from "@fred3d/ammo"
11
-
12
- export enum RigidbodyShape {
13
- Sphere = 0,
14
- Box = 1,
15
- Capsule = 2,
16
- }
17
-
18
- export enum RigidbodyType {
19
- Static = 0,
20
- Dynamic = 1,
21
- Kinematic = 2,
22
- }
23
-
24
- export interface Rigidbody {
25
- name: string
26
- englishName: string
27
- boneIndex: number
28
- group: number
29
- collisionMask: number
30
- shape: RigidbodyShape
31
- size: Vec3
32
- shapePosition: Vec3 // Bind pose world space position from PMX
33
- shapeRotation: Vec3 // Bind pose world space rotation (Euler angles) from PMX
34
- mass: number
35
- linearDamping: number
36
- angularDamping: number
37
- restitution: number
38
- friction: number
39
- type: RigidbodyType
40
- bodyOffsetMatrixInverse: Mat4 // Inverse of body offset matrix, used to sync rigidbody to bone
41
- bodyOffsetMatrix?: Mat4 // Cached non-inverse for performance (computed once during initialization)
42
- }
43
-
44
- export interface Joint {
45
- name: string
46
- englishName: string
47
- type: number
48
- rigidbodyIndexA: number
49
- rigidbodyIndexB: number
50
- position: Vec3
51
- rotation: Vec3 // Euler angles in radians
52
- positionMin: Vec3
53
- positionMax: Vec3
54
- rotationMin: Vec3 // Euler angles in radians
55
- rotationMax: Vec3 // Euler angles in radians
56
- springPosition: Vec3
57
- springRotation: Vec3 // Spring stiffness values
58
- }
59
-
60
- export interface PhysicsOptions {
61
- // Joint name keywords for per-joint Bullet 2.75 constraint solver behavior.
62
- // Joints whose name contains any keyword get m_useOffsetForConstraintFrame
63
- // disabled (matching Bullet 2.75). All others keep the stable Ammo 2.82+ default.
64
- constraintSolverKeywords?: string[]
65
- }
66
-
67
- export class Physics {
68
- private rigidbodies: Rigidbody[]
69
- private joints: Joint[]
70
- private gravity: Vec3 = new Vec3(0, -98, 0) // Gravity acceleration (cm/s²), MMD-style default
71
- private constraintSolverPattern: RegExp | null = null
72
- private ammoInitialized = false
73
- private ammoPromise: Promise<AmmoInstance> | null = null
74
- private ammo: AmmoInstance | null = null
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- private dynamicsWorld: any = null // btDiscreteDynamicsWorld
77
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
- private ammoRigidbodies: any[] = [] // btRigidBody instances
79
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
- private ammoConstraints: any[] = [] // btTypedConstraint instances
81
- private rigidbodiesInitialized = false // bodyOffsetMatrixInverse computed and bodies positioned
82
- private jointsCreated = false // Joints delayed until after rigidbodies are positioned
83
- private firstFrame = true // Needed to reposition bodies before creating joints
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- private zeroVector: any = null // Cached zero vector for velocity clearing
86
-
87
- constructor(rigidbodies: Rigidbody[], joints: Joint[] = [], options?: PhysicsOptions) {
88
- this.rigidbodies = rigidbodies
89
- this.joints = joints
90
- const keywords = options?.constraintSolverKeywords ?? []
91
- if (keywords.length > 0) {
92
- this.constraintSolverPattern = new RegExp(keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "i")
93
- }
94
- this.initAmmo()
95
- }
96
-
97
- private async initAmmo(): Promise<void> {
98
- if (this.ammoInitialized || this.ammoPromise) return
99
- this.ammoPromise = loadAmmo()
100
- try {
101
- this.ammo = await this.ammoPromise
102
- this.createAmmoWorld()
103
- this.ammoInitialized = true
104
- } catch (error) {
105
- console.error("[Physics] Failed to initialize Ammo:", error)
106
- this.ammoPromise = null
107
- }
108
- }
109
-
110
- setGravity(gravity: Vec3): void {
111
- this.gravity = gravity
112
- if (this.dynamicsWorld && this.ammo) {
113
- const Ammo = this.ammo
114
- const gravityVec = new Ammo.btVector3(gravity.x, gravity.y, gravity.z)
115
- this.dynamicsWorld.setGravity(gravityVec)
116
- Ammo.destroy(gravityVec)
117
- }
118
- }
119
-
120
- getGravity(): Vec3 {
121
- return this.gravity
122
- }
123
-
124
- getRigidbodies(): Rigidbody[] {
125
- return this.rigidbodies
126
- }
127
-
128
- getJoints(): Joint[] {
129
- return this.joints
130
- }
131
-
132
- getRigidbodyTransforms(): Array<{ position: Vec3; rotation: Quat }> {
133
- const transforms: Array<{ position: Vec3; rotation: Quat }> = []
134
-
135
- if (!this.ammo || !this.ammoRigidbodies.length) {
136
- for (let i = 0; i < this.rigidbodies.length; i++) {
137
- transforms.push({
138
- position: new Vec3(
139
- this.rigidbodies[i].shapePosition.x,
140
- this.rigidbodies[i].shapePosition.y,
141
- this.rigidbodies[i].shapePosition.z
142
- ),
143
- rotation: Quat.fromEuler(
144
- this.rigidbodies[i].shapeRotation.x,
145
- this.rigidbodies[i].shapeRotation.y,
146
- this.rigidbodies[i].shapeRotation.z
147
- ),
148
- })
149
- }
150
- return transforms
151
- }
152
-
153
- for (let i = 0; i < this.ammoRigidbodies.length; i++) {
154
- const ammoBody = this.ammoRigidbodies[i]
155
- if (!ammoBody) {
156
- const rb = this.rigidbodies[i]
157
- transforms.push({
158
- position: new Vec3(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z),
159
- rotation: Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z),
160
- })
161
- continue
162
- }
163
-
164
- const transform = ammoBody.getWorldTransform()
165
- const origin = transform.getOrigin()
166
- const rotQuat = transform.getRotation()
167
-
168
- transforms.push({
169
- position: new Vec3(origin.x(), origin.y(), origin.z()),
170
- rotation: new Quat(rotQuat.x(), rotQuat.y(), rotQuat.z(), rotQuat.w()),
171
- })
172
- }
173
-
174
- return transforms
175
- }
176
-
177
- private createAmmoWorld(): void {
178
- if (!this.ammo) return
179
-
180
- const Ammo = this.ammo
181
-
182
- const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration()
183
- const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration)
184
- const overlappingPairCache = new Ammo.btDbvtBroadphase()
185
- const solver = new Ammo.btSequentialImpulseConstraintSolver()
186
-
187
- this.dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(
188
- dispatcher,
189
- overlappingPairCache,
190
- solver,
191
- collisionConfiguration
192
- )
193
-
194
- const gravityVec = new Ammo.btVector3(this.gravity.x, this.gravity.y, this.gravity.z)
195
- this.dynamicsWorld.setGravity(gravityVec)
196
- Ammo.destroy(gravityVec)
197
-
198
- this.createAmmoRigidbodies()
199
- }
200
-
201
- private createAmmoRigidbodies(): void {
202
- if (!this.ammo || !this.dynamicsWorld) return
203
-
204
- const Ammo = this.ammo
205
- this.ammoRigidbodies = []
206
-
207
- for (let i = 0; i < this.rigidbodies.length; i++) {
208
- const rb = this.rigidbodies[i]
209
-
210
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
- let shape: any = null
212
- const size = rb.size
213
-
214
- switch (rb.shape) {
215
- case RigidbodyShape.Sphere:
216
- const radius = size.x
217
- shape = new Ammo.btSphereShape(radius)
218
- break
219
- case RigidbodyShape.Box:
220
- const sizeVector = new Ammo.btVector3(size.x, size.y, size.z)
221
- shape = new Ammo.btBoxShape(sizeVector)
222
- Ammo.destroy(sizeVector)
223
- break
224
- case RigidbodyShape.Capsule:
225
- const capsuleRadius = size.x
226
- const capsuleHalfHeight = size.y
227
- shape = new Ammo.btCapsuleShape(capsuleRadius, capsuleHalfHeight)
228
- break
229
- default:
230
- const defaultHalfExtents = new Ammo.btVector3(size.x / 2, size.y / 2, size.z / 2)
231
- shape = new Ammo.btBoxShape(defaultHalfExtents)
232
- Ammo.destroy(defaultHalfExtents)
233
- break
234
- }
235
-
236
- // Bodies must start at correct position to avoid explosions when joints are created
237
- const transform = new Ammo.btTransform()
238
- transform.setIdentity()
239
-
240
- const shapePos = new Ammo.btVector3(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z)
241
- transform.setOrigin(shapePos)
242
- Ammo.destroy(shapePos)
243
-
244
- const shapeRotQuat = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z)
245
- const quat = new Ammo.btQuaternion(shapeRotQuat.x, shapeRotQuat.y, shapeRotQuat.z, shapeRotQuat.w)
246
- transform.setRotation(quat)
247
- Ammo.destroy(quat)
248
-
249
- // All types use the same motionState constructor
250
- const motionState = new Ammo.btDefaultMotionState(transform)
251
- const mass = rb.type === RigidbodyType.Dynamic ? rb.mass : 0
252
- const isDynamic = rb.type === RigidbodyType.Dynamic
253
-
254
- const localInertia = new Ammo.btVector3(0, 0, 0)
255
- if (isDynamic && mass > 0) {
256
- shape.calculateLocalInertia(mass, localInertia)
257
- }
258
-
259
- const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia)
260
- rbInfo.set_m_restitution(rb.restitution)
261
- rbInfo.set_m_friction(rb.friction)
262
- rbInfo.set_m_linearDamping(rb.linearDamping)
263
- rbInfo.set_m_angularDamping(rb.angularDamping)
264
-
265
- const body = new Ammo.btRigidBody(rbInfo)
266
-
267
- body.setSleepingThresholds(0.0, 0.0)
268
-
269
- // Static (FollowBone) should be kinematic, not static - must follow bones
270
- if (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) {
271
- body.setCollisionFlags(body.getCollisionFlags() | 2) // CF_KINEMATIC_OBJECT
272
- body.setActivationState(4) // DISABLE_DEACTIVATION
273
- }
274
-
275
- const collisionGroup = 1 << rb.group
276
- const collisionMask = rb.collisionMask
277
-
278
- const isZeroVolume =
279
- (rb.shape === RigidbodyShape.Sphere && rb.size.x === 0) ||
280
- (rb.shape === RigidbodyShape.Box && (rb.size.x === 0 || rb.size.y === 0 || rb.size.z === 0)) ||
281
- (rb.shape === RigidbodyShape.Capsule && (rb.size.x === 0 || rb.size.y === 0))
282
-
283
- if (collisionMask === 0 || isZeroVolume) {
284
- body.setCollisionFlags(body.getCollisionFlags() | 4) // CF_NO_CONTACT_RESPONSE
285
- }
286
-
287
- this.dynamicsWorld.addRigidBody(body, collisionGroup, collisionMask)
288
-
289
- this.ammoRigidbodies.push(body)
290
-
291
- Ammo.destroy(rbInfo)
292
- Ammo.destroy(localInertia)
293
- }
294
- }
295
-
296
- private createAmmoJoints(): void {
297
- if (!this.ammo || !this.dynamicsWorld || this.ammoRigidbodies.length === 0) return
298
-
299
- const Ammo = this.ammo
300
- this.ammoConstraints = []
301
-
302
- for (const joint of this.joints) {
303
- const rbIndexA = joint.rigidbodyIndexA
304
- const rbIndexB = joint.rigidbodyIndexB
305
-
306
- if (
307
- rbIndexA < 0 ||
308
- rbIndexA >= this.ammoRigidbodies.length ||
309
- rbIndexB < 0 ||
310
- rbIndexB >= this.ammoRigidbodies.length
311
- ) {
312
- console.warn(`[Physics] Invalid joint indices: ${rbIndexA}, ${rbIndexB}`)
313
- continue
314
- }
315
-
316
- const bodyA = this.ammoRigidbodies[rbIndexA]
317
- const bodyB = this.ammoRigidbodies[rbIndexB]
318
-
319
- if (!bodyA || !bodyB) {
320
- console.warn(`[Physics] Body not found for joint ${joint.name}: bodyA=${!!bodyA}, bodyB=${!!bodyB}`)
321
- continue
322
- }
323
-
324
- // Compute joint frames using actual current body positions (after repositioning)
325
- const bodyATransform = bodyA.getWorldTransform()
326
- const bodyBTransform = bodyB.getWorldTransform()
327
-
328
- const bodyAOrigin = bodyATransform.getOrigin()
329
- const bodyARotQuat = bodyATransform.getRotation()
330
- const bodyAPos = new Vec3(bodyAOrigin.x(), bodyAOrigin.y(), bodyAOrigin.z())
331
- const bodyARot = new Quat(bodyARotQuat.x(), bodyARotQuat.y(), bodyARotQuat.z(), bodyARotQuat.w())
332
- const bodyAMat = Mat4.fromPositionRotation(bodyAPos, bodyARot)
333
-
334
- const bodyBOrigin = bodyBTransform.getOrigin()
335
- const bodyBRotQuat = bodyBTransform.getRotation()
336
- const bodyBPos = new Vec3(bodyBOrigin.x(), bodyBOrigin.y(), bodyBOrigin.z())
337
- const bodyBRot = new Quat(bodyBRotQuat.x(), bodyBRotQuat.y(), bodyBRotQuat.z(), bodyBRotQuat.w())
338
- const bodyBMat = Mat4.fromPositionRotation(bodyBPos, bodyBRot)
339
-
340
- const scalingFactor = 1.0
341
- const jointRotQuat = Quat.fromEuler(joint.rotation.x, joint.rotation.y, joint.rotation.z)
342
- const jointPos = new Vec3(
343
- joint.position.x * scalingFactor,
344
- joint.position.y * scalingFactor,
345
- joint.position.z * scalingFactor
346
- )
347
- const jointTransform = Mat4.fromPositionRotation(jointPos, jointRotQuat)
348
-
349
- // Transform joint world position to body A's local space
350
- const frameInAMat = bodyAMat.inverse().multiply(jointTransform)
351
- const framePosA = frameInAMat.getPosition()
352
- const frameRotA = frameInAMat.toQuat()
353
-
354
- // Transform joint world position to body B's local space
355
- const frameInBMat = bodyBMat.inverse().multiply(jointTransform)
356
- const framePosB = frameInBMat.getPosition()
357
- const frameRotB = frameInBMat.toQuat()
358
-
359
- const frameInA = new Ammo.btTransform()
360
- frameInA.setIdentity()
361
- const pivotInA = new Ammo.btVector3(framePosA.x, framePosA.y, framePosA.z)
362
- frameInA.setOrigin(pivotInA)
363
- const quatA = new Ammo.btQuaternion(frameRotA.x, frameRotA.y, frameRotA.z, frameRotA.w)
364
- frameInA.setRotation(quatA)
365
-
366
- const frameInB = new Ammo.btTransform()
367
- frameInB.setIdentity()
368
- const pivotInB = new Ammo.btVector3(framePosB.x, framePosB.y, framePosB.z)
369
- frameInB.setOrigin(pivotInB)
370
- const quatB = new Ammo.btQuaternion(frameRotB.x, frameRotB.y, frameRotB.z, frameRotB.w)
371
- frameInB.setRotation(quatB)
372
-
373
- const useLinearReferenceFrameA = true
374
- const constraint = new Ammo.btGeneric6DofSpringConstraint(
375
- bodyA,
376
- bodyB,
377
- frameInA,
378
- frameInB,
379
- useLinearReferenceFrameA
380
- )
381
-
382
- // Per-joint Bullet 2.75 constraint solver: disable m_useOffsetForConstraintFrame for
383
- // joints whose name matches constraintSolverKeywords.
384
- if (this.constraintSolverPattern && this.constraintSolverPattern.test(joint.name)) {
385
- let jointPtr: number | undefined
386
- if (typeof Ammo.getPointer === "function") {
387
- jointPtr = Ammo.getPointer(constraint)
388
- } else {
389
- const constraintWithPtr = constraint as { ptr?: number }
390
- jointPtr = constraintWithPtr.ptr
391
- }
392
-
393
- if (jointPtr !== undefined && Ammo.HEAP8) {
394
- const heap8 = Ammo.HEAP8 as Uint8Array
395
- if (heap8[jointPtr + 1300] === (useLinearReferenceFrameA ? 1 : 0) && heap8[jointPtr + 1301] === 1) {
396
- heap8[jointPtr + 1301] = 0
397
- }
398
- }
399
- }
400
-
401
- for (let i = 0; i < 6; ++i) {
402
- constraint.setParam(2, 0.475, i) // BT_CONSTRAINT_STOP_ERP
403
- }
404
-
405
- const lowerLinear = new Ammo.btVector3(joint.positionMin.x, joint.positionMin.y, joint.positionMin.z)
406
- const upperLinear = new Ammo.btVector3(joint.positionMax.x, joint.positionMax.y, joint.positionMax.z)
407
- constraint.setLinearLowerLimit(lowerLinear)
408
- constraint.setLinearUpperLimit(upperLinear)
409
-
410
- const lowerAngular = new Ammo.btVector3(
411
- this.normalizeAngle(joint.rotationMin.x),
412
- this.normalizeAngle(joint.rotationMin.y),
413
- this.normalizeAngle(joint.rotationMin.z)
414
- )
415
- const upperAngular = new Ammo.btVector3(
416
- this.normalizeAngle(joint.rotationMax.x),
417
- this.normalizeAngle(joint.rotationMax.y),
418
- this.normalizeAngle(joint.rotationMax.z)
419
- )
420
- constraint.setAngularLowerLimit(lowerAngular)
421
- constraint.setAngularUpperLimit(upperAngular)
422
-
423
- // Linear springs: only enable if stiffness is non-zero
424
- if (joint.springPosition.x !== 0) {
425
- constraint.setStiffness(0, joint.springPosition.x)
426
- constraint.enableSpring(0, true)
427
- } else {
428
- constraint.enableSpring(0, false)
429
- }
430
- if (joint.springPosition.y !== 0) {
431
- constraint.setStiffness(1, joint.springPosition.y)
432
- constraint.enableSpring(1, true)
433
- } else {
434
- constraint.enableSpring(1, false)
435
- }
436
- if (joint.springPosition.z !== 0) {
437
- constraint.setStiffness(2, joint.springPosition.z)
438
- constraint.enableSpring(2, true)
439
- } else {
440
- constraint.enableSpring(2, false)
441
- }
442
-
443
- // Angular springs: always enable
444
- constraint.setStiffness(3, joint.springRotation.x)
445
- constraint.enableSpring(3, true)
446
- constraint.setStiffness(4, joint.springRotation.y)
447
- constraint.enableSpring(4, true)
448
- constraint.setStiffness(5, joint.springRotation.z)
449
- constraint.enableSpring(5, true)
450
-
451
- this.dynamicsWorld.addConstraint(constraint, false)
452
-
453
- this.ammoConstraints.push(constraint)
454
- Ammo.destroy(pivotInA)
455
- Ammo.destroy(pivotInB)
456
- Ammo.destroy(quatA)
457
- Ammo.destroy(quatB)
458
- Ammo.destroy(lowerLinear)
459
- Ammo.destroy(upperLinear)
460
- Ammo.destroy(lowerAngular)
461
- Ammo.destroy(upperAngular)
462
- }
463
- }
464
-
465
- // Normalize angle to [-π, π] range
466
- private normalizeAngle(angle: number): number {
467
- const pi = Math.PI
468
- const twoPi = 2 * pi
469
- angle = angle % twoPi
470
- if (angle < -pi) {
471
- angle += twoPi
472
- } else if (angle > pi) {
473
- angle -= twoPi
474
- }
475
- return angle
476
- }
477
-
478
- // Re-snap all rigidbodies to current bone poses and zero velocities / forces.
479
- // Use when simulation has diverged (explosion, NaN, extreme external teleport).
480
- reset(boneWorldMatrices: Mat4[]): void {
481
- if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) return
482
- if (!this.rigidbodiesInitialized) return
483
-
484
- this.positionBodiesFromBones(boneWorldMatrices, boneWorldMatrices.length)
485
-
486
- if (this.dynamicsWorld.clearForces) {
487
- this.dynamicsWorld.clearForces()
488
- }
489
- if (this.dynamicsWorld.stepSimulation) {
490
- this.dynamicsWorld.stepSimulation(0, 0, 0)
491
- }
492
- }
493
-
494
- // Syncs bones to rigidbodies, simulates dynamics, solves constraints
495
- // Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
496
- step(dt: number, boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void {
497
- // Wait for Ammo to initialize
498
- if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
499
- return
500
- }
501
-
502
- const boneCount = boneWorldMatrices.length
503
-
504
- if (this.firstFrame) {
505
- if (!this.rigidbodiesInitialized) {
506
- this.computeBodyOffsets(boneInverseBindMatrices, boneCount)
507
- this.rigidbodiesInitialized = true
508
- }
509
-
510
- // Position bodies based on current bone poses (not bind pose) before creating joints
511
- this.positionBodiesFromBones(boneWorldMatrices, boneCount)
512
-
513
- if (!this.jointsCreated) {
514
- this.createAmmoJoints()
515
- this.jointsCreated = true
516
- }
517
-
518
- if (this.dynamicsWorld.stepSimulation) {
519
- this.dynamicsWorld.stepSimulation(0, 0, 0)
520
- }
521
-
522
- this.firstFrame = false
523
- }
524
-
525
- // Step order: 1) Sync Static/Kinematic from bones, 2) Step physics, 3) Apply dynamic to bones
526
- this.syncFromBones(boneWorldMatrices, boneCount)
527
-
528
- this.stepAmmoPhysics(dt)
529
-
530
- this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneCount)
531
- }
532
-
533
- // Compute bodyOffsetMatrixInverse for all rigidbodies (called once during initialization)
534
- private computeBodyOffsets(boneInverseBindMatrices: Float32Array, boneCount: number): void {
535
- if (!this.ammo || !this.dynamicsWorld) return
536
-
537
- for (let i = 0; i < this.rigidbodies.length; i++) {
538
- const rb = this.rigidbodies[i]
539
- if (rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
540
- const boneIdx = rb.boneIndex
541
- const invBindIdx = boneIdx * 16
542
-
543
- const invBindMat = new Mat4(boneInverseBindMatrices.subarray(invBindIdx, invBindIdx + 16))
544
-
545
- // Compute shape transform in bone-local space: shapeLocal = boneInverseBind × shapeWorldBind
546
- const shapeRotQuat = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z)
547
- const shapeWorldBind = Mat4.fromPositionRotation(rb.shapePosition, shapeRotQuat)
548
-
549
- // shapeLocal = boneInverseBind × shapeWorldBind (not shapeWorldBind × boneInverseBind)
550
- const bodyOffsetMatrix = invBindMat.multiply(shapeWorldBind)
551
- rb.bodyOffsetMatrixInverse = bodyOffsetMatrix.inverse()
552
- rb.bodyOffsetMatrix = bodyOffsetMatrix // Cache non-inverse to avoid expensive inverse() calls
553
- } else {
554
- rb.bodyOffsetMatrixInverse = Mat4.identity()
555
- rb.bodyOffsetMatrix = Mat4.identity() // Cache non-inverse
556
- }
557
- }
558
- }
559
-
560
- // Position bodies based on current bone transforms (called on first frame only)
561
- private positionBodiesFromBones(boneWorldMatrices: Mat4[], boneCount: number): void {
562
- if (!this.ammo || !this.dynamicsWorld) return
563
-
564
- const Ammo = this.ammo
565
-
566
- for (let i = 0; i < this.rigidbodies.length; i++) {
567
- const rb = this.rigidbodies[i]
568
- const ammoBody = this.ammoRigidbodies[i]
569
- if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount) continue
570
-
571
- const boneIdx = rb.boneIndex
572
- const boneWorldMat = boneWorldMatrices[boneIdx]
573
-
574
- // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
575
- const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse()
576
- const nodeWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix)
577
-
578
- const worldPos = nodeWorldMatrix.getPosition()
579
- const worldRot = nodeWorldMatrix.toQuat()
580
-
581
- const transform = new Ammo.btTransform()
582
- const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z)
583
- const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w)
584
-
585
- transform.setOrigin(pos)
586
- transform.setRotation(quat)
587
-
588
- if (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) {
589
- ammoBody.setCollisionFlags(ammoBody.getCollisionFlags() | 2) // CF_KINEMATIC_OBJECT
590
- ammoBody.setActivationState(4) // DISABLE_DEACTIVATION
591
- }
592
-
593
- ammoBody.setWorldTransform(transform)
594
- ammoBody.getMotionState().setWorldTransform(transform)
595
-
596
- if (!this.zeroVector) {
597
- this.zeroVector = new Ammo.btVector3(0, 0, 0)
598
- }
599
- ammoBody.setLinearVelocity(this.zeroVector)
600
- ammoBody.setAngularVelocity(this.zeroVector)
601
-
602
- Ammo.destroy(pos)
603
- Ammo.destroy(quat)
604
- Ammo.destroy(transform)
605
- }
606
- }
607
-
608
- // Sync Static (FollowBone) and Kinematic rigidbodies to follow bone transforms
609
- private syncFromBones(boneWorldMatrices: Mat4[], boneCount: number): void {
610
- if (!this.ammo || !this.dynamicsWorld) return
611
-
612
- const Ammo = this.ammo
613
-
614
- for (let i = 0; i < this.rigidbodies.length; i++) {
615
- const rb = this.rigidbodies[i]
616
- const ammoBody = this.ammoRigidbodies[i]
617
- if (!ammoBody) continue
618
-
619
- // Sync both Static (FollowBone) and Kinematic bodies - they both follow bones
620
- if (
621
- (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) &&
622
- rb.boneIndex >= 0 &&
623
- rb.boneIndex < boneCount
624
- ) {
625
- const boneIdx = rb.boneIndex
626
- const boneWorldMat = boneWorldMatrices[boneIdx]
627
-
628
- // Lazy-cache bodyOffsetMatrix on first hit (cold path).
629
- if (!rb.bodyOffsetMatrix) rb.bodyOffsetMatrix = rb.bodyOffsetMatrixInverse.inverse()
630
-
631
- // nodeWorld = boneWorld × bodyOffsetMatrix → _physMat[0]
632
- Mat4.multiplyArrays(boneWorldMat.values, 0, rb.bodyOffsetMatrix.values, 0, _physMat[0], 0)
633
- const nodeVals = _physMat[0]
634
- const wx = nodeVals[12], wy = nodeVals[13], wz = nodeVals[14]
635
- Mat4.toQuatFromArrayInto(nodeVals, 0, _physQuat)
636
-
637
- const transform = new Ammo.btTransform()
638
- const pos = new Ammo.btVector3(wx, wy, wz)
639
- const quat = new Ammo.btQuaternion(_physQuat.x, _physQuat.y, _physQuat.z, _physQuat.w)
640
-
641
- transform.setOrigin(pos)
642
- transform.setRotation(quat)
643
-
644
- ammoBody.setWorldTransform(transform)
645
- ammoBody.getMotionState().setWorldTransform(transform)
646
-
647
- if (!this.zeroVector) {
648
- this.zeroVector = new Ammo.btVector3(0, 0, 0)
649
- }
650
- ammoBody.setLinearVelocity(this.zeroVector)
651
- ammoBody.setAngularVelocity(this.zeroVector)
652
-
653
- Ammo.destroy(pos)
654
- Ammo.destroy(quat)
655
- Ammo.destroy(transform)
656
- }
657
- }
658
- }
659
-
660
- // Step Ammo physics simulation
661
- private stepAmmoPhysics(dt: number): void {
662
- if (!this.ammo || !this.dynamicsWorld) return
663
-
664
- const fixedTimeStep = 1 / 75
665
- const maxSubSteps = 10
666
-
667
- this.dynamicsWorld.stepSimulation(dt, maxSubSteps, fixedTimeStep)
668
- }
669
-
670
- // Apply dynamic rigidbody world transforms to bone world matrices in-place
671
- private applyAmmoRigidbodiesToBones(boneWorldMatrices: Mat4[], boneCount: number): void {
672
- if (!this.ammo || !this.dynamicsWorld) return
673
-
674
- for (let i = 0; i < this.rigidbodies.length; i++) {
675
- const rb = this.rigidbodies[i]
676
- const ammoBody = this.ammoRigidbodies[i]
677
- if (!ammoBody) continue
678
-
679
- // Only dynamic rigidbodies drive bones (Static/Kinematic follow bones)
680
- if (rb.type === RigidbodyType.Dynamic && rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
681
- const boneIdx = rb.boneIndex
682
-
683
- const transform = ammoBody.getWorldTransform()
684
- const origin = transform.getOrigin()
685
- const rotation = transform.getRotation()
686
-
687
- // nodeWorldMatrix → _physMat[0] (from ammo position/rotation directly)
688
- Mat4.fromPositionRotationInto(
689
- origin.x(), origin.y(), origin.z(),
690
- rotation.x(), rotation.y(), rotation.z(), rotation.w(),
691
- _physMat[0]
692
- )
693
-
694
- // boneWorld = nodeWorld × bodyOffsetMatrixInverse → _physMat[1]
695
- const boneVals = _physMat[1]
696
- Mat4.multiplyArrays(_physMat[0], 0, rb.bodyOffsetMatrixInverse.values, 0, boneVals, 0)
697
-
698
- if (!isNaN(boneVals[0]) && !isNaN(boneVals[15]) && Math.abs(boneVals[0]) < 1e6 && Math.abs(boneVals[15]) < 1e6) {
699
- boneWorldMatrices[boneIdx].values.set(boneVals)
700
- } else {
701
- console.warn(`[Physics] Invalid bone world matrix for rigidbody ${i} (${rb.name}), skipping update`)
702
- }
703
- }
704
- }
705
- }
706
- }