using Sandbox; using System; namespace VeloX; public partial class VeloXWheel { /// /// Constant torque acting similar to brake torque. /// Imitates rolling resistance. /// [Property, Range( 0, 500 ), Sync] public float RollingResistanceTorque { get; set; } = 30f; /// /// The percentage this wheel is contributing to the total vehicle load bearing. /// public float LoadContribution { get; set; } = 0.25f; /// /// Maximum load the tire is rated for in [N]. /// Used to calculate friction.Default value is adequate for most cars but /// larger and heavier vehicles such as semi trucks will use higher values. /// A good rule of the thumb is that this value should be 2x the Load /// while vehicle is stationary. /// [Property, Sync] public float LoadRating { get; set; } = 5400; /// /// The amount of torque returned by the wheel. /// Under no-slip conditions this will be equal to the torque that was input. /// When there is wheel spin, the value will be less than the input torque. /// public float CounterTorque { get; private set; } //[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f; public Vector3 FrictionForce; private Vector3 hitContactVelocity; private Vector3 hitForwardDirection; private Vector3 hitSidewaysDirection; public Vector3 ContactRight => hitSidewaysDirection; public Vector3 ContactForward => hitForwardDirection; public float LongitudinalSlip => sx; public float LongitudinalSpeed => vx; public bool IsSkiddingLongitudinally => NormalizedLongitudinalSlip > 0.35f; public float NormalizedLongitudinalSlip => Math.Clamp( Math.Abs( LongitudinalSlip ), 0, 1 ); public float LateralSlip => sy; public float LateralSpeed => vy; public bool IsSkiddingLaterally => NormalizedLateralSlip > 0.35f; public float NormalizedLateralSlip => Math.Clamp( Math.Abs( LateralSlip ), 0, 1 ); public bool IsSkidding => IsSkiddingLaterally || IsSkiddingLongitudinally; public float NormalizedSlip => (NormalizedLateralSlip + NormalizedLongitudinalSlip) / 2f; // speed [Sync] private float vx { get; set; } [Sync] private float vy { get; set; } // force [Sync] private float fx { get; set; } [Sync] private float fy { get; set; } // slip [Sync] private float sx { get; set; } [Sync] private float sy { get; set; } private void UpdateHitVariables() { if ( IsOnGround ) { hitContactVelocity = Vehicle.Body.GetVelocityAtPoint( ContactPosition + Vehicle.Body.MassCenter ); hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal; hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection; vx = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter(); vy = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter(); } else { vx = 0; vy = 0; } } private Vector3 lowSpeedReferencePosition; private bool lowSpeedReferenceIsSet; private Vector3 currentPosition; private Vector3 referenceError; private Vector3 correctiveForce; public bool wheelIsBlocked; private void UpdateFriction( float dt ) { var motorTorque = DriveTorque; var brakeTorque = BrakeTorque; float allWheelLoadSum = Vehicle.CombinedLoad; LoadContribution = allWheelLoadSum == 0 ? 1f : Fz / allWheelLoadSum; float mRadius = Radius; float invDt = 1f / dt; float invRadius = 1f / mRadius; float inertia = Inertia; float invInertia = 1f / Inertia; float loadClamped = Math.Clamp( Fz, 0, LoadRating ); float forwardLoadFactor = loadClamped * 1.35f; float sideLoadFactor = loadClamped * 1.9f; float loadPercent = Math.Clamp( Fz / LoadRating, 0f, 1f ); float slipLoadModifier = 1f - loadPercent * 0.4f; float mass = Vehicle.Body.Mass; float absForwardSpeed = Math.Abs( vx ); float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt; float absSideSpeed = Math.Abs( vy ); float sideForceClamp = mass * LoadContribution * absSideSpeed * invDt; float forwardSpeedClamp = 1.5f * (dt / 0.005f); forwardSpeedClamp = Math.Clamp( forwardSpeedClamp, 1.5f, 10f ); float clampedAbsForwardSpeed = Math.Max( absForwardSpeed, forwardSpeedClamp ); float peakForwardFrictionForce = 11000 * (1 - MathF.Exp( -0.00014f * forwardLoadFactor )); float absCombinedBrakeTorque = Math.Max( 0, brakeTorque + RollingResistanceTorque ); float signedCombinedBrakeTorque = absCombinedBrakeTorque * (vx > 0 ? -1 : 1); float signedCombinedBrakeForce = signedCombinedBrakeTorque * invRadius; float motorForce = motorTorque * invRadius; float forwardInputForce = motorForce + signedCombinedBrakeForce; float absMotorTorque = Math.Abs( motorTorque ); float absBrakeTorque = Math.Abs( brakeTorque ); float maxForwardForce = Math.Min( peakForwardFrictionForce, forwardForceClamp ); maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce; fx = forwardInputForce > maxForwardForce ? maxForwardForce : forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce; wheelIsBlocked = false; if ( IsOnGround ) { float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity ); float wheelForceClampOverflow = 0; if ( (combinedWheelForce >= 0 && AngularVelocity < 0) || (combinedWheelForce < 0 && AngularVelocity > 0) ) { float absWheelForceClamp = Math.Abs( AngularVelocity ) * inertia * invRadius * invDt; float absCombinedWheelForce = combinedWheelForce < 0 ? -combinedWheelForce : combinedWheelForce; float wheelForceDiff = absCombinedWheelForce - absWheelForceClamp; wheelForceClampOverflow = Math.Max( 0, wheelForceDiff ) * Math.Sign( combinedWheelForce ); combinedWheelForce = Math.Clamp( combinedWheelForce, -absWheelForceClamp, absWheelForceClamp ); } AngularVelocity += combinedWheelForce * mRadius * invInertia * dt; // Surface (corrective) force float noSlipAngularVelocity = vx * invRadius; float angularVelocityError = AngularVelocity - noSlipAngularVelocity; float angularVelocityCorrectionForce = Math.Clamp( -angularVelocityError * inertia * invRadius * invDt, -maxForwardForce, maxForwardForce ); if ( absMotorTorque < absBrakeTorque && Math.Abs( wheelForceClampOverflow ) > Math.Abs( angularVelocityCorrectionForce ) ) { wheelIsBlocked = true; AngularVelocity += vx > 0 ? 1e-10f : -1e-10f; } else { AngularVelocity += angularVelocityCorrectionForce * mRadius * invInertia * dt; } } else { float maxBrakeTorque = AngularVelocity * inertia * invDt + motorTorque; maxBrakeTorque = maxBrakeTorque < 0 ? -maxBrakeTorque : maxBrakeTorque; float brakeTorqueSign = AngularVelocity < 0f ? -1f : 1f; float clampedBrakeTorque = Math.Clamp( absCombinedBrakeTorque, -maxBrakeTorque, maxBrakeTorque ); AngularVelocity += (motorTorque - brakeTorqueSign * clampedBrakeTorque) * invInertia * dt; } float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity; float maxCounterTorque = inertia * absAngularVelocity; CounterTorque = Math.Clamp( (signedCombinedBrakeForce - fx) * mRadius, -maxCounterTorque, maxCounterTorque ); sx = (vx - AngularVelocity * mRadius) / clampedAbsForwardSpeed; sx *= slipLoadModifier; sy = MathF.Atan2( vy, clampedAbsForwardSpeed ); sy *= slipLoadModifier; float sideSlipSign = sy > 0 ? 1 : -1; float absSideSlip = Math.Abs( sy ); float peakSideFrictionForce = 18000 * (1 - MathF.Exp( -0.0001f * sideLoadFactor )); float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * peakSideFrictionForce; fy = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp ); // Calculate effect of camber on friction float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) ); fy *= camberFrictionCoeff; if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f ) { float verticalOffset = RestLength + mRadius; var transformPosition = WorldPosition; var transformUp = TransformRotationSteer.Up; currentPosition.x = transformPosition.x - transformUp.x * verticalOffset; currentPosition.y = transformPosition.y - transformUp.y * verticalOffset; currentPosition.z = transformPosition.z - transformUp.z * verticalOffset; if ( !lowSpeedReferenceIsSet ) { lowSpeedReferenceIsSet = true; lowSpeedReferencePosition = currentPosition; } else { referenceError.x = lowSpeedReferencePosition.x - currentPosition.x; referenceError.y = lowSpeedReferencePosition.y - currentPosition.y; referenceError.z = lowSpeedReferencePosition.z - currentPosition.z; correctiveForce.x = invDt * LoadContribution * mass * referenceError.x; correctiveForce.y = invDt * LoadContribution * mass * referenceError.y; correctiveForce.z = invDt * LoadContribution * mass * referenceError.z; if ( wheelIsBlocked && absAngularVelocity < 0.5f ) { fx += correctiveForce.Dot( hitForwardDirection ); } fy += correctiveForce.Dot( hitSidewaysDirection ); } } else { lowSpeedReferenceIsSet = false; } fx = Math.Clamp( fx, -peakForwardFrictionForce, peakForwardFrictionForce ); fy = Math.Clamp( fy, -peakSideFrictionForce, peakSideFrictionForce ); if ( absForwardSpeed > 0.01f || absAngularVelocity > 0.01f ) { var f = MathF.Sqrt( fx * fx + fy * fy ); var d = Math.Abs( new Vector2( sx, sy ).Normal.y ); fy = f * d * Math.Sign( fy ); } if ( IsOnGround ) { FrictionForce.x = (hitSidewaysDirection.x * fy + hitForwardDirection.x * fx).MeterToInch(); FrictionForce.y = (hitSidewaysDirection.y * fy + hitForwardDirection.y * fx).MeterToInch(); FrictionForce.z = (hitSidewaysDirection.z * fy + hitForwardDirection.z * fx).MeterToInch(); //DebugOverlay.Normal( WorldPosition, hitSidewaysDirection * 10, overlay: true, color: Color.Red ); //DebugOverlay.Normal( WorldPosition, hitForwardDirection * 10, overlay: true, color: Color.Green ); //DebugOverlay.Normal( WorldPosition, FrictionForce.ClampLength( 30 ), overlay: true, color: Color.Cyan ); //DebugOverlay.ScreenText( Scene.Camera.PointToScreenPixels( WorldPosition ), $"{ForwardFriction}\nMotor:{(int)motorTorque}\nBrake:{(int)brakeTorque}", flags: TextFlag.LeftTop ); } else FrictionForce = Vector3.Zero; } }