| | | 1 | | //======================================================================= |
| | | 2 | | // Joint3D.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2026-present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using Chronicler; |
| | | 9 | | using FixedMathSharp; |
| | | 10 | | using FixedMathSharp.Chronicler; |
| | | 11 | | |
| | | 12 | | namespace Gravitas.Constraints; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Context-owned deterministic 3D joint runtime state. |
| | | 16 | | /// </summary> |
| | | 17 | | public sealed class Joint3D : IRecordable |
| | | 18 | | { |
| | 312 | 19 | | private readonly Fixed64[] _accumulatedImpulses = new Fixed64[JointSolver3D.MaxRowsPerJoint]; |
| | | 20 | | private JointCollisionPolicy _collisionPolicy; |
| | 312 | 21 | | private bool _isEnabled = true; |
| | | 22 | | |
| | 312 | 23 | | internal Joint3D( |
| | 312 | 24 | | GravitasConstraint3DService service, |
| | 312 | 25 | | int id, |
| | 312 | 26 | | JointDefinition3D definition, |
| | 312 | 27 | | JointFrame3D localFrameA, |
| | 312 | 28 | | JointFrame3D localFrameB) |
| | | 29 | | { |
| | 312 | 30 | | Service = service; |
| | 312 | 31 | | Context = service.Context; |
| | 312 | 32 | | Id = id; |
| | 312 | 33 | | BodyA = definition.BodyA; |
| | 312 | 34 | | BodyB = definition.BodyB; |
| | 312 | 35 | | LocalFrameA = localFrameA; |
| | 312 | 36 | | LocalFrameB = localFrameB; |
| | 312 | 37 | | Type = definition.Type; |
| | 312 | 38 | | Limits = definition.Limits; |
| | 312 | 39 | | Motor = definition.Motor; |
| | 312 | 40 | | _collisionPolicy = definition.CollisionPolicy; |
| | 312 | 41 | | IsActive = true; |
| | 312 | 42 | | } |
| | | 43 | | |
| | | 44 | | internal GravitasConstraint3DService Service { get; } |
| | | 45 | | |
| | | 46 | | internal RagdollRuntime3D? OwningRagdoll { get; set; } |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// Gets the owning world context. |
| | | 50 | | /// </summary> |
| | | 51 | | public GravitasWorldContext Context { get; } |
| | | 52 | | |
| | | 53 | | /// <summary> |
| | | 54 | | /// Gets this context-local joint ID. |
| | | 55 | | /// </summary> |
| | | 56 | | public int Id { get; } |
| | | 57 | | |
| | | 58 | | /// <summary> |
| | | 59 | | /// Gets the first body linked by the joint. |
| | | 60 | | /// </summary> |
| | | 61 | | public SolidBody BodyA { get; } |
| | | 62 | | |
| | | 63 | | /// <summary> |
| | | 64 | | /// Gets the second body linked by the joint. |
| | | 65 | | /// </summary> |
| | | 66 | | public SolidBody BodyB { get; } |
| | | 67 | | |
| | | 68 | | /// <summary> |
| | | 69 | | /// Gets the first local anchor frame. |
| | | 70 | | /// </summary> |
| | | 71 | | public JointFrame3D LocalFrameA { get; private set; } |
| | | 72 | | |
| | | 73 | | /// <summary> |
| | | 74 | | /// Gets the second local anchor frame. |
| | | 75 | | /// </summary> |
| | | 76 | | public JointFrame3D LocalFrameB { get; private set; } |
| | | 77 | | |
| | | 78 | | /// <summary> |
| | | 79 | | /// Gets the joint type. |
| | | 80 | | /// </summary> |
| | | 81 | | public JointType3D Type { get; private set; } |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Gets optional angular limit data. |
| | | 85 | | /// </summary> |
| | | 86 | | public JointLimit3D Limits { get; private set; } |
| | | 87 | | |
| | | 88 | | /// <summary> |
| | | 89 | | /// Gets optional motor data. |
| | | 90 | | /// </summary> |
| | | 91 | | public JointMotor3D Motor { get; private set; } |
| | | 92 | | |
| | | 93 | | /// <summary> |
| | | 94 | | /// Gets physical collision filtering behavior for the linked colliders. |
| | | 95 | | /// </summary> |
| | 1658 | 96 | | public JointCollisionPolicy CollisionPolicy => _collisionPolicy; |
| | | 97 | | |
| | | 98 | | /// <summary> |
| | | 99 | | /// Gets whether this joint is registered with its owning service. |
| | | 100 | | /// </summary> |
| | | 101 | | public bool IsActive { get; internal set; } |
| | | 102 | | |
| | | 103 | | /// <summary> |
| | | 104 | | /// Gets or sets whether this active joint emits solver rows. |
| | | 105 | | /// </summary> |
| | | 106 | | public bool IsEnabled |
| | | 107 | | { |
| | 16369 | 108 | | get => _isEnabled; |
| | | 109 | | set |
| | | 110 | | { |
| | 77 | 111 | | SwiftThrowHelper.ThrowIfTrue( |
| | 77 | 112 | | !IsActive, |
| | 77 | 113 | | nameof(Joint3D), |
| | 77 | 114 | | "Removed joints cannot mutate simulation state."); |
| | 75 | 115 | | if (_isEnabled == value) |
| | 55 | 116 | | return; |
| | | 117 | | |
| | 20 | 118 | | bool oldValue = _isEnabled; |
| | 20 | 119 | | Service.UpdateJointEnabledState(this, oldValue, value); |
| | 20 | 120 | | _isEnabled = value; |
| | 20 | 121 | | ClearSolverCache(); |
| | 20 | 122 | | WakeBodies(); |
| | 20 | 123 | | } |
| | | 124 | | } |
| | | 125 | | |
| | | 126 | | /// <summary> |
| | | 127 | | /// Gets the number of rows emitted by the most recent solver pass. |
| | | 128 | | /// </summary> |
| | | 129 | | public int LastSolvedRowCount { get; internal set; } |
| | | 130 | | |
| | | 131 | | /// <summary> |
| | | 132 | | /// Gets the cumulative absolute incremental impulse emitted since the |
| | | 133 | | /// solver cache was last cleared. |
| | | 134 | | /// </summary> |
| | | 135 | | public Fixed64 AccumulatedImpulseMagnitude { get; internal set; } |
| | | 136 | | |
| | | 137 | | /// <summary> |
| | | 138 | | /// Gets deterministic metrics from the most recent solver pass. |
| | | 139 | | /// </summary> |
| | | 140 | | public JointSolveMetrics3D LastSolveMetrics { get; internal set; } |
| | | 141 | | |
| | | 142 | | /// <summary> |
| | | 143 | | /// Replaces the motor payload and wakes linked bodies. |
| | | 144 | | /// </summary> |
| | | 145 | | public void SetMotor(JointMotor3D motor) |
| | | 146 | | { |
| | 5 | 147 | | SwiftThrowHelper.ThrowIfTrue( |
| | 5 | 148 | | !IsActive, |
| | 5 | 149 | | nameof(Joint3D), |
| | 5 | 150 | | "Removed joints cannot mutate simulation state."); |
| | 3 | 151 | | motor.Validate(); |
| | 3 | 152 | | Motor = motor; |
| | 3 | 153 | | ClearSolverCache(); |
| | 3 | 154 | | WakeBodies(); |
| | 3 | 155 | | } |
| | | 156 | | |
| | | 157 | | /// <summary> |
| | | 158 | | /// Disables the joint motor and wakes linked bodies. |
| | | 159 | | /// </summary> |
| | 3 | 160 | | public void ClearMotor() => SetMotor(JointMotor3D.Disabled); |
| | | 161 | | |
| | 16295 | 162 | | internal Fixed64 GetCachedImpulse(int rowIndex) => _accumulatedImpulses[rowIndex]; |
| | | 163 | | |
| | 596762 | 164 | | internal void SetCachedImpulse(int rowIndex, Fixed64 impulse) => _accumulatedImpulses[rowIndex] = impulse; |
| | | 165 | | |
| | | 166 | | internal void ClearSolverCache() |
| | | 167 | | { |
| | 9204 | 168 | | for (int i = 0; i < _accumulatedImpulses.Length; i++) |
| | 4248 | 169 | | _accumulatedImpulses[i] = Fixed64.Zero; |
| | | 170 | | |
| | 354 | 171 | | LastSolvedRowCount = 0; |
| | 354 | 172 | | AccumulatedImpulseMagnitude = Fixed64.Zero; |
| | 354 | 173 | | LastSolveMetrics = default; |
| | 354 | 174 | | } |
| | | 175 | | |
| | | 176 | | internal void WakeBodies() |
| | | 177 | | { |
| | 23 | 178 | | BodyA.Wake(); |
| | 23 | 179 | | BodyB.Wake(); |
| | 23 | 180 | | } |
| | | 181 | | |
| | | 182 | | internal void SetCollisionPolicyFromRecord(JointCollisionPolicy collisionPolicy) |
| | | 183 | | { |
| | 9 | 184 | | if (_collisionPolicy == collisionPolicy) |
| | 3 | 185 | | return; |
| | | 186 | | |
| | 6 | 187 | | Service.UpdateJointCollisionPolicy(this, _collisionPolicy, collisionPolicy); |
| | 6 | 188 | | _collisionPolicy = collisionPolicy; |
| | 6 | 189 | | } |
| | | 190 | | |
| | 15027 | 191 | | internal bool HasSolverParticipant() => IsSolverBody(BodyA) || IsSolverBody(BodyB); |
| | | 192 | | |
| | | 193 | | internal static bool IsSolverBody(SolidBody body) => |
| | 16026 | 194 | | body.Active && body.DynamicId >= 0 && body.HasSolverMobility; |
| | | 195 | | |
| | | 196 | | internal void ContributeReplayHash( |
| | | 197 | | ref ChronicleHashWriter writer, |
| | | 198 | | GravitasReplayHashMode mode) |
| | | 199 | | { |
| | 1289 | 200 | | writer.WriteSection("joint.3d", 1); |
| | 1289 | 201 | | writer.WriteInt32(Id); |
| | 1289 | 202 | | writer.WriteInt32(BodyA.DynamicId); |
| | 1289 | 203 | | writer.WriteInt32(BodyB.DynamicId); |
| | 1289 | 204 | | writer.WriteInt32(BodyA.Collider.Id); |
| | 1289 | 205 | | writer.WriteInt32(BodyB.Collider.Id); |
| | 1289 | 206 | | writer.WriteBool(IsActive); |
| | 1289 | 207 | | writer.WriteBool(IsEnabled); |
| | 1289 | 208 | | writer.WriteEnum(Type); |
| | 1289 | 209 | | writer.WriteEnum(Limits.Kind); |
| | 1289 | 210 | | writer.WriteFixed64(Limits.MaxHingeAngle); |
| | 1289 | 211 | | writer.WriteFixed64(Limits.MaxConeAngle); |
| | 1289 | 212 | | writer.WriteFixed64(Limits.MaxTwistAngle); |
| | 1289 | 213 | | writer.WriteQuaternion(Motor.TargetLocalRotation); |
| | 1289 | 214 | | writer.WriteFixed64(Motor.AngularDriveStrength); |
| | 1289 | 215 | | writer.WriteFixed64(Motor.AngularDriveDamping); |
| | 1289 | 216 | | writer.WriteFixed64(Motor.MaximumMotorImpulse); |
| | 1289 | 217 | | writer.WriteEnum(CollisionPolicy); |
| | 1289 | 218 | | writer.WriteVector3d(LocalFrameA.Position); |
| | 1289 | 219 | | writer.WriteQuaternion(LocalFrameA.Rotation); |
| | 1289 | 220 | | writer.WriteVector3d(LocalFrameB.Position); |
| | 1289 | 221 | | writer.WriteQuaternion(LocalFrameB.Rotation); |
| | | 222 | | |
| | 1289 | 223 | | if (mode != GravitasReplayHashMode.AuthoritativeWithSolverCaches) |
| | 7 | 224 | | return; |
| | | 225 | | |
| | 1282 | 226 | | writer.WriteSection("joint.3d.caches", 1); |
| | 1282 | 227 | | writer.WriteInt32(LastSolvedRowCount); |
| | 1282 | 228 | | writer.WriteFixed64(AccumulatedImpulseMagnitude); |
| | 1282 | 229 | | writer.WriteInt32(LastSolveMetrics.PreparedRowCount); |
| | 1282 | 230 | | writer.WriteFixed64(LastSolveMetrics.LinearAnchorErrorMagnitude); |
| | 1282 | 231 | | writer.WriteFixed64(LastSolveMetrics.AngularLimitErrorMagnitude); |
| | 1282 | 232 | | writer.WriteFixed64(LastSolveMetrics.AccumulatedImpulseMagnitude); |
| | 1282 | 233 | | writer.WriteFixed64(LastSolveMetrics.IncrementalImpulseMagnitude); |
| | 1282 | 234 | | writer.WriteFixed64(LastSolveMetrics.MotorImpulseMagnitude); |
| | 1282 | 235 | | writer.WriteFixed64(LastSolveMetrics.MotorErrorMagnitude); |
| | 1282 | 236 | | writer.WriteInt32(LastSolveMetrics.ClampedRowCount); |
| | 33332 | 237 | | for (int i = 0; i < _accumulatedImpulses.Length; i++) |
| | 15384 | 238 | | writer.WriteFixed64(_accumulatedImpulses[i]); |
| | 1282 | 239 | | } |
| | | 240 | | |
| | | 241 | | /// <summary> |
| | | 242 | | /// Records mutable joint state into a Chronicler pass. |
| | | 243 | | /// </summary> |
| | | 244 | | public void RecordData(IChronicler chronicler) |
| | | 245 | | { |
| | 13 | 246 | | SwiftThrowHelper.ThrowIfTrue( |
| | 13 | 247 | | !IsActive, |
| | 13 | 248 | | nameof(Joint3D), |
| | 13 | 249 | | "Removed joints cannot participate in serialization."); |
| | 11 | 250 | | bool isEnabled = IsEnabled; |
| | 11 | 251 | | JointType3D type = Type; |
| | 11 | 252 | | JointLimitKind3D limitKind = Limits.Kind; |
| | 11 | 253 | | Fixed64 maxHingeAngle = Limits.MaxHingeAngle; |
| | 11 | 254 | | Fixed64 maxConeAngle = Limits.MaxConeAngle; |
| | 11 | 255 | | Fixed64 maxTwistAngle = Limits.MaxTwistAngle; |
| | 11 | 256 | | FixedQuaternion motorTarget = Motor.TargetLocalRotation; |
| | 11 | 257 | | Fixed64 motorStrength = Motor.AngularDriveStrength; |
| | 11 | 258 | | Fixed64 motorDamping = Motor.AngularDriveDamping; |
| | 11 | 259 | | Fixed64 maxMotorImpulse = Motor.MaximumMotorImpulse; |
| | 11 | 260 | | JointCollisionPolicy collisionPolicy = CollisionPolicy; |
| | 11 | 261 | | Vector3d frameAPosition = LocalFrameA.Position; |
| | 11 | 262 | | FixedQuaternion frameARotation = LocalFrameA.Rotation; |
| | 11 | 263 | | Vector3d frameBPosition = LocalFrameB.Position; |
| | 11 | 264 | | FixedQuaternion frameBRotation = LocalFrameB.Rotation; |
| | | 265 | | |
| | 11 | 266 | | RecordValues.Look(chronicler, ref isEnabled, "IsEnabled", true); |
| | 11 | 267 | | RecordValues.Look(chronicler, ref type, "Type", JointType3D.BallSocket); |
| | 11 | 268 | | RecordValues.Look(chronicler, ref limitKind, "LimitKind", JointLimitKind3D.Unrestricted); |
| | 11 | 269 | | RecordValues.Look(chronicler, ref maxHingeAngle, "MaxHingeAngle"); |
| | 11 | 270 | | RecordValues.Look(chronicler, ref maxConeAngle, "MaxConeAngle"); |
| | 11 | 271 | | RecordValues.Look(chronicler, ref maxTwistAngle, "MaxTwistAngle"); |
| | 11 | 272 | | RecordValues.Look(chronicler, ref motorTarget, "MotorTarget"); |
| | 11 | 273 | | RecordValues.Look(chronicler, ref motorStrength, "MotorStrength"); |
| | 11 | 274 | | RecordValues.Look(chronicler, ref motorDamping, "MotorDamping"); |
| | 11 | 275 | | RecordValues.Look(chronicler, ref maxMotorImpulse, "MaxMotorImpulse"); |
| | 11 | 276 | | RecordValues.Look(chronicler, ref collisionPolicy, "CollisionPolicy", JointCollisionPolicy.SuppressLinked); |
| | 11 | 277 | | RecordValues.Look(chronicler, ref frameAPosition, "LocalFrameAPosition"); |
| | 11 | 278 | | RecordValues.Look(chronicler, ref frameARotation, "LocalFrameARotation"); |
| | 11 | 279 | | RecordValues.Look(chronicler, ref frameBPosition, "LocalFrameBPosition"); |
| | 11 | 280 | | RecordValues.Look(chronicler, ref frameBRotation, "LocalFrameBRotation"); |
| | | 281 | | |
| | 11 | 282 | | if (chronicler.Mode != SerializationMode.Loading) |
| | 5 | 283 | | return; |
| | | 284 | | |
| | 6 | 285 | | SwiftThrowHelper.ThrowIfArgument(!IsSupportedType(type), nameof(Type), "Unsupported 3D joint type."); |
| | 6 | 286 | | SwiftThrowHelper.ThrowIfArgument(!IsSupportedLimitKind(limitKind), nameof(Limits), "Unsupported joint limit kind |
| | 6 | 287 | | SwiftThrowHelper.ThrowIfArgument( |
| | 6 | 288 | | collisionPolicy != JointCollisionPolicy.SuppressLinked |
| | 6 | 289 | | && collisionPolicy != JointCollisionPolicy.Collide, |
| | 6 | 290 | | nameof(CollisionPolicy), |
| | 6 | 291 | | "Unsupported joint collision policy."); |
| | | 292 | | |
| | 6 | 293 | | JointLimit3D limits = limitKind switch |
| | 6 | 294 | | { |
| | 3 | 295 | | JointLimitKind3D.Hinge => JointLimit3D.Hinge(maxHingeAngle), |
| | 2 | 296 | | JointLimitKind3D.ConeTwist => JointLimit3D.ConeTwist(maxConeAngle, maxTwistAngle), |
| | 1 | 297 | | _ => JointLimit3D.Unrestricted |
| | 6 | 298 | | }; |
| | 6 | 299 | | JointMotor3D motor = new(motorTarget, motorStrength, motorDamping, maxMotorImpulse); |
| | 6 | 300 | | ValidatePayload(type, limits); |
| | | 301 | | |
| | 5 | 302 | | Type = type; |
| | 5 | 303 | | Limits = limits; |
| | 5 | 304 | | Motor = motor; |
| | 5 | 305 | | SetCollisionPolicyFromRecord(collisionPolicy); |
| | 5 | 306 | | LocalFrameA = new JointFrame3D(frameAPosition, frameARotation); |
| | 5 | 307 | | LocalFrameB = new JointFrame3D(frameBPosition, frameBRotation); |
| | 5 | 308 | | if (_isEnabled == isEnabled) |
| | 2 | 309 | | ClearSolverCache(); |
| | | 310 | | else |
| | 3 | 311 | | IsEnabled = isEnabled; |
| | 3 | 312 | | } |
| | | 313 | | |
| | | 314 | | internal static bool IsSupportedType(JointType3D type) => |
| | 380 | 315 | | type == JointType3D.BallSocket |
| | 380 | 316 | | || type == JointType3D.Hinge |
| | 380 | 317 | | || type == JointType3D.ConeTwist |
| | 380 | 318 | | || type == JointType3D.Fixed; |
| | | 319 | | |
| | | 320 | | internal static bool IsSupportedLimitKind(JointLimitKind3D kind) => |
| | 6 | 321 | | kind == JointLimitKind3D.Unrestricted |
| | 6 | 322 | | || kind == JointLimitKind3D.Hinge |
| | 6 | 323 | | || kind == JointLimitKind3D.ConeTwist; |
| | | 324 | | |
| | | 325 | | internal static void ValidatePayload( |
| | | 326 | | JointType3D type, |
| | | 327 | | in JointLimit3D limits) |
| | | 328 | | { |
| | 379 | 329 | | SwiftThrowHelper.ThrowIfArgument( |
| | 379 | 330 | | type == JointType3D.Hinge |
| | 379 | 331 | | && limits.Kind != JointLimitKind3D.Unrestricted |
| | 379 | 332 | | && limits.Kind != JointLimitKind3D.Hinge, |
| | 379 | 333 | | nameof(limits), |
| | 379 | 334 | | "Hinge joints accept only hinge limits."); |
| | 379 | 335 | | SwiftThrowHelper.ThrowIfArgument( |
| | 379 | 336 | | type == JointType3D.ConeTwist |
| | 379 | 337 | | && limits.Kind != JointLimitKind3D.Unrestricted |
| | 379 | 338 | | && limits.Kind != JointLimitKind3D.ConeTwist, |
| | 379 | 339 | | nameof(limits), |
| | 379 | 340 | | "Cone-twist joints accept only cone-twist limits."); |
| | 379 | 341 | | SwiftThrowHelper.ThrowIfArgument( |
| | 379 | 342 | | (type == JointType3D.BallSocket || type == JointType3D.Fixed) |
| | 379 | 343 | | && limits.Kind != JointLimitKind3D.Unrestricted, |
| | 379 | 344 | | nameof(limits), |
| | 379 | 345 | | "Ball-socket and fixed joints do not accept angular limit payloads."); |
| | 376 | 346 | | } |
| | | 347 | | } |