< Summary

Information
Class: Gravitas.Materials.PhysicsMaterial
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Materials/PhysicsMaterial.cs
Line coverage
100%
Covered lines: 87
Uncovered lines: 0
Coverable lines: 87
Total lines: 252
Line coverage: 100%
Branch coverage
100%
Covered branches: 42
Total branches: 42
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Default()100%11100%
get_Frictionless()100%11100%
get_Bouncy()100%11100%
.ctor(...)100%66100%
CombineFriction(...)100%11100%
CombineRestitution(...)100%11100%
CombineScalar(...)100%66100%
ResolveDominantPolicy(...)100%22100%
Equals(...)100%88100%
Equals(...)100%22100%
GetHashCode()100%11100%
op_Equality(...)100%11100%
op_Inequality(...)100%11100%
ValidateFriction(...)100%22100%
ValidateCombine(...)100%44100%
GeometricMean(...)100%66100%
GetPolicyPriority(...)100%66100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Materials/PhysicsMaterial.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsMaterial.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 MemoryPack;
 10using System;
 11using System.Runtime.CompilerServices;
 12using System.Text.Json.Serialization;
 13
 14namespace Gravitas.Materials;
 15
 16/// <summary>
 17/// Deterministic surface material coefficients used by contact response.
 18/// </summary>
 19/// <remarks>
 20/// Static and dynamic friction are non-negative Coulomb coefficients.
 21/// Restitution is the bounce coefficient in the inclusive range [0, 1].
 22/// Dynamic friction cannot exceed static friction.
 23/// </remarks>
 24[Serializable]
 25[MemoryPackable]
 26public readonly partial struct PhysicsMaterial : IEquatable<PhysicsMaterial>
 27{
 28    /// <summary>
 29    /// The release default surface: unit friction, half restitution, geometric
 30    /// friction combine, and minimum restitution combine.
 31    /// </summary>
 32    [JsonIgnore]
 33    [MemoryPackIgnore]
 34    public static PhysicsMaterial Default =>
 8278935        new(Fixed64.One, Fixed64.One, Fixed64.Half);
 36
 37    /// <summary>
 38    /// A surface with no contact friction and no restitution.
 39    /// </summary>
 40    [JsonIgnore]
 41    [MemoryPackIgnore]
 42    public static PhysicsMaterial Frictionless =>
 10043        new(Fixed64.Zero, Fixed64.Zero, Fixed64.Zero);
 44
 45    /// <summary>
 46    /// A high-restitution surface that keeps the default friction values.
 47    /// </summary>
 48    [JsonIgnore]
 49    [MemoryPackIgnore]
 50    public static PhysicsMaterial Bouncy =>
 1051        new(Fixed64.One, Fixed64.One, Fixed64.One);
 52
 53    /// <summary>
 54    /// Creates a deterministic surface material.
 55    /// </summary>
 56    [JsonConstructor]
 57    public PhysicsMaterial(
 58        Fixed64 staticFriction,
 59        Fixed64 dynamicFriction,
 60        Fixed64 restitution,
 61        PhysicsMaterialCombine frictionCombine = PhysicsMaterialCombine.GeometricMean,
 62        PhysicsMaterialCombine restitutionCombine = PhysicsMaterialCombine.Minimum)
 63    {
 8384564        ValidateFriction(staticFriction, nameof(staticFriction));
 8384465        ValidateFriction(dynamicFriction, nameof(dynamicFriction));
 8384366        if (dynamicFriction > staticFriction)
 167            throw new ArgumentOutOfRangeException(
 168                nameof(dynamicFriction),
 169                dynamicFriction,
 170                "Dynamic friction cannot exceed static friction.");
 8384271        if (restitution < Fixed64.Zero || restitution > Fixed64.One)
 272            throw new ArgumentOutOfRangeException(
 273                nameof(restitution),
 274                restitution,
 275                "Restitution must be between zero and one inclusive.");
 8384076        ValidateCombine(frictionCombine, nameof(frictionCombine));
 8383877        ValidateCombine(restitutionCombine, nameof(restitutionCombine));
 78
 8383779        StaticFriction = staticFriction;
 8383780        DynamicFriction = dynamicFriction;
 8383781        Restitution = restitution;
 8383782        FrictionCombine = frictionCombine;
 8383783        RestitutionCombine = restitutionCombine;
 8383784    }
 85
 86    /// <summary>
 87    /// Gets the friction coefficient used while tangential contact motion can be
 88    /// fully resisted by Coulomb static friction.
 89    /// </summary>
 90    public Fixed64 StaticFriction { get; }
 91
 92    /// <summary>
 93    /// Gets the friction coefficient used after tangential motion exceeds the
 94    /// static friction limit.
 95    /// </summary>
 96    public Fixed64 DynamicFriction { get; }
 97
 98    /// <summary>
 99    /// Gets the restitution coefficient. Zero removes closing velocity without
 100    /// bounce; one is fully elastic before the context restitution threshold is
 101    /// applied.
 102    /// </summary>
 103    public Fixed64 Restitution { get; }
 104
 105    /// <summary>
 106    /// Gets this surface's preferred combine policy for static and dynamic
 107    /// friction coefficients.
 108    /// </summary>
 109    public PhysicsMaterialCombine FrictionCombine { get; }
 110
 111    /// <summary>
 112    /// Gets this surface's preferred combine policy for restitution.
 113    /// </summary>
 114    public PhysicsMaterialCombine RestitutionCombine { get; }
 115
 116    /// <summary>
 117    /// Resolves effective static and dynamic friction coefficients for a pair of
 118    /// contacting surfaces.
 119    /// </summary>
 120    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 121    public static void CombineFriction(
 122        PhysicsMaterial left,
 123        PhysicsMaterial right,
 124        out Fixed64 staticFriction,
 125        out Fixed64 dynamicFriction)
 126    {
 10208127        PhysicsMaterialCombine policy = ResolveDominantPolicy(left.FrictionCombine, right.FrictionCombine);
 10208128        staticFriction = CombineScalar(left.StaticFriction, right.StaticFriction, policy);
 10208129        dynamicFriction = CombineScalar(left.DynamicFriction, right.DynamicFriction, policy);
 10208130    }
 131
 132    /// <summary>
 133    /// Resolves the effective restitution coefficient for a pair of contacting
 134    /// surfaces.
 135    /// </summary>
 136    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 137    public static Fixed64 CombineRestitution(PhysicsMaterial left, PhysicsMaterial right)
 138    {
 10851139        PhysicsMaterialCombine policy = ResolveDominantPolicy(left.RestitutionCombine, right.RestitutionCombine);
 10851140        return FixedMath.Clamp(
 10851141            CombineScalar(left.Restitution, right.Restitution, policy),
 10851142            Fixed64.Zero,
 10851143            Fixed64.One);
 144    }
 145
 146    /// <summary>
 147    /// Combines two scalar coefficients with one explicit deterministic policy.
 148    /// </summary>
 149    public static Fixed64 CombineScalar(
 150        Fixed64 left,
 151        Fixed64 right,
 152        PhysicsMaterialCombine policy)
 153    {
 31285154        return policy switch
 31285155        {
 10847156            PhysicsMaterialCombine.Minimum => FixedMath.Min(left, right),
 8157            PhysicsMaterialCombine.Maximum => FixedMath.Max(left, right),
 6158            PhysicsMaterialCombine.Average => FixedMath.Midpoint(left, right),
 1159            PhysicsMaterialCombine.Multiply => left * right,
 20422160            PhysicsMaterialCombine.GeometricMean => GeometricMean(left, right),
 1161            _ => throw new ArgumentOutOfRangeException(
 1162                nameof(policy),
 1163                policy,
 1164                "Unsupported physics material combine policy.")
 31285165        };
 166    }
 167
 168    /// <summary>
 169    /// Resolves the contact policy when two surfaces specify different combine
 170    /// policies. The order is deterministic and independent of collider order.
 171    /// </summary>
 172    public static PhysicsMaterialCombine ResolveDominantPolicy(
 173        PhysicsMaterialCombine left,
 174        PhysicsMaterialCombine right)
 175    {
 21071176        int leftPriority = GetPolicyPriority(left, nameof(left));
 21070177        int rightPriority = GetPolicyPriority(right, nameof(right));
 21069178        return leftPriority >= rightPriority ? left : right;
 179    }
 180
 181    /// <inheritdoc/>
 182    public bool Equals(PhysicsMaterial other) =>
 2373183        StaticFriction == other.StaticFriction
 2373184        && DynamicFriction == other.DynamicFriction
 2373185        && Restitution == other.Restitution
 2373186        && FrictionCombine == other.FrictionCombine
 2373187        && RestitutionCombine == other.RestitutionCombine;
 188
 189    /// <inheritdoc/>
 190    public override bool Equals(object? obj) =>
 72191        obj is PhysicsMaterial other && Equals(other);
 192
 193    /// <inheritdoc/>
 194    public override int GetHashCode()
 195    {
 196        unchecked
 197        {
 6198            int hash = StaticFriction.GetHashCode();
 6199            hash = (hash * 397) ^ DynamicFriction.GetHashCode();
 6200            hash = (hash * 397) ^ Restitution.GetHashCode();
 6201            hash = (hash * 397) ^ FrictionCombine.GetHashCode();
 6202            hash = (hash * 397) ^ RestitutionCombine.GetHashCode();
 6203            return hash;
 204        }
 205    }
 206
 207    /// <summary>Determines whether two materials have equal coefficients and combine policies.</summary>
 208    public static bool operator ==(PhysicsMaterial left, PhysicsMaterial right) =>
 2180209        left.Equals(right);
 210
 211    /// <summary>Determines whether two materials differ in any coefficient or combine policy.</summary>
 212    public static bool operator !=(PhysicsMaterial left, PhysicsMaterial right) =>
 17213        !left.Equals(right);
 214
 215    private static void ValidateFriction(Fixed64 value, string paramName)
 216    {
 167689217        if (value < Fixed64.Zero)
 2218            throw new ArgumentOutOfRangeException(paramName, value, "Friction cannot be negative.");
 167687219    }
 220
 221    private static void ValidateCombine(PhysicsMaterialCombine value, string paramName)
 222    {
 167678223        if (value < PhysicsMaterialCombine.Minimum || value > PhysicsMaterialCombine.GeometricMean)
 3224            throw new ArgumentOutOfRangeException(
 3225                paramName,
 3226                value,
 3227                "Unsupported physics material combine policy.");
 167675228    }
 229
 230    private static Fixed64 GeometricMean(Fixed64 left, Fixed64 right)
 231    {
 20422232        if (left <= Fixed64.Zero || right <= Fixed64.Zero)
 112233            return Fixed64.Zero;
 20310234        return left == right
 20310235            ? left
 20310236            : FixedMath.Sqrt(left) * FixedMath.Sqrt(right);
 237    }
 238
 239    private static int GetPolicyPriority(PhysicsMaterialCombine policy, string paramName) =>
 42141240        policy switch
 42141241        {
 5242            PhysicsMaterialCombine.Average => 0,
 21702243            PhysicsMaterialCombine.Minimum => 1,
 20418244            PhysicsMaterialCombine.GeometricMean => 2,
 4245            PhysicsMaterialCombine.Multiply => 3,
 10246            PhysicsMaterialCombine.Maximum => 4,
 2247            _ => throw new ArgumentOutOfRangeException(
 2248                paramName,
 2249                policy,
 2250                "Unsupported physics material combine policy.")
 42141251        };
 252}