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