From 58df608550ca47c6c5916ce6b371b4c9f1f85660 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Wed, 18 Mar 2026 19:29:45 +0100 Subject: [PATCH] Fix penetration/ricochet bugs: division by zero, edge cases, incidence angle P0 - Division by zero fixes: - Clamp PhysMaterial->Density to min 0.001 before division - Clamp MuzzleVelocity averages to min 1.0 in all divisions - Clamp PhysMaterial->Restitution to [0, 1] P1 - Edge case guards: - Stop bullet immediately when velocity is near-zero (prevents NaN) - Handle near-zero cross product at very shallow grazing angles - Handle zero-length bounceAngle in ricochet calculation P2 - Improvements: - Fix typo: BlockTIme -> BlockTime - Add incidence angle factor to penetration depth: grazing shots penetrate less (5% at ~5deg) while head-on shots get full depth Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Source/EasyBallistics/Private/Trace.cpp | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp index 5fee6c4..173ece1 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp @@ -80,22 +80,58 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn } if (MaterialDensityControlsPenetrationDepth) { - penDepthMultiplier /= PhysMaterial->Density; + float SafeDensity = FMath::Max(PhysMaterial->Density, 0.001f); + penDepthMultiplier /= SafeDensity; } if (MaterialRestitutionControlsRicochet) { - RicochetRestitution *= PhysMaterial->Restitution; + RicochetRestitution *= FMath::Clamp(PhysMaterial->Restitution, 0.0f, 1.0f); } } + // Guard: if bullet has near-zero velocity, stop it immediately + if (Velocity.SizeSquared() < SMALL_NUMBER) + { + SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin); + FVector Impulse = Velocity * Mass * ImpulseMultiplier; + if (AddImpulse && HitResult.Component->IsSimulatingPhysics()) { + HitResult.Component->AddImpulseAtLocation(Impulse, HitResult.Location, HitResult.BoneName); + } + if (HasAuthority()) { + OnImpact(false, false, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), FVector::ZeroVector, Impulse, 0.0f, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult, fireEventID); + } else { + OnNetPredictedImpact(false, false, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), FVector::ZeroVector, Impulse, 0.0f, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult, fireEventID); + } + Velocity = FVector::ZeroVector; + Deactivate(); + return 0.0f; + } + float dot = FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal) + 1.0f; FVector cross = FVector::CrossProduct(Velocity.GetSafeNormal(), HitResult.Normal); - FVector flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross); + + // Guard: near-zero cross product at very shallow grazing angles + FVector flat; + if (cross.SizeSquared() < SMALL_NUMBER) + { + // Bullet nearly parallel to surface: project velocity onto surface plane + flat = FVector::VectorPlaneProject(Velocity.GetSafeNormal(), HitResult.Normal).GetSafeNormal(); + if (flat.IsNearlyZero()) + { + flat = FMath::Abs(HitResult.Normal.Z) < 0.9f + ? FVector::CrossProduct(HitResult.Normal, FVector::UpVector).GetSafeNormal() + : FVector::CrossProduct(HitResult.Normal, FVector::RightVector).GetSafeNormal(); + } + } + else + { + flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross); + } #ifdef WITH_EDITOR if (DebugEnabled) { - FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / MuzzleVelocityMax); + FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / FMath::Max(MuzzleVelocityMax, 1.0f)); DrawDebugLine(GetWorld(), start, HitResult.Location, DebugColor, false, DebugTrailTime, 0, DebugTrailWidth); }; #endif @@ -103,32 +139,43 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent); FVector PenetrationVector = RandomStream.VRandCone(Velocity, penEnterSpread); PenetrationVector = FMath::Lerp(PenetrationVector, -HitResult.Normal, FMath::Lerp(penNormalization, penNormalizationGrazing, GrazingAngle)); - float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f)), 2.0f) * penDepthMultiplier; + float AvgMuzzleVelocity = FMath::Max((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f, 1.0f); + // Incidence angle factor: head-on (dot=2) -> full depth, grazing (dot=0) -> minimal depth + float IncidenceFactor = FMath::Clamp(dot * 0.5f, 0.05f, 1.0f); + float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / AvgMuzzleVelocity), 2.0f) * penDepthMultiplier * IncidenceFactor; float PenetrationDepth = -FVector::DotProduct(PenetrationVector, HitResult.Normal) * PenetrationDistance; - float BlockTIme = 1.0f; + float BlockTime = 1.0f; if (PenetrationDistance > 0.0f) { if (!neverPenetrate) { - BlockTIme = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal); + BlockTime = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal); } } - if (BlockTIme >= 0.999999f) { + if (BlockTime >= 0.999999f) { //no pen SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin); float ricThreshold = 1.0f; - if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / MuzzleVelocityMax; }; + if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / FMath::Max(MuzzleVelocityMax, 1.0f); }; if (!neverRicochet && RandomStream.FRand() * ricThreshold < FMath::Lerp(RicochetProbability * ricProbMultiplier, RicochetProbabilityGrazing * ricProbMultiplier, GrazingAngle)) { //bounce FVector bounceAngle = flat * dot * (1.0f - ricFriction); bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution; - bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceAngle.Size(); - - NewVelocity = bounceAngle * Velocity.Size(); + float bounceSize = bounceAngle.Size(); + if (bounceSize > SMALL_NUMBER) + { + bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceSize; + NewVelocity = bounceAngle * Velocity.Size(); + } + else + { + // bounceAngle is zero (head-on + high friction + low restitution): reflect off normal + NewVelocity = RandomStream.VRandCone(HitResult.Normal, ricSpread) * Velocity.Size() * ricRestitution; + } Ricochet = true; OwnerSafe = false; } @@ -139,7 +186,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn } else { //penetration - float RemainingEnergy = FMath::Pow(1.0f - BlockTIme, 2.0f); + float RemainingEnergy = FMath::Pow(1.0f - BlockTime, 2.0f); SetActorLocation(exitLoc + exitNormal * CollisionMargin); NewVelocity = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy)); NewVelocity = FMath::Lerp(NewVelocity, Velocity.GetSafeNormal(), RemainingEnergy); @@ -189,7 +236,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn #ifdef WITH_EDITOR if (DebugEnabled) { - FLinearColor Color = GetDebugColor(Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f)); + FLinearColor Color = GetDebugColor(Velocity.Size() / FMath::Max((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f, 1.0f)); DrawDebugLine(GetWorld(), start, start + TraceDistance, Color.ToFColor(true), false, DebugTrailTime, 0, 0); } }