< Summary

Information
Class: Gravitas.CollisionHandling.ContactNormalImpulseResult3D
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Response/3D/ContactNormalImpulse3D.cs
Line coverage
100%
Covered lines: 19
Uncovered lines: 0
Coverable lines: 19
Total lines: 919
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Response/3D/ContactNormalImpulse3D.cs

#LineLine coverage
 1//=======================================================================
 2// ContactNormalImpulse3D.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 System.Runtime.CompilerServices;
 11
 12namespace Gravitas.CollisionHandling;
 13
 14/// <summary>
 15/// Side-effect-free 3D contact-normal impulse result for two participants.
 16/// </summary>
 17internal readonly struct ContactNormalImpulseResult3D
 18{
 19    public ContactNormalImpulseResult3D(
 20        Fixed64 normalVelocity,
 21        Fixed64 impulseScalar,
 22        Vector3d linearVelocityDeltaA,
 23        Vector3d angularVelocityDeltaA,
 24        Vector3d linearVelocityDeltaB,
 25        Vector3d angularVelocityDeltaB)
 769326        : this(
 769327            normalVelocity,
 769328            impulseScalar,
 769329            impulseScalar,
 769330            linearVelocityDeltaA,
 769331            angularVelocityDeltaA,
 769332            linearVelocityDeltaB,
 769333            angularVelocityDeltaB)
 34    {
 769335    }
 36
 37    public ContactNormalImpulseResult3D(
 38        Fixed64 normalVelocity,
 39        Fixed64 impulseScalar,
 40        Fixed64 appliedImpulseScalar,
 41        Vector3d linearVelocityDeltaA,
 42        Vector3d angularVelocityDeltaA,
 43        Vector3d linearVelocityDeltaB,
 44        Vector3d angularVelocityDeltaB,
 45        bool hasRepresentableNormalVelocity = true,
 46        bool hasRepresentableAppliedImpulse = true)
 47    {
 908248        NormalVelocity = normalVelocity;
 908249        ImpulseScalar = impulseScalar;
 908250        AppliedImpulseScalar = appliedImpulseScalar;
 908251        LinearVelocityDeltaA = linearVelocityDeltaA;
 908252        AngularVelocityDeltaA = angularVelocityDeltaA;
 908253        LinearVelocityDeltaB = linearVelocityDeltaB;
 908254        AngularVelocityDeltaB = angularVelocityDeltaB;
 908255        HasRepresentableNormalVelocity = hasRepresentableNormalVelocity;
 908256        HasRepresentableAppliedImpulse = hasRepresentableAppliedImpulse;
 908257    }
 58
 59    public Fixed64 NormalVelocity { get; }
 60
 61    public Fixed64 ImpulseScalar { get; }
 62
 63    public Fixed64 AppliedImpulseScalar { get; }
 64
 65    public bool HasRepresentableNormalVelocity { get; }
 66
 67    public bool HasRepresentableAppliedImpulse { get; }
 68
 69    public Vector3d LinearVelocityDeltaA { get; }
 70
 71    public Vector3d AngularVelocityDeltaA { get; }
 72
 73    public Vector3d LinearVelocityDeltaB { get; }
 74
 75    public Vector3d AngularVelocityDeltaB { get; }
 76}
 77
 78/// <summary>
 79/// Side-effect-free 3D velocity deltas for a normal response whose impulse
 80/// scalar does not need to be representable.
 81/// </summary>
 82internal readonly struct ContactNormalVelocityDeltaResult3D
 83{
 84    public ContactNormalVelocityDeltaResult3D(
 85        Fixed64 normalVelocity,
 86        Vector3d linearVelocityDeltaA,
 87        Vector3d angularVelocityDeltaA,
 88        Vector3d linearVelocityDeltaB,
 89        Vector3d angularVelocityDeltaB)
 90        : this(
 91            normalVelocity,
 92            linearVelocityDeltaA,
 93            angularVelocityDeltaA,
 94            linearVelocityDeltaB,
 95            angularVelocityDeltaB,
 96            normalVelocity < Fixed64.Zero,
 97            hasRepresentableNormalVelocity: true)
 98    {
 99    }
 100
 101    public ContactNormalVelocityDeltaResult3D(
 102        Fixed64 normalVelocity,
 103        Vector3d linearVelocityDeltaA,
 104        Vector3d angularVelocityDeltaA,
 105        Vector3d linearVelocityDeltaB,
 106        Vector3d angularVelocityDeltaB,
 107        bool isClosing,
 108        bool hasRepresentableNormalVelocity)
 109    {
 110        NormalVelocity = normalVelocity;
 111        LinearVelocityDeltaA = linearVelocityDeltaA;
 112        AngularVelocityDeltaA = angularVelocityDeltaA;
 113        LinearVelocityDeltaB = linearVelocityDeltaB;
 114        AngularVelocityDeltaB = angularVelocityDeltaB;
 115        IsClosing = isClosing;
 116        HasRepresentableNormalVelocity = hasRepresentableNormalVelocity;
 117    }
 118
 119    public Fixed64 NormalVelocity { get; }
 120
 121    public bool IsClosing { get; }
 122
 123    public bool HasRepresentableNormalVelocity { get; }
 124
 125    public Vector3d LinearVelocityDeltaA { get; }
 126
 127    public Vector3d AngularVelocityDeltaA { get; }
 128
 129    public Vector3d LinearVelocityDeltaB { get; }
 130
 131    public Vector3d AngularVelocityDeltaB { get; }
 132}
 133
 134internal readonly struct ContactEffectiveMassTerms3D
 135{
 136    internal ContactEffectiveMassTerms3D(
 137        Fixed64 linearA,
 138        Fixed64 linearB,
 139        Fixed64 angularA,
 140        Fixed64 angularB)
 141    {
 142        LinearA = linearA;
 143        LinearB = linearB;
 144        AngularA = angularA;
 145        AngularB = angularB;
 146    }
 147
 148    internal Fixed64 LinearA { get; }
 149
 150    internal Fixed64 LinearB { get; }
 151
 152    internal Fixed64 AngularA { get; }
 153
 154    internal Fixed64 AngularB { get; }
 155
 156    internal Fixed64 SaturatedSum =>
 157        LinearA + LinearB + AngularA + AngularB;
 158
 159    internal bool TryGetValue(out Fixed64 value)
 160    {
 161        bool resolved =
 162            Fixed64.TryAdd(LinearA, LinearB, out Fixed64 linear);
 163        resolved &= Fixed64.TryAdd(
 164            linear,
 165            AngularA,
 166            out Fixed64 first);
 167        resolved &= Fixed64.TryAdd(first, AngularB, out value);
 168        if (!resolved)
 169            value = default;
 170        return resolved;
 171    }
 172}
 173
 174/// <summary>
 175/// Calculates allocation-free 3D contact-point normal response without mutating either body.
 176/// </summary>
 177internal static class ContactNormalImpulse3D
 178{
 179    internal static bool TryCalculateVelocityDeltas(
 180        SolidBody? bodyA,
 181        Vector3d linearVelocityA,
 182        Vector3d angularVelocityA,
 183        Vector3d relativeContactPointA,
 184        SolidBody? bodyB,
 185        Vector3d linearVelocityB,
 186        Vector3d angularVelocityB,
 187        Vector3d relativeContactPointB,
 188        Vector3d normal,
 189        Fixed64 restitution,
 190        Fixed64 restitutionVelocityThreshold,
 191        out ContactNormalVelocityDeltaResult3D result)
 192    {
 193        result = default;
 194        if (!TryComputeNormalVelocity(
 195                linearVelocityA,
 196                angularVelocityA,
 197                relativeContactPointA,
 198                linearVelocityB,
 199                angularVelocityB,
 200                relativeContactPointB,
 201                normal,
 202                out Fixed64 normalVelocity))
 203        {
 204            return false;
 205        }
 206
 207        if (normalVelocity >= Fixed64.Zero)
 208        {
 209            result = ZeroVelocityDelta(normalVelocity);
 210            return true;
 211        }
 212
 213        if (!TryComputeDenominator(
 214                bodyA,
 215                relativeContactPointA,
 216                bodyB,
 217                relativeContactPointB,
 218                normal,
 219                out ContactEffectiveMassTerms3D denominator)
 220            || denominator.SaturatedSum <= Fixed64.Zero)
 221        {
 222            return false;
 223        }
 224
 225        Fixed64 appliedRestitution = normalVelocity < -restitutionVelocityThreshold
 226            ? restitution
 227            : Fixed64.Zero;
 228        Fixed64 responseFactor = -(Fixed64.One + appliedRestitution);
 229        bool linearAResolved = TryResolveVelocityDelta(
 230            bodyA?.ProjectLinearMotion(-normal) ?? Vector3d.Zero,
 231            normalVelocity,
 232            responseFactor,
 233            bodyA?.EffectiveInverseMass ?? Fixed64.Zero,
 234            denominator,
 235            out Vector3d linearVelocityDeltaA);
 236        bool angularAResolved = TryResolveAngularVelocityDelta(
 237                bodyA,
 238                relativeContactPointA,
 239                -normal,
 240                normalVelocity,
 241                responseFactor,
 242                denominator,
 243                out Vector3d angularVelocityDeltaA);
 244        bool linearBResolved = TryResolveVelocityDelta(
 245            bodyB?.ProjectLinearMotion(normal) ?? Vector3d.Zero,
 246            normalVelocity,
 247            responseFactor,
 248            bodyB?.EffectiveInverseMass ?? Fixed64.Zero,
 249            denominator,
 250            out Vector3d linearVelocityDeltaB);
 251        bool angularBResolved = TryResolveAngularVelocityDelta(
 252                bodyB,
 253                relativeContactPointB,
 254                normal,
 255                normalVelocity,
 256                responseFactor,
 257                denominator,
 258                out Vector3d angularVelocityDeltaB);
 259        if (!(linearAResolved
 260                & angularAResolved
 261                & linearBResolved
 262                & angularBResolved))
 263        {
 264            return false;
 265        }
 266
 267        result = new ContactNormalVelocityDeltaResult3D(
 268            normalVelocity,
 269            linearVelocityDeltaA,
 270            angularVelocityDeltaA,
 271            linearVelocityDeltaB,
 272            angularVelocityDeltaB);
 273        return true;
 274    }
 275
 276    internal static bool TryCalculateVelocityDeltasExact(
 277        SolidBody? bodyA,
 278        Vector3d linearVelocityA,
 279        Vector3d angularVelocityA,
 280        in ExactLever3D relativeContactPointA,
 281        SolidBody? bodyB,
 282        Vector3d linearVelocityB,
 283        Vector3d angularVelocityB,
 284        in ExactLever3D relativeContactPointB,
 285        Vector3d normal,
 286        Fixed64 restitution,
 287        Fixed64 restitutionVelocityThreshold,
 288        out ContactNormalVelocityDeltaResult3D result)
 289    {
 290        result = default;
 291        if (!ExactContactLever3D.TryGetNormalResponse(
 292                bodyA,
 293                linearVelocityA,
 294                angularVelocityA,
 295                relativeContactPointA,
 296                bodyB,
 297                linearVelocityB,
 298                angularVelocityB,
 299                relativeContactPointB,
 300                normal,
 301                restitution,
 302                restitutionVelocityThreshold,
 303                out ExactNormalResponse3D response))
 304        {
 305            return false;
 306        }
 307
 308        bool hasNormalVelocity =
 309            response.TryGetNormalVelocity(out Fixed64 normalVelocity);
 310        result = new ContactNormalVelocityDeltaResult3D(
 311            normalVelocity,
 312            response.FirstLinearVelocityDelta,
 313            response.FirstAngularVelocityDelta,
 314            response.SecondLinearVelocityDelta,
 315            response.SecondAngularVelocityDelta,
 316            response.IsClosing,
 317            hasNormalVelocity);
 318        return true;
 319    }
 320
 321    internal static ContactNormalImpulseResult3D CalculateAccumulatedDelta(
 322        SolidBody? bodyA,
 323        Vector3d linearVelocityA,
 324        Vector3d angularVelocityA,
 325        Vector3d relativeContactPointA,
 326        SolidBody? bodyB,
 327        Vector3d linearVelocityB,
 328        Vector3d angularVelocityB,
 329        Vector3d relativeContactPointB,
 330        Vector3d normal,
 331        Fixed64 restitution,
 332        Fixed64 restitutionVelocityThreshold,
 333        Fixed64 accumulatedImpulse,
 334        Fixed64 positiveImpulseScale,
 335        Fixed64 negativeImpulseScale)
 336    {
 337        _ = TryCalculateAccumulatedDelta(
 338            bodyA,
 339            linearVelocityA,
 340            angularVelocityA,
 341            relativeContactPointA,
 342            bodyB,
 343            linearVelocityB,
 344            angularVelocityB,
 345            relativeContactPointB,
 346            normal,
 347            restitution,
 348            restitutionVelocityThreshold,
 349            accumulatedImpulse,
 350            positiveImpulseScale,
 351            negativeImpulseScale,
 352            out ContactNormalImpulseResult3D result);
 353        return result;
 354    }
 355
 356    internal static bool TryCalculateAccumulatedDelta(
 357        SolidBody? bodyA,
 358        Vector3d linearVelocityA,
 359        Vector3d angularVelocityA,
 360        Vector3d relativeContactPointA,
 361        SolidBody? bodyB,
 362        Vector3d linearVelocityB,
 363        Vector3d angularVelocityB,
 364        Vector3d relativeContactPointB,
 365        Vector3d normal,
 366        Fixed64 restitution,
 367        Fixed64 restitutionVelocityThreshold,
 368        Fixed64 accumulatedImpulse,
 369        Fixed64 positiveImpulseScale,
 370        Fixed64 negativeImpulseScale,
 371        out ContactNormalImpulseResult3D result)
 372    {
 373        result = default;
 374        if (!TryComputeNormalVelocity(
 375                linearVelocityA,
 376                angularVelocityA,
 377                relativeContactPointA,
 378                linearVelocityB,
 379                angularVelocityB,
 380                relativeContactPointB,
 381                normal,
 382                out Fixed64 normalVelocity)
 383            || !TryComputeDenominator(
 384                bodyA,
 385                relativeContactPointA,
 386                bodyB,
 387                relativeContactPointB,
 388                normal,
 389                out ContactEffectiveMassTerms3D denominator))
 390        {
 391            return false;
 392        }
 393
 394        if (denominator.SaturatedSum <= Fixed64.Zero)
 395        {
 396            result = Zero(normalVelocity);
 397            return true;
 398        }
 399
 400        if (!TryCalculateAccumulatedImpulseDelta(
 401                normalVelocity,
 402                denominator,
 403                restitution,
 404                restitutionVelocityThreshold,
 405                accumulatedImpulse,
 406                positiveImpulseScale,
 407                negativeImpulseScale,
 408                out Fixed64 impulseScalar))
 409        {
 410            return false;
 411        }
 412
 413        if (impulseScalar == Fixed64.Zero)
 414        {
 415            result = Zero(normalVelocity);
 416            return true;
 417        }
 418
 419        Vector3d impulseB = normal * impulseScalar;
 420        Vector3d impulseA = -impulseB;
 421        bool linearAResolved = TryComputeLinearVelocityDelta(
 422            bodyA,
 423            impulseA,
 424            out Vector3d linearVelocityDeltaA);
 425        bool angularAResolved = TryComputeAngularVelocityDelta(
 426            bodyA,
 427            relativeContactPointA,
 428            impulseA,
 429            out Vector3d angularVelocityDeltaA);
 430        bool linearBResolved = TryComputeLinearVelocityDelta(
 431            bodyB,
 432            impulseB,
 433            out Vector3d linearVelocityDeltaB);
 434        bool angularBResolved = TryComputeAngularVelocityDelta(
 435            bodyB,
 436            relativeContactPointB,
 437            impulseB,
 438            out Vector3d angularVelocityDeltaB);
 439        if (!(linearAResolved
 440            & angularAResolved
 441            & linearBResolved
 442            & angularBResolved))
 443        {
 444            return false;
 445        }
 446
 447        result = new ContactNormalImpulseResult3D(
 448            normalVelocity,
 449            impulseScalar,
 450            linearVelocityDeltaA,
 451            angularVelocityDeltaA,
 452            linearVelocityDeltaB,
 453            angularVelocityDeltaB);
 454        return true;
 455    }
 456
 457    internal static bool TryCalculateAccumulatedDeltaExact(
 458        SolidBody? bodyA,
 459        Vector3d linearVelocityA,
 460        Vector3d angularVelocityA,
 461        in ExactLever3D relativeContactPointA,
 462        SolidBody? bodyB,
 463        Vector3d linearVelocityB,
 464        Vector3d angularVelocityB,
 465        in ExactLever3D relativeContactPointB,
 466        Vector3d normal,
 467        Fixed64 restitution,
 468        Fixed64 restitutionVelocityThreshold,
 469        Fixed64 accumulatedImpulse,
 470        Fixed64 positiveImpulseScale,
 471        Fixed64 negativeImpulseScale,
 472        out ContactNormalImpulseResult3D result)
 473    {
 474        result = default;
 475        if (!ExactContactLever3D.TryGetAccumulatedNormalResponse(
 476                bodyA,
 477                linearVelocityA,
 478                angularVelocityA,
 479                relativeContactPointA,
 480                bodyB,
 481                linearVelocityB,
 482                angularVelocityB,
 483                relativeContactPointB,
 484                normal,
 485                restitution,
 486                restitutionVelocityThreshold,
 487                accumulatedImpulse,
 488                positiveImpulseScale,
 489                negativeImpulseScale,
 490                out ExactNormalResponse3D response))
 491        {
 492            return false;
 493        }
 494
 495        bool hasNormalVelocity =
 496            response.TryGetNormalVelocity(out Fixed64 normalVelocity);
 497        bool hasAppliedImpulse =
 498            response.TryGetAppliedImpulse(out Fixed64 appliedImpulse);
 499        Fixed64 impulseScalar;
 500        if (response.TryGetAccumulatedImpulse(
 501                out Fixed64 newAccumulatedImpulse))
 502        {
 503            // Both values are nonnegative, so their difference is always in
 504            // [-Fixed64.MaxValue, Fixed64.MaxValue].
 505            impulseScalar = newAccumulatedImpulse - accumulatedImpulse;
 506        }
 507        else
 508        {
 509            impulseScalar = -accumulatedImpulse;
 510        }
 511
 512        result = new ContactNormalImpulseResult3D(
 513            normalVelocity,
 514            impulseScalar,
 515            appliedImpulse,
 516            response.FirstLinearVelocityDelta,
 517            response.FirstAngularVelocityDelta,
 518            response.SecondLinearVelocityDelta,
 519            response.SecondAngularVelocityDelta,
 520            hasNormalVelocity,
 521            hasAppliedImpulse);
 522        return true;
 523    }
 524
 525    private static bool TryCalculateAccumulatedImpulseDelta(
 526        Fixed64 normalVelocity,
 527        in ContactEffectiveMassTerms3D denominator,
 528        Fixed64 restitution,
 529        Fixed64 restitutionVelocityThreshold,
 530        Fixed64 accumulatedImpulse,
 531        Fixed64 positiveImpulseScale,
 532        Fixed64 negativeImpulseScale,
 533        out Fixed64 impulseDelta)
 534    {
 535        Fixed64 appliedRestitution =
 536            normalVelocity < -restitutionVelocityThreshold
 537                ? restitution
 538                : Fixed64.Zero;
 539        Fixed64 responseFactor = -(Fixed64.One + appliedRestitution);
 540        Fixed64 impulseScale = normalVelocity < Fixed64.Zero
 541            ? positiveImpulseScale
 542            : negativeImpulseScale;
 543        if (!Fixed64.TryMultiplyDivideBySum(
 544                normalVelocity,
 545                responseFactor,
 546                impulseScale,
 547                Fixed64.One,
 548                denominator.LinearA,
 549                denominator.LinearB,
 550                denominator.AngularA,
 551                denominator.AngularB,
 552                out Fixed64 scaledImpulse))
 553        {
 554            impulseDelta = default;
 555            return normalVelocity >= Fixed64.Zero
 556                && responseFactor <= Fixed64.Zero
 557                && impulseScale >= Fixed64.Zero
 558                && accumulatedImpulse >= Fixed64.Zero
 559                && Fixed64.TrySubtract(
 560                    Fixed64.Zero,
 561                    accumulatedImpulse,
 562                    out impulseDelta);
 563        }
 564
 565        if (!Fixed64.TryAdd(
 566                accumulatedImpulse,
 567                scaledImpulse,
 568                out Fixed64 accumulated))
 569        {
 570            impulseDelta = default;
 571            return false;
 572        }
 573
 574        return Fixed64.TrySubtract(
 575            FixedMath.Max(Fixed64.Zero, accumulated),
 576            accumulatedImpulse,
 577            out impulseDelta);
 578    }
 579
 580    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 581    private static Fixed64 GetConstrainedInverseMass(SolidBody? body, Vector3d axis) =>
 582        body?.GetConstrainedInverseMass(axis) ?? Fixed64.Zero;
 583
 584    internal static bool TryComputeNormalVelocity(
 585        Vector3d linearVelocityA,
 586        Vector3d angularVelocityA,
 587        Vector3d relativeContactPointA,
 588        Vector3d linearVelocityB,
 589        Vector3d angularVelocityB,
 590        Vector3d relativeContactPointB,
 591        Vector3d normal,
 592        out Fixed64 normalVelocity)
 593    {
 594        if (ContactResponseArithmetic3D.CanUseFastPointVelocity(
 595                linearVelocityA,
 596                angularVelocityA,
 597                relativeContactPointA,
 598                linearVelocityB,
 599                angularVelocityB,
 600                relativeContactPointB,
 601                normal))
 602        {
 603            Vector3d fastPointVelocityA = linearVelocityA
 604                + Vector3d.Cross(
 605                    angularVelocityA,
 606                    relativeContactPointA);
 607            Vector3d fastPointVelocityB = linearVelocityB
 608                + Vector3d.Cross(
 609                    angularVelocityB,
 610                    relativeContactPointB);
 611            normalVelocity = Vector3d.Dot(
 612                fastPointVelocityB - fastPointVelocityA,
 613                normal);
 614            return true;
 615        }
 616
 617        bool resolved = ContactResponseArithmetic3D.TryCross(
 618                angularVelocityA,
 619                relativeContactPointA,
 620                out Vector3d angularA)
 621            & ContactResponseArithmetic3D.TryCross(
 622                angularVelocityB,
 623                relativeContactPointB,
 624                out Vector3d angularB);
 625        if (!resolved
 626            || !Vector3d.TryAdd(
 627                linearVelocityA,
 628                angularA,
 629                out Vector3d pointVelocityA)
 630            || !Vector3d.TryAdd(
 631                linearVelocityB,
 632                angularB,
 633                out Vector3d pointVelocityB)
 634            || !Vector3d.TrySubtract(
 635                pointVelocityB,
 636                pointVelocityA,
 637                out Vector3d relativeVelocity))
 638        {
 639            normalVelocity = default;
 640            return false;
 641        }
 642
 643        return ContactResponseArithmetic3D.TryDot(
 644            relativeVelocity,
 645            normal,
 646            out normalVelocity);
 647    }
 648
 649    private static bool TryComputeDenominator(
 650        SolidBody? bodyA,
 651        Vector3d relativeContactPointA,
 652        SolidBody? bodyB,
 653        Vector3d relativeContactPointB,
 654        Vector3d normal,
 655        out ContactEffectiveMassTerms3D denominator)
 656    {
 657        denominator = default;
 658        bool angularAResolved = TryComputeAngularDenominator(
 659            bodyA,
 660            relativeContactPointA,
 661            normal,
 662            out Fixed64 angularA);
 663        bool angularBResolved = TryComputeAngularDenominator(
 664            bodyB,
 665            relativeContactPointB,
 666            normal,
 667            out Fixed64 angularB);
 668        if (!(angularAResolved & angularBResolved))
 669        {
 670            return false;
 671        }
 672
 673        denominator = new ContactEffectiveMassTerms3D(
 674            GetConstrainedInverseMass(bodyA, normal),
 675            GetConstrainedInverseMass(bodyB, normal),
 676            angularA,
 677            angularB);
 678        return true;
 679    }
 680
 681    internal static bool TryComputeAngularDenominator(
 682        SolidBody? body,
 683        Vector3d relativeContactPoint,
 684        Vector3d axis,
 685        out Fixed64 denominator)
 686    {
 687        denominator = Fixed64.Zero;
 688        if (body?.CanRotate != true)
 689            return true;
 690
 691        Fixed3x3 inverseInertia =
 692            body.GetConstrainedInverseInertiaTensor();
 693        if (ContactResponseArithmetic3D.CanUseFastAngularResponse(
 694                relativeContactPoint,
 695                axis,
 696                inverseInertia))
 697        {
 698            Vector3d fastTorqueAxis =
 699                Vector3d.Cross(relativeContactPoint, axis);
 700            Vector3d fastAngularVelocityDelta =
 701                Fixed3x3.TransformDirection(
 702                    inverseInertia,
 703                    fastTorqueAxis);
 704            Vector3d fastAngular = Vector3d.Cross(
 705                fastAngularVelocityDelta,
 706                relativeContactPoint);
 707            denominator = Vector3d.Dot(fastAngular, axis);
 708            bool responsePreserved =
 709                ContactResponseArithmetic3D.PreservesNonzeroCrossProduct(
 710                    relativeContactPoint,
 711                    axis,
 712                    fastTorqueAxis);
 713            responsePreserved &=
 714                ContactResponseArithmetic3D
 715                .PreservesNonzeroTransformDirection(
 716                    inverseInertia,
 717                    fastTorqueAxis,
 718                    fastAngularVelocityDelta);
 719            responsePreserved &=
 720                ContactResponseArithmetic3D
 721                .PreservesNonzeroCrossProduct(
 722                    fastAngularVelocityDelta,
 723                    relativeContactPoint,
 724                    fastAngular);
 725            responsePreserved &=
 726                ContactResponseArithmetic3D.PreservesNonzeroDotProduct(
 727                    fastAngular,
 728                    axis,
 729                    denominator);
 730            if (!responsePreserved)
 731            {
 732                denominator = default;
 733                return false;
 734            }
 735
 736            denominator = FixedMath.Max(denominator, Fixed64.Zero);
 737            return true;
 738        }
 739
 740        if (!ContactResponseArithmetic3D.TryCross(
 741                relativeContactPoint,
 742                axis,
 743                out Vector3d torqueAxis)
 744            || !ContactResponseArithmetic3D.TryTransformDirection(
 745                inverseInertia,
 746                torqueAxis,
 747                out Vector3d angularVelocityDelta)
 748            || !ContactResponseArithmetic3D.TryCross(
 749                angularVelocityDelta,
 750                relativeContactPoint,
 751                out Vector3d angular)
 752            || !ContactResponseArithmetic3D.TryDot(
 753                angular,
 754                axis,
 755                out denominator))
 756        {
 757            denominator = default;
 758            return false;
 759        }
 760
 761        denominator = FixedMath.Max(denominator, Fixed64.Zero);
 762        return true;
 763    }
 764
 765    internal static bool TryComputeLinearVelocityDelta(
 766        SolidBody? body,
 767        Vector3d impulse,
 768        out Vector3d velocityDelta)
 769    {
 770        if (body != null)
 771            impulse = body.ProjectLinearMotion(impulse);
 772
 773        if (!ContactResponseArithmetic3D.TryScale(
 774                impulse,
 775                body?.EffectiveInverseMass ?? Fixed64.Zero,
 776                out velocityDelta))
 777        {
 778            return false;
 779        }
 780
 781        return true;
 782    }
 783
 784    internal static bool TryComputeAngularVelocityDelta(
 785        SolidBody? body,
 786        Vector3d relativeContactPoint,
 787        Vector3d impulse,
 788        out Vector3d velocityDelta)
 789    {
 790        velocityDelta = Vector3d.Zero;
 791        if (body?.CanRotate != true)
 792            return true;
 793
 794        Fixed3x3 inverseInertia =
 795            body.GetConstrainedInverseInertiaTensor();
 796        if (ContactResponseArithmetic3D.CanUseFastAngularResponse(
 797                relativeContactPoint,
 798                impulse,
 799                inverseInertia))
 800        {
 801            if (!ContactResponseArithmetic3D.TryCross(
 802                    relativeContactPoint,
 803                    impulse,
 804                    out Vector3d torqueAxis))
 805                return false;
 806
 807            velocityDelta = Fixed3x3.TransformDirection(
 808                inverseInertia,
 809                torqueAxis);
 810            return ContactResponseArithmetic3D
 811                .PreservesNonzeroTransformDirection(
 812                    inverseInertia,
 813                    torqueAxis,
 814                    velocityDelta);
 815        }
 816
 817        bool torqueResolved = ContactResponseArithmetic3D.TryCross(
 818                relativeContactPoint,
 819                impulse,
 820                out Vector3d checkedTorqueAxis);
 821        bool transformResolved =
 822            ContactResponseArithmetic3D.TryTransformDirection(
 823                inverseInertia,
 824                checkedTorqueAxis,
 825                out velocityDelta);
 826        return torqueResolved & transformResolved;
 827    }
 828
 829    private static bool TryResolveAngularVelocityDelta(
 830        SolidBody? body,
 831        Vector3d relativeContactPoint,
 832        Vector3d signedNormal,
 833        Fixed64 normalVelocity,
 834        Fixed64 responseFactor,
 835        in ContactEffectiveMassTerms3D denominator,
 836        out Vector3d velocityDelta)
 837    {
 838        velocityDelta = Vector3d.Zero;
 839        if (body?.CanRotate != true)
 840            return true;
 841
 842        bool responseResolved = TryComputeAngularVelocityDelta(
 843            body,
 844            relativeContactPoint,
 845            signedNormal,
 846            out Vector3d response);
 847        bool deltaResolved = TryResolveVelocityDelta(
 848            response,
 849            normalVelocity,
 850            responseFactor,
 851            Fixed64.One,
 852            denominator,
 853            out velocityDelta);
 854        return responseResolved & deltaResolved;
 855    }
 856
 857    private static bool TryResolveVelocityDelta(
 858        Vector3d response,
 859        Fixed64 firstMultiplier,
 860        Fixed64 secondMultiplier,
 861        Fixed64 thirdMultiplier,
 862        in ContactEffectiveMassTerms3D denominator,
 863        out Vector3d velocityDelta)
 864    {
 865        bool xResolved = Fixed64.TryMultiplyDivideBySum(
 866            response.X,
 867            firstMultiplier,
 868            secondMultiplier,
 869            thirdMultiplier,
 870            denominator.LinearA,
 871            denominator.LinearB,
 872            denominator.AngularA,
 873            denominator.AngularB,
 874            out Fixed64 x);
 875        bool yResolved = Fixed64.TryMultiplyDivideBySum(
 876            response.Y,
 877            firstMultiplier,
 878            secondMultiplier,
 879            thirdMultiplier,
 880            denominator.LinearA,
 881            denominator.LinearB,
 882            denominator.AngularA,
 883            denominator.AngularB,
 884            out Fixed64 y);
 885        bool zResolved = Fixed64.TryMultiplyDivideBySum(
 886            response.Z,
 887            firstMultiplier,
 888            secondMultiplier,
 889            thirdMultiplier,
 890            denominator.LinearA,
 891            denominator.LinearB,
 892            denominator.AngularA,
 893            denominator.AngularB,
 894            out Fixed64 z);
 895        velocityDelta = xResolved & yResolved & zResolved
 896            ? new Vector3d(x, y, z)
 897            : default;
 898        return xResolved & yResolved & zResolved;
 899    }
 900
 901    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 902    private static ContactNormalImpulseResult3D Zero(Fixed64 normalVelocity) =>
 903        new(
 904            normalVelocity,
 905            Fixed64.Zero,
 906            Vector3d.Zero,
 907            Vector3d.Zero,
 908            Vector3d.Zero,
 909            Vector3d.Zero);
 910
 911    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 912    private static ContactNormalVelocityDeltaResult3D ZeroVelocityDelta(Fixed64 normalVelocity) =>
 913        new(
 914            normalVelocity,
 915            Vector3d.Zero,
 916            Vector3d.Zero,
 917            Vector3d.Zero,
 918            Vector3d.Zero);
 919}