Path: blob/master/thirdparty/jolt_physics/Jolt/Physics/Constraints/SwingTwistConstraint.cpp
9912 views
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)1// SPDX-FileCopyrightText: 2021 Jorrit Rouwe2// SPDX-License-Identifier: MIT34#include <Jolt/Jolt.h>56#include <Jolt/Physics/Constraints/SwingTwistConstraint.h>7#include <Jolt/Physics/Body/Body.h>8#include <Jolt/ObjectStream/TypeDeclarations.h>9#include <Jolt/Core/StreamIn.h>10#include <Jolt/Core/StreamOut.h>11#ifdef JPH_DEBUG_RENDERER12#include <Jolt/Renderer/DebugRenderer.h>13#endif // JPH_DEBUG_RENDERER1415JPH_NAMESPACE_BEGIN1617JPH_IMPLEMENT_SERIALIZABLE_VIRTUAL(SwingTwistConstraintSettings)18{19JPH_ADD_BASE_CLASS(SwingTwistConstraintSettings, TwoBodyConstraintSettings)2021JPH_ADD_ENUM_ATTRIBUTE(SwingTwistConstraintSettings, mSpace)22JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mPosition1)23JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mTwistAxis1)24JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mPlaneAxis1)25JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mPosition2)26JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mTwistAxis2)27JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mPlaneAxis2)28JPH_ADD_ENUM_ATTRIBUTE(SwingTwistConstraintSettings, mSwingType)29JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mNormalHalfConeAngle)30JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mPlaneHalfConeAngle)31JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mTwistMinAngle)32JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mTwistMaxAngle)33JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mMaxFrictionTorque)34JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mSwingMotorSettings)35JPH_ADD_ATTRIBUTE(SwingTwistConstraintSettings, mTwistMotorSettings)36}3738void SwingTwistConstraintSettings::SaveBinaryState(StreamOut &inStream) const39{40ConstraintSettings::SaveBinaryState(inStream);4142inStream.Write(mSpace);43inStream.Write(mPosition1);44inStream.Write(mTwistAxis1);45inStream.Write(mPlaneAxis1);46inStream.Write(mPosition2);47inStream.Write(mTwistAxis2);48inStream.Write(mPlaneAxis2);49inStream.Write(mSwingType);50inStream.Write(mNormalHalfConeAngle);51inStream.Write(mPlaneHalfConeAngle);52inStream.Write(mTwistMinAngle);53inStream.Write(mTwistMaxAngle);54inStream.Write(mMaxFrictionTorque);55mSwingMotorSettings.SaveBinaryState(inStream);56mTwistMotorSettings.SaveBinaryState(inStream);57}5859void SwingTwistConstraintSettings::RestoreBinaryState(StreamIn &inStream)60{61ConstraintSettings::RestoreBinaryState(inStream);6263inStream.Read(mSpace);64inStream.Read(mPosition1);65inStream.Read(mTwistAxis1);66inStream.Read(mPlaneAxis1);67inStream.Read(mPosition2);68inStream.Read(mTwistAxis2);69inStream.Read(mPlaneAxis2);70inStream.Read(mSwingType);71inStream.Read(mNormalHalfConeAngle);72inStream.Read(mPlaneHalfConeAngle);73inStream.Read(mTwistMinAngle);74inStream.Read(mTwistMaxAngle);75inStream.Read(mMaxFrictionTorque);76mSwingMotorSettings.RestoreBinaryState(inStream);77mTwistMotorSettings.RestoreBinaryState(inStream);78}7980TwoBodyConstraint *SwingTwistConstraintSettings::Create(Body &inBody1, Body &inBody2) const81{82return new SwingTwistConstraint(inBody1, inBody2, *this);83}8485void SwingTwistConstraint::UpdateLimits()86{87// Pass limits on to swing twist constraint part88mSwingTwistConstraintPart.SetLimits(mTwistMinAngle, mTwistMaxAngle, -mPlaneHalfConeAngle, mPlaneHalfConeAngle, -mNormalHalfConeAngle, mNormalHalfConeAngle);89}9091SwingTwistConstraint::SwingTwistConstraint(Body &inBody1, Body &inBody2, const SwingTwistConstraintSettings &inSettings) :92TwoBodyConstraint(inBody1, inBody2, inSettings),93mNormalHalfConeAngle(inSettings.mNormalHalfConeAngle),94mPlaneHalfConeAngle(inSettings.mPlaneHalfConeAngle),95mTwistMinAngle(inSettings.mTwistMinAngle),96mTwistMaxAngle(inSettings.mTwistMaxAngle),97mMaxFrictionTorque(inSettings.mMaxFrictionTorque),98mSwingMotorSettings(inSettings.mSwingMotorSettings),99mTwistMotorSettings(inSettings.mTwistMotorSettings)100{101// Override swing type102mSwingTwistConstraintPart.SetSwingType(inSettings.mSwingType);103104// Calculate rotation needed to go from constraint space to body1 local space105Vec3 normal_axis1 = inSettings.mPlaneAxis1.Cross(inSettings.mTwistAxis1);106Mat44 c_to_b1(Vec4(inSettings.mTwistAxis1, 0), Vec4(normal_axis1, 0), Vec4(inSettings.mPlaneAxis1, 0), Vec4(0, 0, 0, 1));107mConstraintToBody1 = c_to_b1.GetQuaternion();108109// Calculate rotation needed to go from constraint space to body2 local space110Vec3 normal_axis2 = inSettings.mPlaneAxis2.Cross(inSettings.mTwistAxis2);111Mat44 c_to_b2(Vec4(inSettings.mTwistAxis2, 0), Vec4(normal_axis2, 0), Vec4(inSettings.mPlaneAxis2, 0), Vec4(0, 0, 0, 1));112mConstraintToBody2 = c_to_b2.GetQuaternion();113114if (inSettings.mSpace == EConstraintSpace::WorldSpace)115{116// If all properties were specified in world space, take them to local space now117mLocalSpacePosition1 = Vec3(inBody1.GetInverseCenterOfMassTransform() * inSettings.mPosition1);118mConstraintToBody1 = inBody1.GetRotation().Conjugated() * mConstraintToBody1;119120mLocalSpacePosition2 = Vec3(inBody2.GetInverseCenterOfMassTransform() * inSettings.mPosition2);121mConstraintToBody2 = inBody2.GetRotation().Conjugated() * mConstraintToBody2;122}123else124{125mLocalSpacePosition1 = Vec3(inSettings.mPosition1);126mLocalSpacePosition2 = Vec3(inSettings.mPosition2);127}128129UpdateLimits();130}131132void SwingTwistConstraint::NotifyShapeChanged(const BodyID &inBodyID, Vec3Arg inDeltaCOM)133{134if (mBody1->GetID() == inBodyID)135mLocalSpacePosition1 -= inDeltaCOM;136else if (mBody2->GetID() == inBodyID)137mLocalSpacePosition2 -= inDeltaCOM;138}139140Quat SwingTwistConstraint::GetRotationInConstraintSpace() const141{142// Let b1, b2 be the center of mass transform of body1 and body2 (For body1 this is mBody1->GetCenterOfMassTransform())143// Let c1, c2 be the transform that takes a vector from constraint space to local space of body1 and body2 (For body1 this is Mat44::sRotationTranslation(mConstraintToBody1, mLocalSpacePosition1))144// Let q be the rotation of the constraint in constraint space145// b2 takes a vector from the local space of body2 to world space146// To express this in terms of b1: b2 = b1 * c1 * q * c2^-1147// c2^-1 goes from local body 2 space to constraint space148// q rotates the constraint149// c1 goes from constraint space to body 1 local space150// b1 goes from body 1 local space to world space151// So when the body rotations are given, q = (b1 * c1)^-1 * b2 c2152// Or: q = (q1 * c1)^-1 * (q2 * c2) if we're only interested in rotations153Quat constraint_body1_to_world = mBody1->GetRotation() * mConstraintToBody1;154Quat constraint_body2_to_world = mBody2->GetRotation() * mConstraintToBody2;155return constraint_body1_to_world.Conjugated() * constraint_body2_to_world;156}157158void SwingTwistConstraint::SetSwingMotorState(EMotorState inState)159{160JPH_ASSERT(inState == EMotorState::Off || mSwingMotorSettings.IsValid());161162if (mSwingMotorState != inState)163{164mSwingMotorState = inState;165166// Ensure that warm starting next frame doesn't apply any impulses (motor parts are repurposed for different modes)167for (AngleConstraintPart &c : mMotorConstraintPart)168c.Deactivate();169}170}171172void SwingTwistConstraint::SetTwistMotorState(EMotorState inState)173{174JPH_ASSERT(inState == EMotorState::Off || mTwistMotorSettings.IsValid());175176if (mTwistMotorState != inState)177{178mTwistMotorState = inState;179180// Ensure that warm starting next frame doesn't apply any impulses (motor parts are repurposed for different modes)181mMotorConstraintPart[0].Deactivate();182}183}184185void SwingTwistConstraint::SetTargetOrientationCS(QuatArg inOrientation)186{187Quat q_swing, q_twist;188inOrientation.GetSwingTwist(q_swing, q_twist);189190uint clamped_axis;191mSwingTwistConstraintPart.ClampSwingTwist(q_swing, q_twist, clamped_axis);192193if (clamped_axis != 0)194mTargetOrientation = q_swing * q_twist;195else196mTargetOrientation = inOrientation;197}198199void SwingTwistConstraint::SetupVelocityConstraint(float inDeltaTime)200{201// Setup point constraint202Mat44 rotation1 = Mat44::sRotation(mBody1->GetRotation());203Mat44 rotation2 = Mat44::sRotation(mBody2->GetRotation());204mPointConstraintPart.CalculateConstraintProperties(*mBody1, rotation1, mLocalSpacePosition1, *mBody2, rotation2, mLocalSpacePosition2);205206// GetRotationInConstraintSpace written out since we reuse the sub expressions207Quat constraint_body1_to_world = mBody1->GetRotation() * mConstraintToBody1;208Quat constraint_body2_to_world = mBody2->GetRotation() * mConstraintToBody2;209Quat q = constraint_body1_to_world.Conjugated() * constraint_body2_to_world;210211// Calculate constraint properties for the swing twist limit212mSwingTwistConstraintPart.CalculateConstraintProperties(*mBody1, *mBody2, q, constraint_body1_to_world);213214if (mSwingMotorState != EMotorState::Off || mTwistMotorState != EMotorState::Off || mMaxFrictionTorque > 0.0f)215{216// Calculate rotation motor axis217Mat44 ws_axis = Mat44::sRotation(constraint_body2_to_world);218for (int i = 0; i < 3; ++i)219mWorldSpaceMotorAxis[i] = ws_axis.GetColumn3(i);220221Vec3 rotation_error;222if (mSwingMotorState == EMotorState::Position || mTwistMotorState == EMotorState::Position)223{224// Get target orientation along the shortest path from q225Quat target_orientation = q.Dot(mTargetOrientation) > 0.0f? mTargetOrientation : -mTargetOrientation;226227// The definition of the constraint rotation q:228// R2 * ConstraintToBody2 = R1 * ConstraintToBody1 * q (1)229//230// R2' is the rotation of body 2 when reaching the target_orientation:231// R2' * ConstraintToBody2 = R1 * ConstraintToBody1 * target_orientation (2)232//233// The difference in body 2 space:234// R2' = R2 * diff_body2 (3)235//236// We want to specify the difference in the constraint space of body 2:237// diff_body2 = ConstraintToBody2 * diff * ConstraintToBody2^* (4)238//239// Extracting R2' from 2: R2' = R1 * ConstraintToBody1 * target_orientation * ConstraintToBody2^* (5)240// Combining 3 & 4: R2' = R2 * ConstraintToBody2 * diff * ConstraintToBody2^* (6)241// Combining 1 & 6: R2' = R1 * ConstraintToBody1 * q * diff * ConstraintToBody2^* (7)242// Combining 5 & 7: R1 * ConstraintToBody1 * target_orientation * ConstraintToBody2^* = R1 * ConstraintToBody1 * q * diff * ConstraintToBody2^*243// <=> target_orientation = q * diff244// <=> diff = q^* * target_orientation245Quat diff = q.Conjugated() * target_orientation;246247// Approximate error angles248// The imaginary part of a quaternion is rotation_axis * sin(angle / 2)249// If angle is small, sin(x) = x so angle[i] ~ 2.0f * rotation_axis[i]250// We'll be making small time steps, so if the angle is not small at least the sign will be correct and we'll move in the right direction251rotation_error = -2.0f * diff.GetXYZ();252}253254// Swing motor255switch (mSwingMotorState)256{257case EMotorState::Off:258if (mMaxFrictionTorque > 0.0f)259{260// Enable friction261for (int i = 1; i < 3; ++i)262mMotorConstraintPart[i].CalculateConstraintProperties(*mBody1, *mBody2, mWorldSpaceMotorAxis[i], 0.0f);263}264else265{266// Disable friction267for (AngleConstraintPart &c : mMotorConstraintPart)268c.Deactivate();269}270break;271272case EMotorState::Velocity:273// Use motor to create angular velocity around desired axis274for (int i = 1; i < 3; ++i)275mMotorConstraintPart[i].CalculateConstraintProperties(*mBody1, *mBody2, mWorldSpaceMotorAxis[i], -mTargetAngularVelocity[i]);276break;277278case EMotorState::Position:279// Use motor to drive rotation error to zero280if (mSwingMotorSettings.mSpringSettings.HasStiffness())281{282for (int i = 1; i < 3; ++i)283mMotorConstraintPart[i].CalculateConstraintPropertiesWithSettings(inDeltaTime, *mBody1, *mBody2, mWorldSpaceMotorAxis[i], 0.0f, rotation_error[i], mSwingMotorSettings.mSpringSettings);284}285else286{287for (int i = 1; i < 3; ++i)288mMotorConstraintPart[i].Deactivate();289}290break;291}292293// Twist motor294switch (mTwistMotorState)295{296case EMotorState::Off:297if (mMaxFrictionTorque > 0.0f)298{299// Enable friction300mMotorConstraintPart[0].CalculateConstraintProperties(*mBody1, *mBody2, mWorldSpaceMotorAxis[0], 0.0f);301}302else303{304// Disable friction305mMotorConstraintPart[0].Deactivate();306}307break;308309case EMotorState::Velocity:310// Use motor to create angular velocity around desired axis311mMotorConstraintPart[0].CalculateConstraintProperties(*mBody1, *mBody2, mWorldSpaceMotorAxis[0], -mTargetAngularVelocity[0]);312break;313314case EMotorState::Position:315// Use motor to drive rotation error to zero316if (mTwistMotorSettings.mSpringSettings.HasStiffness())317mMotorConstraintPart[0].CalculateConstraintPropertiesWithSettings(inDeltaTime, *mBody1, *mBody2, mWorldSpaceMotorAxis[0], 0.0f, rotation_error[0], mTwistMotorSettings.mSpringSettings);318else319mMotorConstraintPart[0].Deactivate();320break;321}322}323else324{325// Disable rotation motor326for (AngleConstraintPart &c : mMotorConstraintPart)327c.Deactivate();328}329}330331void SwingTwistConstraint::ResetWarmStart()332{333for (AngleConstraintPart &c : mMotorConstraintPart)334c.Deactivate();335mSwingTwistConstraintPart.Deactivate();336mPointConstraintPart.Deactivate();337}338339void SwingTwistConstraint::WarmStartVelocityConstraint(float inWarmStartImpulseRatio)340{341// Warm starting: Apply previous frame impulse342for (AngleConstraintPart &c : mMotorConstraintPart)343c.WarmStart(*mBody1, *mBody2, inWarmStartImpulseRatio);344mSwingTwistConstraintPart.WarmStart(*mBody1, *mBody2, inWarmStartImpulseRatio);345mPointConstraintPart.WarmStart(*mBody1, *mBody2, inWarmStartImpulseRatio);346}347348bool SwingTwistConstraint::SolveVelocityConstraint(float inDeltaTime)349{350bool impulse = false;351352// Solve twist rotation motor353if (mMotorConstraintPart[0].IsActive())354{355// Twist limits356float min_twist_limit, max_twist_limit;357if (mTwistMotorState == EMotorState::Off)358{359max_twist_limit = inDeltaTime * mMaxFrictionTorque;360min_twist_limit = -max_twist_limit;361}362else363{364min_twist_limit = inDeltaTime * mTwistMotorSettings.mMinTorqueLimit;365max_twist_limit = inDeltaTime * mTwistMotorSettings.mMaxTorqueLimit;366}367368impulse |= mMotorConstraintPart[0].SolveVelocityConstraint(*mBody1, *mBody2, mWorldSpaceMotorAxis[0], min_twist_limit, max_twist_limit);369}370371// Solve swing rotation motor372if (mMotorConstraintPart[1].IsActive())373{374// Swing parts should turn on / off together375JPH_ASSERT(mMotorConstraintPart[2].IsActive());376377// Swing limits378float min_swing_limit, max_swing_limit;379if (mSwingMotorState == EMotorState::Off)380{381max_swing_limit = inDeltaTime * mMaxFrictionTorque;382min_swing_limit = -max_swing_limit;383}384else385{386min_swing_limit = inDeltaTime * mSwingMotorSettings.mMinTorqueLimit;387max_swing_limit = inDeltaTime * mSwingMotorSettings.mMaxTorqueLimit;388}389390for (int i = 1; i < 3; ++i)391impulse |= mMotorConstraintPart[i].SolveVelocityConstraint(*mBody1, *mBody2, mWorldSpaceMotorAxis[i], min_swing_limit, max_swing_limit);392}393else394{395// Swing parts should turn on / off together396JPH_ASSERT(!mMotorConstraintPart[2].IsActive());397}398399// Solve rotation limits400impulse |= mSwingTwistConstraintPart.SolveVelocityConstraint(*mBody1, *mBody2);401402// Solve position constraint403impulse |= mPointConstraintPart.SolveVelocityConstraint(*mBody1, *mBody2);404405return impulse;406}407408bool SwingTwistConstraint::SolvePositionConstraint(float inDeltaTime, float inBaumgarte)409{410bool impulse = false;411412// Solve rotation violations413Quat q = GetRotationInConstraintSpace();414impulse |= mSwingTwistConstraintPart.SolvePositionConstraint(*mBody1, *mBody2, q, mConstraintToBody1, mConstraintToBody2, inBaumgarte);415416// Solve position violations417mPointConstraintPart.CalculateConstraintProperties(*mBody1, Mat44::sRotation(mBody1->GetRotation()), mLocalSpacePosition1, *mBody2, Mat44::sRotation(mBody2->GetRotation()), mLocalSpacePosition2);418impulse |= mPointConstraintPart.SolvePositionConstraint(*mBody1, *mBody2, inBaumgarte);419420return impulse;421}422423#ifdef JPH_DEBUG_RENDERER424void SwingTwistConstraint::DrawConstraint(DebugRenderer *inRenderer) const425{426// Get constraint properties in world space427RMat44 transform1 = mBody1->GetCenterOfMassTransform();428RVec3 position1 = transform1 * mLocalSpacePosition1;429Quat rotation1 = mBody1->GetRotation() * mConstraintToBody1;430Quat rotation2 = mBody2->GetRotation() * mConstraintToBody2;431432// Draw constraint orientation433inRenderer->DrawCoordinateSystem(RMat44::sRotationTranslation(rotation1, position1), mDrawConstraintSize);434435// Draw current swing and twist436Quat q = GetRotationInConstraintSpace();437Quat q_swing, q_twist;438q.GetSwingTwist(q_swing, q_twist);439inRenderer->DrawLine(position1, position1 + mDrawConstraintSize * (rotation1 * q_twist).RotateAxisY(), Color::sWhite);440inRenderer->DrawLine(position1, position1 + mDrawConstraintSize * (rotation1 * q_swing).RotateAxisX(), Color::sWhite);441442if (mSwingMotorState == EMotorState::Velocity || mTwistMotorState == EMotorState::Velocity)443{444// Draw target angular velocity445inRenderer->DrawArrow(position1, position1 + rotation2 * mTargetAngularVelocity, Color::sRed, 0.1f);446}447if (mSwingMotorState == EMotorState::Position || mTwistMotorState == EMotorState::Position)448{449// Draw motor swing and twist450Quat swing, twist;451mTargetOrientation.GetSwingTwist(swing, twist);452inRenderer->DrawLine(position1, position1 + mDrawConstraintSize * (rotation1 * twist).RotateAxisY(), Color::sYellow);453inRenderer->DrawLine(position1, position1 + mDrawConstraintSize * (rotation1 * swing).RotateAxisX(), Color::sCyan);454}455}456457void SwingTwistConstraint::DrawConstraintLimits(DebugRenderer *inRenderer) const458{459// Get matrix that transforms from constraint space to world space460RMat44 constraint_to_world = RMat44::sRotationTranslation(mBody1->GetRotation() * mConstraintToBody1, mBody1->GetCenterOfMassTransform() * mLocalSpacePosition1);461462// Draw limits463if (mSwingTwistConstraintPart.GetSwingType() == ESwingType::Pyramid)464inRenderer->DrawSwingPyramidLimits(constraint_to_world, -mPlaneHalfConeAngle, mPlaneHalfConeAngle, -mNormalHalfConeAngle, mNormalHalfConeAngle, mDrawConstraintSize, Color::sGreen, DebugRenderer::ECastShadow::Off);465else466inRenderer->DrawSwingConeLimits(constraint_to_world, mPlaneHalfConeAngle, mNormalHalfConeAngle, mDrawConstraintSize, Color::sGreen, DebugRenderer::ECastShadow::Off);467inRenderer->DrawPie(constraint_to_world.GetTranslation(), mDrawConstraintSize, constraint_to_world.GetAxisX(), constraint_to_world.GetAxisY(), mTwistMinAngle, mTwistMaxAngle, Color::sPurple, DebugRenderer::ECastShadow::Off);468}469#endif // JPH_DEBUG_RENDERER470471void SwingTwistConstraint::SaveState(StateRecorder &inStream) const472{473TwoBodyConstraint::SaveState(inStream);474475mPointConstraintPart.SaveState(inStream);476mSwingTwistConstraintPart.SaveState(inStream);477for (const AngleConstraintPart &c : mMotorConstraintPart)478c.SaveState(inStream);479480inStream.Write(mSwingMotorState);481inStream.Write(mTwistMotorState);482inStream.Write(mTargetAngularVelocity);483inStream.Write(mTargetOrientation);484}485486void SwingTwistConstraint::RestoreState(StateRecorder &inStream)487{488TwoBodyConstraint::RestoreState(inStream);489490mPointConstraintPart.RestoreState(inStream);491mSwingTwistConstraintPart.RestoreState(inStream);492for (AngleConstraintPart &c : mMotorConstraintPart)493c.RestoreState(inStream);494495inStream.Read(mSwingMotorState);496inStream.Read(mTwistMotorState);497inStream.Read(mTargetAngularVelocity);498inStream.Read(mTargetOrientation);499}500501Ref<ConstraintSettings> SwingTwistConstraint::GetConstraintSettings() const502{503SwingTwistConstraintSettings *settings = new SwingTwistConstraintSettings;504ToConstraintSettings(*settings);505settings->mSpace = EConstraintSpace::LocalToBodyCOM;506settings->mPosition1 = RVec3(mLocalSpacePosition1);507settings->mTwistAxis1 = mConstraintToBody1.RotateAxisX();508settings->mPlaneAxis1 = mConstraintToBody1.RotateAxisZ();509settings->mPosition2 = RVec3(mLocalSpacePosition2);510settings->mTwistAxis2 = mConstraintToBody2.RotateAxisX();511settings->mPlaneAxis2 = mConstraintToBody2.RotateAxisZ();512settings->mSwingType = mSwingTwistConstraintPart.GetSwingType();513settings->mNormalHalfConeAngle = mNormalHalfConeAngle;514settings->mPlaneHalfConeAngle = mPlaneHalfConeAngle;515settings->mTwistMinAngle = mTwistMinAngle;516settings->mTwistMaxAngle = mTwistMaxAngle;517settings->mMaxFrictionTorque = mMaxFrictionTorque;518settings->mSwingMotorSettings = mSwingMotorSettings;519settings->mTwistMotorSettings = mTwistMotorSettings;520return settings;521}522523JPH_NAMESPACE_END524525526