SwingTwistConstraintPart.h 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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 <= inTwistMinAngle);
  31. JPH_ASSERT(inSwingYHalfAngle >= 0.0f && inSwingYHalfAngle <= JPH_PI);
  32. JPH_ASSERT(inSwingZHalfAngle >= 0.0f && inSwingZHalfAngle <= JPH_PI);
  33. // Calculate the sine and cosine of the half angles
  34. Vec4 s, c;
  35. (0.5f * Vec4(inTwistMinAngle, inTwistMaxAngle, inSwingYHalfAngle, inSwingZHalfAngle)).SinCos(s, c);
  36. // Store axis flags which are used at runtime to quickly decided which contraints to apply
  37. mRotationFlags = 0;
  38. if (inTwistMinAngle > -cLockedAngle && inTwistMaxAngle < cLockedAngle)
  39. {
  40. mRotationFlags |= TwistXLocked;
  41. mSinTwistHalfMinAngle = 0.0f;
  42. mSinTwistHalfMaxAngle = 0.0f;
  43. mCosTwistHalfMinAngle = 1.0f;
  44. mCosTwistHalfMaxAngle = 1.0f;
  45. }
  46. else if (inTwistMinAngle < -cFreeAngle && inTwistMaxAngle > cFreeAngle)
  47. {
  48. mRotationFlags |= TwistXFree;
  49. mSinTwistHalfMinAngle = -1.0f;
  50. mSinTwistHalfMaxAngle = 1.0f;
  51. mCosTwistHalfMinAngle = 0.0f;
  52. mCosTwistHalfMaxAngle = 0.0f;
  53. }
  54. else
  55. {
  56. mSinTwistHalfMinAngle = s.GetX();
  57. mSinTwistHalfMaxAngle = s.GetY();
  58. mCosTwistHalfMinAngle = c.GetX();
  59. mCosTwistHalfMaxAngle = c.GetY();
  60. }
  61. if (inSwingYHalfAngle < cLockedAngle)
  62. {
  63. mRotationFlags |= SwingYLocked;
  64. mSinSwingYQuarterAngle = 0.0f;
  65. }
  66. else if (inSwingYHalfAngle > cFreeAngle)
  67. {
  68. mRotationFlags |= SwingYFree;
  69. mSinSwingYQuarterAngle = 1.0f;
  70. }
  71. else
  72. {
  73. mSinSwingYQuarterAngle = s.GetZ();
  74. }
  75. if (inSwingZHalfAngle < cLockedAngle)
  76. {
  77. mRotationFlags |= SwingZLocked;
  78. mSinSwingZQuarterAngle = 0.0f;
  79. }
  80. else if (inSwingZHalfAngle > cFreeAngle)
  81. {
  82. mRotationFlags |= SwingZFree;
  83. mSinSwingZQuarterAngle = 1.0f;
  84. }
  85. else
  86. {
  87. mSinSwingZQuarterAngle = s.GetW();
  88. }
  89. }
  90. /// Clamp twist and swing against the constraint limits, returns which parts were clamped (everything assumed in constraint space)
  91. inline void ClampSwingTwist(Quat &ioSwing, bool &outSwingYClamped, bool &outSwingZClamped, Quat &ioTwist, bool &outTwistClampedToMin, bool &outTwistClampedToMax) const
  92. {
  93. // Start with not clamped
  94. outTwistClampedToMin = false;
  95. outTwistClampedToMax = 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. outTwistClampedToMin = outTwistClampedToMax = 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. {
  137. ioTwist = Quat(mSinTwistHalfMinAngle, 0, 0, mCosTwistHalfMinAngle);
  138. outTwistClampedToMin = true;
  139. }
  140. else
  141. {
  142. ioTwist = Quat(mSinTwistHalfMaxAngle, 0, 0, mCosTwistHalfMaxAngle);
  143. outTwistClampedToMax = true;
  144. }
  145. }
  146. }
  147. // Clamp swing
  148. if (mRotationFlags & SwingYLocked)
  149. {
  150. if (mRotationFlags & SwingZLocked)
  151. {
  152. // Both swing Y and Z are disabled, no degrees of freedom in swing
  153. outSwingYClamped = ioSwing.GetY() != 0.0f;
  154. outSwingZClamped = ioSwing.GetZ() != 0.0f;
  155. if (outSwingYClamped || outSwingZClamped)
  156. ioSwing = Quat::sIdentity();
  157. }
  158. else
  159. {
  160. // Swing Y angle disabled, only 1 degree of freedom in swing
  161. float z = Clamp(ioSwing.GetZ(), -mSinSwingZQuarterAngle, mSinSwingZQuarterAngle);
  162. outSwingYClamped = ioSwing.GetY() != 0.0f;
  163. outSwingZClamped = z != ioSwing.GetZ();
  164. if (outSwingYClamped || outSwingZClamped)
  165. ioSwing = Quat(0, 0, z, sqrt(1.0f - Square(z)));
  166. }
  167. }
  168. else if (mRotationFlags & SwingZLocked)
  169. {
  170. // Swing Z angle disabled, only 1 degree of freedom in swing
  171. float y = Clamp(ioSwing.GetY(), -mSinSwingYQuarterAngle, mSinSwingYQuarterAngle);
  172. outSwingYClamped = y != ioSwing.GetY();
  173. outSwingZClamped = ioSwing.GetZ() != 0.0f;
  174. if (outSwingYClamped || outSwingZClamped)
  175. ioSwing = Quat(0, y, 0, sqrt(1.0f - Square(y)));
  176. }
  177. else
  178. {
  179. // Two degrees of freedom, use ellipse to solve limits
  180. Ellipse ellipse(mSinSwingYQuarterAngle, mSinSwingZQuarterAngle);
  181. Float2 point(ioSwing.GetY(), ioSwing.GetZ());
  182. if (!ellipse.IsInside(point))
  183. {
  184. Float2 closest = ellipse.GetClosestPoint(point);
  185. ioSwing = Quat(0, closest.x, closest.y, sqrt(max(0.0f, 1.0f - Square(closest.x) - Square(closest.y))));
  186. outSwingYClamped = true;
  187. outSwingZClamped = true;
  188. }
  189. }
  190. // Flip sign back
  191. if (negate_swing)
  192. ioSwing = -ioSwing;
  193. if (negate_twist)
  194. ioTwist = -ioTwist;
  195. JPH_ASSERT(ioSwing.IsNormalized());
  196. JPH_ASSERT(ioTwist.IsNormalized());
  197. }
  198. /// Calculate properties used during the functions below
  199. /// @param inBody1 The first body that this constraint is attached to
  200. /// @param inBody2 The second body that this constraint is attached to
  201. /// @param inConstraintRotation The current rotation of the constraint in constraint space
  202. /// @param inConstraintToWorld Rotates from constraint space into world space
  203. inline void CalculateConstraintProperties(const Body &inBody1, const Body &inBody2, QuatArg inConstraintRotation, QuatArg inConstraintToWorld)
  204. {
  205. // Decompose into swing and twist
  206. Quat q_swing, q_twist;
  207. inConstraintRotation.GetSwingTwist(q_swing, q_twist);
  208. // Clamp against joint limits
  209. Quat q_clamped_swing = q_swing, q_clamped_twist = q_twist;
  210. bool swing_y_clamped, swing_z_clamped, twist_clamped_to_min, twist_clamped_to_max;
  211. ClampSwingTwist(q_clamped_swing, swing_y_clamped, swing_z_clamped, q_clamped_twist, twist_clamped_to_min, twist_clamped_to_max);
  212. if (mRotationFlags & SwingYLocked)
  213. {
  214. Quat twist_to_world = inConstraintToWorld * q_swing;
  215. mWorldSpaceSwingLimitYRotationAxis = twist_to_world.RotateAxisY();
  216. mWorldSpaceSwingLimitZRotationAxis = twist_to_world.RotateAxisZ();
  217. if (mRotationFlags & SwingZLocked)
  218. {
  219. // Swing fully locked
  220. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  221. mSwingLimitZConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  222. }
  223. else
  224. {
  225. // Swing only locked around Y
  226. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  227. if (swing_z_clamped)
  228. {
  229. if (Sign(q_swing.GetW()) * q_swing.GetZ() < 0.0f)
  230. mWorldSpaceSwingLimitZRotationAxis = -mWorldSpaceSwingLimitZRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  231. mSwingLimitZConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  232. }
  233. else
  234. mSwingLimitZConstraintPart.Deactivate();
  235. }
  236. }
  237. else if (mRotationFlags & SwingZLocked)
  238. {
  239. // Swing only locked around Z
  240. Quat twist_to_world = inConstraintToWorld * q_swing;
  241. mWorldSpaceSwingLimitYRotationAxis = twist_to_world.RotateAxisY();
  242. mWorldSpaceSwingLimitZRotationAxis = twist_to_world.RotateAxisZ();
  243. if (swing_y_clamped)
  244. {
  245. if (Sign(q_swing.GetW()) * q_swing.GetY() < 0.0f)
  246. mWorldSpaceSwingLimitYRotationAxis = -mWorldSpaceSwingLimitYRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  247. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  248. }
  249. else
  250. mSwingLimitYConstraintPart.Deactivate();
  251. mSwingLimitZConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  252. }
  253. else if ((mRotationFlags & SwingYZFree) != SwingYZFree)
  254. {
  255. // Swing has limits around Y and Z
  256. if (swing_y_clamped || swing_z_clamped)
  257. {
  258. // Calculate axis of rotation from clamped swing to swing
  259. Vec3 current = (inConstraintToWorld * q_swing).RotateAxisX();
  260. Vec3 desired = (inConstraintToWorld * q_clamped_swing).RotateAxisX();
  261. mWorldSpaceSwingLimitYRotationAxis = desired.Cross(current);
  262. float len = mWorldSpaceSwingLimitYRotationAxis.Length();
  263. if (len != 0.0f)
  264. {
  265. mWorldSpaceSwingLimitYRotationAxis /= len;
  266. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  267. }
  268. else
  269. mSwingLimitYConstraintPart.Deactivate();
  270. }
  271. else
  272. mSwingLimitYConstraintPart.Deactivate();
  273. mSwingLimitZConstraintPart.Deactivate();
  274. }
  275. else
  276. {
  277. // No swing limits
  278. mSwingLimitYConstraintPart.Deactivate();
  279. mSwingLimitZConstraintPart.Deactivate();
  280. }
  281. if (mRotationFlags & TwistXLocked)
  282. {
  283. // Twist locked, always activate constraint
  284. mWorldSpaceTwistLimitRotationAxis = (inConstraintToWorld * q_swing).RotateAxisX();
  285. mTwistLimitConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceTwistLimitRotationAxis);
  286. }
  287. else if ((mRotationFlags & TwistXFree) == 0)
  288. {
  289. // Twist has limits
  290. if (twist_clamped_to_min || twist_clamped_to_max)
  291. {
  292. mWorldSpaceTwistLimitRotationAxis = (inConstraintToWorld * q_swing).RotateAxisX();
  293. if (twist_clamped_to_min)
  294. mWorldSpaceTwistLimitRotationAxis = -mWorldSpaceTwistLimitRotationAxis; // Flip axis if hittin min limit because the impulse limit is going to be between [-FLT_MAX, 0]
  295. mTwistLimitConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceTwistLimitRotationAxis);
  296. }
  297. else
  298. mTwistLimitConstraintPart.Deactivate();
  299. }
  300. else
  301. {
  302. // No twist limits
  303. mTwistLimitConstraintPart.Deactivate();
  304. }
  305. }
  306. /// Deactivate this constraint
  307. void Deactivate()
  308. {
  309. mSwingLimitYConstraintPart.Deactivate();
  310. mSwingLimitZConstraintPart.Deactivate();
  311. mTwistLimitConstraintPart.Deactivate();
  312. }
  313. /// Check if constraint is active
  314. inline bool IsActive() const
  315. {
  316. return mSwingLimitYConstraintPart.IsActive() || mSwingLimitZConstraintPart.IsActive() || mTwistLimitConstraintPart.IsActive();
  317. }
  318. /// Must be called from the WarmStartVelocityConstraint call to apply the previous frame's impulses
  319. inline void WarmStart(Body &ioBody1, Body &ioBody2, float inWarmStartImpulseRatio)
  320. {
  321. mSwingLimitYConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  322. mSwingLimitZConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  323. mTwistLimitConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  324. }
  325. /// Iteratively update the velocity constraint. Makes sure d/dt C(...) = 0, where C is the constraint equation.
  326. inline bool SolveVelocityConstraint(Body &ioBody1, Body &ioBody2)
  327. {
  328. bool impulse = false;
  329. // Solve swing constraint
  330. if (mSwingLimitYConstraintPart.IsActive())
  331. impulse |= mSwingLimitYConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceSwingLimitYRotationAxis, -FLT_MAX, (mRotationFlags & SwingYLocked)? FLT_MAX : 0.0f);
  332. if (mSwingLimitZConstraintPart.IsActive())
  333. impulse |= mSwingLimitZConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceSwingLimitZRotationAxis, -FLT_MAX, (mRotationFlags & SwingZLocked)? FLT_MAX : 0.0f);
  334. // Solve twist constraint
  335. if (mTwistLimitConstraintPart.IsActive())
  336. impulse |= mTwistLimitConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceTwistLimitRotationAxis, -FLT_MAX, (mRotationFlags & TwistXLocked)? FLT_MAX : 0.0f);
  337. return impulse;
  338. }
  339. /// Iteratively update the position constraint. Makes sure C(...) = 0.
  340. /// @param ioBody1 The first body that this constraint is attached to
  341. /// @param ioBody2 The second body that this constraint is attached to
  342. /// @param inConstraintRotation The current rotation of the constraint in constraint space
  343. /// @param inConstraintToBody1 , inConstraintToBody2 Rotates from constraint space to body 1/2 space
  344. /// @param inBaumgarte Baumgarte constant (fraction of the error to correct)
  345. inline bool SolvePositionConstraint(Body &ioBody1, Body &ioBody2, QuatArg inConstraintRotation, QuatArg inConstraintToBody1, QuatArg inConstraintToBody2, float inBaumgarte) const
  346. {
  347. Quat q_swing, q_twist;
  348. inConstraintRotation.GetSwingTwist(q_swing, q_twist);
  349. bool swing_y_clamped, swing_z_clamped, twist_clamped_to_min, twist_clamped_to_max;
  350. ClampSwingTwist(q_swing, swing_y_clamped, swing_z_clamped, q_twist, twist_clamped_to_min, twist_clamped_to_max);
  351. // Solve rotation violations
  352. if (swing_y_clamped || swing_z_clamped || twist_clamped_to_min || twist_clamped_to_max)
  353. {
  354. RotationEulerConstraintPart part;
  355. Quat inv_initial_orientation = inConstraintToBody2 * (inConstraintToBody1 * q_swing * q_twist).Conjugated();
  356. part.CalculateConstraintProperties(ioBody1, Mat44::sRotation(ioBody1.GetRotation()), ioBody2, Mat44::sRotation(ioBody2.GetRotation()));
  357. return part.SolvePositionConstraint(ioBody1, ioBody2, inv_initial_orientation, inBaumgarte);
  358. }
  359. return false;
  360. }
  361. /// Return lagrange multiplier for swing
  362. inline float GetTotalSwingYLambda() const
  363. {
  364. return mSwingLimitYConstraintPart.GetTotalLambda();
  365. }
  366. inline float GetTotalSwingZLambda() const
  367. {
  368. return mSwingLimitZConstraintPart.GetTotalLambda();
  369. }
  370. /// Return lagrange multiplier for twist
  371. inline float GetTotalTwistLambda() const
  372. {
  373. return mTwistLimitConstraintPart.GetTotalLambda();
  374. }
  375. /// Save state of this constraint part
  376. void SaveState(StateRecorder &inStream) const
  377. {
  378. mSwingLimitYConstraintPart.SaveState(inStream);
  379. mSwingLimitZConstraintPart.SaveState(inStream);
  380. mTwistLimitConstraintPart.SaveState(inStream);
  381. }
  382. /// Restore state of this constraint part
  383. void RestoreState(StateRecorder &inStream)
  384. {
  385. mSwingLimitYConstraintPart.RestoreState(inStream);
  386. mSwingLimitZConstraintPart.RestoreState(inStream);
  387. mTwistLimitConstraintPart.RestoreState(inStream);
  388. }
  389. private:
  390. // CONFIGURATION PROPERTIES FOLLOW
  391. enum ERotationFlags
  392. {
  393. /// Indicates that axis is completely locked (cannot rotate around this axis)
  394. TwistXLocked = 1 << 0,
  395. SwingYLocked = 1 << 1,
  396. SwingZLocked = 1 << 2,
  397. /// Indicates that axis is completely free (can rotate around without limits)
  398. TwistXFree = 1 << 3,
  399. SwingYFree = 1 << 4,
  400. SwingZFree = 1 << 5,
  401. SwingYZFree = SwingYFree | SwingZFree
  402. };
  403. uint8 mRotationFlags;
  404. // Constants
  405. float mSinTwistHalfMinAngle;
  406. float mSinTwistHalfMaxAngle;
  407. float mCosTwistHalfMinAngle;
  408. float mCosTwistHalfMaxAngle;
  409. float mSinSwingYQuarterAngle;
  410. float mSinSwingZQuarterAngle;
  411. // RUN TIME PROPERTIES FOLLOW
  412. /// Rotation axis for the angle constraint parts
  413. Vec3 mWorldSpaceSwingLimitYRotationAxis;
  414. Vec3 mWorldSpaceSwingLimitZRotationAxis;
  415. Vec3 mWorldSpaceTwistLimitRotationAxis;
  416. /// The constraint parts
  417. AngleConstraintPart mSwingLimitYConstraintPart;
  418. AngleConstraintPart mSwingLimitZConstraintPart;
  419. AngleConstraintPart mTwistLimitConstraintPart;
  420. };
  421. JPH_NAMESPACE_END