@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.
- package/dist/authoring.d.ts +54 -0
- package/dist/authoring.js +165 -0
- package/dist/authoring.js.map +1 -0
- package/dist/authoringValidation.d.ts +110 -0
- package/dist/authoringValidation.js +488 -0
- package/dist/authoringValidation.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/fileTypes.d.ts +5 -0
- package/dist/fileTypes.js +51 -0
- package/dist/fileTypes.js.map +1 -0
- package/dist/importMap.d.ts +15 -0
- package/dist/importMap.js +62 -0
- package/dist/importMap.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +2756 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.js +5 -0
- package/dist/migrations.js.map +1 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +13 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugins/GeneratorComponent.d.ts +40 -0
- package/dist/plugins/HtmlUiComponent.d.ts +52 -0
- package/dist/plugins/HtmlUiComponent.example.d.ts +0 -0
- package/dist/plugins/cannon/Cannon3DBodyComponent.d.ts +27 -0
- package/dist/plugins/cannon/Cannon3DShapeComponent.d.ts +57 -0
- package/dist/plugins/cannon/CannonPhysicsPlugin.d.ts +63 -0
- package/dist/plugins/cannon/CannonRagdollComponent.d.ts +148 -0
- package/dist/plugins/cannon/helper.d.ts +24 -0
- package/dist/plugins/cannon/threeToCannon.d.ts +63 -0
- package/dist/plugins/cannon/utils.d.ts +9 -0
- package/dist/projectFormat.js +108 -0
- package/dist/projectFormat.js.map +1 -0
- package/dist/runtime/createGame.d.ts +29 -0
- package/dist/runtime/index.d.ts +18 -0
- package/dist/runtime/migrations.d.ts +5 -0
- package/dist/runtime/nestedAssets.d.ts +26 -0
- package/dist/runtime/projectFormat.d.ts +83 -0
- package/dist/runtime/version.d.ts +1 -0
- package/dist/runtime.js +72835 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sceneSerialization.d.ts +17 -0
- package/dist/sceneSerialization.js +174 -0
- package/dist/sceneSerialization.js.map +1 -0
- package/dist/scripts.d.ts +17 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +86 -0
- package/src/authoring.ts +293 -0
- package/src/authoringValidation.ts +815 -0
- package/src/defaults.ts +277 -0
- package/src/fileTypes.ts +53 -0
- package/src/importMap.ts +105 -0
- package/src/index.ts +15 -0
- package/src/paths.ts +9 -0
- package/src/plugins/GeneratorComponent.ts +275 -0
- package/src/plugins/HtmlUiComponent.example.ts +202 -0
- package/src/plugins/HtmlUiComponent.md +219 -0
- package/src/plugins/HtmlUiComponent.ts +320 -0
- package/src/plugins/cannon/Cannon3DBodyComponent.ts +227 -0
- package/src/plugins/cannon/Cannon3DShapeComponent.ts +258 -0
- package/src/plugins/cannon/CannonPhysicsPlugin.ts +409 -0
- package/src/plugins/cannon/CannonRagdollComponent.ts +1170 -0
- package/src/plugins/cannon/helper.ts +255 -0
- package/src/plugins/cannon/threeToCannon.ts +441 -0
- package/src/plugins/cannon/utils.ts +185 -0
- package/src/runtime/createGame.ts +337 -0
- package/src/runtime/index.ts +19 -0
- package/src/runtime/migrations.ts +6 -0
- package/src/runtime/nestedAssets.ts +226 -0
- package/src/runtime/projectFormat.ts +233 -0
- package/src/runtime/version.ts +3 -0
- package/src/sceneSerialization.ts +289 -0
- 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
|
+
}
|