< Summary

Information
Class: Gravitas.Colliders.LSCompoundCollider
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/3D/LSCompoundCollider.cs
Line coverage
100%
Covered lines: 211
Uncovered lines: 0
Coverable lines: 211
Total lines: 411
Line coverage: 100%
Branch coverage
100%
Covered branches: 66
Total branches: 66
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/3D/LSCompoundCollider.cs

#LineLine coverage
 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
 8using FixedMathSharp;
 9using FixedMathSharp.Geometry;
 10using Gravitas.Materials;
 11using Gravitas.Queries;
 12using SwiftCollections;
 13using System;
 14using System.Runtime.CompilerServices;
 15
 16namespace 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>
 22public 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>
 15830    public LSCompoundCollider(params CompoundColliderPart[] parts)
 31    {
 15832        SwiftThrowHelper.ThrowIfNull(parts, nameof(parts));
 15833        SwiftThrowHelper.ThrowIfArgument(parts.Length == 0, nameof(parts), "Compound collider must contain at least one 
 34
 89035        for (int i = 0; i < parts.Length; i++)
 28836            ValidatePart(parts[i]);
 37
 15738        _parts = new CompoundColliderPart[parts.Length];
 15739        _partColliders = new LSCollider[parts.Length];
 15740        _massPointScratch = new ExactMassPoint3D[parts.Length];
 15741        _massWeightScratch = new ExactMassWeight[parts.Length];
 88842        for (int i = 0; i < parts.Length; i++)
 43        {
 28744            _parts[i] = parts[i];
 28745            _partColliders[i] = MaterializePartCollider(parts[i]);
 28746            _partColliders[i].ReserveCompoundPart(
 28747                this,
 28748                parts[i].LocalRotation,
 28749                parts[i].LocalScale);
 50        }
 15751    }
 52
 53    /// <inheritdoc/>
 49854    public override ColliderType Shape => ColliderType.Compound;
 55
 56    /// <inheritdoc/>
 6357    public override int Priority => ColliderSettings.GetPriority(Shape);
 58
 59    /// <inheritdoc/>
 60    public override Fixed64 ScaledRadius
 61    {
 62        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1663        get => HasCommittedShape
 1664            ? CanonicalCenteredProxyRadius
 1665            : ColliderCanonicalBounds
 1666                .GetCurrentCenteredProxyRadius(this);
 67    }
 68
 69    /// <summary>Gets the authored parts in stable source order.</summary>
 3670    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)]
 144176        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    {
 283        SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _parts.Length, nameof(index));
 284        return index;
 85    }
 86
 87    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 88    internal LSCollider GetPartCollider(int index)
 89    {
 104190        SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _parts.Length, nameof(index));
 104191        return _partColliders[index];
 92    }
 93
 94    private protected override void PrepareShape(in ColliderShapeSnapshot snapshot)
 95    {
 17096        Vector3d min = Vector3d.Zero;
 17097        Vector3d max = Vector3d.Zero;
 98
 95899        for (int i = 0; i < _parts.Length; i++)
 100        {
 313101            CompoundColliderPart part = _parts[i];
 313102            LSCollider partCollider = _partColliders[i];
 313103            partCollider.PrepareCompoundPart(
 313104                snapshot,
 313105                part.LocalRotation,
 313106                part.LocalScale,
 313107                PreparedContext);
 108
 309109            if (i == 0)
 110            {
 167111                min = partCollider.PreparedShapeBounds.Min;
 167112                max = partCollider.PreparedShapeBounds.Max;
 113            }
 114            else
 115            {
 142116                min = Vector3d.Min(min, partCollider.PreparedShapeBounds.Min);
 142117                max = Vector3d.Max(max, partCollider.PreparedShapeBounds.Max);
 118            }
 119        }
 120
 166121        _ = CalculatePreparedLocalMassPoint();
 165122        _ = CalculatePreparedMassPropertyWeight();
 165123        SetPreparedBounds(FixedBoundBox.FromMinMax(min, max));
 165124    }
 125
 126    private protected override void PublishShape()
 127    {
 165128        Fixed64 area = Fixed64.Zero;
 944129        for (int i = 0; i < _parts.Length; i++)
 130        {
 307131            CompoundColliderPart part = _parts[i];
 307132            _partColliders[i].PublishCompoundPart(
 307133                part.LocalRotation,
 307134                part.LocalScale,
 307135                PreparedContext);
 307136            area += _partColliders[i].Area;
 137        }
 138
 165139        Area = area;
 165140    }
 141
 142    internal override ExactMassPoint3D CalculateLocalMassPoint() =>
 366143        CalculateAggregateMassPoint(usePrepared: false);
 144
 145    internal override ExactMassPoint3D CalculatePreparedLocalMassPoint() =>
 301146        CalculateAggregateMassPoint(usePrepared: true);
 147
 148    private ExactMassPoint3D CalculateAggregateMassPoint(bool usePrepared)
 149    {
 667150        bool hasPositiveWeight = false;
 3614151        for (int i = 0; i < _partColliders.Length; i++)
 152        {
 1140153            LSCollider part = _partColliders[i];
 1140154            ExactMassWeight weight = usePrepared
 1140155                ? part.CalculatePreparedMassPropertyWeight()
 1140156                : part.CalculateMassPropertyWeight();
 1140157            _massPointScratch[i] = usePrepared
 1140158                ? part.CalculatePreparedLocalMassPoint()
 1140159                : part.CalculateLocalMassPoint();
 1140160            _massWeightScratch[i] = weight;
 1140161            hasPositiveWeight |= !weight.IsZero;
 162        }
 163
 667164        if (!hasPositiveWeight)
 165        {
 13166            int supportedPartCount = 0;
 56167            for (int i = 0; i < _massWeightScratch.Length; i++)
 168            {
 15169                if (_partColliders[i].SupportsMassProperties)
 170                {
 9171                    _massWeightScratch[i] = ExactMassWeight.One;
 9172                    supportedPartCount++;
 173                }
 174            }
 175
 13176            if (supportedPartCount == 0)
 177            {
 24178                for (int i = 0; i < _massWeightScratch.Length; i++)
 6179                    _massWeightScratch[i] = ExactMassWeight.One;
 180            }
 181        }
 182
 667183        if (!ExactMassPoint3D.TryGetWeightedAverage(
 667184                _massPointScratch,
 667185                _massWeightScratch,
 667186                out Vector3d center))
 187        {
 2188            throw new InvalidOperationException(
 2189                usePrepared
 2190                    ? "Prepared compound mass-property point is outside the Fixed64 coordinate domain."
 2191                    : "The compound collider's center of mass is outside the Fixed64 coordinate domain.");
 192        }
 665193        return ExactMassPoint3D.FromPoint(center);
 194    }
 195
 196    internal override ExactMassWeight CalculateMassPropertyWeight() =>
 1197        CalculateAggregateMassWeight(usePrepared: false);
 198
 199    internal override ExactMassWeight CalculatePreparedMassPropertyWeight() =>
 165200        CalculateAggregateMassWeight(usePrepared: true);
 201
 202    private ExactMassWeight CalculateAggregateMassWeight(bool usePrepared)
 203    {
 166204        ExactMassWeight totalWeight = ExactMassWeight.Zero;
 948205        for (int i = 0; i < _parts.Length; i++)
 206        {
 308207            ExactMassWeight weight = usePrepared
 308208                ? _partColliders[i].CalculatePreparedMassPropertyWeight()
 308209                : _partColliders[i].CalculateMassPropertyWeight();
 308210            totalWeight = totalWeight.Add(weight);
 211        }
 166212        return totalWeight;
 213    }
 214
 215    internal override Fixed3x3 CalculateCenterOfMassInertiaTensor(
 216        Fixed64 mass)
 217    {
 112218        Vector3d center = CalculateLocalCenterOfMassOffset();
 112219        ExactMassWeight totalWeight = ExactMassWeight.Zero;
 112220        int residualPartIndex = _parts.Length - 1;
 112221        int eligiblePartCount = 0;
 598222        for (int i = 0; i < _parts.Length; i++)
 223        {
 187224            LSCollider part = _partColliders[i];
 187225            if (!part.SupportsMassProperties)
 226            {
 1227                _massWeightScratch[i] = ExactMassWeight.Zero;
 1228                continue;
 229            }
 230
 186231            eligiblePartCount++;
 186232            ExactMassWeight weight = part.CalculateMassPropertyWeight();
 186233            _massWeightScratch[i] = weight;
 186234            totalWeight = totalWeight.Add(weight);
 186235            residualPartIndex = i;
 236        }
 237
 112238        if (eligiblePartCount == 0)
 239        {
 1240            throw new InvalidOperationException(
 1241                "Compound inertia requires at least one part with valid mass properties.");
 242        }
 243
 111244        ExactMassWeight cumulativeWeight = ExactMassWeight.Zero;
 111245        Fixed64 assignedMass = Fixed64.Zero;
 111246        Fixed3x3 tensor = Fixed3x3.Zero;
 247
 588248        for (int i = 0; i < _parts.Length; i++)
 249        {
 185250            LSCollider part = _partColliders[i];
 185251            ExactMassWeight weight = _massWeightScratch[i];
 185252            cumulativeWeight = cumulativeWeight.Add(weight);
 253            Fixed64 partMass;
 185254            if (i == residualPartIndex)
 255            {
 110256                partMass = mass - assignedMass;
 110257                assignedMass = mass;
 258            }
 259            else
 260            {
 75261                _ = cumulativeWeight.TryGetProportionalShare(
 75262                    mass,
 75263                    totalWeight,
 75264                    out Fixed64 cumulativeMass);
 75265                partMass = cumulativeMass - assignedMass;
 75266                assignedMass = cumulativeMass;
 267            }
 268
 185269            if (partMass == Fixed64.Zero)
 270                continue;
 271
 181272            ExactMassPoint3D partCenterOfMass =
 181273                part.CalculateLocalMassPoint();
 181274            Fixed3x3 partTensor =
 181275                part.CalculateCenterOfMassInertiaTensor(partMass);
 181276            partTensor = InertiaTensorMath.RotateToFrame(partTensor, part.CompoundLocalRotation);
 181277            if (!partCenterOfMass.TryAddParallelAxisTensor(
 181278                    partTensor,
 181279                    partMass,
 181280                    center,
 181281                    out Fixed3x3 contribution)
 181282                || !TryAddTensor(
 181283                    tensor,
 181284                    contribution,
 181285                    out tensor))
 286            {
 2287                throw new InvalidOperationException(
 2288                    "The compound collider's inertia tensor is outside the Fixed64 scalar domain.");
 289            }
 290        }
 291
 109292        return tensor;
 293    }
 294
 295    private static bool TryAddTensor(
 296        Fixed3x3 first,
 297        Fixed3x3 second,
 298        out Fixed3x3 result)
 299    {
 180300        bool representable = Fixed64.TryAdd(first.M11, second.M11, out Fixed64 m11)
 180301            & Fixed64.TryAdd(first.M12, second.M12, out Fixed64 m12)
 180302            & Fixed64.TryAdd(first.M13, second.M13, out Fixed64 m13)
 180303            & Fixed64.TryAdd(first.M21, second.M21, out Fixed64 m21)
 180304            & Fixed64.TryAdd(first.M22, second.M22, out Fixed64 m22)
 180305            & Fixed64.TryAdd(first.M23, second.M23, out Fixed64 m23)
 180306            & Fixed64.TryAdd(first.M31, second.M31, out Fixed64 m31)
 180307            & Fixed64.TryAdd(first.M32, second.M32, out Fixed64 m32)
 180308            & Fixed64.TryAdd(first.M33, second.M33, out Fixed64 m33);
 180309        result = representable
 180310            ? new Fixed3x3(
 180311                m11, m12, m13,
 180312                m21, m22, m23,
 180313                m31, m32, m33)
 180314            : default;
 180315        return representable;
 316    }
 317
 318    /// <inheritdoc/>
 319    public override Fixed64 GetFrontalArea(Vector3d direction)
 320    {
 32321        Fixed64 area = Fixed64.Zero;
 192322        for (int i = 0; i < _parts.Length; i++)
 64323            area += _partColliders[i].GetFrontalArea(direction);
 32324        return area;
 325    }
 326
 327    /// <inheritdoc/>
 328    public override Vector3d ClosestPointOnSurface(Vector3d other)
 329    {
 16330        FixedPointAnchor anchor =
 16331            GetClosestSurfaceAnchor(other, out _);
 16332        if (anchor.TryGetPoint(out Vector3d point))
 15333            return point;
 334
 1335        throw new InvalidOperationException(
 1336            "The closest compound surface point is outside the representable coordinate domain.");
 337    }
 338
 339    /// <inheritdoc/>
 340    public override Vector3d GetNormalAtPoint(Vector3d point)
 341    {
 18342        _ = GetClosestSurfaceAnchor(
 18343            point,
 18344            out Vector3d normal);
 18345        return normal;
 346    }
 347
 348    internal override FixedPointAnchor GetClosestSurfaceAnchor(
 349        Vector3d point,
 350        out Vector3d normal)
 351    {
 34352        var reference = new FixedPointAnchor(
 34353            point,
 34354            FixedQuaternion.Identity,
 34355            Vector3d.Zero);
 34356        FixedPointAnchor closest =
 34357            _partColliders[0].GetClosestSurfaceAnchor(
 34358                point,
 34359                out normal);
 112360        for (int i = 1; i < _partColliders.Length; i++)
 361        {
 22362            FixedPointAnchor candidate =
 22363                _partColliders[i].GetClosestSurfaceAnchor(
 22364                    point,
 22365                    out Vector3d candidateNormal);
 22366            if (reference.CompareSquaredDistance(
 22367                    candidate,
 22368                    closest) >= 0)
 369            {
 370                continue;
 371            }
 372
 10373            closest = candidate;
 10374            normal = candidateNormal;
 375        }
 376
 34377        return closest;
 378    }
 379
 380    /// <inheritdoc/>
 381    public override bool ColliderOverlapsRay(RaycastSegmentWorker worker, ref SwiftList<Vector3d> outputIntersectionPoin
 382    {
 5383        bool hit = false;
 30384        for (int i = 0; i < _parts.Length; i++)
 10385            hit |= _partColliders[i].ColliderOverlapsRay(worker, ref outputIntersectionPoints);
 5386        return hit;
 387    }
 388
 389    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 390    internal bool ContainsPartCollider(LSCollider collider) =>
 12391        ReferenceEquals(collider.CompoundOwner, this);
 392
 393    private static LSCollider MaterializePartCollider(CompoundColliderPart part)
 394    {
 287395        LSCollider collider = part.Shape.CreateRuntimeCollider();
 287396        collider.LocalOffset = part.LocalOffset;
 287397        collider.Material = part.ResolveMaterial(PhysicsMaterial.Default);
 287398        return collider;
 399    }
 400
 401    /// <inheritdoc/>
 402    protected override void OnMaterialChanged()
 403    {
 64404        for (int i = 0; i < _parts.Length; i++)
 21405            _partColliders[i].Material = _parts[i].ResolveMaterial(Material);
 11406    }
 407
 408    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 409    private static void ValidatePart(CompoundColliderPart part) =>
 288410        SwiftThrowHelper.ThrowIfArgument(part.IsDefault, nameof(part), "Compound collider part cannot be default.");
 411}