velox/Code/Base/Wheel/VeloXWheel.cs
2025-06-15 03:23:47 +07:00

267 lines
8.2 KiB
C#

using Sandbox;
using Sandbox.Services;
using System;
using System.Collections.Specialized;
using System.Numerics;
using System.Text.RegularExpressions;
using static Sandbox.CameraComponent;
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 => angularVelocity;
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; }
private float BaseInertia => Mass * MathF.Pow( Radius.InchToMeter(), 2 );
public float Inertia => BaseInertia;
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 )
{
var entityAngles = vehicle.WorldRotation;
Spin -= angularVelocity.MeterToInch() * 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;
public void DoPhysics( VeloXBase vehicle )
{
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
var ang = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) );
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 );
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(
2500,
angularVelocity * R,
forwardSpeed,
sideSpeed,
camber_rad,
out var tireState
);
float linearSpeed = angularVelocity * Radius.InchToMeter();
float F_roll = TirePreset.GetRollingResistance( linearSpeed, 1.0f );
F_roll = -MathF.Sign( forwardSpeed ) * F_roll;
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 + T_brake - Fx_total * R;
// not work
Vector3 c = pos.Cross( forward );
Vector3 v = (c * vehicle.Body.PhysicsBody.Inertia).Cross( pos );
var m = 1 / (1 / vehicle.Body.Mass + forward.Dot( v ));
m *= Inertia / (m * R * R + Inertia);
float ve = forward.Dot( vel ).InchToMeter() - angularVelocity;
angularVelocity += ve * m * Radius * (1 / Inertia);// totalTorque * (1 / Inertia) * Time.Delta;
forwardFriction = new Friction()
{
Slip = tireState.slip,
Force = Fx_total,
Speed = forwardSpeed
};
sideFriction = new Friction()
{
Slip = tireState.slip_angle,
Force = tireState.fy,
Speed = sideSpeed
};
Vector3 frictionForce = forward * forwardFriction.Force + right * sideFriction.Force;
vehicle.Body.ApplyForceAt( contactPos, (force + frictionForce) / Time.Delta );
}
else
{
// Колесо в воздухе: сбрасываем силы
forwardFriction = new Friction();
sideFriction = new Friction();
// Обновление угловой скорости только от мотора/тормозов
float T_brake = Brake * BrakePowerMax;
if ( angularVelocity > 0 ) T_brake = -T_brake;
else T_brake = angularVelocity < 0 ? T_brake : -MathF.Sign( Torque ) * T_brake;
angularVelocity += (Torque + T_brake) / Inertia * Time.Delta;
}
}
// debug
protected override void OnUpdate()
{
DebugOverlay.Normal( contactPos, forward * forwardFriction.Force / 100f, Color.Red, overlay: true );
DebugOverlay.Normal( contactPos, right * sideFriction.Force / 100f, Color.Green, overlay: true );
DebugOverlay.Normal( contactPos, up * force / 1000f, Color.Blue, overlay: true );
}
}