355 lines
9.4 KiB
C#
355 lines
9.4 KiB
C#
using Sandbox;
|
|
using Sandbox.UI;
|
|
using System;
|
|
using System.Buffers.Text;
|
|
using System.Numerics;
|
|
using System.Runtime.Intrinsics.Arm;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
|
|
namespace VeloX;
|
|
|
|
[Group( "VeloX" )]
|
|
[Title( "VeloX - Wheel" )]
|
|
public partial class VeloXWheel : Component
|
|
{
|
|
|
|
[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;
|
|
|
|
public FrictionPreset LongitudinalFrictionPreset => WheelFriction.Longitudinal;
|
|
public FrictionPreset LateralFrictionPreset => WheelFriction.Lateral;
|
|
public FrictionPreset AligningFrictionPreset => WheelFriction.Aligning;
|
|
|
|
[Property] public WheelFriction WheelFriction { get; set; }
|
|
|
|
|
|
[Property] public float Width { get; set; } = 6;
|
|
[Sync] public float SideSlip { get; private set; }
|
|
[Sync] public float ForwardSlip { get; private set; }
|
|
[Sync, Range( 0, 1 )] public float BrakePower { get; set; }
|
|
[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, Group( "Suspension" )] float SuspensionLength { get; set; } = 10;
|
|
[Property, Group( "Suspension" )] float SpringStrength { get; set; } = 800;
|
|
[Property, Group( "Suspension" )] float SpringDamper { get; set; } = 3000;
|
|
|
|
public float Spin { get; private set; }
|
|
|
|
public float RPM { get => angularVelocity * 60f / MathF.Tau; set => angularVelocity = value / (60 / MathF.Tau); }
|
|
internal float DistributionFactor { get; set; }
|
|
|
|
private Vector3 StartPos { get; set; }
|
|
private static Rotation CylinderOffset => Rotation.FromRoll( 90 );
|
|
|
|
public SceneTraceResult Trace { get; private set; }
|
|
public bool IsOnGround => Trace.Hit;
|
|
|
|
private float lastSpringOffset;
|
|
private float angularVelocity;
|
|
private float load;
|
|
private float lastFraction;
|
|
private RealTimeUntil expandSoundCD;
|
|
private RealTimeUntil contractSoundCD;
|
|
|
|
private Vector3 contactPos;
|
|
private Vector3 forward;
|
|
private Vector3 right;
|
|
private Vector3 up;
|
|
|
|
private Friction forwardFriction;
|
|
private Friction sideFriction;
|
|
private Vector3 force;
|
|
|
|
private float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
|
|
private float Inertia => BaseInertia;
|
|
|
|
protected override void OnAwake()
|
|
{
|
|
base.OnAwake();
|
|
if ( StartPos.IsNearZeroLength )
|
|
StartPos = LocalPosition;
|
|
}
|
|
|
|
private void DoSuspensionSounds( VeloXBase vehicle, float change )
|
|
{
|
|
if ( change > 0.1f && expandSoundCD )
|
|
{
|
|
expandSoundCD = 0.3f;
|
|
var sound = Sound.Play( vehicle.SuspensionUpSound, WorldPosition );
|
|
sound.Volume = Math.Clamp( Math.Abs( change ) * 5f, 0, 0.5f );
|
|
}
|
|
|
|
if ( change < -0.1f && contractSoundCD )
|
|
{
|
|
contractSoundCD = 0.3f;
|
|
change = MathF.Abs( change );
|
|
var sound = Sound.Play( change > 0.3f ? vehicle.SuspensionHeavySound : vehicle.SuspensionDownSound, WorldPosition );
|
|
sound.Volume = Math.Clamp( change * 5f, 0, 1 );
|
|
}
|
|
}
|
|
|
|
internal void Update( VeloXBase vehicle, in float dt )
|
|
{
|
|
UpdateVisuals( vehicle, dt );
|
|
}
|
|
|
|
private void UpdateVisuals( VeloXBase vehicle, in float dt )
|
|
{
|
|
var entityAngles = vehicle.WorldRotation;
|
|
|
|
Spin -= angularVelocity.MeterToInch() * dt;
|
|
|
|
|
|
var steerRotated = entityAngles.RotateAroundAxis( Vector3.Up, vehicle.SteerAngle.yaw * SteerMultiplier + ToeAngle );
|
|
var camberRotated = steerRotated.RotateAroundAxis( Vector3.Forward, -CamberAngle );
|
|
var angularVelocityRotated = camberRotated.RotateAroundAxis( Vector3.Right, Spin );
|
|
|
|
WorldRotation = angularVelocityRotated;
|
|
|
|
}
|
|
|
|
private (float, float, float, float) StepLongitudinal( float Vx, float Lc, float kFx, float kSx, float dt )
|
|
{
|
|
float Tm = Torque;
|
|
float Tb = Brake * BrakePowerMax + RollingResistance;
|
|
float R = Radius.InchToMeter();
|
|
float I = Inertia;
|
|
|
|
float Winit = angularVelocity;
|
|
float W = angularVelocity;
|
|
|
|
float VxAbs = MathF.Abs( Vx );
|
|
float Sx;
|
|
if ( VxAbs >= 0.1f )
|
|
Sx = (Vx - W * R) / VxAbs;
|
|
|
|
else
|
|
Sx = (Vx - W * R) * 0.6f;
|
|
|
|
Sx = Math.Clamp( Sx * kSx, -1, 1 );
|
|
|
|
W += Tm / I * dt;
|
|
|
|
Tb *= W > 0 ? -1 : 1;
|
|
|
|
float TbCap = MathF.Abs( W ) * I / dt;
|
|
float Tbr = MathF.Abs( Tb ) - MathF.Abs( TbCap );
|
|
Tbr = MathF.Max( Tbr, 0 );
|
|
Tb = Math.Clamp( Tb, -TbCap, TbCap );
|
|
W += Tb / I * dt;
|
|
|
|
float maxTorque = LongitudinalFrictionPreset.Evaluate( Sx ) * Lc * kFx;
|
|
|
|
float errorTorque = (W - Vx / R) * I / dt;
|
|
|
|
float surfaceTorque = MathX.Clamp( errorTorque, -maxTorque, maxTorque );
|
|
|
|
W -= surfaceTorque / I * dt;
|
|
|
|
float Fx = surfaceTorque / R;
|
|
|
|
|
|
Tbr *= W > 0 ? -1 : 1;
|
|
float TbCap2 = MathF.Abs( W ) * I / dt;
|
|
|
|
float Tb2 = Math.Clamp( Tbr, -TbCap2, TbCap2 );
|
|
|
|
W += Tb2 / I * dt;
|
|
|
|
float deltaOmegaTorque = (W - Winit) * I / dt;
|
|
|
|
float Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque;
|
|
if ( Lc < 0.001f )
|
|
Sx = 0;
|
|
|
|
return (W, Sx, Fx, Tcnt);
|
|
}
|
|
|
|
private (float, float) StepLateral( float Vx, float Vy, float Lc, float kFy, float kSy, float dt )
|
|
{
|
|
float VxAbs = MathF.Abs( Vx );
|
|
float Sy;
|
|
|
|
if ( VxAbs > 0.1f )
|
|
Sy = MathF.Atan( Vy / VxAbs ).RadianToDegree() * 0.01111f;
|
|
else
|
|
Sy = Vy * (0.003f / dt);
|
|
|
|
|
|
Sy *= kSy * 0.95f;
|
|
Sy = Math.Clamp( Sy * kSy, -1, 1 );
|
|
float Fy = -MathF.Sign( Sy ) * LateralFrictionPreset.Evaluate( Sy ) * Lc * kFy;
|
|
if ( Lc < 0.0001f )
|
|
Sy = 0;
|
|
|
|
return (Sy, Fy);
|
|
}
|
|
|
|
private void SlipCircle( float Sx, float Sy, float Fx, ref float Fy )
|
|
{
|
|
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 * SlipCircleShape,
|
|
SyClamped
|
|
);
|
|
|
|
Vector2 slipDir = combinedSlip.Normal;
|
|
|
|
float F = MathF.Sqrt( Fx * Fx + Fy * Fy );
|
|
|
|
float absSlipDirY = MathF.Abs( slipDir.y );
|
|
|
|
Fy = F * absSlipDirY * MathF.Sign( Fy );
|
|
}
|
|
}
|
|
|
|
|
|
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 ));
|
|
|
|
public void DoPhysics( VeloXBase vehicle, in float dt )
|
|
{
|
|
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
|
|
|
|
var ang = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier );
|
|
|
|
forward = ang.Forward;
|
|
right = ang.Right;
|
|
up = ang.Up;
|
|
|
|
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( false )
|
|
.WithoutTags( vehicle.WheelIgnoredTags )
|
|
.Run();
|
|
|
|
var fraction = Trace.Fraction;
|
|
|
|
contactPos = pos - maxLen * fraction * up;
|
|
|
|
LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );
|
|
|
|
DoSuspensionSounds( vehicle, fraction - lastFraction );
|
|
lastFraction = fraction;
|
|
|
|
var normal = Trace.Normal;
|
|
|
|
var vel = vehicle.Body.GetVelocityAtPoint( pos );
|
|
|
|
vel.x = vel.x.InchToMeter();
|
|
vel.y = vel.y.InchToMeter();
|
|
vel.z = vel.z.InchToMeter();
|
|
|
|
if ( !IsOnGround )
|
|
{
|
|
SideSlip = 0;
|
|
ForwardSlip = 0;
|
|
return;
|
|
}
|
|
|
|
var offset = maxLen - (fraction * maxLen);
|
|
var springForce = (offset * SpringStrength);
|
|
var damperForce = (lastSpringOffset - offset) * SpringDamper;
|
|
lastSpringOffset = offset;
|
|
force = (springForce - damperForce) * MathF.Max( 0, up.Dot( normal ) ) * normal / dt;
|
|
|
|
// Vector3.CalculateVelocityOffset is broken (need fix)
|
|
//var velU = normal.Dot( vel ).MeterToInch();
|
|
//if ( velU < 0 && offset + Math.Abs( velU * dt ) > SuspensionLength )
|
|
//{
|
|
// var (linearVel, angularVel) = vehicle.Body.PhysicsBody.CalculateVelocityOffset( (-velU.InchToMeter() / dt) * normal, pos );
|
|
// vehicle.Body.Velocity += linearVel;
|
|
// vehicle.Body.AngularVelocity += angularVel;
|
|
//}
|
|
|
|
|
|
load = springForce - damperForce;
|
|
load = Math.Max( load, 0 );
|
|
|
|
var longitudinalLoadCoefficient = GetLongitudinalLoadCoefficient( load );
|
|
var lateralLoadCoefficient = GetLateralLoadCoefficient( load );
|
|
|
|
float forwardSpeed = 0;
|
|
float sideSpeed = 0;
|
|
|
|
|
|
if ( IsOnGround )
|
|
{
|
|
forwardSpeed = vel.Dot( forward );
|
|
sideSpeed = vel.Dot( right );
|
|
}
|
|
|
|
(float W, float Sx, float Fx, float _) = StepLongitudinal(
|
|
forwardSpeed,
|
|
longitudinalLoadCoefficient,
|
|
0.95f,
|
|
0.9f,
|
|
dt
|
|
);
|
|
|
|
(float Sy, float Fy) = StepLateral(
|
|
forwardSpeed,
|
|
sideSpeed,
|
|
lateralLoadCoefficient,
|
|
0.95f,
|
|
0.9f,
|
|
dt
|
|
);
|
|
|
|
SlipCircle( Sx, Sy, Fx, ref Fy );
|
|
angularVelocity = W;
|
|
|
|
forwardFriction = new Friction()
|
|
{
|
|
Slip = Sx,
|
|
Force = Fx.MeterToInch(),
|
|
Speed = forwardSpeed
|
|
};
|
|
|
|
sideFriction = new Friction()
|
|
{
|
|
Slip = Sy,
|
|
Force = Fy.MeterToInch(),
|
|
Speed = sideSpeed
|
|
};
|
|
|
|
|
|
var frictionforce = right * sideFriction.Force + forward * forwardFriction.Force;
|
|
|
|
vehicle.Body.ApplyForceAt( contactPos, force + frictionforce );
|
|
|
|
}
|
|
|
|
|
|
// debug
|
|
protected override void OnUpdate()
|
|
{
|
|
DebugOverlay.Normal( contactPos, forward * forwardFriction.Force / 10000f, Color.Red, overlay: true );
|
|
DebugOverlay.Normal( contactPos, right * sideFriction.Force / 10000f, Color.Green, overlay: true );
|
|
DebugOverlay.Normal( contactPos, up * force / 50000f, Color.Blue, overlay: true );
|
|
}
|
|
}
|