SwingTwistConstraintPart.h 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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 inBody1 The first body that this constraint is attached to
  195. /// @param inBody2 The second body that this constraint is attached to
  196. /// @param inConstraintRotation The current rotation of the constraint in constraint space
  197. /// @param inConstraintToWorld Rotates from constraint space into world space
  198. inline void CalculateConstraintProperties(const Body &inBody1, const Body &inBody2, QuatArg inConstraintRotation, QuatArg inConstraintToWorld)
  199. {
  200. // Decompose into swing and twist
  201. Quat q_swing, q_twist;
  202. inConstraintRotation.GetSwingTwist(q_swing, q_twist);
  203. // Clamp against joint limits
  204. Quat q_clamped_swing = q_swing, q_clamped_twist = q_twist;
  205. bool swing_y_clamped, swing_z_clamped, twist_clamped;
  206. ClampSwingTwist(q_clamped_swing, swing_y_clamped, swing_z_clamped, q_clamped_twist, twist_clamped);
  207. if (mRotationFlags & SwingYLocked)
  208. {
  209. Quat twist_to_world = inConstraintToWorld * q_swing;
  210. mWorldSpaceSwingLimitYRotationAxis = twist_to_world.RotateAxisY();
  211. mWorldSpaceSwingLimitZRotationAxis = twist_to_world.RotateAxisZ();
  212. if (mRotationFlags & SwingZLocked)
  213. {
  214. // Swing fully locked
  215. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  216. mSwingLimitZConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  217. }
  218. else
  219. {
  220. // Swing only locked around Y
  221. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  222. if (swing_z_clamped)
  223. {
  224. if (Sign(q_swing.GetW()) * q_swing.GetZ() < 0.0f)
  225. mWorldSpaceSwingLimitZRotationAxis = -mWorldSpaceSwingLimitZRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  226. mSwingLimitZConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  227. }
  228. else
  229. mSwingLimitZConstraintPart.Deactivate();
  230. }
  231. }
  232. else if (mRotationFlags & SwingZLocked)
  233. {
  234. // Swing only locked around Z
  235. Quat twist_to_world = inConstraintToWorld * q_swing;
  236. mWorldSpaceSwingLimitYRotationAxis = twist_to_world.RotateAxisY();
  237. mWorldSpaceSwingLimitZRotationAxis = twist_to_world.RotateAxisZ();
  238. if (swing_y_clamped)
  239. {
  240. if (Sign(q_swing.GetW()) * q_swing.GetY() < 0.0f)
  241. mWorldSpaceSwingLimitYRotationAxis = -mWorldSpaceSwingLimitYRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  242. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  243. }
  244. else
  245. mSwingLimitYConstraintPart.Deactivate();
  246. mSwingLimitZConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitZRotationAxis);
  247. }
  248. else if ((mRotationFlags & SwingYZFree) != SwingYZFree)
  249. {
  250. // Swing has limits around Y and Z
  251. if (swing_y_clamped || swing_z_clamped)
  252. {
  253. // Calculate axis of rotation from clamped swing to swing
  254. Vec3 current = (inConstraintToWorld * q_swing).RotateAxisX();
  255. Vec3 desired = (inConstraintToWorld * q_clamped_swing).RotateAxisX();
  256. mWorldSpaceSwingLimitYRotationAxis = desired.Cross(current);
  257. float len = mWorldSpaceSwingLimitYRotationAxis.Length();
  258. if (len != 0.0f)
  259. {
  260. mWorldSpaceSwingLimitYRotationAxis /= len;
  261. mSwingLimitYConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceSwingLimitYRotationAxis);
  262. }
  263. else
  264. mSwingLimitYConstraintPart.Deactivate();
  265. }
  266. else
  267. mSwingLimitYConstraintPart.Deactivate();
  268. mSwingLimitZConstraintPart.Deactivate();
  269. }
  270. else
  271. {
  272. // No swing limits
  273. mSwingLimitYConstraintPart.Deactivate();
  274. mSwingLimitZConstraintPart.Deactivate();
  275. }
  276. if (mRotationFlags & TwistXLocked)
  277. {
  278. // Twist locked, always activate constraint
  279. mWorldSpaceTwistLimitRotationAxis = (inConstraintToWorld * q_swing).RotateAxisX();
  280. mTwistLimitConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceTwistLimitRotationAxis);
  281. }
  282. else if ((mRotationFlags & TwistXFree) == 0)
  283. {
  284. // Twist has limits
  285. if (twist_clamped)
  286. {
  287. mWorldSpaceTwistLimitRotationAxis = (inConstraintToWorld * q_swing).RotateAxisX();
  288. if (Sign(q_twist.GetW()) * q_twist.GetX() < 0.0f)
  289. mWorldSpaceTwistLimitRotationAxis = -mWorldSpaceTwistLimitRotationAxis; // Flip axis if angle is negative because the impulse limit is going to be between [-FLT_MAX, 0]
  290. mTwistLimitConstraintPart.CalculateConstraintProperties(inBody1, inBody2, mWorldSpaceTwistLimitRotationAxis);
  291. }
  292. else
  293. mTwistLimitConstraintPart.Deactivate();
  294. }
  295. else
  296. {
  297. // No twist limits
  298. mTwistLimitConstraintPart.Deactivate();
  299. }
  300. }
  301. /// Deactivate this constraint
  302. void Deactivate()
  303. {
  304. mSwingLimitYConstraintPart.Deactivate();
  305. mSwingLimitZConstraintPart.Deactivate();
  306. mTwistLimitConstraintPart.Deactivate();
  307. }
  308. /// Check if constraint is active
  309. inline bool IsActive() const
  310. {
  311. return mSwingLimitYConstraintPart.IsActive() || mSwingLimitZConstraintPart.IsActive() || mTwistLimitConstraintPart.IsActive();
  312. }
  313. /// Must be called from the WarmStartVelocityConstraint call to apply the previous frame's impulses
  314. inline void WarmStart(Body &ioBody1, Body &ioBody2, float inWarmStartImpulseRatio)
  315. {
  316. mSwingLimitYConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  317. mSwingLimitZConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  318. mTwistLimitConstraintPart.WarmStart(ioBody1, ioBody2, inWarmStartImpulseRatio);
  319. }
  320. /// Iteratively update the velocity constraint. Makes sure d/dt C(...) = 0, where C is the constraint equation.
  321. inline bool SolveVelocityConstraint(Body &ioBody1, Body &ioBody2)
  322. {
  323. bool impulse = false;
  324. // Solve swing constraint
  325. if (mSwingLimitYConstraintPart.IsActive())
  326. impulse |= mSwingLimitYConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceSwingLimitYRotationAxis, -FLT_MAX, (mRotationFlags & SwingYLocked)? FLT_MAX : 0.0f);
  327. if (mSwingLimitZConstraintPart.IsActive())
  328. impulse |= mSwingLimitZConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceSwingLimitZRotationAxis, -FLT_MAX, (mRotationFlags & SwingZLocked)? FLT_MAX : 0.0f);
  329. // Solve twist constraint
  330. if (mTwistLimitConstraintPart.IsActive())
  331. impulse |= mTwistLimitConstraintPart.SolveVelocityConstraint(ioBody1, ioBody2, mWorldSpaceTwistLimitRotationAxis, -FLT_MAX, (mRotationFlags & TwistXLocked)? FLT_MAX : 0.0f);
  332. return impulse;
  333. }
  334. /// Iteratively update the position constraint. Makes sure C(...) = 0.
  335. /// @param ioBody1 The first body that this constraint is attached to
  336. /// @param ioBody2 The second body that this constraint is attached to
  337. /// @param inConstraintRotation The current rotation of the constraint in constraint space
  338. /// @param inConstraintToBody1 , inConstraintToBody2 Rotates from constraint space to body 1/2 space
  339. /// @param inBaumgarte Baumgarte constant (fraction of the error to correct)
  340. inline bool SolvePositionConstraint(Body &ioBody1, Body &ioBody2, QuatArg inConstraintRotation, QuatArg inConstraintToBody1, QuatArg inConstraintToBody2, float inBaumgarte) const
  341. {
  342. Quat q_swing, q_twist;
  343. inConstraintRotation.GetSwingTwist(q_swing, q_twist);
  344. bool swing_y_clamped, swing_z_clamped, twist_clamped;
  345. ClampSwingTwist(q_swing, swing_y_clamped, swing_z_clamped, q_twist, twist_clamped);
  346. // Solve rotation violations
  347. if (swing_y_clamped || swing_z_clamped || twist_clamped)
  348. {
  349. RotationEulerConstraintPart part;
  350. Quat inv_initial_orientation = inConstraintToBody2 * (inConstraintToBody1 * q_swing * q_twist).Conjugated();
  351. part.CalculateConstraintProperties(ioBody1, Mat44::sRotation(ioBody1.GetRotation()), ioBody2, Mat44::sRotation(ioBody2.GetRotation()));
  352. return part.SolvePositionConstraint(ioBody1, ioBody2, inv_initial_orientation, inBaumgarte);
  353. }
  354. return false;
  355. }
  356. /// Return lagrange multiplier for swing
  357. inline float GetTotalSwingYLambda() const
  358. {
  359. return mSwingLimitYConstraintPart.GetTotalLambda();
  360. }
  361. inline float GetTotalSwingZLambda() const
  362. {
  363. return mSwingLimitZConstraintPart.GetTotalLambda();
  364. }
  365. /// Return lagrange multiplier for twist
  366. inline float GetTotalTwistLambda() const
  367. {
  368. return mTwistLimitConstraintPart.GetTotalLambda();
  369. }
  370. /// Save state of this constraint part
  371. void SaveState(StateRecorder &inStream) const
  372. {
  373. mSwingLimitYConstraintPart.SaveState(inStream);
  374. mSwingLimitZConstraintPart.SaveState(inStream);
  375. mTwistLimitConstraintPart.SaveState(inStream);
  376. }
  377. /// Restore state of this constraint part
  378. void RestoreState(StateRecorder &inStream)
  379. {
  380. mSwingLimitYConstraintPart.RestoreState(inStream);
  381. mSwingLimitZConstraintPart.RestoreState(inStream);
  382. mTwistLimitConstraintPart.RestoreState(inStream);
  383. }
  384. private:
  385. // CONFIGURATION PROPERTIES FOLLOW
  386. enum ERotationFlags
  387. {
  388. /// Indicates that axis is completely locked (cannot rotate around this axis)
  389. TwistXLocked = 1 << 0,
  390. SwingYLocked = 1 << 1,
  391. SwingZLocked = 1 << 2,
  392. /// Indicates that axis is completely free (can rotate around without limits)
  393. TwistXFree = 1 << 3,
  394. SwingYFree = 1 << 4,
  395. SwingZFree = 1 << 5,
  396. SwingYZFree = SwingYFree | SwingZFree
  397. };
  398. uint8 mRotationFlags;
  399. // Constants
  400. float mSinTwistHalfMinAngle;
  401. float mSinTwistHalfMaxAngle;
  402. float mCosTwistHalfMinAngle;
  403. float mCosTwistHalfMaxAngle;
  404. float mSinSwingYQuarterAngle;
  405. float mSinSwingZQuarterAngle;
  406. // RUN TIME PROPERTIES FOLLOW
  407. /// Rotation axis for the angle constraint parts
  408. Vec3 mWorldSpaceSwingLimitYRotationAxis;
  409. Vec3 mWorldSpaceSwingLimitZRotationAxis;
  410. Vec3 mWorldSpaceTwistLimitRotationAxis;
  411. /// The constraint parts
  412. AngleConstraintPart mSwingLimitYConstraintPart;
  413. AngleConstraintPart mSwingLimitZConstraintPart;
  414. AngleConstraintPart mTwistLimitConstraintPart;
  415. };
  416. JPH_NAMESPACE_END