From 0905876b993ab39794fd626f116df05360bd1e93 Mon Sep 17 00:00:00 2001 From: Valera <108022376+kekobka@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:05:48 +0700 Subject: [PATCH] update --- Assets/frictions/default.tire | 54 +---- Code/Base/VeloXBase.Phys.cs | 19 +- Code/Base/Wheel/Pacejka.cs | 314 +++--------------------------- Code/Base/Wheel/VeloXWheel.cs | 268 +++++++++++++------------ Code/Car/VeloXCar.Steering.cs | 8 +- Code/Utils/PhysicsExtensions.cs | 91 ++++----- Editor/Wheel/PacejkaWidget.cs | 46 +---- Editor/Wheel/TirePresetEditor.cs | 70 ------- Editor/Wheel/TirePresetPreview.cs | 229 ++++++++++------------ 9 files changed, 347 insertions(+), 752 deletions(-) delete mode 100644 Editor/Wheel/TirePresetEditor.cs diff --git a/Assets/frictions/default.tire b/Assets/frictions/default.tire index 728b15d..c64e7a2 100644 --- a/Assets/frictions/default.tire +++ b/Assets/frictions/default.tire @@ -1,54 +1,16 @@ { "Pacejka": { "Lateral": { - "a0": 1.4, - "a1": -0, - "a2": 1688, - "a3": 2400, - "a4": 6.026, - "a5": 0, - "a6": -0.359, - "a7": 1, - "a8": 0, - "a9": 0, - "a10": 0, - "a111": 0, - "a112": 0, - "a12": 0, - "a13": 0 + "B": 12, + "C": 1.3, + "D": 1.8, + "E": -1.8 }, "Longitudinal": { - "b0": 1.65, - "b1": 0, - "b2": 1690, - "b3": 0, - "b4": 229, - "b5": 0, - "b6": 0, - "b7": 0, - "b8": -10, - "b9": 0, - "b10": 0 - }, - "Aligning": { - "c0": 2, - "c1": -3.8, - "c2": -3.14, - "c3": -1.16, - "c4": -7.2, - "c5": 0, - "c6": 0, - "c7": 0.044, - "c8": -0.58, - "c9": 0.18, - "c10": 0, - "c11": 0, - "c12": 0, - "c13": 0, - "c14": 0.14, - "c15": -1.029, - "c16": 0, - "c17": 0 + "B": 10.86, + "C": 2.15, + "D": 2, + "E": 0.992 } }, "RollResistanceLin": 0.001, diff --git a/Code/Base/VeloXBase.Phys.cs b/Code/Base/VeloXBase.Phys.cs index 8c52b3f..79d90cf 100644 --- a/Code/Base/VeloXBase.Phys.cs +++ b/Code/Base/VeloXBase.Phys.cs @@ -1,4 +1,7 @@ -namespace VeloX; +using Sandbox; +using System; + +namespace VeloX; public abstract partial class VeloXBase { @@ -19,6 +22,20 @@ public abstract partial class VeloXBase foreach ( var v in Wheels ) if ( v.AutoPhysics ) v.DoPhysics( this ); + var totalSpeed = TotalSpeed + Math.Abs( Body.AngularVelocity.z ); + + var factor = 1 - Math.Clamp( totalSpeed / 30, 0, 1 ); + + if ( factor > 0.1f ) + { + var vel = Body.Velocity; + + var rt = WorldRotation.Right; + + var force = rt.Dot( vel ) / Time.Delta * mass * factor * rt; + Body.ApplyForce( -force ); + } + Body.ApplyTorque( angForce ); } } diff --git a/Code/Base/Wheel/Pacejka.cs b/Code/Base/Wheel/Pacejka.cs index ac3dde6..65c8b1d 100644 --- a/Code/Base/Wheel/Pacejka.cs +++ b/Code/Base/Wheel/Pacejka.cs @@ -1,303 +1,43 @@ using Sandbox; using Sandbox.Services; using System; +using System.Threading; namespace VeloX; public class Pacejka { - public struct LateralForce() + public class PacejkaPreset { - [Description( "Shape factor" )] - [Range( 1, 3 )] public float a0 { get; set; } = 1.4f; // 0 + [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; - [Description( "Load infl on lat friction coeff (*1000) (1/kN)" )] - [Range( -100, 100 )] public float a1 { get; set; } = -0f; // 1 + public float Evaluate( float slip ) => D * MathF.Sin( C * MathF.Atan( B * slip - E * (B * slip - MathF.Atan( B * slip )) ) ); - [Description( "Lateral friction coefficient at load = 0 (*1000)" )] - [Range( 1, 2500 )] public float a2 { get; set; } = 1688f; // 2 - - [Description( "Maximum stiffness (N/deg)" )] - [Range( 1, 5000 )] public float a3 { get; set; } = 2400f; // 3 - - [Description( "Load at maximum stiffness (kN)" )] - [Range( -100, 100 )] public float a4 { get; set; } = 6.026f; // 4 - - [Description( "Camber infiuence on stiffness (%/deg/100)" )] - [Range( -10, 10 )] public float a5 { get; set; } = 0f; // 5 - - [Description( "Curvature change with load" )] - [Range( -10, 10 )] public float a6 { get; set; } = -0.359f; // 6 - - [Description( "Curvature at load = 0" )] - [Range( -10, 10 )] public float a7 { get; set; } = 1.0f; // 7 - - [Description( "Horizontal shift because of camber (deg/deg)" )] - [Range( -10, 10 )] public float a8 { get; set; } = 0f; // 8 - - [Description( "Load influence on horizontal shift (deg/kN)" )] - [Range( -10, 10 )] public float a9 { get; set; } = 0;// 9 - - [Description( "Horizontal shift at load = 0 (deg)" )] - [Range( -10, 10 )] public float a10 { get; set; } = 0;// 10 - - [Description( "Camber influence on vertical shift (N/deg/kN)" )] - [Range( -10, 100 )] public float a111 { get; set; } = 0f; // 11 - - [Description( "Camber influence on vertical shift (N/deg/kN**2" )] - [Range( -10, 10 )] public float a112 { get; set; } = 0f; // 12 - - [Description( "Load influence on vertical shift (N/kN)" )] - [Range( -100, 100 )] public float a12 { get; set; } = 0f; // 13 - - [Description( "Vertical shift at load = 0 (N)" )] - [Range( -10, 10 )] public float a13 { get; set; } = 0f; // 14 - } - - public struct LongitudinalForce() - { - [Description( "Shape factor" )] - [Range( 1, 3 )] public float b0 { get; set; } = 1.65f; // 0 - - [Description( "Load infl on long friction coeff (*1000) (1/kN)" )] - [Range( -300, 300 )] public float b1 { get; set; } = 0f; // 1 - - [Description( "Longitudinal friction coefficient at load = 0 (*1000)" )] - [Range( 0, 10000 )] public float b2 { get; set; } = 1690f; // 2 - - [Description( "Curvature factor of stiffness (N/%/kN**2)" )] - [Range( -100, 100 )] public float b3 { get; set; } = 0f; // 3 - - [Description( "Change of stiffness with load at load = 0 (N/%/kN)" )] - [Range( -1000, 1000 )] public float b4 { get; set; } = 229f; // 4 - - [Description( "Change of progressivity of stiffness/load (1/kN)" )] - [Range( -10, 10 )] public float b5 { get; set; } = 0f; // 5 - - [Description( "Curvature change with load" )] - [Range( -10, 10 )] public float b6 { get; set; } = 0f; // 6 - - [Description( "Curvature change with load" )] - [Range( -10, 10 )] public float b7 { get; set; } = 0f; // 7 - - [Description( "Curvature at load = 0" )] - [Range( -10, 10 )] public float b8 { get; set; } = -10f; // 7 - - [Description( "Load influence on horizontal shift (%/kN)" )] - [Range( -10, 10 )] public float b9 { get; set; } = 0f; // 9 - - [Description( "Horizontal shift at load = 0 (%)" )] - [Range( -10, 10 )] public float b10 { get; set; } = 0f; // 10 - } - - - public struct AligningMoment() - { - [Description( "Shape factor" )] - [Range( 1, 7 )] public float c0 { get; set; } = 2.0f; // 0 - - [Description( "Load influence of peak value (Nm/kN**2)" )] - [Range( -10, 10 )] public float c1 { get; set; } = -3.8f; // 1 - - [Description( "Load influence of peak value (Nm/kN)" )] - [Range( -10, 10 )] public float c2 { get; set; } = -3.14f; // 2 - - [Description( "Curvature factor of stiffness (Nm/deg/kN**2" )] - [Range( -10, 10 )] public float c3 { get; set; } = -1.16f; // 3 - - [Description( "Change of stiffness with load at load = 0 (Nm/deg/kN)" )] - [Range( -100, 100 )] public float c4 { get; set; } = -7.2f; // 4 - - [Description( "Change of progressivity of stiffness/load (1/kN)" )] - [Range( -10, 10 )] public float c5 { get; set; } = 0.0f; // 5 - - [Description( "Camber influence on stiffness (%/deg/100)" )] - [Range( -10, 10 )] public float c6 { get; set; } = 0.0f; // 6 - - [Description( "Curvature change with load" )] - [Range( -10, 10 )] public float c7 { get; set; } = 0.044f; // 7 - - [Description( "Curvature change with load" )] - [Range( -10, 10 )] public float c8 { get; set; } = -0.58f; // 8 - - [Description( "Curvature at load = 0" )] - [Range( -10, 10 )] public float c9 { get; set; } = 0.18f; // 9 - - [Description( "Camber influence of stiffness" )] - [Range( -10, 10 )] public float c10 { get; set; } = 0.0f; // 10 - - [Description( "Camber influence on horizontal shift (deg/deg)" )] - [Range( -10, 10 )] public float c11 { get; set; } = 0.0f; // 11 - - [Description( "Load influence on horizontal shift (deg/kN)" )] - [Range( -10, 10 )] public float c12 { get; set; } = 0.0f; // 12 - - [Description( "Horizontal shift at load = 0 (deg)" )] - [Range( -10, 10 )] public float c13 { get; set; } = 0.0f; // 13 - - [Description( "Camber influence on vertical shift (Nm/deg/kN**2" )] - [Range( -10, 10 )] public float c14 { get; set; } = 0.14f; // 14 - - [Description( "Camber influence on vertical shift (Nm/deg/kN)" )] - [Range( -10, 10 )] public float c15 { get; set; } = -1.029f; // 15 - - [Description( "Load influence on vertical shift (Nm/kN)" )] - [Range( -10, 10 )] public float c16 { get; set; } = 0.0f; // 16 - - [Description( "Vertical shift at load = 0 (Nm)" )] - [Range( -10, 10 )] public float c17 { get; set; } = 0.0f; // 17 - } - public struct CombiningForce - { - public float gy1 = 1; // 0 - public float gy2 = 1; // 1 - public float gx1 = 1; // 2 - public float gx2 = 1f; // 3 - - public CombiningForce() + 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 LateralForce Lateral { get; set; } = new(); - public LongitudinalForce Longitudinal { get; set; } = new(); - public AligningMoment Aligning { get; set; } = new(); - public CombiningForce Combining = new(); - - - - /// pacejka magic formula for longitudinal force - public float PacejkaFx( float sigma, float Fz, float friction_coeff, out float maxforce_output ) - { - var b = Longitudinal; - - // shape factor - float C = b.b0; - - // peak factor - float D = (b.b1 * Fz + b.b2) * Fz; - - // stiffness at sigma = 0 - float BCD = (b.b3 * Fz + b.b4) * Fz * MathF.Exp( -b.b5 * Fz ); - - // stiffness factor - float B = BCD / (C * D); - - // curvature factor - float E = (b.b6 * Fz + b.b7) * Fz + b.b8; - - // horizontal shift - float Sh = b.b9 * Fz + b.b10; - - // composite - float S = 100 * sigma + Sh; - - // longitudinal force - float BS = B * S; - float Fx = D * MathF.Sin( C * MathF.Atan( BS - E * (BS - MathF.Atan( BS )) ) ); - - // scale by surface friction - Fx *= friction_coeff; - maxforce_output = D; - - return Fx; - } - - /// pacejka magic formula for lateral force - public float PacejkaFy( float alpha, float Fz, float gamma, float friction_coeff, out float camber_alpha ) - { - var a = Lateral; - - // shape factor - float C = a.a0; - - // peak factor - float D = (a.a1 * Fz + a.a2) * Fz; - - // stiffness at alpha = 0 - float BCD = a.a3 * MathF.Atan2( Fz, a.a4 ) * (1 - a.a5 * MathF.Abs( gamma )); - // stiffness factor - float B = BCD / (C * D); - - // curvature factor - float E = a.a6 * Fz + a.a7; - - // horizontal shift - float Sh = a.a8 * gamma + a.a9 * Fz + a.a10; - - // vertical shift - float Sv = ((a.a111 * Fz + a.a112) * gamma + a.a12) * Fz + a.a13; - - // composite slip angle - float S = alpha + Sh; - - // lateral force - float BS = B * S; - float Fy = D * MathF.Sin( C * MathF.Atan( BS - E * (BS - MathF.Atan( BS )) ) ) + Sv; - - // scale by surface friction - Fy *= friction_coeff; - camber_alpha = Sh + Sv / BCD * friction_coeff; - - return Fy; - } - - /// pacejka magic formula for aligning torque - public float PacejkaMz( float alpha, float Fz, float gamma, float friction_coeff ) - { - var c = Aligning; - - // shape factor - float C = c.c0; - - // peak factor - float D = (c.c1 * Fz + c.c2) * Fz; - - // stiffness at alpha = 0 - float BCD = (c.c3 * Fz + c.c4) * Fz * (1 - c.c6 * MathF.Abs( gamma )) * MathF.Exp( -c.c5 * Fz ); - - // stiffness factor - float B = BCD / (C * D); - - // curvature factor - float E = (c.c7 * Fz * Fz + c.c8 * Fz + c.c9) * (1 - c.c10 * MathF.Abs( gamma )); - - // horizontal shift - float Sh = c.c11 * gamma + c.c12 * Fz + c.c13; - - // composite slip angle - float S = alpha + Sh; - - // vertical shift - float Sv = (c.c14 * Fz * Fz + c.c15 * Fz) * gamma + c.c16 * Fz + c.c17; - - // self-aligning torque - float BS = B * S; - float Mz = D * MathF.Sin( C * MathF.Atan( BS - E * (BS - MathF.Atan( BS )) ) ) + Sv; - - // scale by surface friction - Mz *= friction_coeff; - - return Mz; - } - - /// pacejka magic formula for the longitudinal combining factor - public float PacejkaGx( float sigma, float alpha ) - { - var p = Combining; - float a = p.gx2 * sigma; - float b = p.gx1 * alpha; - float c = a * a + 1; - return MathF.Sqrt( c / (c + b * b) ); - } - - /// pacejka magic formula for the lateral combining factor - public float PacejkaGy( float sigma, float alpha ) - { - var p = Combining; - float a = p.gy2 * alpha; - float b = p.gy1 * sigma; - float c = a * a + 1; - return MathF.Sqrt( c / (c + b * b) ); - } + public float PacejkaFx( float slip ) => Longitudinal.Evaluate( slip ); + public float PacejkaFy( float slip ) => Lateral.Evaluate( slip ); } diff --git a/Code/Base/Wheel/VeloXWheel.cs b/Code/Base/Wheel/VeloXWheel.cs index 9855dfc..4908d34 100644 --- a/Code/Base/Wheel/VeloXWheel.cs +++ b/Code/Base/Wheel/VeloXWheel.cs @@ -1,8 +1,10 @@ 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; @@ -23,8 +25,8 @@ public partial class VeloXWheel : Component [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(); + public float SideSlip { get; private set; } + public float ForwardSlip { get; private set; } [Sync] public float Torque { get; set; } [Sync, Range( 0, 1 )] public float Brake { get; set; } [Property] float BrakePowerMax { get; set; } = 3000; @@ -64,8 +66,8 @@ public partial class VeloXWheel : Component private Vector3 right; private Vector3 up; - private Friction forwardFriction; - private Friction sideFriction; + private float forwardFriction; + private float sideFriction; private Vector3 force; public float CounterTorque { get; private set; } @@ -111,9 +113,101 @@ public partial class VeloXWheel : Component 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; + + private (float, float, float, float) StepLongitudinal( float Tm, float Tb, float Vx, float W, float Lc, float R, float I ) + { + float wInit = W; + float vxAbs = Math.Abs( Vx ); + float Sx; + if ( Lc < 0.01f ) + { + Sx = 0; + } + else if ( vxAbs >= 0.01f ) + { + Sx = (W * R - Vx) / vxAbs; + } + else + { + Sx = (W * R - Vx) * 0.6f; + } + + 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 ) + { + float VxAbs = Math.Abs( Vx ); + + if ( Lc < 0.01f ) + { + Sy = 0; + } + else if ( VxAbs > 0.1f ) + { + + Sy = MathX.RadianToDegree( MathF.Atan( Vy / VxAbs ) ) / 50; + } + else + { + + Sy = Vy * (0.003f / Time.Delta); + } + + Sy = Math.Clamp( Sy, -1, 1 ); + float slipSign = Sy < 0 ? -1 : 1; + Fy = -slipSign * TirePreset.Pacejka.PacejkaFy( Math.Abs( Sy ) ) * Lc; + } + + 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 * 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 ); @@ -128,163 +222,95 @@ public partial class VeloXWheel : Component .Cylinder( Width, Radius, pos, endPos ) .Rotated( vehicle.WorldTransform.Rotation * CylinderOffset ) .UseRenderMeshes( false ) - .UseHitPosition( false ) + .UseHitPosition( true ) .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; + forward = Vector3.VectorPlaneProject( ang.Forward, Trace.Normal ); + right = Vector3.VectorPlaneProject( ang.Right, Trace.Normal ); var fraction = Trace.Fraction; - contactPos = pos - maxLen * fraction * up; + contactPos = pos - maxLen * fraction * ang.Up; LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos ); DoSuspensionSounds( vehicle, fraction - lastFraction ); lastFraction = fraction; - var normal = Trace.Normal; + if ( !IsOnGround ) + return; var vel = vehicle.Body.GetVelocityAtPoint( contactPos ); var offset = maxLen - (fraction * maxLen); - var springForce = (offset * SpringStrength); + var springForce = offset * SpringStrength; var damperForce = (lastSpringOffset - offset) * SpringDamper; + lastSpringOffset = offset; - // Vector3.CalculateVelocityOffset is broken (need fix) - var velU = normal.Dot( vel ); + + var velU = Trace.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; + vehicle.Body.CalculateVelocityOffset( -velU / Time.Delta * Trace.Normal, pos, out var linearImp, out var angularImp ); - Vector3 r = pos - com; + vehicle.Body.Velocity += linearImp; + vehicle.Body.AngularVelocity += angularImp; + vehicle.Body.CalculateVelocityOffset( Trace.HitPosition - (contactPos + Trace.Normal * velU * Time.Delta), pos, out var lin, out _ ); - 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; + vehicle.WorldPosition += lin / Time.Delta; damperForce = 0; + } - force = (springForce - damperForce) * MathF.Max( 0, up.Dot( normal ) ) * normal; + force = (springForce - damperForce) * Trace.Normal; - load = Math.Max( force.z, 0 ).InchToMeter(); + load = Math.Max( springForce - damperForce, 0 ); + float R = Radius.InchToMeter(); float forwardSpeed = vel.Dot( forward ).InchToMeter(); float sideSpeed = vel.Dot( right ).InchToMeter(); - float camber_rad = CamberAngle.DegreeToRadian(); - float R = Radius.InchToMeter(); + float longitudinalLoadCoefficient = GetLongitudinalLoadCoefficient( load ); + float lateralLoadCoefficient = GetLateralLoadCoefficient( load ); - float linearSpeed = angularVelocity * Radius.InchToMeter(); + 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 + ); - float F_roll = TirePreset.GetRollingResistance( linearSpeed, 1.0f ); - F_roll *= -Math.Clamp( linearSpeed * 0.25f, -1, 1 ); + StepLateral( forwardSpeed, sideSpeed, lateralLoadCoefficient, out float Sy, out float Fy ); - float T_brake = Brake * BrakePowerMax; - T_brake *= -Math.Clamp( linearSpeed * 0.25f, -1, 1 ); - - float Winit = angularVelocity; - - angularVelocity += F_roll * 9.80665f / Inertia * Time.Delta; - - angularVelocity += Torque / Inertia * Time.Delta; - - - angularVelocity += T_brake / Inertia * Time.Delta; - - if ( IsOnGround ) - { - 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) * BaseInertia / Time.Delta; - - var surfaceTorque = Math.Clamp( errorTorque, -maxTorque, maxTorque ); - - angularVelocity -= surfaceTorque / Inertia * Time.Delta; - - - float deltaOmegaTorque = (angularVelocity - Winit) * Inertia / Time.Delta; - - CounterTorque = -surfaceTorque - deltaOmegaTorque; - forwardFriction.Slip = slip; - forwardFriction.Force = -fx; - forwardFriction.Speed = forwardSpeed; - sideFriction.Slip = slip_ang; - sideFriction.Force = fy; - sideFriction.Speed = sideSpeed; - - vehicle.Body.ApplyForceAt( pos, force / Time.Delta * ProjectSettings.Physics.SubSteps ); - - Vector3 frictionForce = forward * forwardFriction.Force + right * sideFriction.Force; - vehicle.Body.ApplyForceAt( pos, frictionForce * ProjectSettings.Physics.SubSteps ); - } - else - { - forwardFriction = new(); - sideFriction = new(); - } + 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 ); } - //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 +#if 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 ); + 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 + } diff --git a/Code/Car/VeloXCar.Steering.cs b/Code/Car/VeloXCar.Steering.cs index b110430..55399bb 100644 --- a/Code/Car/VeloXCar.Steering.cs +++ b/Code/Car/VeloXCar.Steering.cs @@ -34,12 +34,12 @@ public partial class VeloXCar var inputSteer = Input.AnalogMove.y; - VelocityAngle = -SignedAngle( Body.Velocity, WorldRotation.Forward, WorldRotation.Up ); + VelocityAngle = 0;// -SignedAngle( Body.Velocity, WorldRotation.Forward, WorldRotation.Up ); - var steerConeFactor = Math.Clamp( TotalSpeed / SteerConeMaxSpeed, 0, 1 ); - var steerCone = 1 - steerConeFactor * (1 - SteerConeMaxAngle); + //var steerConeFactor = Math.Clamp( TotalSpeed / SteerConeMaxSpeed, 0, 1 ); + //var steerCone = 1 - steerConeFactor * (1 - SteerConeMaxAngle); - inputSteer = ExpDecay( this.inputSteer, inputSteer * steerCone, SteerConeChangeRate, dt ); + inputSteer = ExpDecay( this.inputSteer, inputSteer, SteerConeChangeRate, dt ); this.inputSteer = inputSteer; float target = -inputSteer * MaxSteerAngle; diff --git a/Code/Utils/PhysicsExtensions.cs b/Code/Utils/PhysicsExtensions.cs index b14b0a1..28bc8bd 100644 --- a/Code/Utils/PhysicsExtensions.cs +++ b/Code/Utils/PhysicsExtensions.cs @@ -28,44 +28,38 @@ public static class PhysicsExtensions value.x * (xz2 - wy2) + value.y * (yz2 + wx2) + value.z * (1.0f - xx2 - yy2) ); } + /// - /// Calculates the linear and angular velocities on the center of mass for an offset impulse. + /// Calculates the linear and angular velocities on the object's center of mass for an offset impulse. /// /// The physics object /// The impulse acting on the object in kg*units/s (World frame) /// The location of the impulse in world coordinates - /// - /// Vector1: Linear velocity from the impulse (World frame) - /// Vector2: Angular velocity from the impulse (Local frame) - /// - public static (Vector3 LinearVelocity, Vector3 AngularVelocity) CalculateVelocityOffset( this PhysicsBody physObj, Vector3 impulse, Vector3 position ) + /// Linear velocity on center of mass (World frame) + /// Angular velocity on center of mass (World frame) + public static void CalculateVelocityOffset( + this Rigidbody physObj, + Vector3 impulse, + Vector3 position, + out Vector3 LinearVelocity, + out Vector3 AngularVelocity ) { if ( !physObj.IsValid() || !physObj.MotionEnabled ) - return (Vector3.Zero, Vector3.Zero); + { + LinearVelocity = 0; + AngularVelocity = 0; + return; + } + Vector3 com = physObj.WorldTransform.PointToWorld( physObj.MassCenter ); + Rotation bodyRot = physObj.PhysicsBody.Rotation; - Vector3 linearVelocity = impulse / physObj.Mass; - - Vector3 r = position - physObj.MassCenter; - - - // Calculate torque impulse in world frame: τ = r × impulse - Vector3 torqueImpulseWorld = r.Cross( impulse ); - Rotation worldToLocal = physObj.Rotation.Inverse; - Vector3 torqueImpulseLocal = torqueImpulseWorld.Transform( worldToLocal ); - - var InverseInertiaDiagLocal = physObj.Inertia.Inverse; - - // Compute angular velocity change in rad/s (local frame) - Vector3 angularVelocityRadLocal = new( - InverseInertiaDiagLocal.x * torqueImpulseLocal.x, - InverseInertiaDiagLocal.y * torqueImpulseLocal.y, - InverseInertiaDiagLocal.z * torqueImpulseLocal.z - ); - const float radToDeg = 180f / MathF.PI; - Vector3 angularVelocityDegLocal = angularVelocityRadLocal * radToDeg; - - return (linearVelocity, angularVelocityDegLocal); + Vector3 r = position - com; + Vector3 torque = Vector3.Cross( r, impulse ); + Vector3 torqueLocal = bodyRot.Inverse * torque; + Vector3 angularVelocityLocal = torqueLocal * physObj.PhysicsBody.Inertia.Inverse; + AngularVelocity = bodyRot * angularVelocityLocal; + LinearVelocity = impulse * (1 / physObj.Mass); } /// @@ -74,34 +68,31 @@ public static class PhysicsExtensions /// The physics object /// The impulse acting on the object in kg*units/s (World frame) /// The location of the impulse in world coordinates - /// - /// Vector1: Linear impulse on center of mass (World frame) - /// Vector2: Angular impulse on center of mass (Local frame) - /// - public static (Vector3 LinearImpulse, Vector3 AngularImpulse) CalculateForceOffset( - this PhysicsBody physObj, + /// Linear impulse on center of mass (World frame) + /// Angular impulse on center of mass (World frame) + public static void CalculateForceOffset( + this Rigidbody physObj, Vector3 impulse, - Vector3 position ) + Vector3 position, + out Vector3 LinearImpulse, + out Vector3 AngularImpulse ) { if ( !physObj.IsValid() || !physObj.MotionEnabled ) { - return (Vector3.Zero, Vector3.Zero); + LinearImpulse = 0; + AngularImpulse = 0; + return; } - // 1. Linear impulse is the same as the input impulse (conservation of momentum) - Vector3 linearImpulse = impulse; + Vector3 com = physObj.WorldTransform.PointToWorld( physObj.MassCenter ); + Rotation bodyRot = physObj.PhysicsBody.Rotation; - // 2. Calculate angular impulse (torque) from the offset force - // τ = r * F (cross product of position relative to COM and force) - Vector3 centerOfMass = physObj.MassCenter; - Vector3 relativePosition = position - centerOfMass; - Vector3 worldAngularImpulse = relativePosition.Cross( impulse ); - - // Convert angular impulse to local space (since we'll use it with LocalInertia) - Rotation bodyRotation = physObj.Transform.Rotation; - Vector3 localAngularImpulse = bodyRotation.Inverse * worldAngularImpulse; - - return (linearImpulse, localAngularImpulse); + Vector3 r = position - com; + Vector3 torque = Vector3.Cross( r, impulse ); + Vector3 torqueLocal = bodyRot.Inverse * torque; + Vector3 angularImpulseLocal = torqueLocal * physObj.PhysicsBody.Inertia.Inverse; + AngularImpulse = bodyRot * angularImpulseLocal; + LinearImpulse = impulse; } } diff --git a/Editor/Wheel/PacejkaWidget.cs b/Editor/Wheel/PacejkaWidget.cs index 37ded7c..8efcaa8 100644 --- a/Editor/Wheel/PacejkaWidget.cs +++ b/Editor/Wheel/PacejkaWidget.cs @@ -9,7 +9,7 @@ public class PacejkaWidget : ControlObjectWidget public override bool SupportsMultiEdit => false; public override bool IncludeLabel => false; - [CustomEditor( typeof( Pacejka.LateralForce ) )] + [CustomEditor( typeof( Pacejka.PacejkaPreset ) )] private class LateralForceWidget : ControlObjectWidget { public LateralForceWidget( SerializedProperty property ) : base( property, true ) @@ -17,7 +17,7 @@ public class PacejkaWidget : ControlObjectWidget Layout = Layout.Column(); Layout.Margin = 8f; Layout.Spacing = 8; - foreach ( var item in TypeLibrary.GetType().Properties ) + foreach ( var item in TypeLibrary.GetType().Properties ) { var row = Layout.AddRow(); row.Spacing = 8; @@ -27,45 +27,6 @@ public class PacejkaWidget : ControlObjectWidget } } } - - [CustomEditor( typeof( Pacejka.LongitudinalForce ) )] - private class LongitudinalForceWidget : ControlObjectWidget - { - public LongitudinalForceWidget( SerializedProperty property ) : base( property, true ) - { - Layout = Layout.Column(); - Layout.Margin = 8f; - Layout.Spacing = 8; - foreach ( var item in TypeLibrary.GetType().Properties ) - { - var row = Layout.AddRow(); - row.Spacing = 8; - var propetry = SerializedObject.GetProperty( item.Name ); - row.Add( new Label( propetry.Name ) ); - row.Add( Create( propetry ) ); - } - } - } - - [CustomEditor( typeof( Pacejka.AligningMoment ) )] - private class AligningMomentWidget : ControlObjectWidget - { - public AligningMomentWidget( SerializedProperty property ) : base( property, true ) - { - Layout = Layout.Column(); - Layout.Margin = 8f; - Layout.Spacing = 8; - foreach ( var item in TypeLibrary.GetType().Properties ) - { - var row = Layout.AddRow(); - row.Spacing = 8; - var propetry = SerializedObject.GetProperty( item.Name ); - row.Add( new Label( propetry.Name ) ); - row.Add( Create( propetry ) ); - } - } - } - private Pacejka Pacejka; public PacejkaWidget( SerializedProperty property ) : base( property, true ) { @@ -83,8 +44,5 @@ public class PacejkaWidget : ControlObjectWidget tabs.AddPage( nameof( Pacejka.Longitudinal ), null, Layout.Add( Create( obj.GetProperty( nameof( Pacejka.Longitudinal ) ) ) ) ); - tabs.AddPage( nameof( Pacejka.Aligning ), null, - Layout.Add( Create( obj.GetProperty( nameof( Pacejka.Aligning ) ) ) ) - ); } } diff --git a/Editor/Wheel/TirePresetEditor.cs b/Editor/Wheel/TirePresetEditor.cs deleted file mode 100644 index 695161d..0000000 --- a/Editor/Wheel/TirePresetEditor.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Editor; -using Editor.Assets; -using Editor.Inspectors; -using Sandbox; -using static Editor.Inspectors.AssetInspector; - -namespace VeloX; - -[CanEdit( "asset:tire" )] -public class TirePresetEditor : Widget, IAssetInspector -{ - TirePreset Tire; - ControlSheet MainSheet; - TirePresetPreview TirePreview; - public TirePresetEditor( Widget parent ) : base( parent ) - { - Layout = Layout.Column(); - Layout.Margin = 4; - Layout.Spacing = 4; - - // Create a ontrolSheet that will display all our Properties - MainSheet = new ControlSheet(); - Layout.Add( MainSheet ); - - //// Add a randomize button below the ControlSheet - //var button = Layout.Add( new Button( "Randomize", "casino", this ) ); - //button.Clicked += () => - //{ - // foreach ( var prop in Test.GetSerialized() ) - // { - // // Randomize all the float values from 0-100 - // if ( prop.PropertyType != typeof( float ) ) continue; - // prop.SetValue( Random.Shared.Float( 0, 100 ) ); - // } - //}; - - Layout.AddStretchCell(); - - RebuildSheet(); - Focus(); - - } - - [EditorEvent.Hotload] - void RebuildSheet() - { - if ( Tire is null ) return; - if ( MainSheet is null ) return; - MainSheet.Clear( true ); - - var so = Tire.GetSerialized(); - so.OnPropertyChanged += x => - { - Tire.StateHasChanged(); - TirePreview.Widget.UpdatePixmap(); - }; - MainSheet.AddObject( so ); - } - - void IAssetInspector.SetAssetPreview( AssetPreview preview ) - { - TirePreview = preview as TirePresetPreview; - } - - public void SetAsset( Asset asset ) - { - Tire = asset.LoadResource(); - RebuildSheet(); - } -} diff --git a/Editor/Wheel/TirePresetPreview.cs b/Editor/Wheel/TirePresetPreview.cs index 4bf7564..7575ddf 100644 --- a/Editor/Wheel/TirePresetPreview.cs +++ b/Editor/Wheel/TirePresetPreview.cs @@ -9,29 +9,17 @@ using System.Threading.Tasks; namespace VeloX; [AssetPreview( "tire" )] -class TirePresetPreview( Asset asset ) : PixmapAssetPreview( asset ) +class TirePresetPreview : AssetPreview { - - private TirePreset Tire = asset.LoadResource(); + private Texture texture; + public override bool IsAnimatedPreview => false; + [Range( 0.01f, 1 )] private float Zoom { get; set; } = 1; + private TirePreset Tire; public AssetPreviewWidget Widget { get; private set; } - [Range( 100, 10000 )] private float Load { get; set; } = 2500f; - [Range( 0, 1 )] private float Zoom { get; set; } = 0; - [Range( -10, 10 )] private float Camber { get; set; } = 0; - public override Widget CreateWidget( Widget parent ) { Widget = parent as AssetPreviewWidget; - Task.Run( async () => - { - while ( Widget != null ) - { - await MainThread.Wait(); - await Widget.UpdatePixmap(); - await Task.Delay( 100 ); - } - } ); - return null; } public override Widget CreateToolbar() @@ -42,116 +30,6 @@ class TirePresetPreview( Asset asset ) : PixmapAssetPreview( asset ) info.MouseLeftPress = () => OpenSettings( info ); return info; } - static List pointCache = []; - private void DrawPacejka() - { - - float load = Load * 0.001f; - float zoom = (1.0f - Zoom) * 4.0f + 0.1f; - var tire = Tire.Pacejka; - var width = Paint.LocalRect.Width; - var height = Paint.LocalRect.Height; - - - { // draw lateral line - pointCache.Clear(); - - Paint.SetPen( Color.Red, 1 ); - float x0 = -zoom * 0.5f * 20.0f; - float xn = zoom * 0.5f * 20.0f; - float ymin = -1000.0f; - float ymax = 1000.0f; - int points = 500; - - - for ( float x = x0; x <= xn; x += (xn - x0) / points ) - { - - float yval = tire.PacejkaFy( x, load, Camber, 1.0f, out float _ ) / load; - float xval = width * (x - x0) / (xn - x0); - yval /= ymax - ymin; - yval = (yval + 1.0f) * 0.5f; - yval = 1.0f - yval; - yval *= height; - pointCache.Add( new( xval, yval ) ); - } - - Paint.DrawLine( pointCache ); - } - - { // draw longitudinal line - pointCache.Clear(); - - Paint.SetPen( Color.Green, 1 ); - float x0 = -zoom * 0.5f; - float xn = zoom * 0.5f; - float ymin = -1000.0f; - float ymax = 1000.0f; - int points = 500; - - for ( float x = x0; x <= xn; x += (xn - x0) / points ) - { - float yval = tire.PacejkaFx( x, load, 1.0f, out var _ ) / load; - float xval = width * (x - x0) / (xn - x0); - yval /= ymax - ymin; - yval = (yval + 1.0f) * 0.5f; - yval = 1.0f - yval; - yval *= height; - pointCache.Add( new( xval, yval ) ); - } - Paint.DrawLine( pointCache ); - } - - { // draw aligning line - pointCache.Clear(); - - Paint.SetPen( Color.Blue, 1 ); - float x0 = -zoom * 0.5f * 10.0f; - float xn = zoom * 0.5f * 10.0f; - float ymin = -60.0f; - float ymax = 60.0f; - int points = 500; - - for ( float x = x0; x <= xn; x += (xn - x0) / points ) - { - float yval = tire.PacejkaMz( x, load, Camber * (180.0f / MathF.PI), 1.0f ) / load; - float xval = width * (x - x0) / (xn - x0); - yval /= ymax - ymin; - yval = (yval + 1.0f) * 0.5f; - yval = 1.0f - yval; - yval *= height; - pointCache.Add( new( xval, yval ) ); - } - Paint.DrawLine( pointCache ); - } - - pointCache.Clear(); - } - - public override Task RenderToPixmap( Pixmap pixmap ) - { - Paint.ToPixmap( pixmap ); - Paint.Antialiasing = true; - - Paint.SetBrush( Color.Gray ); - Paint.DrawRect( Paint.LocalRect ); - Paint.ClearBrush(); - - Paint.SetPen( Color.Black, 1 ); - - var width = Paint.LocalRect.Width; - var height = Paint.LocalRect.Height; - float xc = width / 2; - float yc = height / 2; - - Paint.DrawLine( new( xc, 0 ), new( xc, yc * 2 ) ); - Paint.DrawLine( new( 0, yc ), new( xc * 2, yc ) ); - - DrawPacejka(); - - return Task.CompletedTask; - } - public void OpenSettings( Widget parent ) { var popup = new PopupWidget( parent ) @@ -164,9 +42,7 @@ class TirePresetPreview( Asset asset ) : PixmapAssetPreview( asset ) var ps = new ControlSheet(); - ps.AddProperty( this, x => x.Load ); ps.AddProperty( this, x => x.Zoom ); - ps.AddProperty( this, x => x.Camber ); popup.Layout.Add( ps ); popup.MaximumWidth = 300; @@ -175,4 +51,99 @@ class TirePresetPreview( Asset asset ) : PixmapAssetPreview( asset ) popup.ConstrainToScreen(); } + + public override async Task InitializeAsset() + { + await Task.Yield(); + + using ( Scene.Push() ) + { + PrimaryObject = new() + { + WorldTransform = Transform.Zero + }; + + var plane = PrimaryObject.AddComponent(); + plane.Model = Model.Plane; + plane.LocalScale = new Vector3( 1, 1, 1 ); + plane.MaterialOverride = Material.Load( "materials/dev/reflectivity_30.vmat" ); + plane.Tint = new Color( 0.02f, 0.04f, 0.03f ); + + var bounds = PrimaryObject.GetBounds(); + SceneCenter = bounds.Center; + SceneSize = bounds.Size; + } + + return; + } + public override void UpdateScene( float cycle, float timeStep ) + { + if ( !Widget.IsValid() ) + return; + + Camera.WorldPosition = Vector3.Up * 300; + Camera.Orthographic = true; + Camera.WorldRotation = new Angles( 90, 0, 0 ); + + var bitmap = new Bitmap( 512, 512 ); + + Draw( bitmap ); + + texture.Clear( Color.Black ); + //texture.Update( bitmap ); + DebugOverlaySystem.Current.Texture( texture, new Rect( 0, Widget.Size ) ); + + FrameScene(); + } + + private readonly List pointCache = []; + + public TirePresetPreview( Asset asset ) : base( asset ) + { + texture = Texture.CreateRenderTarget().WithDynamicUsage().WithScreenFormat().WithSize( 512, 512 ).Create(); + Tire = Asset.LoadResource(); + } + + private void DrawPacejka( Bitmap bitmap ) + { + var tire = Tire.Pacejka; + var width = bitmap.Width; + var height = bitmap.Height; + + + { // draw lateral line + pointCache.Clear(); + + bitmap.SetPen( Color.Red, 1 ); + + for ( float x = 0; x <= 1; x += 0.01f ) + { + float val = tire.PacejkaFy( x ) * Zoom; + pointCache.Add( new( width * x, height - height * val ) ); + } + + bitmap.DrawLines( pointCache.ToArray() ); + } + + { // draw longitudinal line + pointCache.Clear(); + + bitmap.SetPen( Color.Green, 1 ); + + for ( float x = 0; x <= 1; x += 0.01f ) + { + float val = tire.PacejkaFx( x ) * Zoom; + pointCache.Add( new( width * x, height - height * val ) ); + } + bitmap.DrawLines( pointCache.ToArray() ); + } + + pointCache.Clear(); + } + private void Draw( Bitmap bitmap ) + { + bitmap.Clear( Color.Black ); + bitmap.SetAntialias( true ); + DrawPacejka( bitmap ); + } }