Compare commits

...

5 Commits

Author SHA1 Message Date
Valera
0905876b99 update 2025-07-18 16:05:48 +07:00
Valera
55a178e8c5 cleanup code 2025-06-16 14:46:43 +07:00
Valera
f0f89ff947 уже лучше 2025-06-15 21:59:42 +07:00
Valera
4912d0ae1a make it savable 2025-06-15 17:17:08 +07:00
Valera
4899a38265 not work 2025-06-15 03:23:47 +07:00
18 changed files with 527 additions and 881 deletions

View File

@@ -1,5 +1,18 @@
{
"Pacejka": {},
"Pacejka": {
"Lateral": {
"B": 12,
"C": 1.3,
"D": 1.8,
"E": -1.8
},
"Longitudinal": {
"B": 10.86,
"C": 2.15,
"D": 2,
"E": 0.992
}
},
"RollResistanceLin": 0.001,
"RollResistanceQuad": 1E-06,
"__references": [],

View File

@@ -7,7 +7,11 @@ public class PowerWheel : PowertrainComponent
{
[Property] public VeloXWheel Wheel { get; set; }
public override float QueryInertia() => Wheel.Inertia;
public override float QueryInertia()
{
float dtScale = Math.Clamp( Time.Delta, 0.01f, 0.05f ) / 0.005f;
return Wheel.BaseInertia * dtScale;
}
public override float QueryAngularVelocity( float angularVelocity )
{
@@ -19,7 +23,8 @@ public class PowerWheel : PowertrainComponent
Wheel.AutoPhysics = false;
Wheel.Torque = torque;
Wheel.Brake = Vehicle.Brake;
Inertia = Wheel.BaseInertia + inertia;
Wheel.Inertia = inertia;
Wheel.DoPhysics( Vehicle );
angularVelocity = Wheel.AngularVelocity;

View File

@@ -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 );
}
}

View File

@@ -15,7 +15,7 @@ public abstract partial class VeloXBase : Component
[Property, Group( "Components" )] public Rigidbody Body { get; protected set; }
[Property, Group( "Components" )] public Collider Collider { get; protected set; }
[Sync] public Angles SteerAngle { get; set; }
[Sync( SyncFlags.Interpolate )] public Angles SteerAngle { get; set; }
public Vector3 LocalVelocity;
public float ForwardSpeed;

View File

@@ -1,347 +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 = 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 = -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 = 1688f; // 2
[Description( "Maximum stiffness (N/deg)" )]
[Range( 1, 5000 )] public float a3 = 2400f; // 3
[Description( "Load at maximum stiffness (kN)" )]
[Range( -100, 100 )] public float a4 = 6.026f; // 4
[Description( "Camber infiuence on stiffness (%/deg/100)" )]
[Range( -10, 10 )] public float a5 = 0f; // 5
[Description( "Curvature change with load" )]
[Range( -10, 10 )] public float a6 = -0.359f; // 6
[Description( "Curvature at load = 0" )]
[Range( -10, 10 )] public float a7 = 1.0f; // 7
[Description( "Horizontal shift because of camber (deg/deg)" )]
[Range( -10, 10 )] public float a8 = 0f; // 8
[Description( "Load influence on horizontal shift (deg/kN)" )]
[Range( -10, 10 )] public float a9 = -0.00611f;// 9
[Description( "Horizontal shift at load = 0 (deg)" )]
[Range( -10, 10 )] public float a10 = -0.0322f;// 10
[Description( "Camber influence on vertical shift (N/deg/kN)" )]
[Range( -10, 100 )] public float a111 = 0f; // 11
[Description( "Camber influence on vertical shift (N/deg/kN**2" )]
[Range( -10, 10 )] public float a112 = 0f; // 12
[Description( "Load influence on vertical shift (N/kN)" )]
[Range( -100, 100 )] public float a12 = 0f; // 13
[Description( "Vertical shift at load = 0 (N)" )]
[Range( -10, 10 )] public float a13 = 0f; // 14
}
public struct LongitudinalForce()
public float GetPeakSlip()
{
[Description( "Shape factor" )]
[Range( 1, 3 )] public float b0 = 1.65f; // 0
float peakSlip = -1;
float yMax = 0;
[Description( "Load infl on long friction coeff (*1000) (1/kN)" )]
[Range( -300, 300 )] public float b1 = 0f; // 1
[Description( "Longitudinal friction coefficient at load = 0 (*1000)" )]
[Range( -10, 10 )] public float b2 = 1690f; // 2
[Description( "Curvature factor of stiffness (N/%/kN**2)" )]
[Range( -100, 100 )] public float b3 = 0f; // 3
[Description( "Change of stiffness with load at load = 0 (N/%/kN)" )]
[Range( -1000, 1000 )] public float b4 = 229f; // 4
[Description( "Change of progressivity of stiffness/load (1/kN)" )]
[Range( -10, 10 )] public float b5 = 0f; // 5
[Description( "Curvature change with load" )]
[Range( -10, 10 )] public float b6 = 0f; // 6
[Description( "Curvature change with load" )]
[Range( -10, 10 )] public float b7 = 0f; // 7
[Description( "Curvature at load = 0" )]
[Range( -10, 10 )] public float b8 = -10f; // 7
[Description( "Load influence on horizontal shift (%/kN)" )]
[Range( -10, 10 )] public float b9 = 0f; // 9
[Description( "Horizontal shift at load = 0 (%)" )]
[Range( -10, 10 )] public float b10 = 0f; // 10
[Description( "Load influence on vertical shift (N/kN)" )]
[Range( -10, 10 )] public float b11 = 0f; // 10
[Description( "Vertical shift at load = 0 (N)" )]
[Range( -10, 10 )] public float b12 = 0f; // 10
}
public struct AligningMoment()
for ( float i = 0; i < 1f; i += 0.01f )
{
[Description( "Shape factor" )]
[Range( 1, 7 )] public float c0 = 2.0f; // 0
[Description( "Load influence of peak value (Nm/kN**2)" )]
[Range( -10, 10 )] public float c1 = -3.8f; // 1
[Description( "Load influence of peak value (Nm/kN)" )]
[Range( -10, 10 )] public float c2 = -3.14f; // 2
[Description( "Curvature factor of stiffness (Nm/deg/kN**2" )]
[Range( -10, 10 )] public float c3 = -1.16f; // 3
[Description( "Change of stiffness with load at load = 0 (Nm/deg/kN)" )]
[Range( -100, 100 )] public float c4 = -7.2f; // 4
[Description( "Change of progressivity of stiffness/load (1/kN)" )]
[Range( -10, 10 )] public float c5 = 0.0f; // 5
[Description( "Camber influence on stiffness (%/deg/100)" )]
[Range( -10, 10 )] public float c6 = 0.0f; // 6
[Description( "Curvature change with load" )]
[Range( -10, 10 )] public float c7 = 0.044f; // 7
[Description( "Curvature change with load" )]
[Range( -10, 10 )] public float c8 = -0.58f; // 8
[Description( "Curvature at load = 0" )]
[Range( -10, 10 )] public float c9 = 0.18f; // 9
[Description( "Camber influence of stiffness" )]
[Range( -10, 10 )] public float c10 = 0.0f; // 10
[Description( "Camber influence on horizontal shift (deg/deg)" )]
[Range( -10, 10 )] public float c11 = 0.0f; // 11
[Description( "Load influence on horizontal shift (deg/kN)" )]
[Range( -10, 10 )] public float c12 = 0.0f; // 12
[Description( "Horizontal shift at load = 0 (deg)" )]
[Range( -10, 10 )] public float c13 = 0.0f; // 13
[Description( "Camber influence on vertical shift (Nm/deg/kN**2" )]
[Range( -10, 10 )] public float c14 = 0.14f; // 14
[Description( "Camber influence on vertical shift (Nm/deg/kN)" )]
[Range( -10, 10 )] public float c15 = -1.029f; // 15
[Description( "Load influence on vertical shift (Nm/kN)" )]
[Range( -10, 10 )] public float c16 = 0.0f; // 16
[Description( "Vertical shift at load = 0 (Nm)" )]
[Range( -10, 10 )] public float c17 = 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()
float y = Evaluate( i );
if ( y > yMax )
{
yMax = y;
peakSlip = i;
}
}
public LateralForce Lateral = new();
public LongitudinalForce Longitudinal = new();
public AligningMoment Aligning = new();
public CombiningForce Combining = new();
/// pacejka magic formula for longitudinal force
public float PacejkaFx( float sigma, float Fz, float friction_coeff )
{
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 * Sin3Pi2( C * MathF.Atan( BS - E * (BS - MathF.Atan( BS )) ) );
// scale by surface friction
Fx *= friction_coeff;
return Fx;
return peakSlip;
}
/// 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 * Sin2Atan( 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 * Sin3Pi2( 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;
public PacejkaPreset Lateral { get; set; } = new();
public PacejkaPreset Longitudinal { get; set; } = new();
// 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 * Sin3Pi2( 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 static float SinPi2( float x )
{
float s = x * x;
float p = -1.8447486103462252e-04f;
p = 8.3109378830028557e-03f + p * s;
p = -1.6665578084732124e-01f + p * s;
p = 1.0f + p * s;
return p * x;
}
// Вычисление sin(x) для |x| <= 3π/2
public static float Sin3Pi2( float x )
{
// Приведение x к интервалу [-π, π]
if ( x < -MathF.PI )
x += 2 * MathF.PI;
else if ( x > MathF.PI )
x -= 2 * MathF.PI;
// Отражение в интервал [-π/2, π/2] с использованием симметрии
if ( x < -MathF.PI / 2 )
x = -MathF.PI - x;
else if ( x > MathF.PI / 2 )
x = MathF.PI - x;
return SinPi2( x );
}
public static float Sin2Atan( float x )
{
return 2 * x / (x * x + 1);
}
public static float Sin2Atan( float y, float x )
{
return 2 * x * y / (x * x + y * y);
}
public float PacejkaFx( float slip ) => Longitudinal.Evaluate( slip );
public float PacejkaFy( float slip ) => Lateral.Evaluate( slip );
}

View File

@@ -0,0 +1,18 @@
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 );
}
}

View File

@@ -0,0 +1,26 @@
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 );
}
}

View File

@@ -0,0 +1,32 @@
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 );
}
}

View File

@@ -24,12 +24,25 @@ public class TirePreset : GameResource
return resistance;
}
public void ComputeSlip( float vlon, float vlat, float vrot, out float slip_ratio, out float slip_angle )
public static void ComputeSlip( float lon_velocity, float lat_velocity, float rot_velocity, float wheel_radius, out float slip_ratio, out float slip_angle )
{
float rvlon = 1 / MathF.Max( MathF.Abs( vlon ), 1E-3f );
float vslip = vrot - vlon;
slip_ratio = vslip * rvlon;
slip_angle = -MathF.Atan( vlat * rvlon );
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 );
}
/// approximate asin(x) = x + x^3/6 for +-18 deg range
@@ -39,60 +52,6 @@ public class TirePreset : GameResource
return ((1 / 6.0f) * (sc * sc) + 1) * sc;
}
public struct TireState
{
public float friction = 0; // surface friction coefficient
public float camber = 0; // tire camber angle relative to track surface
public float vcam = 0; // camber thrust induced lateral slip velocity
public float slip = 0; // ratio of tire contact patch speed to road speed
public float slip_angle = 0; // the angle between the wheel heading and the wheel velocity
public float ideal_slip = 0; // peak force slip ratio
public float ideal_slip_angle = 0; // peak force slip angle
public float fx = 0; // positive during traction
public float fy = 0; // positive in a right turn
public float mz = 0; // positive in a left turn
public TireState()
{
}
};
public void ComputeState(
float normal_force,
float rot_velocity,
float lon_velocity,
float lat_velocity,
float camber_angle,
out TireState s
)
{
s = new TireState
{
camber = camber_angle,
friction = 1.0f
};
if ( normal_force * s.friction < 1E-6f )
{
s.slip = s.slip_angle = 0;
s.fx = s.fy = s.mz = 0;
return;
}
float Fz = Math.Min( normal_force * 1E-3f, 30f );
ComputeSlip( lon_velocity, lat_velocity, rot_velocity, out float slip, out float slip_angle );
float sigma = slip;
float alpha = slip_angle.RadianToDegree();
float gamma = s.camber.RadianToDegree();
float Fx = Pacejka.PacejkaFx( sigma, Fz, s.friction );
float Fy = Pacejka.PacejkaFy( alpha, Fz, gamma, s.friction, out float camber_alpha );
s.vcam = ComputeCamberVelocity( camber_alpha.DegreeToRadian(), lon_velocity );
s.slip = slip;
s.slip_angle = slip_angle;
s.fx = Fx;
s.fy = Fy;
}
public static float ComputeCamberVelocity( float sa, float vx )
{
float tansa = (1 / 3.0f * (sa * sa) + 1) * sa;

View File

@@ -0,0 +1,28 @@
using Sandbox;
using System;
namespace VeloX;
public partial class VeloXWheel : Component
{
private RealTimeUntil expandSoundCD;
private RealTimeUntil contractSoundCD;
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 );
}
}
}

View File

@@ -1,9 +1,16 @@
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,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;
@@ -38,8 +45,8 @@ public partial class VeloXWheel : Component
public float Spin { get; private set; }
public float RPM { get => angularVelocity * 60f / MathF.Tau; set => angularVelocity = value / (60 / MathF.Tau); }
public float AngularVelocity => angularVelocity;
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; }
@@ -53,21 +60,23 @@ public partial class VeloXWheel : Component
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 float forwardFriction;
private float sideFriction;
private Vector3 force;
public float CounterTorque { get; private set; }
private float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
public float Inertia => BaseInertia;
internal float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
public float Inertia
{
get => BaseInertia + inertia;
set => inertia = value;
}
protected override void OnAwake()
@@ -77,24 +86,6 @@ public partial class VeloXWheel : Component
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 );
@@ -102,18 +93,14 @@ public partial class VeloXWheel : Component
private void UpdateVisuals( VeloXBase vehicle, in float dt )
{
var entityAngles = vehicle.WorldRotation;
Spin -= angularVelocity.MeterToInch() * 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 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 );
@@ -126,6 +113,100 @@ 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 ));
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 )
{
@@ -133,10 +214,6 @@ public partial class VeloXWheel : Component
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;
@@ -145,123 +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 = 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;
var vel = vehicle.Body.GetVelocityAtPoint( pos );
vel.x = vel.x.InchToMeter();
vel.y = vel.y.InchToMeter();
vel.z = vel.z.InchToMeter();
if ( !IsOnGround )
{
forwardFriction = new Friction();
sideFriction = new Friction();
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;
force = (springForce - damperForce) * MathF.Max( 0, up.Dot( normal ) ) * normal / Time.Delta;
// 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 = Math.Max( force.z.InchToMeter(), 0 );
var velU = Trace.Normal.Dot( vel );
if ( IsOnGround )
if ( velU < 0 && offset + Math.Abs( velU * Time.Delta ) > SuspensionLength )
{
float forwardSpeed = vel.Dot( forward );
float sideSpeed = vel.Dot( right );
vehicle.Body.CalculateVelocityOffset( -velU / Time.Delta * Trace.Normal, pos, out var linearImp, out var angularImp );
float camber_rad = CamberAngle.DegreeToRadian();
vehicle.Body.Velocity += linearImp;
vehicle.Body.AngularVelocity += angularImp;
vehicle.Body.CalculateVelocityOffset( Trace.HitPosition - (contactPos + Trace.Normal * velU * Time.Delta), pos, out var lin, out _ );
TirePreset.ComputeState(
load,
angularVelocity,
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,
sideSpeed,
camber_rad,
out var tireState
angularVelocity,
longitudinalLoadCoefficient,
R,
Inertia
);
float linearSpeed = angularVelocity * Radius.InchToMeter();
float F_roll = TirePreset.GetRollingResistance( linearSpeed, 1.0f );
F_roll = -MathF.Sign( forwardSpeed ) * F_roll;
StepLateral( forwardSpeed, sideSpeed, lateralLoadCoefficient, out float Sy, out float Fy );
float Fx_total = tireState.fx + F_roll;
SlipCircle( Sx, Sy, Fx, ref Fy );
float R = Radius.InchToMeter();
float I = Inertia;
float T_brake = Brake * BrakePowerMax;
CounterTorque = counterTq;
angularVelocity = W;
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;
angularVelocity += totalTorque / I * Time.Delta;
forwardFriction = new Friction()
{
Slip = tireState.slip,
Force = Fx_total.MeterToInch(),
Speed = forwardSpeed
};
sideFriction = new Friction()
{
Slip = tireState.slip_angle,
Force = tireState.fy.MeterToInch(),
Speed = sideSpeed
};
Vector3 frictionForce = forward * forwardFriction.Force + right * sideFriction.Force;
vehicle.Body.ApplyForceAt( contactPos, force + frictionForce );
}
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;
}
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 );
}
// debug
#if 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 );
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
}

View File

@@ -14,33 +14,40 @@ public partial class VeloXCar
[Property, Feature( "Steer" )] public float MaxSteerAngle { get; set; } = 35f;
[Sync] public float Steering { get; private set; }
private float jTurnMultiplier;
private float inputSteer;
public static float SignedAngle( Vector3 from, Vector3 to, Vector3 axis )
{
float unsignedAngle = Vector3.GetAngle( from, to );
float cross_x = from.y * to.z - from.z * to.y;
float cross_y = from.z * to.x - from.x * to.z;
float cross_z = from.x * to.y - from.y * to.x;
float sign = MathF.Sign( axis.x * cross_x + axis.y * cross_y + axis.z * cross_z );
return unsignedAngle * sign;
}
public float VelocityAngle { get; private set; }
public int CarDirection { get { return ForwardSpeed.InchToMeter() < 5 ? 0 : (VelocityAngle < 90 && VelocityAngle > -90 ? 1 : -1); } }
private void UpdateSteering( float dt )
{
var inputSteer = Input.AnalogMove.y;
var absInputSteer = Math.Abs( inputSteer );
var sideSlip = Math.Clamp( avgSideSlip, -1, 1 );
var steerConeFactor = Math.Clamp( TotalSpeed / SteerConeMaxSpeed, 0, 1 );
var steerCone = 1 - steerConeFactor * (1 - SteerConeMaxAngle);
VelocityAngle = 0;// -SignedAngle( Body.Velocity, WorldRotation.Forward, WorldRotation.Up );
steerCone = Math.Clamp( steerCone, Math.Abs( sideSlip ), 1 );
//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;
var counterSteer = sideSlip * steerConeFactor * (1 - absInputSteer);
counterSteer = Math.Clamp( counterSteer, -1, 1 ) * CounterSteer;
inputSteer = Math.Clamp( inputSteer + counterSteer, -1, 1 );
float target = -inputSteer * MaxSteerAngle;
if ( CarDirection > 0 )
target -= VelocityAngle * CounterSteer;
inputSteer = Math.Clamp( inputSteer, -1, 1 );
Steering = inputSteer;
SteerAngle = new( 0, inputSteer * MaxSteerAngle, 0 );
if ( ForwardSpeed < -100 )
jTurnMultiplier = 0.5f;
else
jTurnMultiplier = ExpDecay( jTurnMultiplier, 1, 2, dt );
SteerAngle = new( 0, target, 0 );
}
}

View File

@@ -2,27 +2,9 @@
public partial class VeloXCar
{
private float avgSideSlip;
private float avgPoweredRPM;
private float avgForwardSlip;
private void WheelThink( in float dt )
{
float avgRPM = 0, totalSideSlip = 0, totalForwardSlip = 0;
foreach ( var w in Wheels )
{
w.Update( this, dt );
totalSideSlip += w.SideSlip;
totalForwardSlip += w.ForwardSlip;
var rpm = w.RPM;
avgRPM += rpm * w.DistributionFactor;
}
avgPoweredRPM = avgRPM;
avgSideSlip = totalSideSlip / Wheels.Count;
avgForwardSlip = totalForwardSlip / Wheels.Count;
}
}

View File

@@ -7,32 +7,6 @@ namespace VeloX;
[Title( "VeloX - Car" )]
public partial class VeloXCar : VeloXBase
{
protected override void OnStart()
{
base.OnStart();
//StreamPlayer = new( Stream );
//if ( IsDriver )
//{
// UpdateGearList();
// UpdatePowerDistribution();
//}
}
protected override void OnUpdate()
{
base.OnUpdate();
//if ( StreamPlayer is not null )
//{
// StreamPlayer.Throttle = Throttle;
// StreamPlayer.RPMPercent = RPMPercent;
// StreamPlayer.EngineState = EngineState;
// StreamPlayer.IsRedlining = IsRedlining;
// StreamPlayer.Update( Time.Delta, WorldPosition );
//}
}
protected override void OnFixedUpdate()
{
if ( !IsDriver )

View File

@@ -28,44 +28,38 @@ public static class PhysicsExtensions
value.x * (xz2 - wy2) + value.y * (yz2 + wx2) + value.z * (1.0f - xx2 - yy2)
);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="physObj">The physics object</param>
/// <param name="impulse">The impulse acting on the object in kg*units/s (World frame)</param>
/// <param name="position">The location of the impulse in world coordinates</param>
/// <returns>
/// Vector1: Linear velocity from the impulse (World frame)
/// Vector2: Angular velocity from the impulse (Local frame)
/// </returns>
public static (Vector3 LinearVelocity, Vector3 AngularVelocity) CalculateVelocityOffset( this PhysicsBody physObj, Vector3 impulse, Vector3 position )
/// <param name="LinearVelocity">Linear velocity on center of mass (World frame)</param>
/// <param name="AngularVelocity">Angular velocity on center of mass (World frame)</param>
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);
}
/// <summary>
@@ -74,34 +68,31 @@ public static class PhysicsExtensions
/// <param name="physObj">The physics object</param>
/// <param name="impulse">The impulse acting on the object in kg*units/s (World frame)</param>
/// <param name="position">The location of the impulse in world coordinates</param>
/// <returns>
/// Vector1: Linear impulse on center of mass (World frame)
/// Vector2: Angular impulse on center of mass (Local frame)
/// </returns>
public static (Vector3 LinearImpulse, Vector3 AngularImpulse) CalculateForceOffset(
this PhysicsBody physObj,
/// <param name="LinearImpulse">Linear impulse on center of mass (World frame)</param>
/// <param name="AngularImpulse">Angular impulse on center of mass (World frame)</param>
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;
}
}

View File

@@ -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<Pacejka.LateralForce>().Fields )
foreach ( var item in TypeLibrary.GetType<Pacejka.PacejkaPreset>().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<Pacejka.LongitudinalForce>().Fields )
{
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<Pacejka.AligningMoment>().Fields )
{
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 ) ) ) )
);
}
}

View File

@@ -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<TirePreset>();
RebuildSheet();
}
}

View File

@@ -1,7 +1,6 @@
using Editor;
using Editor.Assets;
using Editor.Inspectors;
using Editor.ShaderGraph.Nodes;
using Sandbox;
using System;
using System.Collections.Generic;
@@ -10,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<TirePreset>();
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()
@@ -43,125 +30,6 @@ class TirePresetPreview( Asset asset ) : PixmapAssetPreview( asset )
info.MouseLeftPress = () => OpenSettings( info );
return info;
}
static List<Vector2> 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 maxforce ) / load;
float xval = width * (x - x0) / (xn - x0);
yval /= ymax - ymin;
yval = (yval + 1.0f) * 0.5f;
yval = 1.0f - yval;
yval *= height;
if ( x == x0 )
pointCache.Add( new( xval, yval ) );
else
pointCache.Add( new( xval, yval ) );
}
Paint.DrawLine( pointCache );
}
{ // draw longitudinal 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;
if ( x == x0 )
pointCache.Add( new( xval, yval ) );
else
pointCache.Add( new( xval, yval ) );
}
Paint.DrawLine( pointCache );
}
{ // draw aligning 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 ) / load;
float xval = width * (x - x0) / (xn - x0);
yval /= ymax - ymin;
yval = (yval + 1.0f) * 0.5f;
yval = 1.0f - yval;
yval *= height;
if ( x == x0 )
pointCache.Add( new( xval, yval ) );
else
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 )
@@ -174,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;
@@ -185,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<ModelRenderer>();
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<Vector2> pointCache = [];
public TirePresetPreview( Asset asset ) : base( asset )
{
texture = Texture.CreateRenderTarget().WithDynamicUsage().WithScreenFormat().WithSize( 512, 512 ).Create();
Tire = Asset.LoadResource<TirePreset>();
}
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 );
}
}