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) <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-18 19:29:45 +01:00
parent ba6b35b3d9
commit 58df608550

View File

@ -80,22 +80,58 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
} }
if (MaterialDensityControlsPenetrationDepth) { if (MaterialDensityControlsPenetrationDepth) {
penDepthMultiplier /= PhysMaterial->Density; float SafeDensity = FMath::Max(PhysMaterial->Density, 0.001f);
penDepthMultiplier /= SafeDensity;
} }
if (MaterialRestitutionControlsRicochet) { 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; float dot = FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal) + 1.0f;
FVector cross = FVector::CrossProduct(Velocity.GetSafeNormal(), HitResult.Normal); 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 #ifdef WITH_EDITOR
if (DebugEnabled) { 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); DrawDebugLine(GetWorld(), start, HitResult.Location, DebugColor, false, DebugTrailTime, 0, DebugTrailWidth);
}; };
#endif #endif
@ -103,32 +139,43 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent); float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent);
FVector PenetrationVector = RandomStream.VRandCone(Velocity, penEnterSpread); FVector PenetrationVector = RandomStream.VRandCone(Velocity, penEnterSpread);
PenetrationVector = FMath::Lerp(PenetrationVector, -HitResult.Normal, FMath::Lerp(penNormalization, penNormalizationGrazing, GrazingAngle)); 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 PenetrationDepth = -FVector::DotProduct(PenetrationVector, HitResult.Normal) * PenetrationDistance;
float BlockTIme = 1.0f; float BlockTime = 1.0f;
if (PenetrationDistance > 0.0f) { if (PenetrationDistance > 0.0f) {
if (!neverPenetrate) { 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 //no pen
SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin); SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin);
float ricThreshold = 1.0f; 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)) { if (!neverRicochet && RandomStream.FRand() * ricThreshold < FMath::Lerp(RicochetProbability * ricProbMultiplier, RicochetProbabilityGrazing * ricProbMultiplier, GrazingAngle)) {
//bounce //bounce
FVector bounceAngle = flat * dot * (1.0f - ricFriction); FVector bounceAngle = flat * dot * (1.0f - ricFriction);
bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution; bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution;
bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceAngle.Size(); float bounceSize = bounceAngle.Size();
if (bounceSize > SMALL_NUMBER)
{
bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceSize;
NewVelocity = bounceAngle * Velocity.Size(); 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; Ricochet = true;
OwnerSafe = false; OwnerSafe = false;
} }
@ -139,7 +186,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
} }
else { else {
//penetration //penetration
float RemainingEnergy = FMath::Pow(1.0f - BlockTIme, 2.0f); float RemainingEnergy = FMath::Pow(1.0f - BlockTime, 2.0f);
SetActorLocation(exitLoc + exitNormal * CollisionMargin); SetActorLocation(exitLoc + exitNormal * CollisionMargin);
NewVelocity = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy)); NewVelocity = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy));
NewVelocity = FMath::Lerp(NewVelocity, Velocity.GetSafeNormal(), 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 #ifdef WITH_EDITOR
if (DebugEnabled) { 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); DrawDebugLine(GetWorld(), start, start + TraceDistance, Color.ToFColor(true), false, DebugTrailTime, 0, 0);
} }
} }