SwingTwistConstraintPart.h 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
  2. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  3. // SPDX-License-Identifier: MIT
  4. #pragma once
  5. #include <Jolt/Geometry/Ellipse.h>
  6. #include <Jolt/Physics/Constraints/ConstraintPart/RotationEulerConstraintPart.h>
  7. #include <Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h>
  8. JPH_NAMESPACE_BEGIN
  9. /// Quaternion based constraint that decomposes the rotation in constraint space in swing and twist: q = q_swing * q_twist
  10. /// where q_swing.x = 0 and where q_twist.y = q_twist.z = 0
  11. ///
  12. /// - Rotation around the twist (x-axis) is within [inTwistMinAngle, inTwistMaxAngle].
  13. /// - Rotation around the swing axis (y and z axis) are limited to an ellipsoid in quaternion space formed by the equation:
  14. ///
  15. /// (q_swing.y / sin(inSwingYHalfAngle / 2))^2 + (q_swing.z / sin(inSwingZHalfAngle / 2))^2 <= 1
  16. ///
  17. /// Which roughly corresponds to an elliptic cone shape with major axis (inSwingYHalfAngle, inSwingZHalfAngle).
  18. ///
  19. /// In case inSwingYHalfAngle = 0, the rotation around Y will be constrained to 0 and the rotation around Z
  20. /// will be constrained between [-inSwingZHalfAngle, inSwingZHalfAngle]. Vice versa if inSwingZHalfAngle = 0.
  21. class SwingTwistConstraintPart
  22. {
  23. public:
  24. /// Set limits for this constraint (see description above for parameters)
  25. void SetLimits(float inTwistMinAngle, float inTwistMaxAngle, float inSwingYHalfAngle, float inSwingZHalfAngle)
  26. {
  27. constexpr float cLockedAngle = DegreesToRadians(0.5f);
  28. constexpr float cFreeAngle = DegreesToRadians(179.5f);
  29. // Assume sane input
  30. JPH_ASSERT(inTwistMinAngle <= 0.0f && inTwistMinAngle >= -JPH_PI);
  31. JPH_ASSERT(inTwistMaxAngle >= 0.0f && inTwistMaxAngle <= JPH_PI);
  32. JPH_ASSERT(inSwingYHalfAngle >= 0.0f && inSwingYHalfAngle <= JPH_PI);
  33. JPH_ASSERT(inSwingZHalfAngle >= 0.0f && inSwingZHalfAngle <= JPH_PI);
  34. // Calculate the sine and cosine of the half angles
  35. Vec4 s, c;
  36. (0.5f * Vec4(inTwistMinAngle, inTwistMaxAngle, inSwingYHalfAngle, inSwingZHalfAngle)).SinCos(s, c);
  37. // Store axis flags which are used at runtime to quickly decided which contraints to apply
  38. mRotationFlags = 0;
  39. if (inTwistMinAngle > -cLockedAngle && inTwistMaxAngle < cLockedAngle)
  40. {
  41. mRotationFlags |= TwistXLocked;
  42. mSinTwistHalfMinAngle = 0.0f;
  43. mSinTwistHalfMaxAngle = 0.0f;
  44. mCosTwistHalfMinAngle = 1.0f;
  45. mCosTwistHalfMaxAngle = 1.0f;
  46. }
  47. else if (inTwistMinAngle < -cFreeAngle && inTwistMaxAngle > cFreeAngle)
  48. {
  49. mRotationFlags |= TwistXFree;
  50. mSinTwistHalfMinAngle = -1.0f;
  51. mSinTwistHalfMaxAngle = 1.0f;
  52. mCosTwistHalfMinAngle = 0.0f;
  53. mCosTwistHalfMaxAngle = 0.0f;
  54. }
  55. else
  56. {
  57. mSinTwistHalfMinAngle = s.GetX();
  58. mSinTwistHalfMaxAngle = s.GetY();
  59. mCosTwistHalfMinAngle = c.GetX();
  60. mCosTwistHalfMaxAngle = c.GetY();
  61. }
  62. if (inSwingYHalfAngle < cLockedAngle)
  63. {
  64. mRotationFlags |= SwingYLocked;
  65. mSinSwingYQuarterAngle = 0.0f;
  66. }
  67. else if (inSwingYHalfAngle > cFreeAngle)
  68. {
  69. mRotationFlags |= SwingYFree;
  70. mSinSwingYQuarterAngle = 1.0f;
  71. }
  72. else
  73. {
  74. mSinSwingYQuarterAngle = s.GetZ();
  75. }
  76. if (inSwingZHalfAngle < cLockedAngle)
  77. {
  78. mRotationFlags |= SwingZLocked;
  79. mSinSwingZQuarterAngle = 0.0f;
  80. }
  81. else if (inSwingZHalfAngle > cFreeAngle)
  82. {
  83. mRotationFlags |= SwingZFree;
  84. mSinSwingZQuarterAngle = 1.0f;
  85. }
  86. else
  87. {
  88. mSinSwingZQuarterAngle = s.GetW();
  89. }
  90. }
  91. /// Clamp twist and swing against the constraint limits, returns which parts were clamped (everything assumed in constraint space)
  92. inline void ClampSwingTwist(Quat &ioSwing, bool &outSwingYClamped, bool &outSwingZClamped, Quat &ioTwist, bool &outTwistClamped) const
  93. {
  94. // Start with not clamped
  95. outTwistClamped = false;
  96. outSwingYClamped = false;
  97. outSwingZClamped = false;
  98. // Check that swing and twist quaternions don't contain rotations around the wrong axis
  99. JPH_ASSERT(ioSwing.GetX() == 0.0f);
  100. JPH_ASSERT(ioTwist.GetY() == 0.0f);
  101. JPH_ASSERT(ioTwist.GetZ() == 0.0f);
  102. // Ensure quaternions have w > 0
  103. bool negate_swing = ioSwing.GetW() < 0.0f;
  104. if (negate_swing)
  105. ioSwing = -ioSwing;
  106. bool negate_twist = ioTwist.GetW() < 0.0f;
  107. if (negate_twist)
  108. ioTwist = -ioTwist;
  109. if (mRotationFlags & TwistXLocked)
  110. {
  111. // Twist axis is locked, clamp whenever twist is not identity
  112. if (ioTwist.GetX() != 0.0f)
  113. {
  114. ioTwist = Quat::sIdentity();
  115. outTwistClamped = true;
  116. }
  117. }
  118. else if ((mRotationFlags & TwistXFree) == 0)
  119. {
  120. // Twist axis has limit, clamp whenever out of range
  121. float delta_min = mSinTwistHalfMinAngle - ioTwist.GetX();
  122. float delta_max = ioTwist.GetX() - mSinTwistHalfMaxAngle;
  123. if (delta_min > 0.0f || delta_max > 0.0f)
  124. {
  125. // We're outside of the limits, get actual delta to min/max range
  126. // Note that a twist of -1 and 1 represent the same angle, so if the difference is bigger than 1, the shortest angle is the other way around (2 - difference)
  127. // We should actually be working with angles rather than sin(angle / 2). When the difference is small the approximation is accurate, but
  128. // when working with extreme values the calculation is off and e.g. when the limit is between 0 and 180 a value of approx -60 will clamp
  129. // to 180 rather than 0 (you'd expect anything > -90 to go to 0).
  130. delta_min = abs(delta_min);
  131. if (delta_min > 1.0f) delta_min = 2.0f - delta_min;
  132. delta_max = abs(delta_max);
  133. if (delta_max > 1.0f) delta_max = 2.0f - delta_max;
  134. // Pick the twist that corresponds to the smallest delta
  135. if (delta_min < delta_max)
  136. ioTwist = Quat(mSinTwistHalfMinAngle, 0, 0, mCosTwistHalfMinAngle);
  137. else
  138. ioTwist = Quat(mSinTwistHalfMaxAngle, 0, 0, mCosTwistHalfMaxAngle);
  139. outTwistClamped = true;
  140. }
  141. }
  142. // Clamp swing
  143. if (mRotationFlags & SwingYLocked)
  144. {
  145. if (mRotationFlags & SwingZLocked)
  146. {
  147. // Both swing Y and Z are disabled, no degrees of freedom in swing
  148. outSwingYClamped = ioSwing.GetY() != 0.0f;
  149. outSwingZClamped = ioSwing.GetZ() != 0.0f;
  150. if (outSwingYClamped || outSwingZClamped)
  151. ioSwing = Quat::sIdentity();
  152. }
  153. else
  154. {
  155. // Swing Y angle disabled, only 1 degree of freedom in swing
  156. float z = Clamp(ioSwing.GetZ(), -mSinSwingZQuarterAngle, mSinSwingZQuarterAngle);
  157. outSwingYClamped = ioSwing.GetY() != 0.0f;
  158. outSwingZClamped = z != ioSwing.GetZ();
  159. if (outSwingYClamped || outSwingZClamped)
  160. ioSwing = Quat(0, 0, z, sqrt(1.0f - Square(z)));
  161. }
  162. }
  163. else if (mRotationFlags & SwingZLocked)
  164. {
  165. // Swing Z angle disabled, only 1 degree of freedom in swing
  166. float y = Clamp(ioSwing.GetY(), -mSinSwingYQuarterAngle, mSinSwingYQuarterAngle);
  167. outSwingYClamped = y != ioSwing.GetY();
  168. outSwingZClamped = ioSwing.GetZ() != 0.0f;
  169. if (outSwingYClamped || outSwingZClamped)
  170. ioSwing = Quat(0, y, 0, sqrt(1.0f - Square(y)));
  171. }
  172. else
  173. {
  174. // Two degrees of freedom, use ellipse to solve limits
  175. Ellipse ellipse(mSinSwingYQuarterAngle, mSinSwingZQuarterAngle);
  176. Float2 point(ioSwing.GetY(), ioSwing.GetZ());
  177. if (!ellipse.IsInside(point))
  178. {
  179. Float2 closest = ellipse.GetClosestPoint(point);
  180. ioSwing = Quat(0, closest.x, closest.y, sqrt(max(0.0f, 1.0f - Square(closest.x) - Square(closest.y))));
  181. outSwingYClamped = true;
  182. outSwingZClamped = true;
  183. }
  184. }
  185. // Flip sign back
  186. if (negate_swing)
  187. ioSwing = -ioSwing;
  188. if (negate_twist)
  189. ioTwist = -ioTwist;
  190. JPH_ASSERT(ioSwing.IsNormalized());
  191. JPH_ASSERT(ioTwist.IsNormalized());
  192. }
  193. /// Calculate properties used during the functions below
  194. /// @param inDeltaTime Time step
  195. /// @param inBody1 The first body that this constraint is attached to
  196. /// @param inBody2 The second body that this constraint is attached to
  197. /// @param inConstraintRotation The current rotation of the constraint in constraint space
  198. /// @param inConstraintToWorld Rotates from constraint space into world space
  199. inline void CalculateConstraintProperties(float inDeltaTime, const Body &inBody1, const Body &inBody2, QuatArg inConstraintRotation, QuatArg inConstraintToWorld)
  200. {
  201. // Decompose into swing and twist
  202. Quat q_swing, q_twist;
  203. inConstraintRotation.GetSwingTwist(q_swing, q_twist);
  204. // Clamp against joint limits
  205. Quat q_clamped_swing = q_swing, q_clamped_twist = q_twist;
  206. bool swing_y_clamped, swing_z_clamped, twist_clamped;
  207. ClampSwingTwist(q_clamped_swing, swing_y_clamped, swing_z_clamped, q_clamped_twist, twist_clamped);
  208. if (mRotationFlags & SwingYLocked)
  209. {
  210. Quat twist_to_world = inConstraintToWorld * q_swing;
  211. mWorldSpaceSwingLimitYRotationAxis = twist_to_world.RotateAxisY();
  212. mWorldSpaceSwingLimitZRotationAxis = twist_to_world.RotateAxisZ();
  213. if (mRotationFlags & SwingZLocked)
  214. {
  215. // Swing fully locked
  216. mSwingLimitYConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  217. mSwingLimitZConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  218. }
  219. else
  220. {
  221. // Swing only locked around Y
  222. mSwingLimitYConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  223. if (swing_z_clamped)
  224. {
  225. if (Sign(q_swing.GetW()) * q_swing.GetZ() < 0.0f)
  226. mWorldSpaceSwingLimitZRotationAxis = -mWorldSpaceSwingLimitZRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  227. mSwingLimitZConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  228. }
  229. else
  230. mSwingLimitZConstraintPart.Deactivate();
  231. }
  232. }
  233. else if (mRotationFlags & SwingZLocked)
  234. {
  235. // Swing only locked around Z
  236. Quat twist_to_world = inConstraintToWorld * q_swing;
  237. mWorldSpaceSwingLimitYRotationAxis = twist_to_world.RotateAxisY();
  238. mWorldSpaceSwingLimitZRotationAxis = twist_to_world.RotateAxisZ();
  239. if (swing_y_clamped)
  240. {
  241. if (Sign(q_swing.GetW()) * q_swing.GetY() < 0.0f)
  242. mWorldSpaceSwingLimitYRotationAxis = -mWorldSpaceSwingLimitYRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  243. mSwingLimitYConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  244. }
  245. else
  246. mSwingLimitYConstraintPart.Deactivate();
  247. mSwingLimitZConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  248. }
  249. else if ((mRotationFlags & SwingYZFree) != SwingYZFree)
  250. {
  251. // Swing has limits around Y and Z
  252. if (swing_y_clamped || swing_z_clamped)
  253. {
  254. // Calculate axis of rotation from clamped swing to swing
  255. Vec3 current = (inConstraintToWorld * q_swing).RotateAxisX();
  256. Vec3 desired = (inConstraintToWorld * q_clamped_swing).RotateAxisX();
  257. mWorldSpaceSwingLimitYRotationAxis = desired.Cross(current);
  258. float len = mWorldSpaceSwingLimitYRotationAxis.Length();
  259. if (len != 0.0f)
  260. {
  261. mWorldSpaceSwingLimitYRotationAxis /= len;
  262. mSwingLimitYConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  263. }
  264. else
  265. mSwingLimitYConstraintPart.Deactivate();
  266. }
  267. else
  268. mSwingLimitYConstraintPart.Deactivate();
  269. mSwingLimitZConstraintPart.Deactivate();
  270. }
  271. else
  272. {
  273. // No swing limits
  274. mSwingLimitYConstraintPart.Deactivate();
  275. mSwingLimitZConstraintPart.Deactivate();
  276. }
  277. if (mRotationFlags & TwistXLocked)
  278. {
  279. // Twist locked, always activate constraint
  280. mWorldSpaceTwistLimitRotationAxis = (inConstraintToWorld * q_swing).RotateAxisX();
  281. mTwistLimitConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceTwistLimitRotationAxis);
  282. }
  283. else if ((mRotationFlags & TwistXFree) == 0)
  284. {
  285. // Twist has limits
  286. if (twist_clamped)
  287. {
  288. mWorldSpaceTwistLimitRotationAxis = (inConstraintToWorld * q_swing).RotateAxisX();
  289. if (Sign(q_twist.GetW()) * q_twist.GetX() < 0.0f)
  290. mWorldSpaceTwistLimitRotationAxis = -mWorldSpaceTwistLimitRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  291. mTwistLimitConstraintPart.CalculateConstraintProperties(inDeltaTime, inBody1, inBody2, mWorldSpaceTwistLimitRotationAxis);
  292. }
  293. else
  294. mTwistLimitConstraintPart.Deactivate();
  295. }
  296. else
  297. {
  298. // No twist limits
  299. mTwistLimitConstraintPart.Deactivate();
  300. }
  301. }
  302. /// Deactivate this constraint
  303. void Deactivate()
  304. {
  305. mSwingLimitYConstraintPart.Deactivate();
  306. mSwingLimitZConstraintPart.Deactivate();
  307. mTwistLimitConstraintPart.Deactivate();
  308. }
  309. /// Check if constraint is active
  310. inline bool IsActive() const
  311. {
  312. return mSwingLimitYConstraintPart.IsActive() || mSwingLimitZConstraintPart.IsActive() || mTwistLimitConstraintPart.IsActive();
  313. }
  314. /// Must be called from the WarmStartVelocityConstraint call to apply the previous frame's impulses
  315. inline void WarmStart(Body &ioBody1, Body &ioBody2, float inWarmStartImpulseRatio)
  316. {
  317. mSwingLimitYConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  318. mSwingLimitZConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  319. mTwistLimitConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  320. }
  321. /// Iteratively update the velocity constraint. Makes sure d/dt C(...) = 0, where C is the constraint equation.
  322. inline bool SolveVelocityConstraint(Body &ioBody1, Body &ioBody2)
  323. {
  324. bool impulse = false;
  325. // Solve swing constraint
  326. if (mSwingLimitYConstraintPart.IsActive())
  327. impulse |= mSwingLimitYConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceSwingLimitYRotationAxis, -FLT_MAX, (mRotationFlags & SwingYLocked)? FLT_MAX : 0.0f);
  328. if (mSwingLimitZConstraintPart.IsActive())
  329. impulse |= mSwingLimitZConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceSwingLimitZRotationAxis, -FLT_MAX, (mRotationFlags & SwingZLocked)? FLT_MAX : 0.0f);
  330. // Solve twist constraint
  331. if (mTwistLimitConstraintPart.IsActive())
  332. impulse |= mTwistLimitConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceTwistLimitRotationAxis, -FLT_MAX, (mRotationFlags & TwistXLocked)? FLT_MAX : 0.0f);
  333. return impulse;
  334. }
  335. /// Iteratively update the position constraint. Makes sure C(...) = 0.
  336. /// @param ioBody1 The first body that this constraint is attached to
  337. /// @param ioBody2 The second body that this constraint is attached to
  338. /// @param inConstraintRotation The current rotation of the constraint in constraint space
  339. /// @param inConstraintToBody1 , inConstraintToBody2 Rotates from constraint space to body 1/2 space
  340. /// @param inBaumgarte Baumgarte constant (fraction of the error to correct)
  341. inline bool SolvePositionConstraint(Body &ioBody1, Body &ioBody2, QuatArg inConstraintRotation, QuatArg inConstraintToBody1, QuatArg inConstraintToBody2, float inBaumgarte) const
  342. {
  343. Quat q_swing, q_twist;
  344. inConstraintRotation.GetSwingTwist(q_swing, q_twist);
  345. bool swing_y_clamped, swing_z_clamped, twist_clamped;
  346. ClampSwingTwist(q_swing, swing_y_clamped, swing_z_clamped, q_twist, twist_clamped);
  347. // Solve rotation violations
  348. if (swing_y_clamped || swing_z_clamped || twist_clamped)
  349. {
  350. RotationEulerConstraintPart part;
  351. Quat inv_initial_orientation = inConstraintToBody2 * (inConstraintToBody1 * q_swing * q_twist).Conjugated();
  352. part.CalculateConstraintProperties(ioBody1, Mat44::sRotation(ioBody1.GetRotation()), ioBody2, Mat44::sRotation(ioBody2.GetRotation()));
  353. return part.SolvePositionConstraint(ioBody1, ioBody2, inv_initial_orientation, inBaumgarte);
  354. }
  355. return false;
  356. }
  357. /// Return lagrange multiplier for swing
  358. inline float GetTotalSwingYLambda() const
  359. {
  360. return mSwingLimitYConstraintPart.GetTotalLambda();
  361. }
  362. inline float GetTotalSwingZLambda() const
  363. {
  364. return mSwingLimitZConstraintPart.GetTotalLambda();
  365. }
  366. /// Return lagrange multiplier for twist
  367. inline float GetTotalTwistLambda() const
  368. {
  369. return mTwistLimitConstraintPart.GetTotalLambda();
  370. }
  371. /// Save state of this constraint part
  372. void SaveState(StateRecorder &inStream) const
  373. {
  374. mSwingLimitYConstraintPart.SaveState(inStream);
  375. mSwingLimitZConstraintPart.SaveState(inStream);
  376. mTwistLimitConstraintPart.SaveState(inStream);
  377. }
  378. /// Restore state of this constraint part
  379. void RestoreState(StateRecorder &inStream)
  380. {
  381. mSwingLimitYConstraintPart.RestoreState(inStream);
  382. mSwingLimitZConstraintPart.RestoreState(inStream);
  383. mTwistLimitConstraintPart.RestoreState(inStream);
  384. }
  385. private:
  386. // CONFIGURATION PROPERTIES FOLLOW
  387. enum ERotationFlags
  388. {
  389. /// Indicates that axis is completely locked (cannot rotate around this axis)
  390. TwistXLocked = 1 << 0,
  391. SwingYLocked = 1 << 1,
  392. SwingZLocked = 1 << 2,
  393. /// Indicates that axis is completely free (can rotate around without limits)
  394. TwistXFree = 1 << 3,
  395. SwingYFree = 1 << 4,
  396. SwingZFree = 1 << 5,
  397. SwingYZFree = SwingYFree | SwingZFree
  398. };
  399. uint8 mRotationFlags;
  400. // Constants
  401. float mSinTwistHalfMinAngle;
  402. float mSinTwistHalfMaxAngle;
  403. float mCosTwistHalfMinAngle;
  404. float mCosTwistHalfMaxAngle;
  405. float mSinSwingYQuarterAngle;
  406. float mSinSwingZQuarterAngle;
  407. // RUN TIME PROPERTIES FOLLOW
  408. /// Rotation axis for the angle constraint parts
  409. Vec3 mWorldSpaceSwingLimitYRotationAxis;
  410. Vec3 mWorldSpaceSwingLimitZRotationAxis;
  411. Vec3 mWorldSpaceTwistLimitRotationAxis;
  412. /// The constraint parts
  413. AngleConstraintPart mSwingLimitYConstraintPart;
  414. AngleConstraintPart mSwingLimitZConstraintPart;
  415. AngleConstraintPart mTwistLimitConstraintPart;
  416. };
  417. JPH_NAMESPACE_END