SwingTwistConstraintPart.h 17 KB

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