@kite3d/engine 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 (74) hide show
  1. package/dist/authoring.d.ts +54 -0
  2. package/dist/authoring.js +165 -0
  3. package/dist/authoring.js.map +1 -0
  4. package/dist/authoringValidation.d.ts +110 -0
  5. package/dist/authoringValidation.js +488 -0
  6. package/dist/authoringValidation.js.map +1 -0
  7. package/dist/defaults.d.ts +2 -0
  8. package/dist/fileTypes.d.ts +5 -0
  9. package/dist/fileTypes.js +51 -0
  10. package/dist/fileTypes.js.map +1 -0
  11. package/dist/importMap.d.ts +15 -0
  12. package/dist/importMap.js +62 -0
  13. package/dist/importMap.js.map +1 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +2756 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/migrations.js +5 -0
  18. package/dist/migrations.js.map +1 -0
  19. package/dist/paths.d.ts +9 -0
  20. package/dist/paths.js +13 -0
  21. package/dist/paths.js.map +1 -0
  22. package/dist/plugins/GeneratorComponent.d.ts +40 -0
  23. package/dist/plugins/HtmlUiComponent.d.ts +52 -0
  24. package/dist/plugins/HtmlUiComponent.example.d.ts +0 -0
  25. package/dist/plugins/cannon/Cannon3DBodyComponent.d.ts +27 -0
  26. package/dist/plugins/cannon/Cannon3DShapeComponent.d.ts +57 -0
  27. package/dist/plugins/cannon/CannonPhysicsPlugin.d.ts +63 -0
  28. package/dist/plugins/cannon/CannonRagdollComponent.d.ts +148 -0
  29. package/dist/plugins/cannon/helper.d.ts +24 -0
  30. package/dist/plugins/cannon/threeToCannon.d.ts +63 -0
  31. package/dist/plugins/cannon/utils.d.ts +9 -0
  32. package/dist/projectFormat.js +108 -0
  33. package/dist/projectFormat.js.map +1 -0
  34. package/dist/runtime/createGame.d.ts +29 -0
  35. package/dist/runtime/index.d.ts +18 -0
  36. package/dist/runtime/migrations.d.ts +5 -0
  37. package/dist/runtime/nestedAssets.d.ts +26 -0
  38. package/dist/runtime/projectFormat.d.ts +83 -0
  39. package/dist/runtime/version.d.ts +1 -0
  40. package/dist/runtime.js +72835 -0
  41. package/dist/runtime.js.map +1 -0
  42. package/dist/sceneSerialization.d.ts +17 -0
  43. package/dist/sceneSerialization.js +174 -0
  44. package/dist/sceneSerialization.js.map +1 -0
  45. package/dist/scripts.d.ts +17 -0
  46. package/dist/version.js +7 -0
  47. package/dist/version.js.map +1 -0
  48. package/package.json +86 -0
  49. package/src/authoring.ts +293 -0
  50. package/src/authoringValidation.ts +815 -0
  51. package/src/defaults.ts +277 -0
  52. package/src/fileTypes.ts +53 -0
  53. package/src/importMap.ts +105 -0
  54. package/src/index.ts +15 -0
  55. package/src/paths.ts +9 -0
  56. package/src/plugins/GeneratorComponent.ts +275 -0
  57. package/src/plugins/HtmlUiComponent.example.ts +202 -0
  58. package/src/plugins/HtmlUiComponent.md +219 -0
  59. package/src/plugins/HtmlUiComponent.ts +320 -0
  60. package/src/plugins/cannon/Cannon3DBodyComponent.ts +227 -0
  61. package/src/plugins/cannon/Cannon3DShapeComponent.ts +258 -0
  62. package/src/plugins/cannon/CannonPhysicsPlugin.ts +409 -0
  63. package/src/plugins/cannon/CannonRagdollComponent.ts +1170 -0
  64. package/src/plugins/cannon/helper.ts +255 -0
  65. package/src/plugins/cannon/threeToCannon.ts +441 -0
  66. package/src/plugins/cannon/utils.ts +185 -0
  67. package/src/runtime/createGame.ts +337 -0
  68. package/src/runtime/index.ts +19 -0
  69. package/src/runtime/migrations.ts +6 -0
  70. package/src/runtime/nestedAssets.ts +226 -0
  71. package/src/runtime/projectFormat.ts +233 -0
  72. package/src/runtime/version.ts +3 -0
  73. package/src/sceneSerialization.ts +289 -0
  74. package/src/scripts.ts +52 -0
@@ -0,0 +1,1170 @@
1
+ import { Body, Box, ConeTwistConstraint, Constraint, Sphere, Vec3 } from "cannon-es"
2
+ import {
3
+ BoxGeometry,
4
+ Bone,
5
+ ComponentDefn,
6
+ ComponentJSON,
7
+ IObject3D,
8
+ Mesh,
9
+ MeshStandardMaterial,
10
+ Object3D,
11
+ Object3DComponent,
12
+ Quaternion,
13
+ SkinnedMesh,
14
+ SphereGeometry,
15
+ uiFolderContainer,
16
+ Vector3,
17
+ ViewerEventMap
18
+ } from "threepipe"
19
+ import { CannonMaterial2, CannonPhysicsPlugin } from "./CannonPhysicsPlugin.ts"
20
+
21
+ export interface RagdollConfig {
22
+ scale: number
23
+ angle: number
24
+ angleShoulders: number
25
+ twistAngle: number
26
+ }
27
+
28
+ export interface RagdollParts {
29
+ lowerLeftLeg: Body
30
+ lowerRightLeg: Body
31
+ upperLeftLeg: Body
32
+ upperRightLeg: Body
33
+ pelvis: Body
34
+ upperBody: Body
35
+ head: Body
36
+ upperLeftArm: Body
37
+ upperRightArm: Body
38
+ lowerLeftArm: Body
39
+ lowerRightArm: Body
40
+ }
41
+
42
+ export interface RagdollConstraints {
43
+ neckJoint: ConeTwistConstraint
44
+ leftKneeJoint: ConeTwistConstraint
45
+ rightKneeJoint: ConeTwistConstraint
46
+ leftHipJoint: ConeTwistConstraint
47
+ rightHipJoint: ConeTwistConstraint
48
+ spineJoint: ConeTwistConstraint
49
+ leftShoulder: ConeTwistConstraint
50
+ rightShoulder: ConeTwistConstraint
51
+ leftElbowJoint: ConeTwistConstraint
52
+ rightElbowJoint: ConeTwistConstraint
53
+ }
54
+
55
+ @uiFolderContainer('Ragdoll')
56
+ export class CannonRagdollComponent extends Object3DComponent {
57
+ static ComponentType = 'CannonRagdollComponent'
58
+ static StateProperties: ComponentDefn['StateProperties'] = [
59
+ 'scale',
60
+ 'angle',
61
+ 'angleShoulders',
62
+ 'twistAngle',
63
+ 'mass',
64
+ 'autoCreateOnInit',
65
+ 'hideRagdollMeshes'
66
+ ]
67
+
68
+ scale = 1
69
+
70
+ angle = Math.PI / 4
71
+
72
+ // Increased to allow arms to fall more naturally (nearly parallel to body)
73
+ angleShoulders = Math.PI / 2
74
+
75
+ twistAngle = Math.PI / 8
76
+
77
+ mass = 1
78
+
79
+ /** If true, creates ragdoll bodies on init. If false, waits for activate() call */
80
+ autoCreateOnInit = true
81
+
82
+ /** If true, hides the debug ragdoll meshes (useful when using with character model) */
83
+ hideRagdollMeshes = false
84
+
85
+ material: CannonMaterial2 = new CannonMaterial2()
86
+
87
+ private _bodies: Body[] = []
88
+ private _constraints: Constraint[] = []
89
+ private _parts: RagdollParts | null = null
90
+ private _ragdollConstraints: RagdollConstraints | null = null
91
+ private _cannon: CannonPhysicsPlugin | null = null
92
+
93
+ // Visual meshes
94
+ private _meshes: Mesh[] = []
95
+ private _meshContainer: Object3D | null = null
96
+ private _visualMaterial: MeshStandardMaterial | null = null
97
+ private _lastObjectPosition = new Vector3()
98
+ private _lastObjectQuaternion = new Quaternion()
99
+ private _initialized = false
100
+
101
+ // Ragdoll state
102
+ private _isActive = false
103
+ private _characterMeshes: SkinnedMesh[] = []
104
+ private _boneMap: Map<string, Bone> = new Map()
105
+
106
+ // Store initial bone transforms (bind pose) and initial ragdoll body transforms
107
+ private _initialBoneQuats: Map<string, Quaternion> = new Map()
108
+ private _initialBoneWorldQuats: Map<string, Quaternion> = new Map()
109
+ private _initialBodyQuats: Map<Body, Quaternion> = new Map()
110
+
111
+ // Bone name mappings (common Mixamo bone names)
112
+ private _boneNames = {
113
+ head: ['Head', 'mixamorigHead', 'head'],
114
+ neck: ['Neck', 'mixamorigNeck', 'neck'],
115
+ spine: ['Spine', 'mixamorigSpine', 'spine'],
116
+ spine1: ['Spine1', 'mixamorigSpine1', 'spine1'],
117
+ spine2: ['Spine2', 'mixamorigSpine2', 'spine2'],
118
+ hips: ['Hips', 'mixamorigHips', 'hips', 'pelvis'],
119
+ leftUpLeg: ['LeftUpLeg', 'mixamorigLeftUpLeg', 'left_upper_leg', 'LeftThigh'],
120
+ rightUpLeg: ['RightUpLeg', 'mixamorigRightUpLeg', 'right_upper_leg', 'RightThigh'],
121
+ leftLeg: ['LeftLeg', 'mixamorigLeftLeg', 'left_lower_leg', 'LeftShin'],
122
+ rightLeg: ['RightLeg', 'mixamorigRightLeg', 'right_lower_leg', 'RightShin'],
123
+ leftArm: ['LeftArm', 'mixamorigLeftArm', 'left_upper_arm', 'LeftShoulder'],
124
+ rightArm: ['RightArm', 'mixamorigRightArm', 'right_upper_arm', 'RightShoulder'],
125
+ leftForeArm: ['LeftForeArm', 'mixamorigLeftForeArm', 'left_lower_arm', 'LeftElbow'],
126
+ rightForeArm: ['RightForeArm', 'mixamorigRightForeArm', 'right_lower_arm', 'RightElbow'],
127
+ }
128
+
129
+ // Reusable temp objects for transform calculations
130
+ private _currentObjectPosition = new Vector3()
131
+ private _currentObjectQuaternion = new Quaternion()
132
+ private _deltaQuaternion = new Quaternion()
133
+ private _invLastQuaternion = new Quaternion()
134
+ private _tempVec3 = new Vector3()
135
+ private _pivotPoint = new Vector3()
136
+ private _tempWorldPos = new Vector3()
137
+ private _tempWorldQuat = new Quaternion()
138
+
139
+ get bodies(): Body[] {
140
+ return this._bodies
141
+ }
142
+
143
+ get constraints(): Constraint[] {
144
+ return this._constraints
145
+ }
146
+
147
+ get parts(): RagdollParts | null {
148
+ return this._parts
149
+ }
150
+
151
+ get ragdollConstraints(): RagdollConstraints | null {
152
+ return this._ragdollConstraints
153
+ }
154
+
155
+ /** Returns true if ragdoll physics is currently active */
156
+ get isActive(): boolean {
157
+ return this._isActive
158
+ }
159
+
160
+ constructor() {
161
+ super()
162
+ this.onStateChange('scale', () => this._recreateRagdoll())
163
+ this.onStateChange('angle', () => this._updateConstraints())
164
+ this.onStateChange('angleShoulders', () => this._updateConstraints())
165
+ this.onStateChange('twistAngle', () => this._updateConstraints())
166
+ this.onStateChange('mass', () => this._updateMass())
167
+ this.onStateChange('hideRagdollMeshes', () => this._updateMeshVisibility())
168
+ }
169
+
170
+ init(object: IObject3D, state: ComponentJSON['state']) {
171
+ super.init(object, state)
172
+ this._cannon = this.ctx.plugin(CannonPhysicsPlugin)
173
+
174
+ // Clean up any existing ragdoll first
175
+ this._removeRagdoll()
176
+
177
+ // Create visual material
178
+ if (!this._visualMaterial) {
179
+ this._visualMaterial = new MeshStandardMaterial({ color: 0x888888 })
180
+ }
181
+
182
+ // Create container for meshes in the scene (not the object, to avoid saving)
183
+ if (!this._meshContainer) {
184
+ this._meshContainer = new Object3D()
185
+ this._meshContainer.name = `RagdollMeshes_${this.object.uuid}`
186
+ // Add to scene instead of object to prevent serialization
187
+ this.ctx.viewer?.scene.add(this._meshContainer)
188
+ }
189
+
190
+ // Find character meshes and build bone map
191
+ this._findCharacterMeshes()
192
+ this._buildBoneMap()
193
+
194
+ if (this.autoCreateOnInit) {
195
+ this._createRagdoll()
196
+ // Start deactivated - bodies are kinematic until activated
197
+ this._deactivateBodies()
198
+ }
199
+
200
+ this._updateMeshVisibility()
201
+ }
202
+
203
+ destroy(): Record<string, any> {
204
+ this._removeRagdoll()
205
+
206
+ // Remove mesh container from scene
207
+ if (this._meshContainer) {
208
+ this.ctx.viewer?.scene.remove(this._meshContainer)
209
+ this._meshContainer = null
210
+ }
211
+
212
+ // Dispose material
213
+ if (this._visualMaterial) {
214
+ this._visualMaterial.dispose()
215
+ this._visualMaterial = null
216
+ }
217
+
218
+ this._cannon = null
219
+ return super.destroy()
220
+ }
221
+
222
+ // Called automatically by the component system each frame
223
+ preFrame(_event: ViewerEventMap["preFrame"]): void {
224
+ if (this._isActive) {
225
+ // When ragdoll is active, sync bones to follow ragdoll bodies
226
+ this._syncRagdollToBones()
227
+ } else {
228
+ // When not active, sync ragdoll to follow object transform
229
+ this._syncObjectPosition()
230
+ }
231
+ this._updateMeshes()
232
+ }
233
+
234
+ /**
235
+ * Syncs the character's bones to match the ragdoll body positions/rotations.
236
+ * Called each frame when ragdoll is active.
237
+ *
238
+ * The approach:
239
+ * 1. Calculate how much the ragdoll body has rotated in world space (delta from initial)
240
+ * 2. Apply that same world-space delta to the bone's initial world orientation
241
+ * 3. Convert the new world orientation back to local space for the bone
242
+ *
243
+ * Note: Only the root bone (hips) gets position updates. Other bones only get rotation updates
244
+ * because in a skeleton hierarchy, bone positions are fixed (defined by bone length).
245
+ */
246
+ private _syncRagdollToBones() {
247
+ if (!this._parts || this._characterMeshes.length === 0) return
248
+
249
+ // Helper to sync a bone's rotation to a ragdoll body
250
+ const syncBoneRotation = (body: Body, boneNames: string[]) => {
251
+ const bone = this._findBone(boneNames)
252
+ if (!bone) return
253
+
254
+ const boneName = bone.name
255
+
256
+ // Get stored initial values
257
+ const initialBodyQuat = this._initialBodyQuats.get(body)
258
+ const initialBoneWorldQuat = this._initialBoneWorldQuats.get(boneName)
259
+
260
+ if (!initialBodyQuat || !initialBoneWorldQuat) {
261
+ return
262
+ }
263
+
264
+ // Get the body's current world quaternion
265
+ const currentBodyQuat = new Quaternion(
266
+ body.quaternion.x,
267
+ body.quaternion.y,
268
+ body.quaternion.z,
269
+ body.quaternion.w
270
+ )
271
+
272
+ // Calculate delta rotation in world space: how much has the body rotated?
273
+ // deltaQuat = currentBodyQuat * inverse(initialBodyQuat)
274
+ const initialBodyQuatInverse = initialBodyQuat.clone().invert()
275
+ const worldDeltaQuat = currentBodyQuat.clone().multiply(initialBodyQuatInverse)
276
+
277
+ // Apply world delta to the bone's initial world orientation
278
+ // newBoneWorldQuat = worldDeltaQuat * initialBoneWorldQuat
279
+ const newBoneWorldQuat = worldDeltaQuat.clone().multiply(initialBoneWorldQuat)
280
+
281
+ // Convert new world quaternion to local space
282
+ if (bone.parent) {
283
+ // Get parent's current world quaternion
284
+ const parentWorldQuat = new Quaternion()
285
+ bone.parent.getWorldQuaternion(parentWorldQuat)
286
+
287
+ // localQuat = inverse(parentWorldQuat) * newBoneWorldQuat
288
+ const parentWorldQuatInverse = parentWorldQuat.clone().invert()
289
+ const newLocalQuat = parentWorldQuatInverse.clone().multiply(newBoneWorldQuat)
290
+
291
+ bone.quaternion.copy(newLocalQuat)
292
+ } else {
293
+ // No parent, world = local
294
+ bone.quaternion.copy(newBoneWorldQuat)
295
+ }
296
+ }
297
+
298
+ // Sync root bone (hips) - this one gets both position and rotation
299
+ const hipsBone = this._findBone(this._boneNames.hips)
300
+ if (hipsBone && this._parts.pelvis) {
301
+ // Position: only the root bone should move in world space
302
+ this._tempWorldPos.set(
303
+ this._parts.pelvis.position.x,
304
+ this._parts.pelvis.position.y,
305
+ this._parts.pelvis.position.z
306
+ )
307
+
308
+ if (hipsBone.parent) {
309
+ const parentMatrixWorldInverse = hipsBone.parent.matrixWorld.clone().invert()
310
+ this._tempWorldPos.applyMatrix4(parentMatrixWorldInverse)
311
+ }
312
+ hipsBone.position.copy(this._tempWorldPos)
313
+
314
+ // Rotation
315
+ syncBoneRotation(this._parts.pelvis, this._boneNames.hips)
316
+ }
317
+
318
+ // Sync all other bones - rotation only (no position changes)
319
+ syncBoneRotation(this._parts.head, this._boneNames.head)
320
+ syncBoneRotation(this._parts.upperBody, this._boneNames.spine2)
321
+ syncBoneRotation(this._parts.upperLeftLeg, this._boneNames.leftUpLeg)
322
+ syncBoneRotation(this._parts.upperRightLeg, this._boneNames.rightUpLeg)
323
+ syncBoneRotation(this._parts.lowerLeftLeg, this._boneNames.leftLeg)
324
+ syncBoneRotation(this._parts.lowerRightLeg, this._boneNames.rightLeg)
325
+ syncBoneRotation(this._parts.upperLeftArm, this._boneNames.leftArm)
326
+ syncBoneRotation(this._parts.upperRightArm, this._boneNames.rightArm)
327
+ syncBoneRotation(this._parts.lowerLeftArm, this._boneNames.leftForeArm)
328
+ syncBoneRotation(this._parts.lowerRightArm, this._boneNames.rightForeArm)
329
+
330
+ // Update the skeleton's matrix world
331
+ for (const skinnedMesh of this._characterMeshes) {
332
+ if (skinnedMesh.skeleton) {
333
+ skinnedMesh.skeleton.update()
334
+ }
335
+ }
336
+ }
337
+
338
+ private _syncObjectPosition() {
339
+ if (!this._initialized || this._bodies.length === 0) return
340
+
341
+ // Get current object world transform
342
+ this.object.getWorldPosition(this._currentObjectPosition)
343
+ this.object.getWorldQuaternion(this._currentObjectQuaternion)
344
+
345
+ // Calculate position delta
346
+ const deltaX = this._currentObjectPosition.x - this._lastObjectPosition.x
347
+ const deltaY = this._currentObjectPosition.y - this._lastObjectPosition.y
348
+ const deltaZ = this._currentObjectPosition.z - this._lastObjectPosition.z
349
+
350
+ // Check if rotation changed
351
+ const rotationChanged = !this._currentObjectQuaternion.equals(this._lastObjectQuaternion)
352
+
353
+ // If rotation changed, we need to rotate all bodies around the object's position
354
+ if (rotationChanged) {
355
+ // Calculate the delta rotation: deltaQ = currentQ * inverse(lastQ)
356
+ this._invLastQuaternion.copy(this._lastObjectQuaternion).invert()
357
+ this._deltaQuaternion.copy(this._currentObjectQuaternion).multiply(this._invLastQuaternion)
358
+
359
+ // Pivot point is the object's last position (before any position change)
360
+ this._pivotPoint.copy(this._lastObjectPosition)
361
+
362
+ // Rotate each body around the pivot point
363
+ for (const body of this._bodies) {
364
+ // Get body position relative to pivot
365
+ this._tempVec3.set(
366
+ body.position.x - this._pivotPoint.x,
367
+ body.position.y - this._pivotPoint.y,
368
+ body.position.z - this._pivotPoint.z
369
+ )
370
+
371
+ // Rotate the relative position by delta quaternion
372
+ this._tempVec3.applyQuaternion(this._deltaQuaternion)
373
+
374
+ // Set new position (pivot + rotated relative position)
375
+ body.position.x = this._pivotPoint.x + this._tempVec3.x
376
+ body.position.y = this._pivotPoint.y + this._tempVec3.y
377
+ body.position.z = this._pivotPoint.z + this._tempVec3.z
378
+
379
+ // Also rotate the body's own quaternion
380
+ // Convert cannon quaternion to three quaternion, apply delta, convert back
381
+ const bodyQuat = new Quaternion(
382
+ body.quaternion.x,
383
+ body.quaternion.y,
384
+ body.quaternion.z,
385
+ body.quaternion.w
386
+ )
387
+ bodyQuat.premultiply(this._deltaQuaternion)
388
+ body.quaternion.set(bodyQuat.x, bodyQuat.y, bodyQuat.z, bodyQuat.w)
389
+ }
390
+
391
+ // Update last quaternion
392
+ this._lastObjectQuaternion.copy(this._currentObjectQuaternion)
393
+ }
394
+
395
+ // Apply position delta (after rotation, so we translate the already-rotated positions)
396
+ if (deltaX !== 0 || deltaY !== 0 || deltaZ !== 0) {
397
+ for (const body of this._bodies) {
398
+ body.position.x += deltaX
399
+ body.position.y += deltaY
400
+ body.position.z += deltaZ
401
+ }
402
+
403
+ // Update last position
404
+ this._lastObjectPosition.copy(this._currentObjectPosition)
405
+ }
406
+ }
407
+
408
+ private _updateMeshes() {
409
+ if (!this._parts || this._meshes.length === 0) return
410
+
411
+ // Sync each mesh position/rotation with its corresponding body
412
+ for (let i = 0; i < this._bodies.length && i < this._meshes.length; i++) {
413
+ const body = this._bodies[i]
414
+ const mesh = this._meshes[i]
415
+
416
+ mesh.position.set(body.position.x, body.position.y, body.position.z)
417
+ mesh.quaternion.set(
418
+ body.quaternion.x,
419
+ body.quaternion.y,
420
+ body.quaternion.z,
421
+ body.quaternion.w
422
+ )
423
+ }
424
+ }
425
+
426
+ private _recreateRagdoll() {
427
+ if (!this._cannon) return
428
+ this._removeRagdoll()
429
+ this._createRagdoll()
430
+ this.object.setDirty?.({ source: 'CannonRagdollComponent' })
431
+ }
432
+
433
+ private _updateMass() {
434
+ for (const body of this._bodies) {
435
+ body.mass = this.mass
436
+ body.updateMassProperties()
437
+ }
438
+ this.object.setDirty?.({ source: 'CannonRagdollComponent' })
439
+ }
440
+
441
+ private _updateConstraints() {
442
+ // Recreate constraints with new angles
443
+ this._recreateRagdoll()
444
+ }
445
+
446
+ private _createRagdoll() {
447
+ if (!this._cannon) return
448
+
449
+ const scale = this.scale
450
+ const angle = this.angle
451
+ const angleShoulders = this.angleShoulders
452
+ const twistAngle = this.twistAngle
453
+
454
+ // Dimensions (same as original)
455
+ const shouldersDistance = 0.5 * scale
456
+ const upperArmLength = 0.4 * scale
457
+ const lowerArmLength = 0.4 * scale
458
+ const upperArmSize = 0.2 * scale
459
+ const lowerArmSize = 0.2 * scale
460
+ const neckLength = 0.1 * scale
461
+ const headRadius = 0.25 * scale
462
+ const upperBodyLength = 0.6 * scale
463
+ const pelvisLength = 0.4 * scale
464
+ const upperLegLength = 0.5 * scale
465
+ const upperLegSize = 0.2 * scale
466
+ const lowerLegSize = 0.2 * scale
467
+ const lowerLegLength = 0.5 * scale
468
+
469
+ // Shapes - Original uses Z as up, we use Y as up
470
+ // Original: Box(x, y, z) where Z is height for vertical parts
471
+ // Ours: Box(x, y, z) where Y is height for vertical parts
472
+ const headShape = new Sphere(headRadius)
473
+
474
+ // Arms extend along X axis (horizontal)
475
+ const upperArmShape = new Box(
476
+ new Vec3(upperArmLength * 0.5, upperArmSize * 0.5, upperArmSize * 0.5)
477
+ )
478
+ const lowerArmShape = new Box(
479
+ new Vec3(lowerArmLength * 0.5, lowerArmSize * 0.5, lowerArmSize * 0.5)
480
+ )
481
+
482
+ // Upper body: X is width (shoulders), Z is depth, Y is height
483
+ // Original: (shouldersDistance * 0.5, lowerArmSize * 0.5, upperBodyLength * 0.5)
484
+ // Convert Z->Y for height
485
+ const upperBodyShape = new Box(
486
+ new Vec3(shouldersDistance * 0.5, upperBodyLength * 0.5, lowerArmSize * 0.5)
487
+ )
488
+ const pelvisShape = new Box(
489
+ new Vec3(shouldersDistance * 0.5, pelvisLength * 0.5, lowerArmSize * 0.5)
490
+ )
491
+
492
+ // Legs: vertical, so Y is the length
493
+ const upperLegShape = new Box(
494
+ new Vec3(upperLegSize * 0.5, upperLegLength * 0.5, lowerArmSize * 0.5)
495
+ )
496
+ const lowerLegShape = new Box(
497
+ new Vec3(lowerLegSize * 0.5, lowerLegLength * 0.5, lowerArmSize * 0.5)
498
+ )
499
+
500
+ // Get world position and rotation of the object as the base transform
501
+ const basePos = this.object.getWorldPosition(new Vector3())
502
+ const baseQuat = this.object.getWorldQuaternion(new Quaternion())
503
+
504
+ // Helper to transform a local offset by the object's world rotation and add to world position
505
+ const transformLocalToWorld = (localOffset: Vector3): Vec3 => {
506
+ const worldOffset = localOffset.clone().applyQuaternion(baseQuat)
507
+ return new Vec3(
508
+ basePos.x + worldOffset.x,
509
+ basePos.y + worldOffset.y,
510
+ basePos.z + worldOffset.z
511
+ )
512
+ }
513
+
514
+ // Helper to create mesh for a body
515
+ const createMeshForBody = (body: Body, isHead = false): Mesh => {
516
+ let geometry
517
+ if (isHead) {
518
+ geometry = new SphereGeometry(headRadius, 16, 16)
519
+ } else {
520
+ const shape = body.shapes[0] as Box
521
+ geometry = new BoxGeometry(
522
+ shape.halfExtents.x * 2,
523
+ shape.halfExtents.y * 2,
524
+ shape.halfExtents.z * 2
525
+ )
526
+ }
527
+ const mesh = new Mesh(geometry, this._visualMaterial!)
528
+ mesh.position.set(body.position.x, body.position.y, body.position.z)
529
+ this._meshContainer?.add(mesh)
530
+ this._meshes.push(mesh)
531
+ return mesh
532
+ }
533
+
534
+ // Lower legs - positions converted from Z-up to Y-up
535
+ // Original: position(x, 0, lowerLegLength/2) -> ours: position(x, lowerLegLength/2, 0)
536
+ const lowerLeftLeg = new Body({
537
+ mass: this.mass,
538
+ position: transformLocalToWorld(new Vector3(shouldersDistance / 2, lowerLegLength / 2, 0)),
539
+ material: this.material,
540
+ })
541
+ const lowerRightLeg = new Body({
542
+ mass: this.mass,
543
+ position: transformLocalToWorld(new Vector3(-shouldersDistance / 2, lowerLegLength / 2, 0)),
544
+ material: this.material,
545
+ })
546
+ lowerLeftLeg.addShape(lowerLegShape)
547
+ lowerRightLeg.addShape(lowerLegShape)
548
+ lowerLeftLeg.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
549
+ lowerRightLeg.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
550
+ this._bodies.push(lowerLeftLeg, lowerRightLeg)
551
+ createMeshForBody(lowerLeftLeg)
552
+ createMeshForBody(lowerRightLeg)
553
+
554
+ // Upper legs
555
+ const upperLeftLeg = new Body({
556
+ mass: this.mass,
557
+ position: transformLocalToWorld(new Vector3(shouldersDistance / 2, lowerLegLength + upperLegLength / 2, 0)),
558
+ material: this.material,
559
+ })
560
+ const upperRightLeg = new Body({
561
+ mass: this.mass,
562
+ position: transformLocalToWorld(new Vector3(-shouldersDistance / 2, lowerLegLength + upperLegLength / 2, 0)),
563
+ material: this.material,
564
+ })
565
+ upperLeftLeg.addShape(upperLegShape)
566
+ upperRightLeg.addShape(upperLegShape)
567
+ upperLeftLeg.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
568
+ upperRightLeg.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
569
+ this._bodies.push(upperLeftLeg, upperRightLeg)
570
+ createMeshForBody(upperLeftLeg)
571
+ createMeshForBody(upperRightLeg)
572
+
573
+ // Pelvis
574
+ const pelvisLocalY = lowerLegLength + upperLegLength + pelvisLength / 2
575
+ const pelvis = new Body({
576
+ mass: this.mass,
577
+ position: transformLocalToWorld(new Vector3(0, pelvisLocalY, 0)),
578
+ material: this.material,
579
+ })
580
+ pelvis.addShape(pelvisShape)
581
+ pelvis.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
582
+ this._bodies.push(pelvis)
583
+ createMeshForBody(pelvis)
584
+
585
+ // Upper body
586
+ const upperBodyLocalY = pelvisLocalY + pelvisLength / 2 + upperBodyLength / 2
587
+ const upperBody = new Body({
588
+ mass: this.mass,
589
+ position: transformLocalToWorld(new Vector3(0, upperBodyLocalY, 0)),
590
+ material: this.material,
591
+ })
592
+ upperBody.addShape(upperBodyShape)
593
+ upperBody.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
594
+ this._bodies.push(upperBody)
595
+ createMeshForBody(upperBody)
596
+
597
+ // Head
598
+ const headLocalY = upperBodyLocalY + upperBodyLength / 2 + headRadius + neckLength
599
+ const head = new Body({
600
+ mass: this.mass,
601
+ position: transformLocalToWorld(new Vector3(0, headLocalY, 0)),
602
+ material: this.material,
603
+ })
604
+ head.addShape(headShape)
605
+ head.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
606
+ this._bodies.push(head)
607
+ createMeshForBody(head, true)
608
+
609
+ // Upper arms - at shoulder height
610
+ const armLocalY = upperBodyLocalY + upperBodyLength / 2
611
+ const upperLeftArm = new Body({
612
+ mass: this.mass,
613
+ position: transformLocalToWorld(new Vector3(shouldersDistance / 2 + upperArmLength / 2, armLocalY, 0)),
614
+ material: this.material,
615
+ })
616
+ const upperRightArm = new Body({
617
+ mass: this.mass,
618
+ position: transformLocalToWorld(new Vector3(-shouldersDistance / 2 - upperArmLength / 2, armLocalY, 0)),
619
+ material: this.material,
620
+ })
621
+ upperLeftArm.addShape(upperArmShape)
622
+ upperRightArm.addShape(upperArmShape)
623
+ upperLeftArm.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
624
+ upperRightArm.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
625
+ this._bodies.push(upperLeftArm, upperRightArm)
626
+ createMeshForBody(upperLeftArm)
627
+ createMeshForBody(upperRightArm)
628
+
629
+ // Lower arms
630
+ const lowerLeftArm = new Body({
631
+ mass: this.mass,
632
+ position: transformLocalToWorld(new Vector3(
633
+ shouldersDistance / 2 + upperArmLength + lowerArmLength / 2,
634
+ armLocalY,
635
+ 0
636
+ )),
637
+ material: this.material,
638
+ })
639
+ const lowerRightArm = new Body({
640
+ mass: this.mass,
641
+ position: transformLocalToWorld(new Vector3(
642
+ -shouldersDistance / 2 - upperArmLength - lowerArmLength / 2,
643
+ armLocalY,
644
+ 0
645
+ )),
646
+ material: this.material,
647
+ })
648
+ lowerLeftArm.addShape(lowerArmShape)
649
+ lowerRightArm.addShape(lowerArmShape)
650
+ lowerLeftArm.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
651
+ lowerRightArm.quaternion.set(baseQuat.x, baseQuat.y, baseQuat.z, baseQuat.w)
652
+ this._bodies.push(lowerLeftArm, lowerRightArm)
653
+ createMeshForBody(lowerLeftArm)
654
+ createMeshForBody(lowerRightArm)
655
+
656
+ // Store parts reference
657
+ this._parts = {
658
+ lowerLeftLeg,
659
+ lowerRightLeg,
660
+ upperLeftLeg,
661
+ upperRightLeg,
662
+ pelvis,
663
+ upperBody,
664
+ head,
665
+ upperLeftArm,
666
+ upperRightArm,
667
+ lowerLeftArm,
668
+ lowerRightArm,
669
+ }
670
+
671
+ // Add bodies to world
672
+ for (const body of this._bodies) {
673
+ this._cannon.world.addBody(body)
674
+ }
675
+
676
+ // CONSTRAINTS - Convert from Z-up to Y-up
677
+ // Original uses UNIT_Z for vertical axis, we use UNIT_Y
678
+ // Pivots: original (x, y, z) where z is height -> ours (x, y, z) where y is height
679
+ // So pivot (x, 0, z) -> (x, z, 0)
680
+
681
+ // Neck joint
682
+ // Original: pivotA(0, 0, -headRadius - neckLength/2), pivotB(0, 0, upperBodyLength/2)
683
+ // Ours: pivotA(0, -headRadius - neckLength/2, 0), pivotB(0, upperBodyLength/2, 0)
684
+ const neckJoint = new ConeTwistConstraint(head, upperBody, {
685
+ pivotA: new Vec3(0, -headRadius - neckLength / 2, 0),
686
+ pivotB: new Vec3(0, upperBodyLength / 2, 0),
687
+ axisA: Vec3.UNIT_Y,
688
+ axisB: Vec3.UNIT_Y,
689
+ angle,
690
+ twistAngle,
691
+ })
692
+ this._constraints.push(neckJoint)
693
+
694
+ // Knee joints
695
+ // Original: pivotA(0, 0, lowerLegLength/2), pivotB(0, 0, -upperLegLength/2)
696
+ // Ours: pivotA(0, lowerLegLength/2, 0), pivotB(0, -upperLegLength/2, 0)
697
+ const leftKneeJoint = new ConeTwistConstraint(lowerLeftLeg, upperLeftLeg, {
698
+ pivotA: new Vec3(0, lowerLegLength / 2, 0),
699
+ pivotB: new Vec3(0, -upperLegLength / 2, 0),
700
+ axisA: Vec3.UNIT_Y,
701
+ axisB: Vec3.UNIT_Y,
702
+ angle,
703
+ twistAngle,
704
+ })
705
+ const rightKneeJoint = new ConeTwistConstraint(lowerRightLeg, upperRightLeg, {
706
+ pivotA: new Vec3(0, lowerLegLength / 2, 0),
707
+ pivotB: new Vec3(0, -upperLegLength / 2, 0),
708
+ axisA: Vec3.UNIT_Y,
709
+ axisB: Vec3.UNIT_Y,
710
+ angle,
711
+ twistAngle,
712
+ })
713
+ this._constraints.push(leftKneeJoint, rightKneeJoint)
714
+
715
+ // Hip joints
716
+ // Original: pivotA(0, 0, upperLegLength/2), pivotB(shouldersDistance/2, 0, -pelvisLength/2)
717
+ // Ours: pivotA(0, upperLegLength/2, 0), pivotB(shouldersDistance/2, -pelvisLength/2, 0)
718
+ const leftHipJoint = new ConeTwistConstraint(upperLeftLeg, pelvis, {
719
+ pivotA: new Vec3(0, upperLegLength / 2, 0),
720
+ pivotB: new Vec3(shouldersDistance / 2, -pelvisLength / 2, 0),
721
+ axisA: Vec3.UNIT_Y,
722
+ axisB: Vec3.UNIT_Y,
723
+ angle,
724
+ twistAngle,
725
+ })
726
+ const rightHipJoint = new ConeTwistConstraint(upperRightLeg, pelvis, {
727
+ pivotA: new Vec3(0, upperLegLength / 2, 0),
728
+ pivotB: new Vec3(-shouldersDistance / 2, -pelvisLength / 2, 0),
729
+ axisA: Vec3.UNIT_Y,
730
+ axisB: Vec3.UNIT_Y,
731
+ angle,
732
+ twistAngle,
733
+ })
734
+ this._constraints.push(leftHipJoint, rightHipJoint)
735
+
736
+ // Spine
737
+ // Original: pivotA(0, 0, pelvisLength/2), pivotB(0, 0, -upperBodyLength/2)
738
+ // Ours: pivotA(0, pelvisLength/2, 0), pivotB(0, -upperBodyLength/2, 0)
739
+ const spineJoint = new ConeTwistConstraint(pelvis, upperBody, {
740
+ pivotA: new Vec3(0, pelvisLength / 2, 0),
741
+ pivotB: new Vec3(0, -upperBodyLength / 2, 0),
742
+ axisA: Vec3.UNIT_Y,
743
+ axisB: Vec3.UNIT_Y,
744
+ angle,
745
+ twistAngle,
746
+ })
747
+ this._constraints.push(spineJoint)
748
+
749
+ // Shoulders - In Y-up world, arms need to rotate around Z axis to swing down
750
+ // The cone axis should be Z so the arm can swing in the X-Y plane (up/down motion)
751
+ const leftShoulder = new ConeTwistConstraint(upperBody, upperLeftArm, {
752
+ pivotA: new Vec3(shouldersDistance / 2, upperBodyLength / 2, 0),
753
+ pivotB: new Vec3(-upperArmLength / 2, 0, 0),
754
+ axisA: Vec3.UNIT_Z,
755
+ axisB: Vec3.UNIT_Z,
756
+ angle: angleShoulders,
757
+ twistAngle,
758
+ })
759
+ const rightShoulder = new ConeTwistConstraint(upperBody, upperRightArm, {
760
+ pivotA: new Vec3(-shouldersDistance / 2, upperBodyLength / 2, 0),
761
+ pivotB: new Vec3(upperArmLength / 2, 0, 0),
762
+ axisA: Vec3.UNIT_Z,
763
+ axisB: Vec3.UNIT_Z,
764
+ angle: angleShoulders,
765
+ twistAngle,
766
+ })
767
+ this._constraints.push(leftShoulder, rightShoulder)
768
+
769
+ // Elbow joints - also need Z axis for bending in Y-up world
770
+ const leftElbowJoint = new ConeTwistConstraint(lowerLeftArm, upperLeftArm, {
771
+ pivotA: new Vec3(-lowerArmLength / 2, 0, 0),
772
+ pivotB: new Vec3(upperArmLength / 2, 0, 0),
773
+ axisA: Vec3.UNIT_Z,
774
+ axisB: Vec3.UNIT_Z,
775
+ angle,
776
+ twistAngle,
777
+ })
778
+ const rightElbowJoint = new ConeTwistConstraint(lowerRightArm, upperRightArm, {
779
+ pivotA: new Vec3(lowerArmLength / 2, 0, 0),
780
+ pivotB: new Vec3(-upperArmLength / 2, 0, 0),
781
+ axisA: Vec3.UNIT_Z,
782
+ axisB: Vec3.UNIT_Z,
783
+ angle,
784
+ twistAngle,
785
+ })
786
+ this._constraints.push(leftElbowJoint, rightElbowJoint)
787
+
788
+ // Store constraints reference
789
+ this._ragdollConstraints = {
790
+ neckJoint,
791
+ leftKneeJoint,
792
+ rightKneeJoint,
793
+ leftHipJoint,
794
+ rightHipJoint,
795
+ spineJoint,
796
+ leftShoulder,
797
+ rightShoulder,
798
+ leftElbowJoint,
799
+ rightElbowJoint,
800
+ }
801
+
802
+ // Add constraints to world
803
+ for (const constraint of this._constraints) {
804
+ this._cannon.world.addConstraint(constraint)
805
+ }
806
+
807
+ // Store initial object transform for tracking movement
808
+ this.object.getWorldPosition(this._lastObjectPosition)
809
+ this.object.getWorldQuaternion(this._lastObjectQuaternion)
810
+ this._initialized = true
811
+ }
812
+
813
+ private _removeRagdoll() {
814
+ // Remove constraints from world
815
+ for (const constraint of this._constraints) {
816
+ this._cannon?.world.removeConstraint(constraint)
817
+ }
818
+ this._constraints = []
819
+ this._ragdollConstraints = null
820
+
821
+ // Remove bodies from world
822
+ for (const body of this._bodies) {
823
+ this._cannon?.world.removeBody(body)
824
+ }
825
+ this._bodies = []
826
+ this._parts = null
827
+
828
+ // Remove and dispose meshes
829
+ for (const mesh of this._meshes) {
830
+ mesh.geometry.dispose()
831
+ this._meshContainer?.remove(mesh)
832
+ }
833
+ this._meshes = []
834
+ }
835
+
836
+ /**
837
+ * Reset the ragdoll to its initial position relative to the object
838
+ */
839
+ resetPosition() {
840
+ if (!this._parts) return
841
+
842
+ const basePos = this.object.getWorldPosition(new Vector3())
843
+ const scale = this.scale
844
+
845
+ const shouldersDistance = 0.5 * scale
846
+ const lowerLegLength = 0.5 * scale
847
+ const upperLegLength = 0.5 * scale
848
+ const pelvisLength = 0.4 * scale
849
+ const upperBodyLength = 0.6 * scale
850
+ const headRadius = 0.25 * scale
851
+ const neckLength = 0.1 * scale
852
+ const upperArmLength = 0.4 * scale
853
+ const lowerArmLength = 0.4 * scale
854
+
855
+ const resetBody = (body: Body, pos: Vec3) => {
856
+ body.position.copy(pos)
857
+ body.velocity.set(0, 0, 0)
858
+ body.angularVelocity.set(0, 0, 0)
859
+ body.quaternion.set(0, 0, 0, 1)
860
+ }
861
+
862
+ resetBody(this._parts.lowerLeftLeg, new Vec3(
863
+ basePos.x + shouldersDistance / 2,
864
+ basePos.y + lowerLegLength / 2,
865
+ basePos.z
866
+ ))
867
+ resetBody(this._parts.lowerRightLeg, new Vec3(
868
+ basePos.x - shouldersDistance / 2,
869
+ basePos.y + lowerLegLength / 2,
870
+ basePos.z
871
+ ))
872
+ resetBody(this._parts.upperLeftLeg, new Vec3(
873
+ basePos.x + shouldersDistance / 2,
874
+ basePos.y + lowerLegLength + upperLegLength / 2,
875
+ basePos.z
876
+ ))
877
+ resetBody(this._parts.upperRightLeg, new Vec3(
878
+ basePos.x - shouldersDistance / 2,
879
+ basePos.y + lowerLegLength + upperLegLength / 2,
880
+ basePos.z
881
+ ))
882
+
883
+ const pelvisY = basePos.y + lowerLegLength + upperLegLength + pelvisLength / 2
884
+ resetBody(this._parts.pelvis, new Vec3(basePos.x, pelvisY, basePos.z))
885
+
886
+ const upperBodyY = pelvisY + pelvisLength / 2 + upperBodyLength / 2
887
+ resetBody(this._parts.upperBody, new Vec3(basePos.x, upperBodyY, basePos.z))
888
+
889
+ const headY = upperBodyY + upperBodyLength / 2 + headRadius + neckLength
890
+ resetBody(this._parts.head, new Vec3(basePos.x, headY, basePos.z))
891
+
892
+ const armY = upperBodyY + upperBodyLength / 2
893
+ resetBody(this._parts.upperLeftArm, new Vec3(
894
+ basePos.x + shouldersDistance / 2 + upperArmLength / 2,
895
+ armY,
896
+ basePos.z
897
+ ))
898
+ resetBody(this._parts.upperRightArm, new Vec3(
899
+ basePos.x - shouldersDistance / 2 - upperArmLength / 2,
900
+ armY,
901
+ basePos.z
902
+ ))
903
+ resetBody(this._parts.lowerLeftArm, new Vec3(
904
+ basePos.x + shouldersDistance / 2 + upperArmLength + lowerArmLength / 2,
905
+ armY,
906
+ basePos.z
907
+ ))
908
+ resetBody(this._parts.lowerRightArm, new Vec3(
909
+ basePos.x - shouldersDistance / 2 - upperArmLength - lowerArmLength / 2,
910
+ armY,
911
+ basePos.z
912
+ ))
913
+
914
+ this.object.setDirty?.({ source: 'CannonRagdollComponent' })
915
+ }
916
+
917
+ /**
918
+ * Apply an impulse to all bodies
919
+ */
920
+ applyImpulse(impulse: Vec3, worldPoint?: Vec3) {
921
+ for (const body of this._bodies) {
922
+ body.applyImpulse(impulse, worldPoint || body.position)
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Apply an impulse to a specific body part
928
+ */
929
+ applyImpulseToPart(partName: keyof RagdollParts, impulse: Vec3, worldPoint?: Vec3) {
930
+ if (!this._parts) return
931
+ const body = this._parts[partName]
932
+ if (body) {
933
+ body.applyImpulse(impulse, worldPoint || body.position)
934
+ }
935
+ }
936
+
937
+ private _updateMeshVisibility() {
938
+ if (this._meshContainer) {
939
+ this._meshContainer.visible = !this.hideRagdollMeshes
940
+ }
941
+ }
942
+
943
+ private _findCharacterMeshes() {
944
+ this._characterMeshes = []
945
+ this.object.traverse((child) => {
946
+ if ((child as unknown as SkinnedMesh).isSkinnedMesh) {
947
+ this._characterMeshes.push(child as unknown as SkinnedMesh)
948
+ }
949
+ })
950
+ }
951
+
952
+ private _buildBoneMap() {
953
+ this._boneMap.clear()
954
+ this.object.traverse((child) => {
955
+ if ((child as Bone).isBone) {
956
+ const bone = child as Bone
957
+ // Map by exact name
958
+ this._boneMap.set(bone.name, bone)
959
+ // Also map by lowercase for case-insensitive matching
960
+ this._boneMap.set(bone.name.toLowerCase(), bone)
961
+ }
962
+ })
963
+ }
964
+
965
+ private _findBone(boneNames: string[]): Bone | null {
966
+ for (const name of boneNames) {
967
+ const bone = this._boneMap.get(name) || this._boneMap.get(name.toLowerCase())
968
+ if (bone) return bone
969
+ }
970
+ return null
971
+ }
972
+
973
+ private _getBoneWorldPosition(bone: Bone): Vector3 {
974
+ bone.getWorldPosition(this._tempWorldPos)
975
+ return this._tempWorldPos.clone()
976
+ }
977
+
978
+ private _getBoneWorldQuaternion(bone: Bone): Quaternion {
979
+ bone.getWorldQuaternion(this._tempWorldQuat)
980
+ return this._tempWorldQuat.clone()
981
+ }
982
+
983
+ start() {
984
+ super.start();
985
+ setTimeout(()=>{
986
+ this.activate();
987
+ }, 2000)
988
+ }
989
+
990
+ /**
991
+ * Activates the ragdoll physics simulation.
992
+ * Call this when the character should start ragdolling (e.g., on death).
993
+ * @param impulse Optional initial impulse to apply (e.g., from bullet impact)
994
+ * @param impactPoint Optional world point where impulse is applied
995
+ */
996
+ activate(impulse?: Vec3, impactPoint?: Vec3) {
997
+ if (this._isActive) return
998
+
999
+ // Create ragdoll if not created yet
1000
+ if (!this._parts) {
1001
+ this._createRagdoll()
1002
+ }
1003
+
1004
+ // Store initial bone quaternions (local space) before activation
1005
+ this._storeInitialBoneQuats()
1006
+
1007
+ // Store initial body quaternions (world space)
1008
+ this._storeInitialBodyQuats()
1009
+
1010
+ // Activate physics bodies (make them dynamic)
1011
+ this._activateBodies()
1012
+
1013
+ // Show ragdoll meshes if not hidden
1014
+ if (this._meshContainer) {
1015
+ this._meshContainer.visible = !this.hideRagdollMeshes
1016
+ }
1017
+
1018
+ // Apply initial impulse if provided
1019
+ if (impulse) {
1020
+ // Apply impulse to central body parts (pelvis, upperBody, head)
1021
+ // Limbs will follow naturally due to constraints and gravity
1022
+ if (this._parts) {
1023
+ this._parts.pelvis.applyImpulse(impulse, new Vec3(0, 0, 0))
1024
+ this._parts.upperBody.applyImpulse(impulse, new Vec3(0, 0, 0))
1025
+ this._parts.head.applyImpulse(impulse, new Vec3(0, 0, 0))
1026
+ }
1027
+ }
1028
+
1029
+ this._isActive = true
1030
+ }
1031
+
1032
+ /**
1033
+ * Store initial bone quaternions (both local and world space) for rotation offset calculation
1034
+ */
1035
+ private _storeInitialBoneQuats() {
1036
+ this._initialBoneQuats.clear()
1037
+ this._initialBoneWorldQuats.clear()
1038
+
1039
+ const storeBoneQuat = (boneNames: string[]) => {
1040
+ const bone = this._findBone(boneNames)
1041
+ if (bone) {
1042
+ // Store local quaternion
1043
+ this._initialBoneQuats.set(bone.name, bone.quaternion.clone())
1044
+ // Store world quaternion
1045
+ const worldQuat = new Quaternion()
1046
+ bone.getWorldQuaternion(worldQuat)
1047
+ this._initialBoneWorldQuats.set(bone.name, worldQuat)
1048
+ }
1049
+ }
1050
+
1051
+ storeBoneQuat(this._boneNames.head)
1052
+ storeBoneQuat(this._boneNames.spine2)
1053
+ storeBoneQuat(this._boneNames.hips)
1054
+ storeBoneQuat(this._boneNames.leftUpLeg)
1055
+ storeBoneQuat(this._boneNames.rightUpLeg)
1056
+ storeBoneQuat(this._boneNames.leftLeg)
1057
+ storeBoneQuat(this._boneNames.rightLeg)
1058
+ storeBoneQuat(this._boneNames.leftArm)
1059
+ storeBoneQuat(this._boneNames.rightArm)
1060
+ storeBoneQuat(this._boneNames.leftForeArm)
1061
+ storeBoneQuat(this._boneNames.rightForeArm)
1062
+ }
1063
+
1064
+ /**
1065
+ * Store initial body quaternions (world space) for rotation offset calculation
1066
+ */
1067
+ private _storeInitialBodyQuats() {
1068
+ this._initialBodyQuats.clear()
1069
+
1070
+ for (const body of this._bodies) {
1071
+ this._initialBodyQuats.set(body, new Quaternion(
1072
+ body.quaternion.x,
1073
+ body.quaternion.y,
1074
+ body.quaternion.z,
1075
+ body.quaternion.w
1076
+ ))
1077
+ }
1078
+ }
1079
+
1080
+ /**
1081
+ * Deactivates the ragdoll and restores the character.
1082
+ * Call this to reset the character to normal state.
1083
+ */
1084
+ deactivate() {
1085
+ if (!this._isActive) return
1086
+
1087
+ // Deactivate physics bodies (make them kinematic)
1088
+ this._deactivateBodies()
1089
+
1090
+ // Show character mesh, hide ragdoll
1091
+ this._setCharacterVisible(true)
1092
+ if (this._meshContainer) {
1093
+ this._meshContainer.visible = false
1094
+ }
1095
+
1096
+ this._isActive = false
1097
+ }
1098
+
1099
+ private _matchBonesToRagdoll() {
1100
+ if (!this._parts) return
1101
+
1102
+ // Match each ragdoll body to its corresponding bone position
1103
+ const matchBodyToBone = (body: Body, boneNames: string[]) => {
1104
+ const bone = this._findBone(boneNames)
1105
+ if (bone) {
1106
+ const worldPos = this._getBoneWorldPosition(bone)
1107
+ const worldQuat = this._getBoneWorldQuaternion(bone)
1108
+ body.position.set(worldPos.x, worldPos.y, worldPos.z)
1109
+ body.quaternion.set(worldQuat.x, worldQuat.y, worldQuat.z, worldQuat.w)
1110
+ body.velocity.set(0, 0, 0)
1111
+ body.angularVelocity.set(0, 0, 0)
1112
+ }
1113
+ }
1114
+
1115
+ matchBodyToBone(this._parts.head, this._boneNames.head)
1116
+ matchBodyToBone(this._parts.upperBody, this._boneNames.spine2)
1117
+ matchBodyToBone(this._parts.pelvis, this._boneNames.hips)
1118
+ matchBodyToBone(this._parts.upperLeftLeg, this._boneNames.leftUpLeg)
1119
+ matchBodyToBone(this._parts.upperRightLeg, this._boneNames.rightUpLeg)
1120
+ matchBodyToBone(this._parts.lowerLeftLeg, this._boneNames.leftLeg)
1121
+ matchBodyToBone(this._parts.lowerRightLeg, this._boneNames.rightLeg)
1122
+ matchBodyToBone(this._parts.upperLeftArm, this._boneNames.leftArm)
1123
+ matchBodyToBone(this._parts.upperRightArm, this._boneNames.rightArm)
1124
+ matchBodyToBone(this._parts.lowerLeftArm, this._boneNames.leftForeArm)
1125
+ matchBodyToBone(this._parts.lowerRightArm, this._boneNames.rightForeArm)
1126
+ }
1127
+
1128
+ private _activateBodies() {
1129
+ for (const body of this._bodies) {
1130
+ body.type = Body.DYNAMIC
1131
+ body.mass = this.mass
1132
+ body.updateMassProperties()
1133
+ body.wakeUp()
1134
+ }
1135
+ }
1136
+
1137
+ private _deactivateBodies() {
1138
+ for (const body of this._bodies) {
1139
+ body.type = Body.KINEMATIC
1140
+ body.mass = 0
1141
+ body.velocity.set(0, 0, 0)
1142
+ body.angularVelocity.set(0, 0, 0)
1143
+ }
1144
+ }
1145
+
1146
+ private _setCharacterVisible(visible: boolean) {
1147
+ for (const mesh of this._characterMeshes) {
1148
+ mesh.visible = visible
1149
+ }
1150
+ }
1151
+
1152
+ private _findClosestBody(point: Vec3): Body | null {
1153
+ let closest: Body | null = null
1154
+ let minDist = Infinity
1155
+
1156
+ for (const body of this._bodies) {
1157
+ const dx = body.position.x - point.x
1158
+ const dy = body.position.y - point.y
1159
+ const dz = body.position.z - point.z
1160
+ const dist = dx * dx + dy * dy + dz * dz
1161
+
1162
+ if (dist < minDist) {
1163
+ minDist = dist
1164
+ closest = body
1165
+ }
1166
+ }
1167
+
1168
+ return closest
1169
+ }
1170
+ }