ragdoll.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import { Vector3, Matrix, TmpVectors, Quaternion } from "../../Maths/math.vector.js";
  2. import { PhysicsAggregate } from "./physicsAggregate.js";
  3. import { PhysicsConstraint } from "./physicsConstraint.js";
  4. import { Axis, Space } from "../../Maths/math.axis.js";
  5. import { PhysicsShapeType, PhysicsConstraintType, PhysicsMotionType } from "./IPhysicsEnginePlugin.js";
  6. import { Logger } from "../../Misc/logger.js";
  7. import { TransformNode } from "../../Meshes/transformNode.js";
  8. /**
  9. * Ragdoll bone properties
  10. * @experimental
  11. */
  12. export class RagdollBoneProperties {
  13. }
  14. /**
  15. * Ragdoll for Physics V2
  16. * @experimental
  17. */
  18. export class Ragdoll {
  19. /**
  20. * Construct a new Ragdoll object. Once ready, it can be made dynamic by calling `Ragdoll` method
  21. * @param skeleton The skeleton containing bones to be physicalized
  22. * @param rootTransformNode The mesh or its transform used by the skeleton
  23. * @param config an array of `RagdollBoneProperties` corresponding to bones and their properties used to instanciate physics bodies
  24. */
  25. constructor(skeleton, rootTransformNode, config) {
  26. this._boxConfigs = new Array();
  27. this._joints = new Array();
  28. this._bones = new Array();
  29. this._initialRotation = new Array();
  30. // without mesh transform, to figure out later
  31. this._initialRotation2 = new Array();
  32. this._boneNames = [];
  33. this._transforms = new Array();
  34. this._aggregates = new Array();
  35. this._ragdollMode = false;
  36. this._rootBoneName = "";
  37. this._rootBoneIndex = -1;
  38. this._mass = 10;
  39. this._restitution = 0;
  40. /**
  41. * Pause synchronization between physics and bone position/orientation
  42. */
  43. this.pauseSync = false;
  44. this._defaultJoint = PhysicsConstraintType.HINGE;
  45. this._defaultJointMin = -90;
  46. this._defaultJointMax = 90;
  47. this._skeleton = skeleton;
  48. this._scene = skeleton.getScene();
  49. this._rootTransformNode = rootTransformNode;
  50. this._config = config; // initial, user defined box configs. May have several box configs jammed into 1 index.
  51. this._boxConfigs = []; // final box configs. Every element is a separate box config (this.config may have several configs jammed into 1 index).
  52. this._putBoxesInBoneCenter = false;
  53. this._defaultJoint = PhysicsConstraintType.HINGE;
  54. this._init();
  55. }
  56. /**
  57. * Returns the aggregate corresponding to the ragdoll bone index
  58. * @param index ragdoll bone aggregate index
  59. * @returns the aggregate for the bone index for the root aggregate if index is invalid
  60. */
  61. getAggregate(index) {
  62. if (index < 0 || index >= this._aggregates.length) {
  63. return this._aggregates[this._rootBoneIndex];
  64. }
  65. return this._aggregates[index];
  66. }
  67. _createColliders() {
  68. this._rootTransformNode.computeWorldMatrix();
  69. this._skeleton.computeAbsoluteMatrices(true);
  70. this._skeleton.prepare(true);
  71. const config = this._config;
  72. for (let i = 0; i < config.length; i++) {
  73. const boneNames = config[i].bone !== undefined ? [config[i].bone] : config[i].bones;
  74. for (let ii = 0; ii < boneNames.length; ii++) {
  75. const currentBone = this._skeleton.bones[this._skeleton.getBoneIndexByName(boneNames[ii])];
  76. if (currentBone == undefined) {
  77. return;
  78. }
  79. // First define the box dimensions, so we can then use them when calling CreateBox().
  80. const currentRagdollBoneProperties = {
  81. width: this._config[i].width,
  82. depth: this._config[i].depth,
  83. height: this._config[i].height,
  84. size: this._config[i].size,
  85. };
  86. currentRagdollBoneProperties.width = currentRagdollBoneProperties.width ?? currentRagdollBoneProperties.size;
  87. currentRagdollBoneProperties.depth = currentRagdollBoneProperties.depth ?? currentRagdollBoneProperties.size;
  88. currentRagdollBoneProperties.height = currentRagdollBoneProperties.height ?? currentRagdollBoneProperties.size;
  89. const transform = new TransformNode(boneNames[ii] + "_transform", this._scene);
  90. // Define the rest of the box properties.
  91. currentRagdollBoneProperties.joint = config[i].joint !== undefined ? config[i].joint : this._defaultJoint;
  92. currentRagdollBoneProperties.rotationAxis = config[i].rotationAxis !== undefined ? config[i].rotationAxis : Axis.X;
  93. currentRagdollBoneProperties.min = config[i].min !== undefined ? config[i].min : this._defaultJointMin;
  94. currentRagdollBoneProperties.max = config[i].max !== undefined ? config[i].max : this._defaultJointMax;
  95. // Offset value.
  96. let boxOffset = 0;
  97. if ((config[i].putBoxInBoneCenter !== undefined && config[i].putBoxInBoneCenter) || this._putBoxesInBoneCenter) {
  98. if (currentBone.length === undefined) {
  99. Logger.Log("The length property is not defined for bone " + currentBone.name);
  100. }
  101. boxOffset = currentBone.length / 2;
  102. }
  103. else if (config[i].boxOffset !== undefined) {
  104. boxOffset = config[i].boxOffset;
  105. }
  106. currentRagdollBoneProperties.boxOffset = boxOffset;
  107. // Offset axis.
  108. const boneOffsetAxis = config[i].boneOffsetAxis !== undefined ? config[i].boneOffsetAxis : Axis.Y;
  109. const boneDir = currentBone.getDirection(boneOffsetAxis, this._rootTransformNode);
  110. currentRagdollBoneProperties.boneOffsetAxis = boneOffsetAxis;
  111. transform.position = currentBone.getAbsolutePosition(this._rootTransformNode).add(boneDir.scale(boxOffset));
  112. const mass = config[i].mass !== undefined ? config[i].mass : this._mass;
  113. const restitution = config[i].restitution !== undefined ? config[i].restitution : this._restitution;
  114. const aggregate = new PhysicsAggregate(transform, PhysicsShapeType.BOX, {
  115. mass: mass,
  116. restitution: restitution,
  117. friction: 0.6,
  118. extents: new Vector3(currentRagdollBoneProperties.width, currentRagdollBoneProperties.height, currentRagdollBoneProperties.depth),
  119. }, this._scene);
  120. aggregate.body.setCollisionCallbackEnabled(true);
  121. aggregate.body.disablePreStep = false;
  122. aggregate.body.setMotionType(PhysicsMotionType.ANIMATED);
  123. this._aggregates.push(aggregate);
  124. this._bones.push(currentBone);
  125. this._boneNames.push(currentBone.name);
  126. this._transforms.push(transform);
  127. this._boxConfigs.push(currentRagdollBoneProperties);
  128. this._initialRotation.push(currentBone.getRotationQuaternion(Space.WORLD, this._rootTransformNode));
  129. this._initialRotation2.push(currentBone.getRotationQuaternion(Space.WORLD));
  130. }
  131. }
  132. }
  133. _initJoints() {
  134. this._rootTransformNode.computeWorldMatrix();
  135. for (let i = 0; i < this._bones.length; i++) {
  136. // The root bone has no joints.
  137. if (i == this._rootBoneIndex)
  138. continue;
  139. const nearestParent = this._findNearestParent(i);
  140. if (nearestParent == null) {
  141. Logger.Warn("Couldn't find a nearest parent bone in the configs for bone called " + this._boneNames[i]);
  142. return;
  143. }
  144. const boneParentIndex = this._boneNames.indexOf(nearestParent.name);
  145. let distanceFromParentBoxToBone = this._bones[i].getAbsolutePosition(this._rootTransformNode).subtract(this._transforms[boneParentIndex].position);
  146. const wmat = this._transforms[boneParentIndex].computeWorldMatrix();
  147. const invertedWorldMat = Matrix.Invert(wmat);
  148. distanceFromParentBoxToBone = Vector3.TransformCoordinates(this._bones[i].getAbsolutePosition(this._rootTransformNode), invertedWorldMat);
  149. const boneAbsPos = this._bones[i].getAbsolutePosition(this._rootTransformNode);
  150. const boxAbsPos = this._transforms[i].position.clone();
  151. const myConnectedPivot = boneAbsPos.subtract(boxAbsPos);
  152. const joint = new PhysicsConstraint(PhysicsConstraintType.BALL_AND_SOCKET, {
  153. pivotA: distanceFromParentBoxToBone,
  154. pivotB: myConnectedPivot,
  155. axisA: this._boxConfigs[i].rotationAxis,
  156. axisB: this._boxConfigs[i].rotationAxis,
  157. collision: false,
  158. }, this._scene);
  159. this._aggregates[boneParentIndex].body.addConstraint(this._aggregates[i].body, joint);
  160. joint.isEnabled = false;
  161. this._joints.push(joint);
  162. }
  163. }
  164. // set physics body orientation/position from bones
  165. _syncBonesToPhysics() {
  166. const rootMatrix = this._rootTransformNode.getWorldMatrix();
  167. for (let i = 0; i < this._bones.length; i++) {
  168. // position
  169. const transform = this._aggregates[i].transformNode;
  170. const rootPos = this._bones[i].getAbsolutePosition();
  171. Vector3.TransformCoordinatesToRef(rootPos, rootMatrix, transform.position);
  172. // added offset
  173. this._bones[i].getDirectionToRef(this._boxConfigs[i].boneOffsetAxis, this._rootTransformNode, TmpVectors.Vector3[0]);
  174. TmpVectors.Vector3[0].scaleInPlace(this._boxConfigs[i].boxOffset ?? 0);
  175. transform.position.addInPlace(TmpVectors.Vector3[0]);
  176. this._setBoneOrientationToBody(i);
  177. }
  178. }
  179. _setBoneOrientationToBody(boneIndex) {
  180. const transform = this._aggregates[boneIndex].transformNode;
  181. const bone = this._bones[boneIndex];
  182. this._initialRotation[boneIndex].conjugateToRef(TmpVectors.Quaternion[0]);
  183. bone.getRotationQuaternionToRef(Space.WORLD, this._rootTransformNode, TmpVectors.Quaternion[1]);
  184. TmpVectors.Quaternion[1].multiplyToRef(TmpVectors.Quaternion[0], transform.rotationQuaternion);
  185. transform.rotationQuaternion.normalize();
  186. }
  187. _syncBonesAndBoxes() {
  188. if (this.pauseSync) {
  189. return;
  190. }
  191. if (this._ragdollMode) {
  192. this._setBodyOrientationToBone(this._rootBoneIndex);
  193. const rootPos = this._aggregates[this._rootBoneIndex].body.transformNode.position;
  194. this._rootTransformNode.getWorldMatrix().invertToRef(TmpVectors.Matrix[0]);
  195. Vector3.TransformCoordinatesToRef(rootPos, TmpVectors.Matrix[0], TmpVectors.Vector3[0]);
  196. this._bones[this._rootBoneIndex].setAbsolutePosition(TmpVectors.Vector3[0]);
  197. for (let i = 0; i < this._bones.length; i++) {
  198. if (i == this._rootBoneIndex)
  199. continue;
  200. this._setBodyOrientationToBone(i);
  201. }
  202. }
  203. else {
  204. this._syncBonesToPhysics();
  205. }
  206. }
  207. _setBodyOrientationToBone(boneIndex) {
  208. const qmesh = this._rootTransformNode.rotationQuaternion ??
  209. Quaternion.FromEulerAngles(this._rootTransformNode.rotation.x, this._rootTransformNode.rotation.y, this._rootTransformNode.rotation.z);
  210. const qbind = this._initialRotation2[boneIndex];
  211. const qphys = this._aggregates[boneIndex].body?.transformNode?.rotationQuaternion;
  212. qmesh.multiplyToRef(qbind, TmpVectors.Quaternion[1]);
  213. qphys.multiplyToRef(TmpVectors.Quaternion[1], TmpVectors.Quaternion[0]);
  214. this._bones[boneIndex].setRotationQuaternion(TmpVectors.Quaternion[0], Space.WORLD, this._rootTransformNode);
  215. }
  216. // Return true if root bone is valid/exists in this.bonesNames. false otherwise.
  217. _defineRootBone() {
  218. const skeletonRoots = this._skeleton.getChildren();
  219. if (skeletonRoots.length != 1) {
  220. Logger.Log("Ragdoll creation failed: there can only be one root in the skeleton.");
  221. return false;
  222. }
  223. this._rootBoneName = skeletonRoots[0].name;
  224. this._rootBoneIndex = this._boneNames.indexOf(this._rootBoneName);
  225. if (this._rootBoneIndex == -1) {
  226. Logger.Log("Ragdoll creation failed: the array boneNames doesn't have the root bone. The root bone is " + this._skeleton.getChildren());
  227. return false;
  228. }
  229. return true;
  230. }
  231. _findNearestParent(boneIndex) {
  232. let nearestParent = this._bones[boneIndex].getParent();
  233. do {
  234. if (nearestParent != null && this._boneNames.includes(nearestParent.name)) {
  235. break;
  236. }
  237. nearestParent = nearestParent?.getParent();
  238. } while (nearestParent != null);
  239. return nearestParent;
  240. }
  241. _init() {
  242. this._createColliders();
  243. // If this.defineRootBone() returns ... there is not root bone.
  244. if (!this._defineRootBone()) {
  245. return;
  246. }
  247. this._initJoints();
  248. this._scene.registerBeforeRender(() => {
  249. this._syncBonesAndBoxes();
  250. });
  251. this._syncBonesToPhysics();
  252. }
  253. /**
  254. * Enable ragdoll mode. Create physics objects and make them dynamic.
  255. */
  256. ragdoll() {
  257. this._ragdollMode = true;
  258. // detach bones with link transform to let physics have control
  259. this._skeleton.bones.forEach((bone) => {
  260. bone.linkTransformNode(null);
  261. });
  262. for (let i = 0; i < this._joints.length; i++) {
  263. this._joints[i].isEnabled = true;
  264. }
  265. for (let i = 0; i < this._aggregates.length; i++) {
  266. this._aggregates[i].body.setMotionType(PhysicsMotionType.DYNAMIC);
  267. }
  268. }
  269. /**
  270. * Dispose resources and remove physics objects
  271. */
  272. dispose() {
  273. this._aggregates.forEach((aggregate) => {
  274. aggregate.dispose();
  275. });
  276. }
  277. }
  278. //# sourceMappingURL=ragdoll.js.map