void UpdateTireMarks(WheelData wheelData, TireFxData fxData) { // If we are already drawing marks to this wheel, wait before updating. if (fxData.lastMarksIndex != -1 && wheelData.grounded && fxData.marksDelta < updateInterval) { fxData.marksDelta += Time.deltaTime; return; } // deltaT = time since last mark for this wheel float deltaT = fxData.marksDelta; if (deltaT == 0.0f) deltaT = Time.deltaTime; fxData.marksDelta = 0.0f; // Verify: Should we put marks? // - Grounded // - Contacted object has not a rigidbody (assumed to be static) if (!wheelData.grounded || wheelData.hit.collider.attachedRigidbody != null) { fxData.lastMarksIndex = -1; return; } // Have we changed renderer? If so, start a new tread TireMarksRenderer marksRenderer = wheelData.groundMaterial != null? wheelData.groundMaterial.marksRenderer : null; if (marksRenderer != fxData.lastRenderer) { fxData.lastRenderer = marksRenderer; fxData.lastMarksIndex = -1; } if (marksRenderer != null) { float pressureRatio = Mathf.Clamp01(intensity * wheelData.downforceRatio * 0.5f); float skidRatio = Mathf.InverseLerp(minSlip, maxSlip, wheelData.combinedTireSlip); fxData.lastMarksIndex = marksRenderer.AddMark( wheelData.rayHit.point - wheelData.transform.right * wheelData.collider.center.x + wheelData.velocity * deltaT, wheelData.rayHit.normal, pressureRatio, skidRatio, tireWidth, fxData.lastMarksIndex ); } }
void OnEnable() { // Cache/find components and configure rigidbody m_transform = GetComponent<Transform>(); m_rigidbody = GetComponent<Rigidbody>(); m_groundMaterialManager = FindObjectOfType<GroundMaterialManager>(); FindColliders(); m_rigidbody.maxAngularVelocity = 14.0f; m_rigidbody.maxDepenetrationVelocity = 8.0f; if (centerOfMass) m_rigidbody.centerOfMass = m_transform.InverseTransformPoint(centerOfMass.position); if (wheels.Length == 0) { Debug.LogWarning("The wheels property is empty. You must configure wheels and WheelColliders first. Component is disabled."); enabled = false; return; } // Initialize wheel data m_usesHandbrake = false; m_wheelData = new WheelData[wheels.Length]; for (int i = 0; i < m_wheelData.Length; i++) { m_wheelData[i] = new WheelData(); m_wheelData[i].wheel = wheels[i]; if (wheels[i].wheelCollider == null) Debug.LogError("A WheelCollider is missing in the list of wheels for this vehicle: " + gameObject.name); m_wheelData[i].collider = wheels[i].wheelCollider; m_wheelData[i].transform = wheels[i].wheelCollider.transform; if (wheels[i].handbrake) m_usesHandbrake = true; // Calculate the distance of the force app point wrt the center of mass UpdateWheelCollider(m_wheelData[i].collider); m_wheelData[i].forceDistance = GetWheelForceDistance(m_wheelData[i].collider); } // Configure WheelColliders WheelCollider someWheel = GetComponentInChildren<WheelCollider>(); someWheel.ConfigureVehicleSubsteps(1000.0f, 1, 1); foreach (Wheel wheel in wheels) { SetupWheelCollider(wheel.wheelCollider); UpdateWheelCollider(wheel.wheelCollider); } // Initialize other data m_lastImpactedMaterial = new PhysicMaterial(); // A new reference to ensure cache missmatch at the first query }
float FixSteerAngle(WheelData wd, float inputSteerAngle) { // World-space forward vector for the wheel in the desired steer angle Quaternion steerRot = Quaternion.AngleAxis(inputSteerAngle, wd.transform.up); Vector3 wheelForward = steerRot * wd.transform.forward; // Stupid PhysX Vehicle SDK assumes all wheels point in the same direction as the rigidbody. // // Step 1: Project the forward direction into the rigidbody's XZ plane. // This is the vector we want our wheel to point to as seen from the rigidbody. Vector3 rbWheelForward = wheelForward - Vector3.Project(wheelForward, m_transform.up); // Step 2: Calculate the final steer angle to feed PhysX with. return Vector3.Angle(m_transform.forward, rbWheelForward) * Mathf.Sign(Vector3.Dot(m_transform.right, rbWheelForward)); }
void ComputeTireForces(WheelData wd) { // Throttle for this wheel float wheelThrottleInput = wd.wheel.drive? throttleInput : 0.0f; float wheelMaxDriveSlip = maxDriveSlip; if (Mathf.Sign(wheelThrottleInput) != Mathf.Sign(wd.localVelocity.y)) wheelMaxDriveSlip -= wd.localVelocity.y * Mathf.Sign(wheelThrottleInput); // Calculate the combined brake out of brake and handbrake for this wheel float wheelBrakeInput = 0.0f; float wheelBrakeRatio = 0.0f; float wheelBrakeSlip = 0.0f; if (wd.wheel.brake && wd.wheel.handbrake) { wheelBrakeInput = Mathf.Max(brakeInput, handbrakeInput); if (handbrakeInput >= brakeInput) ComputeBrakeValues(wd, handbrakeMode, maxHandbrakeSlip, maxHandbrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio); else ComputeBrakeValues(wd, brakeMode, maxBrakeSlip, maxBrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio); } else if (wd.wheel.brake) { wheelBrakeInput = brakeInput; ComputeBrakeValues(wd, brakeMode, maxBrakeSlip, maxBrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio); } else if (wd.wheel.handbrake) { wheelBrakeInput = handbrakeInput; ComputeBrakeValues(wd, handbrakeMode, maxHandbrakeSlip, maxHandbrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio); } // Combine throttle and brake inputs. There can be only one. // (Not really - EVP uses this simplication. VPP combines throttle and brake in the // physically correct way) float absThrottleInput = Mathf.Abs(wheelThrottleInput); if (absThrottleInput >= wheelBrakeInput) { wd.finalInput = (absThrottleInput - wheelBrakeInput) * Mathf.Sign(wheelThrottleInput); wd.isBraking = false; } else { wd.finalInput = wheelBrakeInput - absThrottleInput; wd.isBraking = true; } // Calculate demanded force coming from the wheel's axle float demandedForce; if (wd.isBraking) demandedForce = wd.finalInput * maxBrakeForce; else demandedForce = ComputeDriveForce(wd.finalInput * maxDriveForce, maxDriveForce, wd.grounded); // ABS and TC limits if (wd.grounded) { if (tcEnabled) wheelMaxDriveSlip = Mathf.Lerp(wheelMaxDriveSlip, 0.1f, tcRatio); if (absEnabled && brakeInput > handbrakeInput) { wheelBrakeSlip = Mathf.Lerp(wheelBrakeSlip, 0.1f, absRatio); wheelBrakeRatio = Mathf.Lerp(wheelBrakeRatio, wheelBrakeRatio * 0.1f, absRatio); } } // Calculate tire forces if (wd.grounded) { wd.tireSlip.x = wd.localVelocity.x; wd.tireSlip.y = wd.localVelocity.y - wd.angularVelocity * wd.collider.radius; // Get the ground properties float groundGrip; float groundDrag; if (wd.groundMaterial != null) { groundGrip = wd.groundMaterial.grip; groundDrag = wd.groundMaterial.drag; } else { groundGrip = defaultGroundGrip; groundDrag = defaultGroundDrag; } // Ensure there's longitudinal slip enough for the demanded longitudinal force float forceMagnitude = tireFriction * wd.downforce * groundGrip; float minSlipY; if (wd.isBraking) { float wheelMaxBrakeSlip = Mathf.Max(Mathf.Abs(wd.localVelocity.y * wheelBrakeRatio), wheelBrakeSlip); minSlipY = Mathf.Clamp(Mathf.Abs(demandedForce * wd.tireSlip.x) / forceMagnitude, 0.0f, wheelMaxBrakeSlip); } else { minSlipY = Mathf.Min(Mathf.Abs(demandedForce * wd.tireSlip.x) / forceMagnitude, wheelMaxDriveSlip); if (demandedForce != 0.0f && minSlipY < 0.1f) minSlipY = 0.1f; } if (Mathf.Abs(wd.tireSlip.y) < minSlipY) wd.tireSlip.y = minSlipY * Mathf.Sign(wd.tireSlip.y); // Compute combined tire forces Vector2 rawTireForce = -forceMagnitude * wd.tireSlip.normalized; rawTireForce.x = Mathf.Abs(rawTireForce.x); rawTireForce.y = Mathf.Abs(rawTireForce.y); // Sideways force wd.tireForce.x = Mathf.Clamp(wd.localRigForce.x, -rawTireForce.x, +rawTireForce.x); // Forward force if (wd.isBraking) { float maxFy = Mathf.Min(rawTireForce.y, demandedForce); wd.tireForce.y = Mathf.Clamp(wd.localRigForce.y, -maxFy, +maxFy); } else { wd.tireForce.y = Mathf.Clamp(demandedForce, -rawTireForce.y, +rawTireForce.y); } // Drag force as for the surface resistance wd.dragForce = -(forceMagnitude * wd.localVelocity.magnitude * groundDrag * 0.001f) * wd.localVelocity; } else { wd.tireSlip = Vector2.zero; wd.tireForce = Vector2.zero; wd.dragForce = Vector2.zero; } // Compute angular velocity for the next step float slipToForce = wd.isBraking? brakeForceToMaxSlip : driveForceToMaxSlip; float slipRatio = Mathf.Clamp01((Mathf.Abs(demandedForce) - Mathf.Abs(wd.tireForce.y)) / slipToForce); float slip; if (wd.isBraking) slip = Mathf.Clamp(-slipRatio * wd.localVelocity.y * wheelBrakeRatio, -wheelBrakeSlip, wheelBrakeSlip); else slip = slipRatio * wheelMaxDriveSlip * Mathf.Sign(demandedForce); wd.angularVelocity = (wd.localVelocity.y + slip) / wd.collider.radius; }
void ComputeExtendedTireData(WheelData wd, float referenceDownforce) { wd.combinedTireSlip = ComputeCombinedSlip(wd.localVelocity, wd.tireSlip); wd.downforceRatio = wd.hit.force / referenceDownforce; }
// Calculate brake ratio and slip based on the current brake method void ComputeBrakeValues(WheelData wd, BrakeMode mode, float maxSlip, float maxRatio, out float brakeSlip, out float brakeRatio) { if (mode == BrakeMode.Slip) { brakeSlip = maxSlip; brakeRatio = 1.0f; } else { brakeSlip = Mathf.Abs(wd.localVelocity.y); brakeRatio = maxRatio; } }
void ApplyTireForces(WheelData wd) { if (wd.grounded) { if (!disallowRuntimeChanges) wd.forceDistance = GetWheelForceDistance(wd.collider); Vector3 sidewaysForcePoint = wd.hit.point + wd.transform.up * antiRoll * wd.forceDistance; Vector3 forwardForce = wd.hit.forwardDir * (wd.tireForce.y + wd.dragForce.y); Vector3 sidewaysForce = wd.hit.sidewaysDir * (wd.tireForce.x + wd.dragForce.x); if (wd.wheel.steer) { if (wd.steerAngle != 0.0f && Mathf.Sign(wd.steerAngle) != Mathf.Sign(wd.tireSlip.x)) { sidewaysForcePoint += wd.hit.forwardDir * steeringOverdrive; } } m_rigidbody.AddForceAtPosition(forwardForce, wd.hit.point); m_rigidbody.AddForceAtPosition(sidewaysForce, sidewaysForcePoint); Rigidbody otherRb = wd.hit.collider.attachedRigidbody; if (otherRb != null && !otherRb.isKinematic) { otherRb.AddForceAtPosition(-forwardForce, wd.hit.point); otherRb.AddForceAtPosition(-sidewaysForce, sidewaysForcePoint); } } }
void UpdateWheelSleep(WheelData wd) { if (wd.localVelocity.magnitude < sleepVelocity && Time.time-m_lastStrongImpactTime > 0.2f && (wd.isBraking && wd.finalInput > 0.01f || m_usesHandbrake && handbrakeInput > 0.1f) ) { wd.collider.motorTorque = 0.0f; } else { wd.collider.motorTorque = 0.00001f; } }
// Set the visual transform for the wheel void UpdateTransform(WheelData wd) { if (wd.wheel.wheelTransform != null) { wd.angularPosition = (wd.angularPosition + wd.angularVelocity * Time.deltaTime) % (Mathf.PI*2.0f); // Wheel position float elongation; if (wheelPositionMode == PositionMode.Fast) { elongation = wd.collider.suspensionDistance * (1.0f - wd.suspensionCompression) + wd.collider.radius * 0.05f; wd.rayHit.point = wd.hit.point; wd.rayHit.normal = wd.hit.normal; } else { if (Physics.Raycast(wd.origin, -wd.transform.up, out wd.rayHit, (wd.collider.suspensionDistance + wd.collider.radius))) elongation = wd.rayHit.distance - wd.collider.radius * 0.95f; else elongation = wd.collider.suspensionDistance + wd.collider.radius * 0.05f; } Vector3 wheelPosition = wd.transform.position - wd.transform.up * elongation; wd.wheel.wheelTransform.position = wheelPosition; // Wheel rotation wd.wheel.wheelTransform.rotation = wd.transform.rotation * Quaternion.Euler(wd.angularPosition * Mathf.Rad2Deg, wd.steerAngle, 0.0f); } else { wd.rayHit.point = wd.hit.point; wd.rayHit.normal = wd.hit.normal; } }
void UpdateSuspension(WheelData wd) { // Retrieve the wheel's contact point wd.grounded = wd.collider.GetGroundHit(out wd.hit); wd.origin = wd.transform.TransformPoint(wd.collider.center); if (wd.grounded && !disableWheelHitCorrection) { RaycastHit rayHit; if (Physics.Raycast(wd.origin, -wd.transform.up, out rayHit, wd.collider.suspensionDistance + wd.collider.radius)) { wd.hit.point = rayHit.point; wd.hit.normal = rayHit.normal; } } // Suspension compression and downforce if (wd.grounded) { wd.suspensionCompression = 1.0f - (-wd.transform.InverseTransformPoint(wd.hit.point).y - wd.collider.radius) / wd.collider.suspensionDistance; if (wd.hit.force < 0.0f) wd.hit.force = 0.0f; wd.downforce = wd.hit.force; } else { wd.suspensionCompression = 0.0f; wd.downforce = 0.0f; } }
//---------------------------------------------------------------------------------------------- void UpdateSteering(WheelData wd) { if (wd.wheel.steer) { if (espEnabled && m_speed > 0.0f) { float forwardSpeed = m_speed * espRatio; float maxEspAngle = Mathf.Asin(Mathf.Clamp01(3.0f / forwardSpeed)) * Mathf.Rad2Deg; maxEspAngle = Mathf.Max(maxEspAngle, m_speedAngle); wd.steerAngle = Mathf.Clamp(maxSteerAngle * steerInput, -maxEspAngle, +maxEspAngle); } else { wd.steerAngle = maxSteerAngle * steerInput; } } else { wd.steerAngle = 0.0f; } wd.collider.steerAngle = disableSteerAngleCorrection? wd.steerAngle : FixSteerAngle(wd, wd.steerAngle); }
void UpdateLocalFrame(WheelData wd) { // Speed of the wheel rig if (!wd.grounded) { // Ensure continuity even when the wheel is lifted wd.hit.point = wd.origin - wd.transform.up * (wd.collider.suspensionDistance + wd.collider.radius); wd.hit.normal = wd.transform.up; wd.hit.collider = null; } Vector3 wheelV = m_rigidbody.GetPointVelocity(wd.hit.point); if (wd.hit.collider != null) { Rigidbody rb = wd.hit.collider.attachedRigidbody; if (rb != null) wheelV -= rb.GetPointVelocity(wd.hit.point); } wd.velocity = wheelV - Vector3.Project(wheelV, wd.hit.normal); wd.localVelocity.y = Vector3.Dot(wd.hit.forwardDir, wd.velocity); wd.localVelocity.x = Vector3.Dot(wd.hit.sidewaysDir, wd.velocity); // Forces related to the wheel rig if (!wd.grounded) { wd.localRigForce = Vector2.zero; return; } Vector2 localSurfaceForce; float surfaceForceRatio = Mathf.InverseLerp(1.0f, 0.25f, wd.velocity.sqrMagnitude); if (surfaceForceRatio > 0.0f) { Vector3 surfaceForce; float upNormal = Vector3.Dot(Vector3.up, wd.hit.normal); if (upNormal > 0.000001f) { Vector3 downForceUp = Vector3.up * wd.hit.force / upNormal; surfaceForce = downForceUp - Vector3.Project(downForceUp, wd.hit.normal); } else { surfaceForce = Vector3.up * 100000.0f; } localSurfaceForce.y = Vector3.Dot(wd.hit.forwardDir, surfaceForce); localSurfaceForce.x = Vector3.Dot(wd.hit.sidewaysDir, surfaceForce); localSurfaceForce *= surfaceForceRatio; } else { localSurfaceForce = Vector2.zero; } float estimatedSprungMass = Mathf.Clamp(wd.hit.force / -Physics.gravity.y, 0.0f, wd.collider.sprungMass) * 0.5f; Vector2 localVelocityForce = -estimatedSprungMass * wd.localVelocity / Time.deltaTime; wd.localRigForce = localVelocityForce + localSurfaceForce; }
void UpdateGroundMaterial(WheelData wd) { if (wd.grounded) UpdateGroundMaterialCached(wd.hit.collider.sharedMaterial, ref wd.lastPhysicMaterial, ref wd.groundMaterial); }
void UpdateTireParticles(WheelData wheelData, TireFxData fxData) { if (!wheelData.grounded) { // Not grounded: clear particle state and decrement the particle slip time fxData.lastParticleTime = -1.0f; fxData.slipTime -= Time.deltaTime; if (fxData.slipTime < 0.0f) fxData.slipTime = 0.0f; return; } TireParticleEmitter particleEmitter = wheelData.groundMaterial != null? wheelData.groundMaterial.particleEmitter : null; if (particleEmitter != fxData.lastEmitter) { fxData.lastEmitter = particleEmitter; fxData.lastParticleTime = -1.0f; } if (particleEmitter != null) { Vector3 position = wheelData.rayHit.point + wheelData.transform.up * tireWidth * 0.5f; Vector3 positionRandom = Random.insideUnitSphere * tireWidth; float pressureRatio = Mathf.Clamp01(wheelData.downforceRatio); float skidRatio = Mathf.InverseLerp(minSlip, maxSlip, wheelData.combinedTireSlip); // Emulate tire "heating" as for the time it has been skidding over the minSlip value. // Tire will "heat" at full rate when completely skidding at full pressure. if (skidRatio > 0.0f && particleEmitter.mode == TireParticleEmitter.Mode.PressureAndSkid) fxData.slipTime += Time.deltaTime * skidRatio * pressureRatio; else fxData.slipTime -= Time.deltaTime; fxData.slipTime = Mathf.Clamp(fxData.slipTime, minIntensityTime, limitIntensityTime); float slipTimeRatio = Mathf.InverseLerp(minIntensityTime, maxIntensityTime, fxData.slipTime); fxData.lastParticleTime = particleEmitter.EmitParticle( position + positionRandom, wheelData.velocity, wheelData.tireSlip.y * wheelData.transform.forward, pressureRatio, skidRatio * slipTimeRatio, fxData.lastParticleTime ); } else { // No particles set up for this material. Assume is not a "heating material" // and "cold down" the tire surface. fxData.slipTime -= Time.deltaTime; if (fxData.slipTime < 0.0f) fxData.slipTime = 0.0f; } }
void DrawWheelGizmos(WheelData wd) { RaycastHit rayHit; if (wd.grounded && Physics.Raycast(wd.transform.TransformPoint(wd.collider.center), -wd.transform.up, out rayHit, (wd.collider.suspensionDistance + wd.collider.radius))) { Debug.DrawLine(rayHit.point, rayHit.point + wd.transform.up * (wd.downforce / 10000.0f), wd.suspensionCompression > 0.99f? Color.magenta : Color.white); CommonTools.DrawCrossMark(wd.transform.position, wd.transform, Color.Lerp(Color.green, Color.gray, 0.5f)); Vector3 forcePoint = rayHit.point + wd.transform.up * target.antiRoll * wd.forceDistance; if (wd.wheel.steer) { if (wd.steerAngle != 0.0f && Mathf.Sign(wd.steerAngle) != Mathf.Sign(wd.tireSlip.x)) forcePoint += wd.hit.forwardDir * target.steeringOverdrive; } CommonTools.DrawCrossMark(forcePoint, wd.transform, Color.Lerp(Color.yellow, Color.gray, 0.5f)); Vector3 tireForce = wd.hit.forwardDir * wd.tireForce.y + wd.hit.sidewaysDir * wd.tireForce.x; Debug.DrawLine(forcePoint, forcePoint + CommonTools.Lin2Log(tireForce) * 0.1f, Color.green); Vector3 tireSlip = wd.hit.forwardDir * wd.tireSlip.y + wd.hit.sidewaysDir * wd.tireSlip.x; Debug.DrawLine(rayHit.point, rayHit.point + CommonTools.Lin2Log(tireSlip) * 0.5f, Color.cyan); // Vector3 wheelVelocity = wd.hit.forwardDir * wd.localVelocity.y + wd.hit.sidewaysDir * wd.localVelocity.x; // Debug.DrawLine(rayHit.point, rayHit.point + CommonTools.Lin2Log(wheelVelocity) * 0.5f, Color.Lerp(Color.blue, Color.white, 0.5f)); // Vector3 rigForce = wd.hit.sidewaysDir * wd.localRigForce.x + wd.hit.forwardDir * wd.localRigForce.y; // Debug.DrawLine(rayHit.point, rayHit.point + rigForce / 10000.0f, Color.Lerp(Color.red, Color.gray, 0.5f)); } }
string GetWheelTelemetry(WheelData wd, ref float suspensionForce) { bool sleeping = !(wd.collider.motorTorque > 0.0f); string text = string.Format("{0,-10}{1}{2,5:0.} rpm ", wd.collider.gameObject.name, sleeping? "×" : ":", wd.angularVelocity * VehicleController.WToRpm); if (wd.grounded) { text += string.Format("C:{0,5:0.00} ", wd.suspensionCompression); // text += string.Format("Vx:{0,6:0.00} Vy:{1,6:0.00} ", wd.localVelocity.x, wd.localVelocity.y); switch (dataMode) { case DataMode.TireSlipAndForce: text += string.Format("F:{0,5:0.} ", wd.downforce); text += string.Format("Sx:{0,6:0.00} Sy:{1,6:0.00} ", wd.tireSlip.x, wd.tireSlip.y); text += string.Format("Fx:{0,5:0.} Fy:{1,5:0.} ", wd.tireForce.x, wd.tireForce.y); break; case DataMode.GroundMaterial: // text += string.Format("Sa:{0,5:0.0} ", wd.GetSlipAngle() * Mathf.Rad2Deg); // text += string.Format("Slip:{0,4:0.0} ", wd.GetCombinedSlip()); text += string.Format("F:{0,4:0.0} % ", wd.downforceRatio); text += string.Format("Slip:{0,4:0.0} ", wd.combinedTireSlip); if (wd.groundMaterial != null) { text += string.Format("Grip:{0,4:0.0} Drag:{1,4:0.0} [{2}]", wd.groundMaterial.grip, wd.groundMaterial.drag, wd.groundMaterial.physicMaterial != null? wd.groundMaterial.physicMaterial.name : "no mat"); } break; } suspensionForce += wd.hit.force; } else { text += string.Format("C: 0.-- "); } return text + "\n"; }