| | | 1 | | //======================================================================= |
| | | 2 | | // LSCompoundCollider.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 FixedMathSharp; |
| | | 9 | | using FixedMathSharp.Geometry; |
| | | 10 | | using Gravitas.Materials; |
| | | 11 | | using Gravitas.Queries; |
| | | 12 | | using SwiftCollections; |
| | | 13 | | using System; |
| | | 14 | | using System.Runtime.CompilerServices; |
| | | 15 | | |
| | | 16 | | namespace Gravitas.Colliders; |
| | | 17 | | |
| | | 18 | | /// <summary> |
| | | 19 | | /// Represents one collider identity whose collision shape is composed from |
| | | 20 | | /// deterministic internal primitive and convex-mesh parts. |
| | | 21 | | /// </summary> |
| | | 22 | | public sealed class LSCompoundCollider : LSCollider |
| | | 23 | | { |
| | | 24 | | private readonly CompoundColliderPart[] _parts; |
| | | 25 | | private readonly LSCollider[] _partColliders; |
| | | 26 | | private readonly ExactMassPoint3D[] _massPointScratch; |
| | | 27 | | private readonly ExactMassWeight[] _massWeightScratch; |
| | | 28 | | |
| | | 29 | | /// <summary>Creates a runtime compound collider from authored 3D parts.</summary> |
| | 158 | 30 | | public LSCompoundCollider(params CompoundColliderPart[] parts) |
| | | 31 | | { |
| | 158 | 32 | | SwiftThrowHelper.ThrowIfNull(parts, nameof(parts)); |
| | 158 | 33 | | SwiftThrowHelper.ThrowIfArgument(parts.Length == 0, nameof(parts), "Compound collider must contain at least one |
| | | 34 | | |
| | 890 | 35 | | for (int i = 0; i < parts.Length; i++) |
| | 288 | 36 | | ValidatePart(parts[i]); |
| | | 37 | | |
| | 157 | 38 | | _parts = new CompoundColliderPart[parts.Length]; |
| | 157 | 39 | | _partColliders = new LSCollider[parts.Length]; |
| | 157 | 40 | | _massPointScratch = new ExactMassPoint3D[parts.Length]; |
| | 157 | 41 | | _massWeightScratch = new ExactMassWeight[parts.Length]; |
| | 888 | 42 | | for (int i = 0; i < parts.Length; i++) |
| | | 43 | | { |
| | 287 | 44 | | _parts[i] = parts[i]; |
| | 287 | 45 | | _partColliders[i] = MaterializePartCollider(parts[i]); |
| | 287 | 46 | | _partColliders[i].ReserveCompoundPart( |
| | 287 | 47 | | this, |
| | 287 | 48 | | parts[i].LocalRotation, |
| | 287 | 49 | | parts[i].LocalScale); |
| | | 50 | | } |
| | 157 | 51 | | } |
| | | 52 | | |
| | | 53 | | /// <inheritdoc/> |
| | 498 | 54 | | public override ColliderType Shape => ColliderType.Compound; |
| | | 55 | | |
| | | 56 | | /// <inheritdoc/> |
| | 63 | 57 | | public override int Priority => ColliderSettings.GetPriority(Shape); |
| | | 58 | | |
| | | 59 | | /// <inheritdoc/> |
| | | 60 | | public override Fixed64 ScaledRadius |
| | | 61 | | { |
| | | 62 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 16 | 63 | | get => HasCommittedShape |
| | 16 | 64 | | ? CanonicalCenteredProxyRadius |
| | 16 | 65 | | : ColliderCanonicalBounds |
| | 16 | 66 | | .GetCurrentCenteredProxyRadius(this); |
| | | 67 | | } |
| | | 68 | | |
| | | 69 | | /// <summary>Gets the authored parts in stable source order.</summary> |
| | 36 | 70 | | public ReadOnlySpan<CompoundColliderPart> Parts => _parts; |
| | | 71 | | |
| | | 72 | | /// <summary>Gets the number of authored compound parts.</summary> |
| | | 73 | | public int PartCount |
| | | 74 | | { |
| | | 75 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 1441 | 76 | | get => _parts.Length; |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | /// <summary>Gets the stable runtime part identifier for a source-order index.</summary> |
| | | 80 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 81 | | public int GetPartId(int index) |
| | | 82 | | { |
| | 2 | 83 | | SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _parts.Length, nameof(index)); |
| | 2 | 84 | | return index; |
| | | 85 | | } |
| | | 86 | | |
| | | 87 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 88 | | internal LSCollider GetPartCollider(int index) |
| | | 89 | | { |
| | 1041 | 90 | | SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _parts.Length, nameof(index)); |
| | 1041 | 91 | | return _partColliders[index]; |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | private protected override void PrepareShape(in ColliderShapeSnapshot snapshot) |
| | | 95 | | { |
| | 170 | 96 | | Vector3d min = Vector3d.Zero; |
| | 170 | 97 | | Vector3d max = Vector3d.Zero; |
| | | 98 | | |
| | 958 | 99 | | for (int i = 0; i < _parts.Length; i++) |
| | | 100 | | { |
| | 313 | 101 | | CompoundColliderPart part = _parts[i]; |
| | 313 | 102 | | LSCollider partCollider = _partColliders[i]; |
| | 313 | 103 | | partCollider.PrepareCompoundPart( |
| | 313 | 104 | | snapshot, |
| | 313 | 105 | | part.LocalRotation, |
| | 313 | 106 | | part.LocalScale, |
| | 313 | 107 | | PreparedContext); |
| | | 108 | | |
| | 309 | 109 | | if (i == 0) |
| | | 110 | | { |
| | 167 | 111 | | min = partCollider.PreparedShapeBounds.Min; |
| | 167 | 112 | | max = partCollider.PreparedShapeBounds.Max; |
| | | 113 | | } |
| | | 114 | | else |
| | | 115 | | { |
| | 142 | 116 | | min = Vector3d.Min(min, partCollider.PreparedShapeBounds.Min); |
| | 142 | 117 | | max = Vector3d.Max(max, partCollider.PreparedShapeBounds.Max); |
| | | 118 | | } |
| | | 119 | | } |
| | | 120 | | |
| | 166 | 121 | | _ = CalculatePreparedLocalMassPoint(); |
| | 165 | 122 | | _ = CalculatePreparedMassPropertyWeight(); |
| | 165 | 123 | | SetPreparedBounds(FixedBoundBox.FromMinMax(min, max)); |
| | 165 | 124 | | } |
| | | 125 | | |
| | | 126 | | private protected override void PublishShape() |
| | | 127 | | { |
| | 165 | 128 | | Fixed64 area = Fixed64.Zero; |
| | 944 | 129 | | for (int i = 0; i < _parts.Length; i++) |
| | | 130 | | { |
| | 307 | 131 | | CompoundColliderPart part = _parts[i]; |
| | 307 | 132 | | _partColliders[i].PublishCompoundPart( |
| | 307 | 133 | | part.LocalRotation, |
| | 307 | 134 | | part.LocalScale, |
| | 307 | 135 | | PreparedContext); |
| | 307 | 136 | | area += _partColliders[i].Area; |
| | | 137 | | } |
| | | 138 | | |
| | 165 | 139 | | Area = area; |
| | 165 | 140 | | } |
| | | 141 | | |
| | | 142 | | internal override ExactMassPoint3D CalculateLocalMassPoint() => |
| | 366 | 143 | | CalculateAggregateMassPoint(usePrepared: false); |
| | | 144 | | |
| | | 145 | | internal override ExactMassPoint3D CalculatePreparedLocalMassPoint() => |
| | 301 | 146 | | CalculateAggregateMassPoint(usePrepared: true); |
| | | 147 | | |
| | | 148 | | private ExactMassPoint3D CalculateAggregateMassPoint(bool usePrepared) |
| | | 149 | | { |
| | 667 | 150 | | bool hasPositiveWeight = false; |
| | 3614 | 151 | | for (int i = 0; i < _partColliders.Length; i++) |
| | | 152 | | { |
| | 1140 | 153 | | LSCollider part = _partColliders[i]; |
| | 1140 | 154 | | ExactMassWeight weight = usePrepared |
| | 1140 | 155 | | ? part.CalculatePreparedMassPropertyWeight() |
| | 1140 | 156 | | : part.CalculateMassPropertyWeight(); |
| | 1140 | 157 | | _massPointScratch[i] = usePrepared |
| | 1140 | 158 | | ? part.CalculatePreparedLocalMassPoint() |
| | 1140 | 159 | | : part.CalculateLocalMassPoint(); |
| | 1140 | 160 | | _massWeightScratch[i] = weight; |
| | 1140 | 161 | | hasPositiveWeight |= !weight.IsZero; |
| | | 162 | | } |
| | | 163 | | |
| | 667 | 164 | | if (!hasPositiveWeight) |
| | | 165 | | { |
| | 13 | 166 | | int supportedPartCount = 0; |
| | 56 | 167 | | for (int i = 0; i < _massWeightScratch.Length; i++) |
| | | 168 | | { |
| | 15 | 169 | | if (_partColliders[i].SupportsMassProperties) |
| | | 170 | | { |
| | 9 | 171 | | _massWeightScratch[i] = ExactMassWeight.One; |
| | 9 | 172 | | supportedPartCount++; |
| | | 173 | | } |
| | | 174 | | } |
| | | 175 | | |
| | 13 | 176 | | if (supportedPartCount == 0) |
| | | 177 | | { |
| | 24 | 178 | | for (int i = 0; i < _massWeightScratch.Length; i++) |
| | 6 | 179 | | _massWeightScratch[i] = ExactMassWeight.One; |
| | | 180 | | } |
| | | 181 | | } |
| | | 182 | | |
| | 667 | 183 | | if (!ExactMassPoint3D.TryGetWeightedAverage( |
| | 667 | 184 | | _massPointScratch, |
| | 667 | 185 | | _massWeightScratch, |
| | 667 | 186 | | out Vector3d center)) |
| | | 187 | | { |
| | 2 | 188 | | throw new InvalidOperationException( |
| | 2 | 189 | | usePrepared |
| | 2 | 190 | | ? "Prepared compound mass-property point is outside the Fixed64 coordinate domain." |
| | 2 | 191 | | : "The compound collider's center of mass is outside the Fixed64 coordinate domain."); |
| | | 192 | | } |
| | 665 | 193 | | return ExactMassPoint3D.FromPoint(center); |
| | | 194 | | } |
| | | 195 | | |
| | | 196 | | internal override ExactMassWeight CalculateMassPropertyWeight() => |
| | 1 | 197 | | CalculateAggregateMassWeight(usePrepared: false); |
| | | 198 | | |
| | | 199 | | internal override ExactMassWeight CalculatePreparedMassPropertyWeight() => |
| | 165 | 200 | | CalculateAggregateMassWeight(usePrepared: true); |
| | | 201 | | |
| | | 202 | | private ExactMassWeight CalculateAggregateMassWeight(bool usePrepared) |
| | | 203 | | { |
| | 166 | 204 | | ExactMassWeight totalWeight = ExactMassWeight.Zero; |
| | 948 | 205 | | for (int i = 0; i < _parts.Length; i++) |
| | | 206 | | { |
| | 308 | 207 | | ExactMassWeight weight = usePrepared |
| | 308 | 208 | | ? _partColliders[i].CalculatePreparedMassPropertyWeight() |
| | 308 | 209 | | : _partColliders[i].CalculateMassPropertyWeight(); |
| | 308 | 210 | | totalWeight = totalWeight.Add(weight); |
| | | 211 | | } |
| | 166 | 212 | | return totalWeight; |
| | | 213 | | } |
| | | 214 | | |
| | | 215 | | internal override Fixed3x3 CalculateCenterOfMassInertiaTensor( |
| | | 216 | | Fixed64 mass) |
| | | 217 | | { |
| | 112 | 218 | | Vector3d center = CalculateLocalCenterOfMassOffset(); |
| | 112 | 219 | | ExactMassWeight totalWeight = ExactMassWeight.Zero; |
| | 112 | 220 | | int residualPartIndex = _parts.Length - 1; |
| | 112 | 221 | | int eligiblePartCount = 0; |
| | 598 | 222 | | for (int i = 0; i < _parts.Length; i++) |
| | | 223 | | { |
| | 187 | 224 | | LSCollider part = _partColliders[i]; |
| | 187 | 225 | | if (!part.SupportsMassProperties) |
| | | 226 | | { |
| | 1 | 227 | | _massWeightScratch[i] = ExactMassWeight.Zero; |
| | 1 | 228 | | continue; |
| | | 229 | | } |
| | | 230 | | |
| | 186 | 231 | | eligiblePartCount++; |
| | 186 | 232 | | ExactMassWeight weight = part.CalculateMassPropertyWeight(); |
| | 186 | 233 | | _massWeightScratch[i] = weight; |
| | 186 | 234 | | totalWeight = totalWeight.Add(weight); |
| | 186 | 235 | | residualPartIndex = i; |
| | | 236 | | } |
| | | 237 | | |
| | 112 | 238 | | if (eligiblePartCount == 0) |
| | | 239 | | { |
| | 1 | 240 | | throw new InvalidOperationException( |
| | 1 | 241 | | "Compound inertia requires at least one part with valid mass properties."); |
| | | 242 | | } |
| | | 243 | | |
| | 111 | 244 | | ExactMassWeight cumulativeWeight = ExactMassWeight.Zero; |
| | 111 | 245 | | Fixed64 assignedMass = Fixed64.Zero; |
| | 111 | 246 | | Fixed3x3 tensor = Fixed3x3.Zero; |
| | | 247 | | |
| | 588 | 248 | | for (int i = 0; i < _parts.Length; i++) |
| | | 249 | | { |
| | 185 | 250 | | LSCollider part = _partColliders[i]; |
| | 185 | 251 | | ExactMassWeight weight = _massWeightScratch[i]; |
| | 185 | 252 | | cumulativeWeight = cumulativeWeight.Add(weight); |
| | | 253 | | Fixed64 partMass; |
| | 185 | 254 | | if (i == residualPartIndex) |
| | | 255 | | { |
| | 110 | 256 | | partMass = mass - assignedMass; |
| | 110 | 257 | | assignedMass = mass; |
| | | 258 | | } |
| | | 259 | | else |
| | | 260 | | { |
| | 75 | 261 | | _ = cumulativeWeight.TryGetProportionalShare( |
| | 75 | 262 | | mass, |
| | 75 | 263 | | totalWeight, |
| | 75 | 264 | | out Fixed64 cumulativeMass); |
| | 75 | 265 | | partMass = cumulativeMass - assignedMass; |
| | 75 | 266 | | assignedMass = cumulativeMass; |
| | | 267 | | } |
| | | 268 | | |
| | 185 | 269 | | if (partMass == Fixed64.Zero) |
| | | 270 | | continue; |
| | | 271 | | |
| | 181 | 272 | | ExactMassPoint3D partCenterOfMass = |
| | 181 | 273 | | part.CalculateLocalMassPoint(); |
| | 181 | 274 | | Fixed3x3 partTensor = |
| | 181 | 275 | | part.CalculateCenterOfMassInertiaTensor(partMass); |
| | 181 | 276 | | partTensor = InertiaTensorMath.RotateToFrame(partTensor, part.CompoundLocalRotation); |
| | 181 | 277 | | if (!partCenterOfMass.TryAddParallelAxisTensor( |
| | 181 | 278 | | partTensor, |
| | 181 | 279 | | partMass, |
| | 181 | 280 | | center, |
| | 181 | 281 | | out Fixed3x3 contribution) |
| | 181 | 282 | | || !TryAddTensor( |
| | 181 | 283 | | tensor, |
| | 181 | 284 | | contribution, |
| | 181 | 285 | | out tensor)) |
| | | 286 | | { |
| | 2 | 287 | | throw new InvalidOperationException( |
| | 2 | 288 | | "The compound collider's inertia tensor is outside the Fixed64 scalar domain."); |
| | | 289 | | } |
| | | 290 | | } |
| | | 291 | | |
| | 109 | 292 | | return tensor; |
| | | 293 | | } |
| | | 294 | | |
| | | 295 | | private static bool TryAddTensor( |
| | | 296 | | Fixed3x3 first, |
| | | 297 | | Fixed3x3 second, |
| | | 298 | | out Fixed3x3 result) |
| | | 299 | | { |
| | 180 | 300 | | bool representable = Fixed64.TryAdd(first.M11, second.M11, out Fixed64 m11) |
| | 180 | 301 | | & Fixed64.TryAdd(first.M12, second.M12, out Fixed64 m12) |
| | 180 | 302 | | & Fixed64.TryAdd(first.M13, second.M13, out Fixed64 m13) |
| | 180 | 303 | | & Fixed64.TryAdd(first.M21, second.M21, out Fixed64 m21) |
| | 180 | 304 | | & Fixed64.TryAdd(first.M22, second.M22, out Fixed64 m22) |
| | 180 | 305 | | & Fixed64.TryAdd(first.M23, second.M23, out Fixed64 m23) |
| | 180 | 306 | | & Fixed64.TryAdd(first.M31, second.M31, out Fixed64 m31) |
| | 180 | 307 | | & Fixed64.TryAdd(first.M32, second.M32, out Fixed64 m32) |
| | 180 | 308 | | & Fixed64.TryAdd(first.M33, second.M33, out Fixed64 m33); |
| | 180 | 309 | | result = representable |
| | 180 | 310 | | ? new Fixed3x3( |
| | 180 | 311 | | m11, m12, m13, |
| | 180 | 312 | | m21, m22, m23, |
| | 180 | 313 | | m31, m32, m33) |
| | 180 | 314 | | : default; |
| | 180 | 315 | | return representable; |
| | | 316 | | } |
| | | 317 | | |
| | | 318 | | /// <inheritdoc/> |
| | | 319 | | public override Fixed64 GetFrontalArea(Vector3d direction) |
| | | 320 | | { |
| | 32 | 321 | | Fixed64 area = Fixed64.Zero; |
| | 192 | 322 | | for (int i = 0; i < _parts.Length; i++) |
| | 64 | 323 | | area += _partColliders[i].GetFrontalArea(direction); |
| | 32 | 324 | | return area; |
| | | 325 | | } |
| | | 326 | | |
| | | 327 | | /// <inheritdoc/> |
| | | 328 | | public override Vector3d ClosestPointOnSurface(Vector3d other) |
| | | 329 | | { |
| | 16 | 330 | | FixedPointAnchor anchor = |
| | 16 | 331 | | GetClosestSurfaceAnchor(other, out _); |
| | 16 | 332 | | if (anchor.TryGetPoint(out Vector3d point)) |
| | 15 | 333 | | return point; |
| | | 334 | | |
| | 1 | 335 | | throw new InvalidOperationException( |
| | 1 | 336 | | "The closest compound surface point is outside the representable coordinate domain."); |
| | | 337 | | } |
| | | 338 | | |
| | | 339 | | /// <inheritdoc/> |
| | | 340 | | public override Vector3d GetNormalAtPoint(Vector3d point) |
| | | 341 | | { |
| | 18 | 342 | | _ = GetClosestSurfaceAnchor( |
| | 18 | 343 | | point, |
| | 18 | 344 | | out Vector3d normal); |
| | 18 | 345 | | return normal; |
| | | 346 | | } |
| | | 347 | | |
| | | 348 | | internal override FixedPointAnchor GetClosestSurfaceAnchor( |
| | | 349 | | Vector3d point, |
| | | 350 | | out Vector3d normal) |
| | | 351 | | { |
| | 34 | 352 | | var reference = new FixedPointAnchor( |
| | 34 | 353 | | point, |
| | 34 | 354 | | FixedQuaternion.Identity, |
| | 34 | 355 | | Vector3d.Zero); |
| | 34 | 356 | | FixedPointAnchor closest = |
| | 34 | 357 | | _partColliders[0].GetClosestSurfaceAnchor( |
| | 34 | 358 | | point, |
| | 34 | 359 | | out normal); |
| | 112 | 360 | | for (int i = 1; i < _partColliders.Length; i++) |
| | | 361 | | { |
| | 22 | 362 | | FixedPointAnchor candidate = |
| | 22 | 363 | | _partColliders[i].GetClosestSurfaceAnchor( |
| | 22 | 364 | | point, |
| | 22 | 365 | | out Vector3d candidateNormal); |
| | 22 | 366 | | if (reference.CompareSquaredDistance( |
| | 22 | 367 | | candidate, |
| | 22 | 368 | | closest) >= 0) |
| | | 369 | | { |
| | | 370 | | continue; |
| | | 371 | | } |
| | | 372 | | |
| | 10 | 373 | | closest = candidate; |
| | 10 | 374 | | normal = candidateNormal; |
| | | 375 | | } |
| | | 376 | | |
| | 34 | 377 | | return closest; |
| | | 378 | | } |
| | | 379 | | |
| | | 380 | | /// <inheritdoc/> |
| | | 381 | | public override bool ColliderOverlapsRay(RaycastSegmentWorker worker, ref SwiftList<Vector3d> outputIntersectionPoin |
| | | 382 | | { |
| | 5 | 383 | | bool hit = false; |
| | 30 | 384 | | for (int i = 0; i < _parts.Length; i++) |
| | 10 | 385 | | hit |= _partColliders[i].ColliderOverlapsRay(worker, ref outputIntersectionPoints); |
| | 5 | 386 | | return hit; |
| | | 387 | | } |
| | | 388 | | |
| | | 389 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 390 | | internal bool ContainsPartCollider(LSCollider collider) => |
| | 12 | 391 | | ReferenceEquals(collider.CompoundOwner, this); |
| | | 392 | | |
| | | 393 | | private static LSCollider MaterializePartCollider(CompoundColliderPart part) |
| | | 394 | | { |
| | 287 | 395 | | LSCollider collider = part.Shape.CreateRuntimeCollider(); |
| | 287 | 396 | | collider.LocalOffset = part.LocalOffset; |
| | 287 | 397 | | collider.Material = part.ResolveMaterial(PhysicsMaterial.Default); |
| | 287 | 398 | | return collider; |
| | | 399 | | } |
| | | 400 | | |
| | | 401 | | /// <inheritdoc/> |
| | | 402 | | protected override void OnMaterialChanged() |
| | | 403 | | { |
| | 64 | 404 | | for (int i = 0; i < _parts.Length; i++) |
| | 21 | 405 | | _partColliders[i].Material = _parts[i].ResolveMaterial(Material); |
| | 11 | 406 | | } |
| | | 407 | | |
| | | 408 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 409 | | private static void ValidatePart(CompoundColliderPart part) => |
| | 288 | 410 | | SwiftThrowHelper.ThrowIfArgument(part.IsDefault, nameof(part), "Compound collider part cannot be default."); |
| | | 411 | | } |