Files
BallisticsDocs/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp
T
2025-07-04 02:33:40 -07:00

699 lines
25 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2016 Mookie. All Rights Reserved.
#include "EBMathematicalBallistics.h"
#include "EBUnitConversions.h"
#include "Engine/Engine.h"
#include "Math/UnrealMathUtility.h"
float UEBMathematicalBallistics::CalculatePenetrationDepth(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float ImpactAngleDegrees)
{
// SANITY CHECK: Validate input parameters
if (VelocityMPS <= 0.0f || !FMath::IsFinite(VelocityMPS) || VelocityMPS > 10000.0f) // Max 10 km/s
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid velocity %.3f m/s for penetration calculation"), VelocityMPS);
return 0.0f;
}
if (!FMath::IsFinite(ImpactAngleDegrees) || ImpactAngleDegrees < 0.0f || ImpactAngleDegrees > 90.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid impact angle %.1f degrees, clamping to valid range"), ImpactAngleDegrees);
ImpactAngleDegrees = FMath::Clamp(ImpactAngleDegrees, 0.0f, 90.0f);
}
// SANITY CHECK: Validate material properties
if (MaterialProps.DensityGPerCm3 <= 0.0f || !FMath::IsFinite(MaterialProps.DensityGPerCm3))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid material density %.3f g/cm³"), MaterialProps.DensityGPerCm3);
return 0.0f;
}
if (MaterialProps.YieldStrengthMPa <= 0.0f || !FMath::IsFinite(MaterialProps.YieldStrengthMPa))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid yield strength %.1f MPa"), MaterialProps.YieldStrengthMPa);
return 0.0f;
}
// Calculate kinetic energy in joules
float KineticEnergy = CalculateKineticEnergy(BulletProps, VelocityMPS);
// SANITY CHECK: Validate kinetic energy
if (KineticEnergy <= 0.0f || !FMath::IsFinite(KineticEnergy))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid kinetic energy %.3f J"), KineticEnergy);
return 0.0f;
}
// Calculate sectional density in proper SI units (kg/m²)
float SectionalDensity = BulletProps.GetSectionalDensityKgPerM2();
// SANITY CHECK: Validate sectional density
if (SectionalDensity <= 0.0f || !FMath::IsFinite(SectionalDensity))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid sectional density %.6f kg/m²"), SectionalDensity);
return 0.0f;
}
// Calculate hardness ratio
float HardnessRatio = CalculateHardnessRatio(BulletProps.BulletHardness, MaterialProps.MaterialHardness);
// Calculate angle factor (normal impact = 1.0, grazing = 0.0)
float AngleFactor = CalculateAngleFactor(ImpactAngleDegrees);
// Calculate shape factor based on bullet type
float ShapeFactor = CalculateShapeFactorFromBulletType(BulletProps.BulletType);
// Modified Taylor-Hopkinson equation for penetration depth
// P = (K * E * SD * HF * AF * SF) / (ρ * σ)
// Where: P = penetration depth (cm), K = constant, E = kinetic energy (J), SD = sectional density (kg/m²)
// HF = hardness factor, AF = angle factor, SF = shape factor, ρ = density (g/cm³), σ = yield strength (MPa)
// Unit conversion factors for proper dimensional analysis:
// KineticEnergy is in Joules (kg⋅m²/s²)
// SectionalDensity is in kg/m²
// DensityGPerCm3 needs conversion: g/cm³ = 1000 kg/m³
// YieldStrengthMPa is in MPa = 10⁶ Pa = 10⁶ N/m²
float DensityKgPerM3 = FEBUnitConversions::GPerCM3ToKGPerM3(MaterialProps.DensityGPerCm3);
float YieldStrengthPa = FEBUnitConversions::MPaToPa(MaterialProps.YieldStrengthMPa);
// SANITY CHECK: Validate converted values
if (DensityKgPerM3 <= 0.0f || !FMath::IsFinite(DensityKgPerM3) ||
YieldStrengthPa <= 0.0f || !FMath::IsFinite(YieldStrengthPa))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid converted material properties"));
return 0.0f;
}
// Empirical constant adjusted for proper units (results in meters)
float Constant = 2.0f; // Adjusted for dimensional consistency
// SANITY CHECK: Prevent division by zero and validate all factors
float Denominator = DensityKgPerM3 * YieldStrengthPa;
if (Denominator <= 0.0f || !FMath::IsFinite(Denominator))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid denominator in penetration calculation"));
return 0.0f;
}
float Numerator = Constant * KineticEnergy * SectionalDensity * HardnessRatio * AngleFactor * ShapeFactor;
if (!FMath::IsFinite(Numerator))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid numerator in penetration calculation"));
return 0.0f;
}
float PenetrationM = Numerator / Denominator;
// SANITY CHECK: Validate result before conversion
if (!FMath::IsFinite(PenetrationM) || PenetrationM < 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid penetration result %.6f m"), PenetrationM);
return 0.0f;
}
// Convert from meters to centimeters for consistency with Unreal Engine units
float PenetrationCm = FEBUnitConversions::MetersToCM(PenetrationM);
// SANITY CHECK: Clamp to reasonable maximum (10 meters = 1000 cm)
const float MaxPenetrationCm = 1000.0f;
if (PenetrationCm > MaxPenetrationCm)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Penetration %.1f cm exceeds maximum, clamping to %.1f cm"), PenetrationCm, MaxPenetrationCm);
PenetrationCm = MaxPenetrationCm;
}
return FMath::Max(0.0f, PenetrationCm);
}
float UEBMathematicalBallistics::CalculateResidualVelocity(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float ThicknessCM,
float ImpactAngleDegrees)
{
// SANITY CHECK: Validate input parameters
if (VelocityMPS <= 0.0f || !FMath::IsFinite(VelocityMPS))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid velocity %.3f m/s for residual velocity calculation"), VelocityMPS);
return 0.0f;
}
if (ThicknessCM <= 0.0f || !FMath::IsFinite(ThicknessCM))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid thickness %.3f cm"), ThicknessCM);
return 0.0f;
}
// Calculate ballistic limit velocity
float BallisticLimit = CalculateRechtIpsonVelocity(BulletProps, MaterialProps, ThicknessCM);
// SANITY CHECK: Validate ballistic limit
if (!FMath::IsFinite(BallisticLimit) || BallisticLimit < 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid ballistic limit %.3f m/s"), BallisticLimit);
return 0.0f;
}
// Adjust for impact angle
float AngleFactor = CalculateAngleFactor(ImpactAngleDegrees);
// SANITY CHECK: Prevent division by zero
if (AngleFactor <= 0.001f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Angle factor too small %.6f, using minimum"), AngleFactor);
AngleFactor = 0.001f;
}
BallisticLimit = BallisticLimit / AngleFactor;
// SANITY CHECK: Validate adjusted ballistic limit
if (!FMath::IsFinite(BallisticLimit))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid adjusted ballistic limit"));
return 0.0f;
}
// If impact velocity is below ballistic limit, no penetration
if (VelocityMPS <= BallisticLimit)
{
return 0.0f;
}
// SANITY CHECK: Ensure square root argument is positive
float VelocitySquared = VelocityMPS * VelocityMPS;
float BallisticLimitSquared = BallisticLimit * BallisticLimit;
float SquareRootArgument = VelocitySquared - BallisticLimitSquared;
if (SquareRootArgument < 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Negative square root argument %.6f, velocity %.3f < ballistic limit %.3f"),
SquareRootArgument, VelocityMPS, BallisticLimit);
return 0.0f;
}
// Calculate residual velocity using Recht-Ipson equation
// Vr = sqrt(V^2 - Vbl^2)
float ResidualVelocity = FMath::Sqrt(SquareRootArgument);
// SANITY CHECK: Validate residual velocity before applying absorption
if (!FMath::IsFinite(ResidualVelocity))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid residual velocity calculated"));
return 0.0f;
}
// SANITY CHECK: Validate energy absorption coefficient
float EnergyAbsorption = FMath::Clamp(MaterialProps.EnergyAbsorptionCoefficient, 0.0f, 0.99f);
if (EnergyAbsorption != MaterialProps.EnergyAbsorptionCoefficient)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Energy absorption coefficient %.3f clamped to %.3f"),
MaterialProps.EnergyAbsorptionCoefficient, EnergyAbsorption);
}
// Apply energy absorption factor
ResidualVelocity *= (1.0f - EnergyAbsorption);
return FMath::Max(0.0f, ResidualVelocity);
}
float UEBMathematicalBallistics::CalculateRicochetProbability(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float ImpactAngleDegrees)
{
// Calculate critical ricochet angle
float CriticalAngle = CalculateCriticalRicochetAngle(BulletProps, MaterialProps, VelocityMPS);
// If impact angle is greater than critical angle, no ricochet
if (ImpactAngleDegrees > CriticalAngle)
{
return 0.0f;
}
// Calculate ricochet probability based on angle and material properties
float AngleRatio = ImpactAngleDegrees / CriticalAngle;
float BaseRicochetProbability = 1.0f - FMath::Pow(AngleRatio, 2.0f);
// Adjust for hardness ratio
float HardnessRatio = CalculateHardnessRatio(BulletProps.BulletHardness, MaterialProps.MaterialHardness);
float HardnessAdjustment = FMath::Clamp(HardnessRatio, 0.1f, 2.0f);
// Adjust for velocity (higher velocity reduces ricochet probability)
float VelocityFactor = CalculateVelocityFactor(VelocityMPS, 500.0f);
float RicochetProbability = BaseRicochetProbability * HardnessAdjustment * VelocityFactor;
return FMath::Clamp(RicochetProbability, 0.0f, 1.0f);
}
float UEBMathematicalBallistics::CalculateBulletExpansion(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS)
{
// Convert velocity to FPS for comparison with expansion threshold
float VelocityFPS = ConvertMPStoFPS(VelocityMPS);
// No expansion if velocity is below threshold
if (VelocityFPS < BulletProps.ExpansionVelocityThreshold)
{
return 1.0f; // No expansion
}
// Calculate expansion based on velocity and bullet type
float VelocityRatio = VelocityFPS / BulletProps.ExpansionVelocityThreshold;
float ExpansionFactor = 1.0f;
switch (BulletProps.BulletType)
{
case EBulletType::BT_HollowPoint:
ExpansionFactor = FMath::Lerp(1.0f, BulletProps.MaxExpansionMultiplier, FMath::Clamp(VelocityRatio - 1.0f, 0.0f, 1.0f));
break;
case EBulletType::BT_SoftPoint:
ExpansionFactor = FMath::Lerp(1.0f, BulletProps.MaxExpansionMultiplier * 0.8f, FMath::Clamp(VelocityRatio - 1.0f, 0.0f, 1.0f));
break;
case EBulletType::BT_Frangible:
ExpansionFactor = FMath::Lerp(1.0f, BulletProps.MaxExpansionMultiplier * 1.2f, FMath::Clamp(VelocityRatio - 1.0f, 0.0f, 1.0f));
break;
case EBulletType::BT_FullMetalJacket:
case EBulletType::BT_ArmorPiercing:
ExpansionFactor = 1.0f; // No expansion
break;
default:
ExpansionFactor = FMath::Lerp(1.0f, BulletProps.MaxExpansionMultiplier * 0.6f, FMath::Clamp(VelocityRatio - 1.0f, 0.0f, 1.0f));
break;
}
return ExpansionFactor;
}
float UEBMathematicalBallistics::CalculateKineticEnergy(
const FMathematicalBulletProperties& BulletProps,
float VelocityMPS)
{
// KE = 0.5 * m * v^2
float MassKg = BulletProps.GetMassKg();
return 0.5f * MassKg * VelocityMPS * VelocityMPS;
}
float UEBMathematicalBallistics::CalculateMomentum(
const FMathematicalBulletProperties& BulletProps,
float VelocityMPS)
{
// p = m * v
float MassKg = BulletProps.GetMassKg();
return MassKg * VelocityMPS;
}
float UEBMathematicalBallistics::CalculateDragCoefficient(
const FMathematicalBulletProperties& BulletProps,
float MachNumber)
{
// Calculate drag coefficient from ballistic coefficient
// Standard drag coefficient for G1 projectile at given Mach number
float StandardDrag = 0.5f; // Simplified - normally would use complex curve
// Adjust for actual ballistic coefficient
float BC = BulletProps.UseG7Model ? BulletProps.BallisticCoefficientG7 : BulletProps.BallisticCoefficientG1;
// CD = Standard_CD / BC
return StandardDrag / BC;
}
float UEBMathematicalBallistics::CalculateTaylorHopkinsonLimit(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps)
{
// Taylor-Hopkinson limit: t = K * ρ * σ / (SD * V^2)
// Rearranged to solve for limiting thickness
float SectionalDensity = BulletProps.GetSectionalDensityKgPerM2();
float Constant = 0.001f; // Empirical constant
// Convert to proper units: result in cm
float DensityKgPerM3 = MaterialProps.DensityGPerCm3 * 1000.0f;
float YieldStrengthPa = MaterialProps.YieldStrengthMPa * 1000000.0f;
float ThicknessM = Constant * DensityKgPerM3 * YieldStrengthPa / SectionalDensity;
return ThicknessM * 100.0f; // Convert to cm
}
float UEBMathematicalBallistics::CalculateRechtIpsonVelocity(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float ThicknessCM)
{
// SANITY CHECK: Validate input parameters
if (ThicknessCM <= 0.0f || !FMath::IsFinite(ThicknessCM))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid thickness %.3f cm for Recht-Ipson calculation"), ThicknessCM);
return 0.0f;
}
if (MaterialProps.YieldStrengthMPa <= 0.0f || !FMath::IsFinite(MaterialProps.YieldStrengthMPa))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid yield strength %.1f MPa"), MaterialProps.YieldStrengthMPa);
return 0.0f;
}
// Recht-Ipson equation for ballistic limit velocity
// Vbl = sqrt(k * σ * t / (ρ * A))
// Where k is a constant, σ is yield strength, t is thickness, ρ is bullet density, A is cross-sectional area
float Constant = 2.0f; // Empirical constant
// Convert all units to SI for calculation
float CrossSectionAreaM2 = BulletProps.GetCrossSectionCm2() * 0.0001f; // cm² to m²
float ThicknessM = ThicknessCM * 0.01f; // cm to m
float YieldStrengthPa = MaterialProps.YieldStrengthMPa * 1000000.0f; // MPa to Pa
// SANITY CHECK: Validate cross-sectional area
if (CrossSectionAreaM2 <= 0.0f || !FMath::IsFinite(CrossSectionAreaM2))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid cross-sectional area %.8f m²"), CrossSectionAreaM2);
return 0.0f;
}
// Calculate bullet density (kg/m³)
float BulletVolumeM3 = CrossSectionAreaM2 * BulletProps.LengthInches * 0.0254f; // Convert length to meters
// SANITY CHECK: Validate bullet volume
if (BulletVolumeM3 <= 0.0f || !FMath::IsFinite(BulletVolumeM3))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet volume %.8f m³"), BulletVolumeM3);
return 0.0f;
}
float BulletMassKg = BulletProps.GetMassKg();
if (BulletMassKg <= 0.0f || !FMath::IsFinite(BulletMassKg))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet mass %.6f kg"), BulletMassKg);
return 0.0f;
}
float BulletDensityKgPerM3 = BulletMassKg / BulletVolumeM3;
// SANITY CHECK: Validate bullet density
if (BulletDensityKgPerM3 <= 0.0f || !FMath::IsFinite(BulletDensityKgPerM3))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet density %.3f kg/m³"), BulletDensityKgPerM3);
return 0.0f;
}
// SANITY CHECK: Calculate and validate square root argument
float Numerator = Constant * YieldStrengthPa * ThicknessM;
float Denominator = BulletDensityKgPerM3 * CrossSectionAreaM2;
if (Denominator <= 0.0f || !FMath::IsFinite(Denominator))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid denominator %.6f in Recht-Ipson equation"), Denominator);
return 0.0f;
}
float SquareRootArgument = Numerator / Denominator;
if (SquareRootArgument < 0.0f || !FMath::IsFinite(SquareRootArgument))
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid square root argument %.6f in Recht-Ipson equation"), SquareRootArgument);
return 0.0f;
}
// Calculate ballistic limit velocity in m/s
float BallisticLimitMPS = FMath::Sqrt(SquareRootArgument);
// SANITY CHECK: Validate final result
if (!FMath::IsFinite(BallisticLimitMPS) || BallisticLimitMPS < 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid ballistic limit velocity %.3f m/s"), BallisticLimitMPS);
return 0.0f;
}
// SANITY CHECK: Clamp to reasonable maximum (5 km/s)
const float MaxBallisticLimit = 5000.0f;
if (BallisticLimitMPS > MaxBallisticLimit)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Ballistic limit %.1f m/s exceeds maximum, clamping to %.1f m/s"),
BallisticLimitMPS, MaxBallisticLimit);
BallisticLimitMPS = MaxBallisticLimit;
}
return BallisticLimitMPS;
}
float UEBMathematicalBallistics::CalculateCriticalRicochetAngle(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS)
{
// Calculate critical ricochet angle using empirical formula
// Critical angle decreases with harder bullets and increases with harder targets
float HardnessRatio = CalculateHardnessRatio(BulletProps.BulletHardness, MaterialProps.MaterialHardness);
float VelocityFactor = CalculateVelocityFactor(VelocityMPS, 300.0f);
// Base critical angle (degrees)
float BaseCriticalAngle = 20.0f;
// Adjust for hardness and velocity
float CriticalAngle = BaseCriticalAngle * HardnessRatio * VelocityFactor;
return FMath::Clamp(CriticalAngle, 5.0f, 45.0f);
}
// Helper function implementations
float UEBMathematicalBallistics::CalculateHardnessRatio(float BulletHardness, float MaterialHardness)
{
// SANITY CHECK: Validate hardness values
if (!FMath::IsFinite(BulletHardness) || BulletHardness < 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet hardness %.3f, using default 15.0"), BulletHardness);
BulletHardness = 15.0f; // Default bullet hardness
}
if (!FMath::IsFinite(MaterialHardness) || MaterialHardness <= 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid material hardness %.3f, using default 50.0"), MaterialHardness);
MaterialHardness = 50.0f; // Default material hardness
}
// Hardness ratio affects penetration and ricochet
float HardnessRatio = BulletHardness / MaterialHardness;
// SANITY CHECK: Ensure reasonable bounds
return FMath::Clamp(HardnessRatio, 0.1f, 10.0f);
}
float UEBMathematicalBallistics::CalculateVelocityFactor(float Velocity, float ThresholdVelocity)
{
// SANITY CHECK: Validate input values
if (!FMath::IsFinite(Velocity) || Velocity < 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid velocity %.3f for velocity factor"), Velocity);
return 0.1f; // Minimum factor
}
if (!FMath::IsFinite(ThresholdVelocity) || ThresholdVelocity <= 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid threshold velocity %.3f, using default 300.0"), ThresholdVelocity);
ThresholdVelocity = 300.0f; // Default threshold
}
// Velocity factor for various calculations
float VelocityFactor = Velocity / ThresholdVelocity;
return FMath::Clamp(VelocityFactor, 0.1f, 5.0f);
}
float UEBMathematicalBallistics::CalculateAngleFactor(float ImpactAngleDegrees)
{
// Convert to radians and calculate cosine (normal impact = 1.0, grazing = 0.0)
float AngleRadians = FMath::DegreesToRadians(ImpactAngleDegrees);
return FMath::Cos(AngleRadians);
}
float UEBMathematicalBallistics::CalculateShapeFactorFromBulletType(EBulletType BulletType)
{
// Shape factor affects penetration efficiency
switch (BulletType)
{
case EBulletType::BT_ArmorPiercing:
return 1.3f;
case EBulletType::BT_FullMetalJacket:
return 1.0f;
case EBulletType::BT_SoftPoint:
return 0.9f;
case EBulletType::BT_HollowPoint:
return 0.8f;
case EBulletType::BT_Frangible:
return 0.6f;
case EBulletType::BT_Wadcutter:
return 0.7f;
case EBulletType::BT_Match:
return 1.1f;
default:
return 1.0f;
}
}
float UEBMathematicalBallistics::CalculateMaterialFactor(EBulletMaterial BulletMaterial)
{
// Material factor affects penetration and expansion
switch (BulletMaterial)
{
case EBulletMaterial::BM_Steel:
return 1.4f;
case EBulletMaterial::BM_Tungsten:
return 1.8f;
case EBulletMaterial::BM_Brass:
return 1.2f;
case EBulletMaterial::BM_Copper:
return 1.1f;
case EBulletMaterial::BM_CopperJacket:
return 1.0f;
case EBulletMaterial::BM_Lead:
return 0.8f;
case EBulletMaterial::BM_LeadAntimony:
return 0.9f;
case EBulletMaterial::BM_Bismuth:
return 0.7f;
case EBulletMaterial::BM_Zinc:
return 0.9f;
default:
return 1.0f;
}
}
// Mathematical Spalling Implementations
bool UEBMathematicalBallistics::ShouldGenerateMathematicalSpalling(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float ImpactAngleDegrees)
{
if (!MaterialProps.EnableMathematicalSpalling)
{
return false;
}
// Additional safety check: prevent spalling from very small fragments
if (BulletProps.GetMassKg() < 0.001f) // Less than 1 gram
{
return false;
}
// Calculate impact stress using Rankine-Hugoniot relations
float BulletDensity = BulletProps.GetMassKg() / (BulletProps.GetCrossSectionCm2() * 0.0001f); // kg/m³
float MaterialDensity = MaterialProps.DensityGPerCm3 * 1000.0f; // kg/m³
// Impact stress (Pa) = 0.5 * ρ * v²
float ImpactStress = 0.5f * FMath::Sqrt(BulletDensity * MaterialDensity) * VelocityMPS * VelocityMPS;
float ImpactStressMPa = ImpactStress / 1000000.0f; // Convert to MPa
// Critical stress for spalling
float CriticalStress = MaterialProps.SpallStrengthMPa * MaterialProps.CriticalStressFactor;
// Impact angle factor (perpendicular impacts more effective)
float AngleFactor = CalculateAngleFactor(ImpactAngleDegrees);
float EffectiveStress = ImpactStressMPa * AngleFactor;
return EffectiveStress >= CriticalStress;
}
int32 UEBMathematicalBallistics::CalculateSpallFragmentCount(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float ImpactAngleDegrees)
{
// Fragment count based on impact area and fragment density
float ImpactArea = BulletProps.GetCrossSectionCm2();
float AngleFactor = CalculateAngleFactor(ImpactAngleDegrees);
float EffectiveArea = ImpactArea / FMath::Max(AngleFactor, 0.1f); // Grazing impacts affect larger area
// Energy factor - higher energy creates more fragments
float KineticEnergy = CalculateKineticEnergy(BulletProps, VelocityMPS);
float EnergyFactor = FMath::Sqrt(KineticEnergy / 1000.0f); // Normalized
// Material factor - harder materials create more, smaller fragments
float MaterialFactor = MaterialProps.MaterialHardness / 100.0f;
float BaseFragmentCount = EffectiveArea * MaterialProps.MaxFragmentDensity * EnergyFactor * MaterialFactor;
return FMath::Clamp(FMath::RoundToInt(BaseFragmentCount), 1, 50);
}
float UEBMathematicalBallistics::CalculateSpallFragmentVelocity(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float FragmentMassRatio)
{
// Gurney equation for fragment velocity: v = √(2E/M)
// Where E is explosive energy equivalent and M is fragment mass
float KineticEnergy = CalculateKineticEnergy(BulletProps, VelocityMPS);
float SpallEnergyFraction = MaterialProps.FragmentVelocityEfficiency;
float AvailableEnergy = KineticEnergy * SpallEnergyFraction;
// Fragment kinetic energy based on mass ratio
float FragmentEnergy = AvailableEnergy * (1.0f / FragmentMassRatio);
// Calculate fragment velocity using kinetic energy equation
float FragmentMass = BulletProps.GetMassKg() * FragmentMassRatio;
float FragmentVelocity = FMath::Sqrt(2.0f * FragmentEnergy / FragmentMass);
// Apply material efficiency factor
return FragmentVelocity * MaterialProps.FragmentVelocityEfficiency;
}
float UEBMathematicalBallistics::CalculateSpallFragmentMass(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
int32 FragmentIndex,
int32 TotalFragments)
{
// Power-law distribution for fragment masses
// Smaller fragments are more common (Mott distribution)
float NormalizedIndex = (float)FragmentIndex / (float)TotalFragments;
// Power-law with typical exponent around -1.6 to -2.0
float SizeDistribution = FMath::Pow(NormalizedIndex + 0.1f, MaterialProps.FragmentSizeExponent);
// Base fragment mass ratio
float BaseMassRatio = MaterialProps.AverageFragmentMassRatio;
float FragmentMassRatio = BaseMassRatio * SizeDistribution;
// Clamp to reasonable range
return FMath::Clamp(FragmentMassRatio, 0.01f, 0.5f);
}
float UEBMathematicalBallistics::CalculateSpallConeAngle(
const FMathematicalBulletProperties& BulletProps,
const FMathematicalMaterialProperties& MaterialProps,
float VelocityMPS,
float ImpactAngleDegrees)
{
// Spall cone angle based on impact angle and material properties
// Perpendicular impacts create tighter cones, grazing impacts wider
float BaseAngle = 30.0f; // Base cone half-angle in degrees
// Impact angle factor (grazing impacts create wider cones)
float AngleFactor = 1.0f + FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees));
// Material factor (harder materials create tighter cones)
float MaterialFactor = 100.0f / MaterialProps.MaterialHardness;
MaterialFactor = FMath::Clamp(MaterialFactor, 0.5f, 2.0f);
// Velocity factor (higher velocity creates wider dispersion)
float VelocityFactor = 1.0f + (VelocityMPS / 1000.0f) * 0.2f;
float ConeAngle = BaseAngle * AngleFactor * MaterialFactor * VelocityFactor;
return FMath::Clamp(ConeAngle, 10.0f, 90.0f);
}