306 lines
9.7 KiB
C#
306 lines
9.7 KiB
C#
using Sandbox;
|
|
using Sandbox.Rendering;
|
|
using Sandbox.Services;
|
|
using System;
|
|
using System.Collections.Specialized;
|
|
using System.Numerics;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using static Sandbox.CameraComponent;
|
|
using static Sandbox.Package;
|
|
using static Sandbox.SkinnedModelRenderer;
|
|
|
|
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;
|
|
[Property] public TirePreset TirePreset { get; set; }
|
|
[Property] public float Width { get; set; } = 6;
|
|
public float SideSlip => sideFriction.Slip.MeterToInch();
|
|
public float ForwardSlip => forwardFriction.Slip.MeterToInch();
|
|
[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; }
|
|
|
|
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 Vector3 contactPos;
|
|
private Vector3 forward;
|
|
private Vector3 right;
|
|
private Vector3 up;
|
|
|
|
private Friction forwardFriction;
|
|
private Friction sideFriction;
|
|
private Vector3 force;
|
|
public float CounterTorque { get; private set; }
|
|
|
|
internal float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
|
|
public float Inertia
|
|
{
|
|
get => BaseInertia + inertia;
|
|
set => inertia = value;
|
|
}
|
|
|
|
|
|
protected override void OnAwake()
|
|
{
|
|
base.OnAwake();
|
|
if ( StartPos.IsNearZeroLength )
|
|
StartPos = LocalPosition;
|
|
}
|
|
|
|
internal void Update( VeloXBase vehicle, in float dt )
|
|
{
|
|
UpdateVisuals( vehicle, dt );
|
|
}
|
|
|
|
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 );
|
|
}
|
|
|
|
private Rotation GetSteer( float steer )
|
|
{
|
|
|
|
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() );
|
|
}
|
|
|
|
|
|
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 ));
|
|
float lastload;
|
|
private float inertia;
|
|
|
|
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( false )
|
|
.WithoutTags( vehicle.WheelIgnoredTags )
|
|
.Run();
|
|
//forward = ang.Forward;
|
|
//right = ang.Right;
|
|
up = ang.Up;
|
|
|
|
forward = Vector3.VectorPlaneProject( ang.Forward, Trace.Normal ).Normal;
|
|
right = Vector3.VectorPlaneProject( ang.Right, Trace.Normal ).Normal;
|
|
|
|
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( contactPos );
|
|
|
|
//var vel = vehicle.Body.GetVelocityAtPoint( pos );
|
|
|
|
if ( !IsOnGround )
|
|
{
|
|
forwardFriction = new Friction();
|
|
sideFriction = new Friction();
|
|
return;
|
|
}
|
|
|
|
var offset = maxLen - (fraction * maxLen);
|
|
var springForce = (offset * SpringStrength);
|
|
var damperForce = (lastSpringOffset - offset) * SpringDamper;
|
|
lastSpringOffset = offset;
|
|
|
|
// Vector3.CalculateVelocityOffset is broken (need fix)
|
|
var velU = normal.Dot( vel );
|
|
if ( velU < 0 && offset + Math.Abs( velU * Time.Delta ) > SuspensionLength )
|
|
{
|
|
var impulse = (-velU / Time.Delta) * normal;
|
|
var body = vehicle.Body.PhysicsBody;
|
|
Vector3 com = body.MassCenter;
|
|
Rotation bodyRot = body.Rotation;
|
|
|
|
Vector3 r = pos - com;
|
|
|
|
Vector3 torque = Vector3.Cross( r, impulse );
|
|
|
|
Vector3 torqueLocal = bodyRot.Inverse * torque;
|
|
Vector3 angularVelocityLocal = torqueLocal * body.Inertia.Inverse;
|
|
|
|
var centerAngularVelocity = bodyRot * angularVelocityLocal;
|
|
|
|
var centerVelocity = impulse * (1 / body.Mass);
|
|
|
|
vehicle.Body.Velocity += centerVelocity * 10;
|
|
vehicle.Body.AngularVelocity += centerAngularVelocity;
|
|
damperForce = 0;
|
|
}
|
|
|
|
force = (springForce - damperForce) * MathF.Max( 0, up.Dot( normal ) ) * normal;
|
|
|
|
load = Math.Max( force.z, 0 ).InchToMeter();
|
|
|
|
if ( IsOnGround )
|
|
{
|
|
float forwardSpeed = vel.Dot( forward ).InchToMeter();
|
|
float sideSpeed = vel.Dot( right ).InchToMeter();
|
|
|
|
float camber_rad = CamberAngle.DegreeToRadian();
|
|
float R = Radius.InchToMeter();
|
|
|
|
//TirePreset.ComputeState(
|
|
// load,
|
|
// angularVelocity,
|
|
// forwardSpeed,
|
|
// sideSpeed,
|
|
// camber_rad,
|
|
// R,
|
|
// Inertia,
|
|
// out var tireState
|
|
//);
|
|
|
|
float linearSpeed = angularVelocity * Radius.InchToMeter();
|
|
float F_roll = TirePreset.GetRollingResistance( linearSpeed, 1.0f );
|
|
F_roll *= -Math.Clamp( linearSpeed * 0.25f, -1, 1 );
|
|
|
|
//float Fx_total = tireState.fx + F_roll;
|
|
|
|
float T_brake = Brake * BrakePowerMax;
|
|
|
|
if ( angularVelocity > 0 ) T_brake = -T_brake;
|
|
else T_brake = angularVelocity < 0 ? T_brake : -MathF.Sign( Torque ) * T_brake;
|
|
//float totalTorque = Torque + tireState.fx;
|
|
|
|
angularVelocity += Torque / Inertia * Time.Delta;
|
|
angularVelocity += T_brake / Inertia * Time.Delta;
|
|
angularVelocity += F_roll * 9.80665f / Inertia * Time.Delta;
|
|
TirePreset.ComputeSlip( forwardSpeed, sideSpeed, angularVelocity, R, out var slip, out var slip_ang );
|
|
var fx = TirePreset.Pacejka.PacejkaFx( slip, load, 1, out var maxTorque );
|
|
var fy = TirePreset.Pacejka.PacejkaFy( slip_ang * load, load, camber_rad, 1, out var _ );
|
|
|
|
maxTorque *= R;
|
|
var errorTorque = (angularVelocity - forwardSpeed / R) * Inertia / Time.Delta;
|
|
|
|
var surfaceTorque = Math.Clamp( errorTorque, -maxTorque, maxTorque );
|
|
|
|
angularVelocity -= surfaceTorque / Inertia * Time.Delta;
|
|
|
|
forwardFriction = new Friction()
|
|
{
|
|
Slip = slip,
|
|
Force = -fx,
|
|
Speed = forwardSpeed
|
|
};
|
|
|
|
sideFriction = new Friction()
|
|
{
|
|
Slip = slip_ang,
|
|
Force = fy,
|
|
Speed = sideSpeed
|
|
};
|
|
|
|
Vector3 frictionForce = forward * forwardFriction.Force + right * sideFriction.Force;
|
|
|
|
vehicle.Body.ApplyForceAt( pos, force / Time.Delta * ProjectSettings.Physics.SubSteps );
|
|
vehicle.Body.ApplyForceAt( pos, frictionForce * ProjectSettings.Physics.SubSteps );
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
//todo
|
|
protected (float Mass, float Inertia) CalcMassAndInertia()
|
|
{
|
|
// section width in millimeters, measured from sidewall to sidewall
|
|
// ratio of sidewall height to section width in percent
|
|
// diameter of the wheel in inches
|
|
var tire_size = new Vector3( 215, 45, 17 );
|
|
|
|
float tire_width = tire_size[0] * 0.001f;
|
|
float tire_aspect_ratio = tire_size[1] * 0.01f;
|
|
float tire_radius = tire_size[2] * 0.5f * 0.0254f + tire_width * tire_aspect_ratio;
|
|
float tire_thickness = 0.02f;
|
|
float tire_density = 1000; // rubber
|
|
|
|
float rim_radius = tire_radius - tire_width * tire_aspect_ratio;
|
|
float rim_width = tire_width;
|
|
float rim_thickness = 0.01f;
|
|
float rim_density = 2700; // aluminium
|
|
|
|
float tire_volume = float.Pi * tire_width * tire_thickness * (2 * tire_radius - tire_thickness);
|
|
float rim_volume = float.Pi * rim_width * rim_thickness * (2 * rim_radius - rim_thickness);
|
|
float tire_mass = tire_density * tire_volume;
|
|
float rim_mass = rim_density * rim_volume;
|
|
float tire_inertia = tire_mass * tire_radius * tire_radius;
|
|
float rim_inertia = rim_mass * rim_radius * rim_radius;
|
|
|
|
float mass = tire_mass + rim_mass;
|
|
float inertia = tire_inertia + rim_inertia;
|
|
|
|
return (mass, inertia);
|
|
}
|
|
|
|
// debug
|
|
protected override void OnUpdate()
|
|
{
|
|
DebugOverlay.Normal( contactPos, forward * forwardFriction.Force / 1000f, Color.Red, overlay: true );
|
|
DebugOverlay.Normal( contactPos, right * sideFriction.Force / 1000f, Color.Green, overlay: true );
|
|
DebugOverlay.Normal( contactPos, up * force / 1000f, Color.Blue, overlay: true );
|
|
}
|
|
}
|