prerelease
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
using Sandbox;
|
||||
using Sandbox.Services;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace VeloX;
|
||||
public class Pacejka
|
||||
{
|
||||
public class PacejkaPreset
|
||||
{
|
||||
[KeyProperty] public float B { get; set; } = 10.86f;
|
||||
[KeyProperty] public float C { get; set; } = 2.15f;
|
||||
[KeyProperty] public float D { get; set; } = 0.933f;
|
||||
[KeyProperty] public float E { get; set; } = 0.992f;
|
||||
|
||||
public float Evaluate( float slip ) => D * MathF.Sin( C * MathF.Atan( B * slip - E * (B * slip - MathF.Atan( B * slip )) ) );
|
||||
|
||||
public float GetPeakSlip()
|
||||
{
|
||||
float peakSlip = -1;
|
||||
float yMax = 0;
|
||||
|
||||
for ( float i = 0; i < 1f; i += 0.01f )
|
||||
{
|
||||
float y = Evaluate( i );
|
||||
if ( y > yMax )
|
||||
{
|
||||
yMax = y;
|
||||
peakSlip = i;
|
||||
}
|
||||
}
|
||||
|
||||
return peakSlip;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public PacejkaPreset Lateral { get; set; } = new();
|
||||
public PacejkaPreset Longitudinal { get; set; } = new();
|
||||
|
||||
public float PacejkaFx( float slip ) => Longitudinal.Evaluate( slip );
|
||||
public float PacejkaFy( float slip ) => Lateral.Evaluate( slip );
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace VeloX;
|
||||
|
||||
public class BasicSuspension
|
||||
{
|
||||
private readonly Hinge Hinge;
|
||||
|
||||
public BasicSuspension( Vector3 wheel, Vector3 hingeBody, Vector3 hingeWheel )
|
||||
{
|
||||
Vector3 hingePoint = wheel - (hingeWheel - hingeBody);
|
||||
Hinge = new Hinge( hingePoint, hingeWheel - hingeBody );
|
||||
}
|
||||
|
||||
public virtual void GetWheelTransform( float travel, out Rotation rotation, out Vector3 position )
|
||||
{
|
||||
rotation = Rotation.Identity;
|
||||
position = Hinge.Rotate( travel );
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
internal readonly struct Hinge( Vector3 hinge_anchor, Vector3 hinge_arm )
|
||||
{
|
||||
[Description( "the point that the wheels are rotated around as the suspension compresses" )]
|
||||
public readonly Vector3 Anchor = hinge_anchor;
|
||||
|
||||
[Description( "anchor to wheel vector" )]
|
||||
public readonly Vector3 Arm = hinge_arm;
|
||||
|
||||
[Description( "arm length squared" )]
|
||||
public readonly float LengthSquared = hinge_arm.Dot( hinge_arm );
|
||||
|
||||
[Description( "1 / arm length in hinge axis normal plane" )]
|
||||
public readonly float NormXY = 1 / MathF.Sqrt( hinge_arm.x * hinge_arm.x + hinge_arm.y * hinge_arm.y );
|
||||
|
||||
public readonly Vector3 Rotate( float travel )
|
||||
{
|
||||
float z = Arm.z + travel;
|
||||
float lengthSq = MathF.Max( LengthSquared - z * z, 0.0f );
|
||||
float nxy = NormXY * MathF.Sqrt( lengthSq );
|
||||
return Anchor + new Vector3( Arm.x * nxy, Arm.y * nxy, z );
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
public class MacPhersonSuspension
|
||||
{
|
||||
public readonly Vector3 WheelOffset;
|
||||
public readonly Vector3 UprightTop;
|
||||
public readonly Vector3 UprightAxis;
|
||||
private readonly Hinge Hinge;
|
||||
|
||||
public MacPhersonSuspension( Vector3 wheel, Vector3 strutBody, Vector3 strutWheel, Vector3 hingeBody )
|
||||
{
|
||||
WheelOffset = wheel - strutWheel;
|
||||
UprightTop = strutBody;
|
||||
UprightAxis = (strutBody - strutWheel).Normal;
|
||||
Hinge = new( hingeBody, strutWheel - hingeBody );
|
||||
}
|
||||
|
||||
public void GetWheelTransform( float travel, out Rotation rotation, out Vector3 position )
|
||||
{
|
||||
Vector3 hingeEnd = Hinge.Rotate( travel );
|
||||
Vector3 uprightAxisNew = (UprightTop - hingeEnd).Normal;
|
||||
|
||||
rotation = Rotation.FromAxis(
|
||||
Vector3.Cross( UprightAxis, uprightAxisNew ),
|
||||
MathF.Acos( Vector3.Dot( UprightAxis, uprightAxisNew ) ).RadianToDegree()
|
||||
);
|
||||
|
||||
position = hingeEnd + WheelOffset.Transform( rotation );
|
||||
}
|
||||
}
|
||||
@@ -4,57 +4,45 @@ using System;
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
[GameResource( "Wheel Friction", "tire", "Wheel Friction", Category = "VeloX", Icon = "radio_button_checked" )]
|
||||
[AssetType( Name = "Wheel Friction", Extension = "tire", Category = "VeloX" )]
|
||||
public class TirePreset : GameResource
|
||||
{
|
||||
public static TirePreset Default { get; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
|
||||
|
||||
[Property] public Pacejka Pacejka { get; set; }
|
||||
private float peakSlip = -1;
|
||||
|
||||
public float RollResistanceLin { get; set; } = 1E-3f;
|
||||
public float RollResistanceQuad { get; set; } = 1E-6f;
|
||||
[Property] public float B { get; set; } = 10.86f;
|
||||
[Property] public float C { get; set; } = 2.15f;
|
||||
[Property] public float D { get; set; } = 0.933f;
|
||||
[Property] public float E { get; set; } = 0.992f;
|
||||
|
||||
public float GetRollingResistance( float velocity, float resistance_factor )
|
||||
{ // surface influence on rolling resistance
|
||||
float resistance = resistance_factor * RollResistanceLin;
|
||||
|
||||
// heat due to tire deformation increases rolling resistance
|
||||
// approximate by quadratic function
|
||||
resistance += velocity * velocity * RollResistanceQuad;
|
||||
|
||||
return resistance;
|
||||
}
|
||||
|
||||
public static void ComputeSlip( float lon_velocity, float lat_velocity, float rot_velocity, float wheel_radius, out float slip_ratio, out float slip_angle )
|
||||
public float GetPeakSlip()
|
||||
{
|
||||
var abs_lon = Math.Max( MathF.Abs( lon_velocity ), 1e-3f );
|
||||
|
||||
slip_ratio = lon_velocity - rot_velocity * wheel_radius;
|
||||
|
||||
if ( abs_lon >= 0.005f )
|
||||
slip_ratio /= abs_lon;
|
||||
else
|
||||
slip_ratio *= abs_lon;
|
||||
|
||||
if ( abs_lon >= 0.5f )
|
||||
slip_angle = MathF.Atan2( -lat_velocity, abs_lon ).RadianToDegree() / 50f;
|
||||
else
|
||||
slip_angle = -lat_velocity * (0.01f / Time.Delta);
|
||||
|
||||
slip_ratio = Math.Clamp( slip_ratio, -1, 1 );
|
||||
slip_angle = Math.Clamp( slip_angle, -1, 1 );
|
||||
if ( peakSlip == -1 )
|
||||
peakSlip = CalcPeakSlip();
|
||||
|
||||
return peakSlip;
|
||||
}
|
||||
|
||||
/// approximate asin(x) = x + x^3/6 for +-18 deg range
|
||||
public static float ComputeCamberAngle( float sin_camber )
|
||||
|
||||
public float Evaluate( float t ) => D * MathF.Sin( C * MathF.Atan( B * t - E * (B * t - MathF.Atan( B * t )) ) );
|
||||
|
||||
private float CalcPeakSlip()
|
||||
{
|
||||
float sc = Math.Clamp( sin_camber, -0.3f, 0.3f );
|
||||
return ((1 / 6.0f) * (sc * sc) + 1) * sc;
|
||||
float peakSlip = -1;
|
||||
float yMax = 0;
|
||||
|
||||
for ( float i = 0; i < 1f; i += 0.01f )
|
||||
{
|
||||
float y = Evaluate( i );
|
||||
if ( y > yMax )
|
||||
{
|
||||
yMax = y;
|
||||
peakSlip = i;
|
||||
}
|
||||
}
|
||||
|
||||
return peakSlip;
|
||||
}
|
||||
|
||||
public static float ComputeCamberVelocity( float sa, float vx )
|
||||
{
|
||||
float tansa = (1 / 3.0f * (sa * sa) + 1) * sa;
|
||||
return tansa * vx;
|
||||
}
|
||||
}
|
||||
|
||||
299
Code/Base/Wheel/VeloXWheel.Friction.cs
Normal file
299
Code/Base/Wheel/VeloXWheel.Friction.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXWheel
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant torque acting similar to brake torque.
|
||||
/// Imitates rolling resistance.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 500 ), Sync] public float RollingResistanceTorque { get; set; } = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// The percentage this wheel is contributing to the total vehicle load bearing.
|
||||
/// </summary>
|
||||
public float LoadContribution { get; set; } = 0.25f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum load the tire is rated for in [N].
|
||||
/// Used to calculate friction.Default value is adequate for most cars but
|
||||
/// larger and heavier vehicles such as semi trucks will use higher values.
|
||||
/// A good rule of the thumb is that this value should be 2x the Load
|
||||
/// while vehicle is stationary.
|
||||
/// </summary>
|
||||
[Property, Sync] public float LoadRating { get; set; } = 5400;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The amount of torque returned by the wheel.
|
||||
/// Under no-slip conditions this will be equal to the torque that was input.
|
||||
/// When there is wheel spin, the value will be less than the input torque.
|
||||
/// </summary>
|
||||
public float CounterTorque { get; private set; }
|
||||
|
||||
[Property, Sync] public bool AutoSetFriction { get; set; } = true;
|
||||
[Property, Sync] public bool UseGroundVelocity { get; set; } = true;
|
||||
|
||||
[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
|
||||
[Property] public Friction ForwardFriction = new();
|
||||
[Property] public Friction SidewayFriction = new();
|
||||
public Vector3 FrictionForce;
|
||||
|
||||
|
||||
private Vector3 hitContactVelocity;
|
||||
private Vector3 hitForwardDirection;
|
||||
private Vector3 hitSidewaysDirection;
|
||||
|
||||
public Vector3 ContactRight => hitSidewaysDirection;
|
||||
public Vector3 ContactForward => hitForwardDirection;
|
||||
|
||||
[Sync]
|
||||
public float LongitudinalSlip
|
||||
{
|
||||
get => ForwardFriction.Slip;
|
||||
private set => ForwardFriction.Slip = value;
|
||||
}
|
||||
|
||||
public float LongitudinalSpeed => ForwardFriction.Speed;
|
||||
public bool IsSkiddingLongitudinally => NormalizedLongitudinalSlip > 0.35f;
|
||||
public float NormalizedLongitudinalSlip => Math.Clamp( Math.Abs( LongitudinalSlip ), 0, 1 );
|
||||
|
||||
|
||||
[Sync]
|
||||
public float LateralSlip
|
||||
{
|
||||
get => SidewayFriction.Slip;
|
||||
private set => SidewayFriction.Slip = value;
|
||||
}
|
||||
|
||||
public float LateralSpeed => SidewayFriction.Speed;
|
||||
public bool IsSkiddingLaterally => NormalizedLateralSlip > 0.35f;
|
||||
public float NormalizedLateralSlip => Math.Clamp( Math.Abs( LateralSlip ), 0, 1 );
|
||||
public bool IsSkidding => IsSkiddingLaterally || IsSkiddingLongitudinally;
|
||||
public float NormalizedSlip => (NormalizedLateralSlip + NormalizedLongitudinalSlip) / 2f;
|
||||
|
||||
private void UpdateHitVariables()
|
||||
{
|
||||
if ( IsOnGround )
|
||||
{
|
||||
hitContactVelocity = Vehicle.Body.GetVelocityAtPoint( ContactPosition + Vehicle.Body.MassCenter );
|
||||
|
||||
hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal;
|
||||
hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection;
|
||||
|
||||
ForwardFriction.Speed = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter();
|
||||
SidewayFriction.Speed = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter();
|
||||
}
|
||||
else
|
||||
{
|
||||
ForwardFriction.Speed = 0f;
|
||||
SidewayFriction.Speed = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Vector3 lowSpeedReferencePosition;
|
||||
private bool lowSpeedReferenceIsSet;
|
||||
private Vector3 currentPosition;
|
||||
private Vector3 referenceError;
|
||||
private Vector3 correctiveForce;
|
||||
private void UpdateFriction( float dt )
|
||||
{
|
||||
|
||||
|
||||
var motorTorque = DriveTorque;
|
||||
var brakeTorque = BrakeTorque * BrakeMult;
|
||||
|
||||
float allWheelLoadSum = Vehicle.CombinedLoad;
|
||||
|
||||
LoadContribution = allWheelLoadSum == 0 ? 1f : Fz / allWheelLoadSum;
|
||||
|
||||
float mRadius = Radius;
|
||||
|
||||
float invDt = 1f / dt;
|
||||
float invRadius = 1f / mRadius;
|
||||
float inertia = Inertia;
|
||||
float invInertia = 1f / Inertia;
|
||||
|
||||
float loadClamped = Math.Clamp( Fz, 0, LoadRating );
|
||||
|
||||
float forwardLoadFactor = loadClamped * 1.35f;
|
||||
float sideLoadFactor = loadClamped * 1.9f;
|
||||
|
||||
float loadPercent = Math.Clamp( Fz / LoadRating, 0f, 1f );
|
||||
float slipLoadModifier = 1f - loadPercent * 0.4f;
|
||||
//DebugOverlay.Text( WorldPosition, SidewayFriction.Speed.ToString(), overlay: true );
|
||||
|
||||
|
||||
float mass = Vehicle.Body.Mass;
|
||||
float absForwardSpeed = Math.Abs( ForwardFriction.Speed );
|
||||
float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt;
|
||||
float absSideSpeed = Math.Abs( SidewayFriction.Speed );
|
||||
float sideForceClamp = mass * LoadContribution * absSideSpeed * invDt;
|
||||
|
||||
float forwardSpeedClamp = 1.5f * (dt / 0.005f);
|
||||
forwardSpeedClamp = Math.Clamp( forwardSpeedClamp, 1.5f, 10f );
|
||||
float clampedAbsForwardSpeed = Math.Max( absForwardSpeed, forwardSpeedClamp );
|
||||
|
||||
// Calculate effect of camber on friction
|
||||
float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) );
|
||||
|
||||
float peakForwardFrictionForce = forwardLoadFactor;
|
||||
float absCombinedBrakeTorque = Math.Max( 0, brakeTorque + RollingResistanceTorque );
|
||||
|
||||
float signedCombinedBrakeTorque = absCombinedBrakeTorque * -Math.Sign( ForwardFriction.Speed );
|
||||
float signedCombinedBrakeForce = signedCombinedBrakeTorque * invRadius;
|
||||
float motorForce = motorTorque * invRadius;
|
||||
float forwardInputForce = motorForce + signedCombinedBrakeForce;
|
||||
float absMotorTorque = Math.Abs( motorTorque );
|
||||
float absBrakeTorque = Math.Abs( brakeTorque );
|
||||
|
||||
float maxForwardForce = Tire.Evaluate( Math.Abs( ForwardFriction.Slip ) ) * Math.Min( peakForwardFrictionForce, forwardForceClamp );
|
||||
|
||||
maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce;
|
||||
ForwardFriction.Force = forwardInputForce > maxForwardForce ? maxForwardForce
|
||||
: forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce;
|
||||
|
||||
bool wheelIsBlocked = false;
|
||||
if ( IsOnGround )
|
||||
{
|
||||
float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity );
|
||||
float wheelForceClampOverflow = 0;
|
||||
if ( (combinedWheelForce >= 0 && AngularVelocity < 0) || (combinedWheelForce < 0 && AngularVelocity > 0) )
|
||||
{
|
||||
float absWheelForceClamp = Math.Abs( AngularVelocity ) * inertia * invRadius * invDt;
|
||||
float absCombinedWheelForce = combinedWheelForce < 0 ? -combinedWheelForce : combinedWheelForce;
|
||||
float wheelForceDiff = absCombinedWheelForce - absWheelForceClamp;
|
||||
wheelForceClampOverflow = Math.Max( 0, wheelForceDiff ) * Math.Sign( combinedWheelForce );
|
||||
combinedWheelForce = Math.Clamp( combinedWheelForce, -absWheelForceClamp, absWheelForceClamp );
|
||||
}
|
||||
AngularVelocity += combinedWheelForce * mRadius * invInertia * dt;
|
||||
|
||||
// Surface (corrective) force
|
||||
float noSlipAngularVelocity = ForwardFriction.Speed * invRadius;
|
||||
float angularVelocityError = AngularVelocity - noSlipAngularVelocity;
|
||||
|
||||
float angularVelocityCorrectionForce = Math.Clamp( -angularVelocityError * inertia * invRadius * invDt, -maxForwardForce, maxForwardForce );
|
||||
if ( absMotorTorque < absBrakeTorque && Math.Abs( wheelForceClampOverflow ) > Math.Abs( angularVelocityCorrectionForce ) )
|
||||
{
|
||||
wheelIsBlocked = true;
|
||||
AngularVelocity += ForwardFriction.Speed > 0 ? 1e-10f : -1e-10f;
|
||||
}
|
||||
else
|
||||
{
|
||||
AngularVelocity += angularVelocityCorrectionForce * mRadius * invInertia * dt;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float maxBrakeTorque = AngularVelocity * inertia * invDt + motorTorque;
|
||||
maxBrakeTorque = maxBrakeTorque < 0 ? -maxBrakeTorque : maxBrakeTorque;
|
||||
float brakeTorqueSign = AngularVelocity < 0f ? -1f : 1f;
|
||||
float clampedBrakeTorque = Math.Clamp( absCombinedBrakeTorque, -maxBrakeTorque, maxBrakeTorque );
|
||||
AngularVelocity += (motorTorque - brakeTorqueSign * clampedBrakeTorque) * invInertia * dt;
|
||||
}
|
||||
|
||||
float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity;
|
||||
float maxCounterTorque = inertia * absAngularVelocity;
|
||||
CounterTorque = Math.Clamp( (signedCombinedBrakeForce - ForwardFriction.Force) * mRadius, -maxCounterTorque, maxCounterTorque );
|
||||
|
||||
|
||||
ForwardFriction.Slip = (ForwardFriction.Speed - AngularVelocity * mRadius) / clampedAbsForwardSpeed;
|
||||
ForwardFriction.Slip *= slipLoadModifier;
|
||||
|
||||
SidewayFriction.Slip = MathF.Atan2( SidewayFriction.Speed, clampedAbsForwardSpeed ).RadianToDegree() * 0.01111f;
|
||||
SidewayFriction.Slip *= slipLoadModifier;
|
||||
|
||||
float sideSlipSign = SidewayFriction.Slip > 0 ? 1 : -1;
|
||||
float absSideSlip = Math.Abs( SidewayFriction.Slip );
|
||||
float peakSideFrictionForce = sideLoadFactor;
|
||||
|
||||
float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * sideLoadFactor;
|
||||
SidewayFriction.Force = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp );
|
||||
SidewayFriction.Force *= camberFrictionCoeff;
|
||||
|
||||
if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f )
|
||||
{
|
||||
float verticalOffset = RestLength + mRadius;
|
||||
var transformPosition = WorldPosition;
|
||||
|
||||
var transformUp = TransformRotationSteer.Up;
|
||||
|
||||
currentPosition.x = transformPosition.x - transformUp.x * verticalOffset;
|
||||
currentPosition.y = transformPosition.y - transformUp.y * verticalOffset;
|
||||
currentPosition.z = transformPosition.z - transformUp.z * verticalOffset;
|
||||
|
||||
if ( !lowSpeedReferenceIsSet )
|
||||
{
|
||||
lowSpeedReferenceIsSet = true;
|
||||
lowSpeedReferencePosition = currentPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
referenceError.x = lowSpeedReferencePosition.x - currentPosition.x;
|
||||
referenceError.y = lowSpeedReferencePosition.y - currentPosition.y;
|
||||
referenceError.z = lowSpeedReferencePosition.z - currentPosition.z;
|
||||
|
||||
correctiveForce.x = invDt * LoadContribution * mass * referenceError.x;
|
||||
correctiveForce.y = invDt * LoadContribution * mass * referenceError.y;
|
||||
correctiveForce.z = invDt * LoadContribution * mass * referenceError.z;
|
||||
|
||||
if ( wheelIsBlocked && absAngularVelocity < 0.5f )
|
||||
{
|
||||
ForwardFriction.Force += correctiveForce.Dot( hitForwardDirection );
|
||||
}
|
||||
SidewayFriction.Force += correctiveForce.Dot( hitSidewaysDirection );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
lowSpeedReferenceIsSet = false;
|
||||
}
|
||||
ForwardFriction.Force = Math.Clamp( ForwardFriction.Force, -peakForwardFrictionForce, peakForwardFrictionForce );
|
||||
|
||||
SidewayFriction.Force = Math.Clamp( SidewayFriction.Force, -peakSideFrictionForce, peakSideFrictionForce );
|
||||
|
||||
if ( absForwardSpeed > 2f || absAngularVelocity > 4f )
|
||||
{
|
||||
|
||||
float forwardSlipPercent = ForwardFriction.Slip / Tire.GetPeakSlip();
|
||||
float sideSlipPercent = SidewayFriction.Slip / Tire.GetPeakSlip();
|
||||
float slipCircleLimit = MathF.Sqrt( forwardSlipPercent * forwardSlipPercent + sideSlipPercent * sideSlipPercent );
|
||||
if ( slipCircleLimit > 1f )
|
||||
{
|
||||
float beta = MathF.Atan2( sideSlipPercent, forwardSlipPercent * 0.9f );
|
||||
float sinBeta = MathF.Sin( beta );
|
||||
float cosBeta = MathF.Cos( beta );
|
||||
|
||||
float absForwardForce = ForwardFriction.Force < 0 ? -ForwardFriction.Force : ForwardFriction.Force;
|
||||
|
||||
float absSideForce = SidewayFriction.Force < 0 ? -SidewayFriction.Force : SidewayFriction.Force;
|
||||
float f = absForwardForce * cosBeta * cosBeta + absSideForce * sinBeta * sinBeta;
|
||||
|
||||
ForwardFriction.Force = 0.5f * ForwardFriction.Force - f * cosBeta;
|
||||
SidewayFriction.Force = 0.5f * SidewayFriction.Force - f * sinBeta;
|
||||
}
|
||||
}
|
||||
|
||||
if ( IsOnGround )
|
||||
{
|
||||
FrictionForce.x = (hitSidewaysDirection.x * SidewayFriction.Force + hitForwardDirection.x * ForwardFriction.Force).MeterToInch();
|
||||
FrictionForce.y = (hitSidewaysDirection.y * SidewayFriction.Force + hitForwardDirection.y * ForwardFriction.Force).MeterToInch();
|
||||
FrictionForce.z = (hitSidewaysDirection.z * SidewayFriction.Force + hitForwardDirection.z * ForwardFriction.Force).MeterToInch();
|
||||
//DebugOverlay.Normal( WorldPosition, hitSidewaysDirection * 10, overlay: true );
|
||||
//DebugOverlay.Normal( WorldPosition, hitForwardDirection * 10, overlay: true );
|
||||
//DebugOverlay.Normal( WorldPosition, FrictionForce / 100, overlay: true );
|
||||
//DebugOverlay.Normal( ContactPosition, ContactNormal * 10, overlay: true );
|
||||
//DebugOverlay.Sphere( new( ContactPosition, 4 ), overlay: true );
|
||||
//Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce );
|
||||
|
||||
}
|
||||
else
|
||||
FrictionForce = Vector3.Zero;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public partial class VeloXWheel : Component
|
||||
//
|
||||
{
|
||||
var suspensionStart = Vector3.Zero;
|
||||
var suspensionEnd = Vector3.Zero + Vector3.Down * SuspensionLength;
|
||||
var suspensionEnd = Vector3.Zero + Vector3.Down * RestLength.MeterToInch();
|
||||
|
||||
Gizmo.Draw.Color = Color.Cyan;
|
||||
Gizmo.Draw.LineThickness = 0.25f;
|
||||
@@ -28,7 +28,7 @@ public partial class VeloXWheel : Component
|
||||
Gizmo.Draw.Line( suspensionStart + Vector3.Forward, suspensionStart + Vector3.Backward );
|
||||
Gizmo.Draw.Line( suspensionEnd + Vector3.Forward, suspensionEnd + Vector3.Backward );
|
||||
}
|
||||
var widthOffset = Vector3.Right * Width * 0.5f;
|
||||
var widthOffset = Vector3.Right * Width.MeterToInch() * 0.5f;
|
||||
//
|
||||
// Wheel radius
|
||||
//
|
||||
@@ -36,7 +36,7 @@ public partial class VeloXWheel : Component
|
||||
Gizmo.Draw.LineThickness = 0.5f;
|
||||
Gizmo.Draw.Color = Color.White;
|
||||
|
||||
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius, Radius, 16 );
|
||||
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius.MeterToInch(), Radius.MeterToInch(), 16 );
|
||||
}
|
||||
|
||||
//
|
||||
@@ -51,11 +51,11 @@ public partial class VeloXWheel : Component
|
||||
for ( float i = 0; i < 16; i++ )
|
||||
{
|
||||
|
||||
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius;
|
||||
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius.MeterToInch();
|
||||
|
||||
Gizmo.Draw.Line( new Line( pos - widthOffset, pos + widthOffset ) );
|
||||
|
||||
var pos2 = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( (i + 1) / 16 * 360, 0, 0 ) ) * Radius;
|
||||
var pos2 = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( (i + 1) / 16 * 360, 0, 0 ) ) * Radius.MeterToInch();
|
||||
Gizmo.Draw.Line( pos - widthOffset, pos2 + widthOffset );
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public partial class VeloXWheel : Component
|
||||
//// Forward direction
|
||||
////
|
||||
//{
|
||||
// var arrowStart = Vector3.Forward * Radius;
|
||||
// var arrowStart = Vector3.Forward * Radius.MeterToInch();
|
||||
// var arrowEnd = arrowStart + Vector3.Forward * 8f;
|
||||
|
||||
// Gizmo.Draw.Color = Color.Red;
|
||||
|
||||
89
Code/Base/Wheel/VeloXWheel.Skid.cs
Normal file
89
Code/Base/Wheel/VeloXWheel.Skid.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Sandbox;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXWheel
|
||||
{
|
||||
|
||||
[ConCmd( "clear_skids" )]
|
||||
public static void ClearSkids()
|
||||
{
|
||||
while ( SkidMarks.Count > 1 )
|
||||
SkidMarks.Dequeue()?.DestroyGameObject();
|
||||
}
|
||||
|
||||
[ConVar( "skidmark_max_skid" )]
|
||||
public static float MaxSkid { get; set; } = 50.0f;
|
||||
|
||||
[ConVar( "skidmark_min_slide" )]
|
||||
public static float MinSlide { get; set; } = 0.1f;
|
||||
|
||||
|
||||
private static readonly Queue<LineRenderer> SkidMarks = [];
|
||||
|
||||
private LineRenderer _skidMark;
|
||||
|
||||
private void ResetSkid()
|
||||
{
|
||||
_skidMark = null;
|
||||
}
|
||||
|
||||
private void CreateSkid()
|
||||
{
|
||||
GameObject go = new()
|
||||
{
|
||||
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * (Radius.MeterToInch() - 0.01f),
|
||||
WorldRotation = Rotation.LookAt( hitSidewaysDirection )
|
||||
};
|
||||
_skidMark = go.AddComponent<LineRenderer>();
|
||||
_skidMark.Face = SceneLineObject.FaceMode.Normal;
|
||||
_skidMark.Points = [go];
|
||||
_skidMark.Color = Color.Black.WithAlpha( 0.5f );
|
||||
_skidMark.Opaque = false;
|
||||
_skidMark.CastShadows = false;
|
||||
_skidMark.Width = Width.MeterToInch() / 2;
|
||||
_skidMark.AutoCalculateNormals = false;
|
||||
_skidMark.SplineInterpolation = 4;
|
||||
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
|
||||
SkidMarks.Enqueue( _skidMark );
|
||||
}
|
||||
|
||||
protected void UpdateSkid()
|
||||
{
|
||||
if ( IsProxy )
|
||||
return;
|
||||
while ( SkidMarks.Count > MaxSkid )
|
||||
{
|
||||
SkidMarks.Dequeue()?.DestroyGameObject();
|
||||
}
|
||||
|
||||
if ( !IsOnGround )
|
||||
{
|
||||
ResetSkid();
|
||||
return;
|
||||
}
|
||||
|
||||
var slideAmount = NormalizedSlip;
|
||||
|
||||
if ( slideAmount < MinSlide * 2 )
|
||||
{
|
||||
ResetSkid();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !_skidMark.IsValid() )
|
||||
{
|
||||
CreateSkid();
|
||||
}
|
||||
GameObject go = new()
|
||||
{
|
||||
Parent = _skidMark.GameObject,
|
||||
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch(),
|
||||
WorldRotation = Rotation.LookAt( ContactNormal.RotateAround( Vector3.Zero, Rotation.FromRoll( 90 ) ) )
|
||||
};
|
||||
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
|
||||
_skidMark.Points.Add( go );
|
||||
}
|
||||
}
|
||||
109
Code/Base/Wheel/VeloXWheel.Smoke.cs
Normal file
109
Code/Base/Wheel/VeloXWheel.Smoke.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXWheel
|
||||
{
|
||||
private static readonly GameObject SmokePrefab = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" );
|
||||
|
||||
private GameObject SmokeObject { get; set; }
|
||||
public const float MIN_DRIFT_ANGLE = 10f;
|
||||
public const float MIN_DRIFT_SPEED = 30f;
|
||||
public const float MAX_DRIFT_ANGLE = 110f;
|
||||
private float smokeMul;
|
||||
private float timeMul;
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
if ( IsProxy )
|
||||
return;
|
||||
|
||||
if ( !SmokeObject.IsValid() )
|
||||
SmokeObject = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" ).Clone( new CloneConfig() { Parent = GameObject, StartEnabled = true } );
|
||||
}
|
||||
public float GetSlip()
|
||||
{
|
||||
if ( !IsOnGround )
|
||||
return 0;
|
||||
var val = Math.Abs( LateralSlip ) + Math.Abs( LongitudinalSlip );
|
||||
timeMul = timeMul.LerpTo( val, 0.1f );
|
||||
|
||||
if ( timeMul > 2 )
|
||||
return val;
|
||||
|
||||
return 0;
|
||||
}
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
base.OnUpdate();
|
||||
if ( IsProxy )
|
||||
return;
|
||||
UpdateSkid();
|
||||
SmokeObject.WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch();
|
||||
smokeMul = Math.Max( 0, GetSlip() - 3 );
|
||||
|
||||
bool density = false;
|
||||
|
||||
if ( smokeMul > 0 )
|
||||
density = true;
|
||||
var emitter = SmokeObject.Components.Get<ParticleSphereEmitter>( FindMode.EverythingInSelfAndDescendants );
|
||||
emitter.Enabled = density;
|
||||
emitter.Radius = 0f;
|
||||
emitter.Velocity = 0;
|
||||
emitter.Duration = 5;
|
||||
emitter.Burst = 5;
|
||||
float sizeMul = 0.7f + smokeMul * 0.3f;
|
||||
emitter.Rate = smokeMul * 100f;
|
||||
emitter.RateOverDistance = smokeMul * 10f;
|
||||
|
||||
var effect = SmokeObject.Components.Get<ParticleEffect>( FindMode.EverythingInSelfAndDescendants );
|
||||
|
||||
effect.MaxParticles = 500;
|
||||
effect.Damping = 0.9f;
|
||||
effect.ApplyRotation = true;
|
||||
effect.ApplyShape = true;
|
||||
|
||||
effect.Roll = new()
|
||||
{
|
||||
Type = ParticleFloat.ValueType.Range,
|
||||
Evaluation = ParticleFloat.EvaluationType.Seed,
|
||||
ConstantA = 0,
|
||||
ConstantB = 360,
|
||||
};
|
||||
effect.Scale = new()
|
||||
{
|
||||
Type = ParticleFloat.ValueType.Curve,
|
||||
Evaluation = ParticleFloat.EvaluationType.Life,
|
||||
CurveA = new( new List<Curve.Frame>() { new( 0, 10f ), new( 0.8f, 50f ), new( 1f, 160f * sizeMul ) } ),
|
||||
};
|
||||
effect.StartDelay = 0.025f + (1 - smokeMul) * 0.03f;
|
||||
|
||||
effect.ApplyColor = true;
|
||||
effect.Gradient = new()
|
||||
{
|
||||
Type = ParticleGradient.ValueType.Range,
|
||||
Evaluation = ParticleGradient.EvaluationType.Particle,
|
||||
ConstantA = Color.White,
|
||||
ConstantB = Color.Transparent,
|
||||
};
|
||||
effect.StartVelocity = new()
|
||||
{
|
||||
Type = ParticleFloat.ValueType.Range,
|
||||
Evaluation = ParticleFloat.EvaluationType.Seed,
|
||||
ConstantA = 10,
|
||||
ConstantB = 70,
|
||||
};
|
||||
effect.Force = true;
|
||||
effect.InitialVelocity = hitForwardDirection * LongitudinalSlip * 10f;
|
||||
effect.ForceDirection = 0;
|
||||
effect.SheetSequence = true;
|
||||
effect.SequenceSpeed = 0.5f;
|
||||
effect.SequenceTime = 1f;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
using Sandbox;
|
||||
using Sandbox.Rendering;
|
||||
using Sandbox.Services;
|
||||
using Sandbox.UI;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using static Sandbox.CameraComponent;
|
||||
using static Sandbox.Package;
|
||||
using static Sandbox.SkinnedModelRenderer;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
@@ -18,72 +7,67 @@ namespace VeloX;
|
||||
[Title( "VeloX - Wheel" )]
|
||||
public partial class VeloXWheel : Component
|
||||
{
|
||||
[Property, Group( "Suspension" )] public float Radius { get; set; } = 0.35f;
|
||||
[Property, Group( "Suspension" )] public float Width { get; set; } = 0.1f;
|
||||
[Property] public float Mass { get; set; } = 5;
|
||||
[Property, Group( "Suspension" )] float RestLength { get; set; } = 0.22f;
|
||||
[Property, Group( "Suspension" )] public float SpringStiffness { get; set; } = 20000.0f;
|
||||
[Property, Group( "Suspension" )] float ReboundStiffness { get; set; } = 2200;
|
||||
[Property, Group( "Suspension" )] float CompressionStiffness { get; set; } = 2400;
|
||||
[Property, Group( "Suspension" )] float BumpStopStiffness { get; set; } = 5000;
|
||||
|
||||
[Property] public float Radius { get; set; } = 15;
|
||||
[Property] public float Mass { get; set; } = 20;
|
||||
[Property] public float RollingResistance { get; set; } = 20;
|
||||
[Property] public float SlipCircleShape { get; set; } = 1.05f;
|
||||
[Property] public TirePreset TirePreset { get; set; }
|
||||
[Property] public float Width { get; set; } = 6;
|
||||
public float SideSlip { get; private set; }
|
||||
public float ForwardSlip { get; private set; }
|
||||
[Property, Group( "Traction" )] public TirePreset Tire { get; set; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
|
||||
[Property, Group( "Traction" )] public float SurfaceGrip { get; set; } = 1f;
|
||||
[Property, Group( "Traction" )] public float SurfaceResistance { get; set; } = 0.05f;
|
||||
public bool AutoSimulate = true;
|
||||
|
||||
|
||||
public float BaseInertia => Mass * (Radius * Radius); // kg·m²
|
||||
[Property] public float Inertia { get; set; } = 1.5f; // kg·m²
|
||||
public float SideSlip => SlipAngle;
|
||||
public float ForwardSlip => DynamicSlipRatio;
|
||||
[Sync] public float Torque { get; set; }
|
||||
[Sync, Range( 0, 1 )] public float Brake { get; set; }
|
||||
[Property] float BrakePowerMax { get; set; } = 3000;
|
||||
[Property] public bool IsFront { get; protected set; }
|
||||
[Property] public float SteerMultiplier { get; set; }
|
||||
[Property] public float CasterAngle { get; set; } = 7; // todo
|
||||
[Property] public float CamberAngle { get; set; } = -3;
|
||||
[Property] public float ToeAngle { get; set; } = 0.5f;
|
||||
[Property] public float Ackermann { get; set; } = 0;
|
||||
|
||||
[Property, Group( "Suspension" )] float SuspensionLength { get; set; } = 10;
|
||||
[Property, Group( "Suspension" )] float SpringStrength { get; set; } = 800;
|
||||
[Property, Group( "Suspension" )] float SpringDamper { get; set; } = 3000;
|
||||
|
||||
[Property] public bool AutoPhysics { get; set; } = true;
|
||||
|
||||
public float Spin { get; private set; }
|
||||
|
||||
public float RPM { get => angularVelocity * 30f / MathF.PI; set => angularVelocity = value / (30f / MathF.PI); }
|
||||
public float AngularVelocity { get => angularVelocity; set => angularVelocity = value; }
|
||||
|
||||
internal float DistributionFactor { get; set; }
|
||||
public float RPM { get => AngularVelocity * 60f / MathF.Tau; set => AngularVelocity = value / (60 / MathF.Tau); }
|
||||
[Sync] internal float DistributionFactor { get; set; }
|
||||
|
||||
private Vector3 StartPos { get; set; }
|
||||
private static Rotation CylinderOffset => Rotation.FromRoll( 90 );
|
||||
private static Rotation CylinderOffset = Rotation.FromRoll( 90 );
|
||||
|
||||
public SceneTraceResult Trace { get; private set; }
|
||||
public bool IsOnGround => Trace.Hit;
|
||||
[Sync] public bool IsOnGround { get; private set; }
|
||||
|
||||
private float lastSpringOffset;
|
||||
private float angularVelocity;
|
||||
private float load;
|
||||
private float lastFraction;
|
||||
|
||||
private Vector3 contactPos;
|
||||
private Vector3 forward;
|
||||
private Vector3 right;
|
||||
private Vector3 up;
|
||||
[Property] public float DriveTorque => Torque;
|
||||
[Property] public float BrakeTorque => Brake * 5000f;
|
||||
|
||||
private float forwardFriction;
|
||||
private float sideFriction;
|
||||
private Vector3 force;
|
||||
public float CounterTorque { get; private set; }
|
||||
public float Compression { get; protected set; } // meters
|
||||
public float LastLength { get; protected set; } // meters
|
||||
public float Fz { get; protected set; } // N
|
||||
public float AngularVelocity { get; protected set; } // rad/s
|
||||
public float Fx { get; protected set; } // N
|
||||
public float Fy { get; protected set; } // N
|
||||
public float RollAngle { get; protected set; } // degrees
|
||||
|
||||
internal float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
|
||||
public float Inertia
|
||||
{
|
||||
get => BaseInertia + inertia;
|
||||
set => inertia = value;
|
||||
}
|
||||
|
||||
private VeloXBase Vehicle;
|
||||
[Sync] public Vector3 ContactNormal { get; protected set; }
|
||||
[Sync] public Vector3 ContactPosition { get; protected set; }
|
||||
public float SlipRatio { get; protected set; }
|
||||
public float SlipAngle { get; protected set; }
|
||||
public float DynamicSlipRatio { get; protected set; }
|
||||
public float DynamicSlipAngle { get; protected set; }
|
||||
Rotation TransformRotationSteer => Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier );
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
Vehicle = Components.Get<VeloXBase>( FindMode.EverythingInSelfAndAncestors );
|
||||
base.OnAwake();
|
||||
if ( StartPos.IsNearZeroLength )
|
||||
StartPos = LocalPosition;
|
||||
Inertia = BaseInertia;
|
||||
}
|
||||
|
||||
internal void Update( VeloXBase vehicle, in float dt )
|
||||
@@ -93,224 +77,198 @@ public partial class VeloXWheel : Component
|
||||
|
||||
private void UpdateVisuals( VeloXBase vehicle, in float dt )
|
||||
{
|
||||
Spin -= angularVelocity.RadianToDegree() * dt;
|
||||
WorldRotation = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) ) * Rotation.FromAxis( Vector3.Right, Spin );
|
||||
WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, -RollAngle );
|
||||
LocalPosition = StartPos + Vector3.Down * LastLength.MeterToInch();
|
||||
}
|
||||
|
||||
private Rotation GetSteer( float steer )
|
||||
private struct WheelTraceData
|
||||
{
|
||||
|
||||
float angle = (-steer * SteerMultiplier).DegreeToRadian();
|
||||
|
||||
float t = MathF.Tan( (MathF.PI / 2) - angle ) - Ackermann;
|
||||
float steering_angle = MathF.CopySign( float.Pi / 2, t ) - MathF.Atan( t );
|
||||
var steering_axis = Vector3.Up * MathF.Cos( -CasterAngle.DegreeToRadian() ) +
|
||||
Vector3.Right * MathF.Sin( -CasterAngle.DegreeToRadian() );
|
||||
|
||||
return Rotation.FromAxis( Vector3.Forward, -CamberAngle ) * Rotation.FromAxis( steering_axis, steering_angle.RadianToDegree() );
|
||||
internal Vector3 ContactNormal;
|
||||
internal Vector3 ContactPosition;
|
||||
internal float Compression;
|
||||
internal float Force;
|
||||
}
|
||||
public void UpdateForce()
|
||||
{
|
||||
Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce + ContactNormal * Fz.MeterToInch() );
|
||||
}
|
||||
|
||||
|
||||
private static float GetLongitudinalLoadCoefficient( float load ) => 11000 * (1 - MathF.Exp( -0.00014f * load ));
|
||||
private static float GetLateralLoadCoefficient( float load ) => 18000 * (1 - MathF.Exp( -0.0001f * load ));
|
||||
private float inertia;
|
||||
|
||||
|
||||
private (float, float, float, float) StepLongitudinal( float Tm, float Tb, float Vx, float W, float Lc, float R, float I )
|
||||
internal void StepPhys( VeloXBase vehicle, in float dt )
|
||||
{
|
||||
float wInit = W;
|
||||
float vxAbs = Math.Abs( Vx );
|
||||
float Sx;
|
||||
if ( Lc < 0.01f )
|
||||
|
||||
var _rigidbody = vehicle.Body;
|
||||
const int numSamples = 3;
|
||||
float halfWidth = Width.MeterToInch() * 0.5f;
|
||||
|
||||
var ang = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier );
|
||||
|
||||
Vector3 right = Vector3.VectorPlaneProject( ang.Right, Vector3.Up ).Normal;
|
||||
|
||||
|
||||
int hitCount = 0;
|
||||
WheelTraceData wheelTraceData = new();
|
||||
for ( int i = 0; i < numSamples; i++ )
|
||||
{
|
||||
Sx = 0;
|
||||
float t = (float)i / (numSamples - 1);
|
||||
float offset = MathX.Lerp( -halfWidth, halfWidth, t );
|
||||
Vector3 start = vehicle.WorldTransform.PointToWorld( StartPos + Vector3.Right * offset );
|
||||
Vector3 end = start + vehicle.WorldRotation.Down * RestLength.MeterToInch();
|
||||
if ( TraceWheel( vehicle, ref wheelTraceData, start, end, Width.MeterToInch() / numSamples, dt ) )
|
||||
hitCount++;
|
||||
}
|
||||
else if ( vxAbs >= 0.01f )
|
||||
|
||||
|
||||
if ( hitCount > 0 )
|
||||
{
|
||||
Sx = (W * R - Vx) / vxAbs;
|
||||
|
||||
IsOnGround = true;
|
||||
|
||||
//// Average all contacts
|
||||
Fz = Math.Max( wheelTraceData.Force / hitCount, 0 );
|
||||
Compression = wheelTraceData.Compression / hitCount;
|
||||
ContactNormal = (wheelTraceData.ContactNormal / hitCount).Normal;
|
||||
ContactPosition = wheelTraceData.ContactPosition / hitCount;
|
||||
//DoSuspensionSounds( vehicle, (RestLength - Compression) * 0.8f);
|
||||
LastLength = RestLength - Compression;
|
||||
|
||||
//DebugOverlay.Normal( ContactPosition, ContactNormal * Fz / 1000 );
|
||||
// Apply suspension force
|
||||
//_rigidbody.ApplyForceAt( ContactPosition, ContactNormal * Fz.MeterToInch() );
|
||||
|
||||
UpdateHitVariables();
|
||||
// Friction
|
||||
|
||||
Vector3 forward = ContactNormal.Cross( right ).Normal;
|
||||
right = Rotation.FromAxis( ContactNormal, 90f ) * forward;
|
||||
|
||||
var velAtContact = _rigidbody.GetVelocityAtPoint( ContactPosition + vehicle.Body.MassCenter ) * 0.0254f;
|
||||
float vx = Vector3.Dot( velAtContact, forward );
|
||||
float vy = Vector3.Dot( velAtContact, right );
|
||||
float wheelLinearVel = AngularVelocity * Radius;
|
||||
|
||||
float creepVel = 0.5f;
|
||||
SlipRatio = (wheelLinearVel - vx) / (MathF.Abs( vx ) + creepVel);
|
||||
SlipAngle = MathX.RadianToDegree( MathF.Atan2( vy, MathF.Abs( vx ) + creepVel ) );
|
||||
|
||||
float latCoeff = 1.0f - MathF.Exp( -MathF.Max( MathF.Abs( vx ), 1f ) * dt / 0.01f );
|
||||
float longCoeff = 1.0f - MathF.Exp( -MathF.Max( MathF.Abs( vx ), 1f ) * dt / 0.01f );
|
||||
|
||||
DynamicSlipAngle += (SlipAngle - DynamicSlipAngle) * latCoeff;
|
||||
DynamicSlipRatio += (SlipRatio - DynamicSlipRatio) * longCoeff;
|
||||
|
||||
|
||||
UpdateFriction( dt );
|
||||
}
|
||||
else
|
||||
{
|
||||
Sx = (W * R - Vx) * 0.6f;
|
||||
IsOnGround = false;
|
||||
// Wheel is off the ground
|
||||
Compression = 0f;
|
||||
Fz = 0f;
|
||||
Fx = 0f;
|
||||
Fy = 0f;
|
||||
ContactNormal = Vector3.Up;
|
||||
ContactPosition = WorldPosition;
|
||||
}
|
||||
|
||||
Sx = Math.Clamp( Sx, -1, 1 );
|
||||
|
||||
W += Tm / I * Time.Delta;
|
||||
|
||||
Tb *= W > 0 ? -1 : 1;
|
||||
|
||||
float tbCap = Math.Abs( W ) * I / Time.Delta;
|
||||
float tbr = Math.Abs( Tb ) - Math.Abs( tbCap );
|
||||
tbr = Math.Max( tbr, 0 );
|
||||
|
||||
Tb = Math.Clamp( Tb, -tbCap, tbCap );
|
||||
|
||||
W += Tb / I * Time.Delta;
|
||||
|
||||
float maxTorque = TirePreset.Pacejka.PacejkaFx( Math.Abs( Sx ) ) * Lc * R;
|
||||
|
||||
float errorTorque = (W - Vx / R) * I / Time.Delta;
|
||||
float surfaceTorque = Math.Clamp( errorTorque, -maxTorque, maxTorque );
|
||||
|
||||
W -= surfaceTorque / I * Time.Delta;
|
||||
float Fx = surfaceTorque / R;
|
||||
|
||||
tbr *= (W > 0 ? -1 : 1);
|
||||
float TbCap2 = Math.Abs( W ) * I / Time.Delta;
|
||||
float Tb2 = Math.Clamp( tbr, -TbCap2, TbCap2 );
|
||||
W += Tb2 / I * Time.Delta;
|
||||
|
||||
float deltaOmegaTorque = (W - wInit) * I / Time.Delta;
|
||||
float Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque;
|
||||
|
||||
return (W, Sx, Fx, Tcnt);
|
||||
}
|
||||
|
||||
private void StepLateral( float Vx, float Vy, float Lc, out float Sy, out float Fy )
|
||||
const string playerTag = "player";
|
||||
|
||||
private bool TraceWheel( VeloXBase vehicle, ref WheelTraceData wheelTraceData, Vector3 start, Vector3 end, float width, in float dt )
|
||||
{
|
||||
float VxAbs = Math.Abs( Vx );
|
||||
|
||||
if ( Lc < 0.01f )
|
||||
{
|
||||
Sy = 0;
|
||||
}
|
||||
else if ( VxAbs > 0.1f )
|
||||
var trace = Scene.Trace
|
||||
.FromTo( start, end )
|
||||
.Cylinder( width, Radius.MeterToInch() )
|
||||
.Rotated( vehicle.WorldRotation * CylinderOffset )
|
||||
.UseHitPosition( false )
|
||||
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
|
||||
.WithoutTags( playerTag )
|
||||
.Run();
|
||||
|
||||
//DebugOverlay.Trace( trace, overlay: true );
|
||||
if ( trace.Hit )
|
||||
{
|
||||
|
||||
Sy = MathX.RadianToDegree( MathF.Atan( Vy / VxAbs ) ) / 50;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 contactPos = trace.EndPosition;
|
||||
Vector3 contactNormal = trace.Normal;
|
||||
float currentLength = trace.Distance.InchToMeter();
|
||||
float compression = (RestLength - currentLength).Clamp( -RestLength, RestLength );
|
||||
|
||||
Sy = Vy * (0.003f / Time.Delta);
|
||||
// Nonlinear spring
|
||||
float springForce = SpringStiffness * compression * (1f + 2f * compression / RestLength);
|
||||
|
||||
// Damping
|
||||
float damperVelocity = (LastLength - currentLength) / dt;
|
||||
float damperForce = damperVelocity > 0 ? damperVelocity * ReboundStiffness : damperVelocity * CompressionStiffness;
|
||||
|
||||
float FzPoint = springForce + damperForce;
|
||||
|
||||
// Bump stop
|
||||
float minCompression = -0.05f;
|
||||
if ( compression <= minCompression ) FzPoint += (minCompression - compression) * BumpStopStiffness;
|
||||
|
||||
wheelTraceData.ContactNormal += contactNormal;
|
||||
wheelTraceData.ContactPosition += contactPos;
|
||||
wheelTraceData.Compression += compression;
|
||||
wheelTraceData.Force += Math.Max( 0, FzPoint );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Sy = Math.Clamp( Sy, -1, 1 );
|
||||
float slipSign = Sy < 0 ? -1 : 1;
|
||||
Fy = -slipSign * TirePreset.Pacejka.PacejkaFy( Math.Abs( Sy ) ) * Lc;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SlipCircle( float Sx, float Sy, float Fx, ref float Fy )
|
||||
|
||||
public void DoPhysics( in float dt )
|
||||
{
|
||||
float SxAbs = Math.Abs( Sx );
|
||||
if ( SxAbs > 0.01f )
|
||||
{
|
||||
float SxClamped = Math.Clamp( Sx, -1, 1 );
|
||||
float SyClamped = Math.Clamp( Sy, -1, 1 );
|
||||
Vector2 combinedSlip = new( SxClamped * 1.05f, SyClamped );
|
||||
Vector2 slipDir = combinedSlip.Normal;
|
||||
|
||||
float F = MathF.Sqrt( Fx * Fx + Fy * Fy );
|
||||
float absSlipDirY = Math.Abs( slipDir.y );
|
||||
|
||||
Fy = F * absSlipDirY * (Fy < 0 ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void DoPhysics( VeloXBase vehicle )
|
||||
{
|
||||
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
|
||||
|
||||
var ang = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) );
|
||||
|
||||
var maxLen = SuspensionLength;
|
||||
|
||||
var endPos = pos + ang.Down * maxLen;
|
||||
Trace = Scene.Trace
|
||||
.IgnoreGameObjectHierarchy( vehicle.GameObject )
|
||||
.Cylinder( Width, Radius, pos, endPos )
|
||||
.Rotated( vehicle.WorldTransform.Rotation * CylinderOffset )
|
||||
.UseRenderMeshes( false )
|
||||
.UseHitPosition( true )
|
||||
.WithoutTags( vehicle.WheelIgnoredTags )
|
||||
.Run();
|
||||
|
||||
forward = Vector3.VectorPlaneProject( ang.Forward, Trace.Normal );
|
||||
right = Vector3.VectorPlaneProject( ang.Right, Trace.Normal );
|
||||
|
||||
var fraction = Trace.Fraction;
|
||||
|
||||
contactPos = pos - maxLen * fraction * ang.Up;
|
||||
|
||||
LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );
|
||||
|
||||
DoSuspensionSounds( vehicle, fraction - lastFraction );
|
||||
lastFraction = fraction;
|
||||
|
||||
if ( !IsOnGround )
|
||||
if ( IsProxy )
|
||||
return;
|
||||
|
||||
var vel = vehicle.Body.GetVelocityAtPoint( contactPos );
|
||||
|
||||
var offset = maxLen - (fraction * maxLen);
|
||||
var springForce = offset * SpringStrength;
|
||||
var damperForce = (lastSpringOffset - offset) * SpringDamper;
|
||||
|
||||
lastSpringOffset = offset;
|
||||
|
||||
|
||||
var velU = Trace.Normal.Dot( vel );
|
||||
|
||||
if ( velU < 0 && offset + Math.Abs( velU * Time.Delta ) > SuspensionLength )
|
||||
{
|
||||
vehicle.Body.CalculateVelocityOffset( -velU / Time.Delta * Trace.Normal, pos, out var linearImp, out var angularImp );
|
||||
|
||||
vehicle.Body.Velocity += linearImp;
|
||||
vehicle.Body.AngularVelocity += angularImp;
|
||||
vehicle.Body.CalculateVelocityOffset( Trace.HitPosition - (contactPos + Trace.Normal * velU * Time.Delta), pos, out var lin, out _ );
|
||||
|
||||
vehicle.WorldPosition += lin / Time.Delta;
|
||||
damperForce = 0;
|
||||
|
||||
}
|
||||
|
||||
force = (springForce - damperForce) * Trace.Normal;
|
||||
|
||||
load = Math.Max( springForce - damperForce, 0 );
|
||||
float R = Radius.InchToMeter();
|
||||
|
||||
float forwardSpeed = vel.Dot( forward ).InchToMeter();
|
||||
float sideSpeed = vel.Dot( right ).InchToMeter();
|
||||
|
||||
float longitudinalLoadCoefficient = GetLongitudinalLoadCoefficient( load );
|
||||
float lateralLoadCoefficient = GetLateralLoadCoefficient( load );
|
||||
|
||||
float F_roll = TirePreset.GetRollingResistance( angularVelocity * R, 1.0f ) * 10000;
|
||||
|
||||
( float W, float Sx, float Fx, float counterTq) = StepLongitudinal(
|
||||
Torque,
|
||||
Brake * BrakePowerMax + F_roll,
|
||||
forwardSpeed,
|
||||
angularVelocity,
|
||||
longitudinalLoadCoefficient,
|
||||
R,
|
||||
Inertia
|
||||
);
|
||||
|
||||
StepLateral( forwardSpeed, sideSpeed, lateralLoadCoefficient, out float Sy, out float Fy );
|
||||
|
||||
SlipCircle( Sx, Sy, Fx, ref Fy );
|
||||
|
||||
CounterTorque = counterTq;
|
||||
angularVelocity = W;
|
||||
|
||||
force += forward * Fx;
|
||||
force += right * Fy * Math.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 );
|
||||
ForwardSlip = Sx;
|
||||
SideSlip = Sy * Math.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 );
|
||||
|
||||
vehicle.Body.ApplyForceAt( pos, force / Time.Delta );
|
||||
|
||||
StepRotation( Vehicle, dt );
|
||||
if ( AutoSimulate )
|
||||
StepPhys( Vehicle, dt );
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
protected override void OnUpdate()
|
||||
private void StepRotation( VeloXBase vehicle, in float dt )
|
||||
{
|
||||
DebugOverlay.Normal( contactPos, forward * forwardFriction, Color.Red, overlay: true );
|
||||
DebugOverlay.Normal( contactPos, right * sideFriction, Color.Green, overlay: true );
|
||||
DebugOverlay.Normal( contactPos, up * force / 1000f, Color.Blue, overlay: true );
|
||||
}
|
||||
#endif
|
||||
float inertia = MathF.Max( 1f, Inertia );
|
||||
|
||||
float roadTorque = Fx * Radius;
|
||||
float externalTorque = DriveTorque - roadTorque;
|
||||
float rollingResistanceTorque = Fz * Radius * SurfaceResistance;
|
||||
|
||||
const float HubCoulombNm = 20f;
|
||||
const float HubViscous = 0.1f;
|
||||
|
||||
float coulombTorque = BrakeTorque + rollingResistanceTorque + HubCoulombNm;
|
||||
|
||||
float omega = AngularVelocity;
|
||||
|
||||
if ( MathF.Abs( omega ) < 1e-6f && MathF.Abs( externalTorque ) <= coulombTorque )
|
||||
{
|
||||
AngularVelocity = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// viscous decay
|
||||
if ( HubViscous > 0f ) omega *= MathF.Exp( -(HubViscous / inertia) * dt );
|
||||
|
||||
// Coulomb drag
|
||||
if ( coulombTorque > 0f && omega != 0f )
|
||||
{
|
||||
float dir = MathF.Sign( omega );
|
||||
float deltaOmega = (coulombTorque / inertia) * dt;
|
||||
if ( deltaOmega >= MathF.Abs( omega ) ) omega = 0f;
|
||||
else omega -= dir * deltaOmega;
|
||||
}
|
||||
|
||||
if ( MathF.Abs( omega ) < 0.01f ) omega = 0f;
|
||||
}
|
||||
AngularVelocity = omega; // wider sanity range
|
||||
|
||||
RollAngle += MathX.RadianToDegree( AngularVelocity ) * dt;
|
||||
RollAngle = (RollAngle % 360f + 360f) % 360f;
|
||||
}
|
||||
}
|
||||
|
||||
58
Code/Base/Wheel/WheelManager.cs
Normal file
58
Code/Base/Wheel/WheelManager.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
namespace VeloX;
|
||||
|
||||
internal sealed class WheelManager : GameObjectSystem
|
||||
{
|
||||
|
||||
|
||||
public WheelManager( Scene scene ) : base( scene )
|
||||
{
|
||||
Listen( Stage.StartFixedUpdate, -99, UpdateWheels, "UpdateWheels" );
|
||||
Listen( Stage.StartFixedUpdate, -100, UpdateEngine, "UpdateEngine" );
|
||||
}
|
||||
|
||||
private void UpdateWheels()
|
||||
{
|
||||
if ( !Game.IsPlaying )
|
||||
return;
|
||||
//Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
var wheels = Scene.GetAll<VeloXWheel>();
|
||||
if ( !wheels.Any() ) return;
|
||||
|
||||
var timeDelta = Time.Delta;
|
||||
Sandbox.Utility.Parallel.ForEach( wheels, item =>
|
||||
{
|
||||
if ( !item.IsProxy )
|
||||
item.DoPhysics( timeDelta );
|
||||
} );
|
||||
foreach ( var wheel in wheels )
|
||||
wheel.UpdateForce();
|
||||
|
||||
//sw.Stop();
|
||||
|
||||
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 30 ), $"Wheel Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );
|
||||
}
|
||||
private void UpdateEngine()
|
||||
{
|
||||
if ( !Game.IsPlaying )
|
||||
return;
|
||||
//Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
var engines = Scene.GetAll<Engine>();
|
||||
if ( !engines.Any() ) return;
|
||||
|
||||
var timeDelta = Time.Delta;
|
||||
Sandbox.Utility.Parallel.ForEach( engines, item =>
|
||||
{
|
||||
if ( !item.IsProxy )
|
||||
item.UpdateEngine( timeDelta );
|
||||
} );
|
||||
//sw.Stop();
|
||||
|
||||
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 54 ), $"Engine Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user