Wheel Friction Curve Revealed

The Wheel Friction Curve used by Unity has major problems.

Most importantly there are significant differences between the actual curves and the documentation! In particular, the extremumValue setting is a curve gradient (i.e. tangent) at extremumSlip not, as stated in the documentation, a peak value of the curve. Similarly asymptoteValue is a gradient or tangent at asymptoteSlip, not (as documented) a value on the curve.

I’ve written a C# script to measure the Wheel Friction Curve and write the results to a csv file which can then be plotted using Excel or Gnuplot. (Gnuplot is an easier option, and a Gnuplot script is appended in a C# comment.)

Here is an example of an actual curve (with settings that are “workable”, considering the obvious problems). PhysX/Unity values for this curve are:
extremumSlip = 0.6
extremumValue = 1.2
asymptoteSlip = 2.0
asymptoteValue = 0.2
stiffness = 760
As you can see the curve is not at all what you’d expect.
1051067--39049--$wfc-0.60-1.20-2.00-0.20-760-6.gif
Far worse is what you get if you select ‘reasonable’ values based on following the documentation (see below). PhysX/Unity values for this curve are:
extremumSlip = 1.0
extremumValue = 1.0
asymptoteSlip = 2.0
asymptoteValue = 0.8
stiffness = 760
What you are expecting is something like the purple curve, what you get is the red curve!! Friction increases enormously with side slipping and the car will probably trip. (That’s what alerted me to the problem - once the car begins slipping it shouldn’t trip.)
1051067--39373--$wfc-1.00-1.00-2.00-0.80-760-6.gif
Notes on the Graphs

  • I’ve added the prefix Physx (e.g. PhysxExtremumValue) on all the variable names to indicate these have meanings under the present situation (Unity v3.5.6) which don’t match the descriptions in the documentation.

  • The green and blue lines are drawn as guides:

  • The green line has a gradient matching the tangent at (PhysxExtremumSlip, PhysxExtremumValue).

  • The blue line has a gradient matching the tangent at (PhysxAsymptoteSlip, PhysxAsymptoteValue). This explains why cars trip at high slip speeds; friction keeps on increasing with slip, when instead the Asymptote should have been horizontal.

  • I’ve experimented a lot and as far as I can tell, the “crazy” behaviour for slip < 0.5 m/s is always present and is not an artefact of my test code.

  • With the current implementation of the Wheel Friction Curve the most important value is PhysxAsymptoteValue. Too high and your car will trip at high slip speeds!

  • For more discussion see the opening comments in the code.

My thoughts on next steps are:

  • Please test my code and confirm that I haven’t made any mistakes!
  • Report this as a bug. Maybe Unity can lobby NVIDIA to make a fix! (My understanding is that Unity just calls the PhysX/NVIDIA Wheel Friction Curve code.)

Your thoughts and comments are welcome.

regards,
Neil

using UnityEngine;
using System.Collections;
using System;
using System.IO;

/*
This code enables the plotting of the Wheel Friction Curve.  See http://docs.unity3d.com/Documentation/ScriptReference/WheelFrictionCurve.html
Please try a few plots and compare your findings to the following:
1. The implementation of the friction curve doesn't match the PhysX/NVIDIA/Unity documentation!
   [Actually, the design intent for the friction curves given in the documentation seems OK, 
   BUT the implementation is incorrect!]
   In particular ExtremumValue setting is a curve gradient (i.e. tangent) at ExtremumSlip NOT, as stated in the
   documentation, a peak value of the curve.
   Similarly AsymptoteValue is a gradient or tangent at AsymptoteSlip, NOT (as documented) a value on the curve.
2. The curve has strange sudden high values when slip < 0.5 m/s.  This may be a PhysX kludge to stop low-speed sliding
   due to an incorrect zero friction gradient at the origin, (x,y) = (0,0)
3. The correct default stiffness setting is 760.  This value gives no scaling of curve parameters.    
4. While you can experiment with settings to get a car to slide OK, you can't simulate reality since no settings will
   give the curves given in the literature or the PhysX/Unity documentation.
   Hopefully NVIDIA will create a new API function with a correct implementation!   
A proper implementation of the intended hermetic cubic splines will fix all these problems.
(Alternatively a piecewise linear curve (of 4 pieces) would probably do a good job.)
*/
/*
This is a script to plot Wheel Friction Curves.  
The curves are plots of fricitionCoefficient [N/N] vs slip [m/s].
For a given sideSlip it calculates the sideForce of the tire.
Note: Side force on the tire = hit.Force * frictionCoeff; where 'hit' is hit data from GetGroundHit (hit)
The shape of the curve is set by cubic spline control tangents (extremumSlip,extremumValue) and (asymptoteSlip,asymptoteValue).
(Note: Contrary to the documentation extremumValue and asymptoteValue are gradients [s/m] NOT friction coefficient values [N/N].)
How it works:
 We applies a gradually increasing side force to a tire.  As the tire begins to slide it uses the acceleration of the
 wheel to calculate the skidding reaction force of the tire.
How to use it:
 1. Create a plane and set the Transform Scale to (100,1,100)
 2. Create empty an GameObject at location (0,0,0). Add this script to the object.
 3. In Inspector choose your settings for PhysX Extremum Value etc.
 4. Run the script.
 5. When it stops set Physx Stiffness = the Reference Stiffness shown in the Inspector and rerun.
 6. Look for a file wfcXXXXXXX.csv in the project directory and plot first two columns using 
    Gnuplot (or Excel).
 7. (A gnuplot script to plot the curves and experiment with a better cspline curve is given below.)

Neil Temperley 25 Aug 2012.
*/
public class FrictionTest : MonoBehaviour
{
	// INPUTS
	public float PhysxExtremumSlip = 0.6f; // [m/s]
	public float PhysxExtremumValue = 1.2f; // [s/m] GRADIENT!!
	public float PhysxAsymptoteSlip = 2.0f; // [m/s]
	public float PhysxAsymptoteValue = 0.2f; // [s/m] GRADIENT!!
	public float PhysxStiffness = 760.0f; // [-]
	public int solverIterationCount = 6; // [-] Input. In some cases value affects correct PhysxStiffness!?
	private float forceIncreaseFactor = 1.05f; // If wheel slows, force is increased by this factor.
	private int skipCountMax = 2; // number of readings skipped after force increase.
	// OUTPUTS:
	public float pushForce = 300.0f; // [N] Input/Output: starting value
	public float reactionForce = 0.0f; // [N]
	public float sidewaysSlipOld = 0.0f; // [m/s]
	public float frictionCoeff = 0.0f; // [N/N]
	private float frictionCoeffOld = 0.0f; // [N/N]
	public float frictionCoeffSlope = 0.0f; // [s/m]
	public float acceleration = 0.0f; // [m/s^2]
	public float referenceStiffness = 0.0f; // [-] Only valid at end.
	public float actualExtremumSlip; // [m/s] Only valid at end.
	public float actualExtremumFrictionCoeff; // [N/N] Only valid at end.
	public string filename;			  // Output filename
	public int skipCount = 0;
	private StreamWriter sw;
	public GameObject wheel;
	public Rigidbody wheelRigidBody;
	public GameObject wheelObject;
	public WheelCollider wheelCollider;
	public WheelHit hit;

	void Start ()
	{
		wheel = GameObject.CreatePrimitive (PrimitiveType.Cylinder);
		wheel.transform.localScale = new  Vector3 (0.25f, 0.08f, 0.25f);
//		wheel = GameObject.CreatePrimitive(PrimitiveType.Sphere);
//		wheel.transform.localScale = new  Vector3(0.25f, 0.25f, 0.25f);
		wheel.name = "Test Wheel";
		wheel.transform.rotation = Quaternion.Euler (new Vector3 (-90.0f, -90.0f, 0.0f));
		Destroy (wheel.collider);
		
		wheelRigidBody = wheel.AddComponent<Rigidbody> ();
		wheelRigidBody.mass = 300.0f; //[kg]
		wheelRigidBody.freezeRotation = true;
		wheelRigidBody.sleepVelocity = 0.001f;

		wheelObject = new GameObject ();
		wheelObject.transform.parent = wheel.transform;
		wheelObject.transform.localScale = new Vector3 (1.0f, 1.0f, 1.0f);
		wheelObject.transform.position = new Vector3 (0.0f, 0.0f, 0.0f);
		wheelObject.transform.rotation = Quaternion.Euler (new Vector3 (0.0f, 0.0f, 0.0f));
		wheelCollider = wheelObject.AddComponent<WheelCollider> ();
		wheelCollider.radius = 0.5f;

		WheelFrictionCurve wfc = new WheelFrictionCurve ();
		wfc.extremumSlip = PhysxExtremumSlip;
		wfc.extremumValue = PhysxExtremumValue;
		wfc.asymptoteSlip = PhysxAsymptoteSlip;
		wfc.asymptoteValue = PhysxAsymptoteValue;
		wfc.stiffness = PhysxStiffness;
		Physics.solverIterationCount = solverIterationCount;
		wheelCollider.sidewaysFriction = wfc;

		wheel.transform.position = new Vector3 (0.0f, 0.12f, 0.0f);
		
		filename = String.Format ("wfc-{0:F2}-{1:F2}-{2:F2}-{3:F2}-{4:F0}-{5:smile:}.csv", 
			wheelCollider.sidewaysFriction.extremumSlip, wheelCollider.sidewaysFriction.extremumValue,
			wheelCollider.sidewaysFriction.asymptoteSlip, wheelCollider.sidewaysFriction.asymptoteValue,
			wheelCollider.sidewaysFriction.stiffness, Physics.solverIterationCount);
		actualExtremumSlip = 0.0f; // [m/s]
		actualExtremumFrictionCoeff = 0.0f; // [N/N]

		skipCount = skipCountMax; // skip first two readings.
		sw = new StreamWriter (filename);
		sw.AutoFlush = true;
		sw.WriteLine ("# Speed [m/s],Friction Coeff [N/N],pushForce [N],hit.force [N],extremumSlip [m/s],extremumValue [s/m],asymptoteSlip [m/s],asymptoteValue [s/m],stiffness [-],solverIterationCount [-]");

	}
	

	// Update is called once per frame
	void Update ()
	{
	
	}
	
	void FixedUpdate ()
	{
		string outString;

		wheelRigidBody.AddForce (transform.right * pushForce);
		if (wheelCollider.GetGroundHit (out hit)  // ) {
			Mathf.Abs ((hit.force / (wheelRigidBody.mass * 9.81f)) - 1.0f) < 0.01f) {
//			if (Mathf.Abs ((hit.force / (rigidbody.mass * 9.81f)) - 1.0f) > 0.005f) { // skip early points where wheel may bounce.
//				Debug.Log("Large hit force!");
//			}
//			Debug.Log (String.Format ("hit.force = {0:F2}; rigidbody.mass * 9.81f = {1:F2}", hit.force, wheelRigidBody.mass * 9.81f));
//			Debug.Log (String.Format ("hit.sidewaysSlip = {0:F2};", hit.sidewaysSlip));
			acceleration = (hit.sidewaysSlip - sidewaysSlipOld) / Time.fixedDeltaTime;
			reactionForce = pushForce - (wheelRigidBody.mass * acceleration);
			frictionCoeff = reactionForce / hit.force;
			frictionCoeffSlope = (frictionCoeff - frictionCoeffOld) / (hit.sidewaysSlip - sidewaysSlipOld); // noisy and not needed!

			if (skipCount <= 0) { // skip points that follow a force change.
				if (hit.sidewaysSlip < wheelCollider.sidewaysFriction.asymptoteSlip 
				   hit.sidewaysSlip >= 0.5
				   frictionCoeff > actualExtremumFrictionCoeff) { // store point of maximum value:
					actualExtremumSlip = hit.sidewaysSlip;
					actualExtremumFrictionCoeff = frictionCoeff;
				}
				if (Mathf.Abs (acceleration) < 0.001f || acceleration < -0.1f) {
					outString = String.Format ("{0:F2},{1:F3},{2:F0},{3:F0},{4:F2},{5:F2},{6:F2},{7:F2},{8:F1},{9:smile:},Force Increase", 
						hit.sidewaysSlip, frictionCoeff, pushForce, hit.force,
						wheelCollider.sidewaysFriction.extremumSlip, wheelCollider.sidewaysFriction.extremumValue,
						wheelCollider.sidewaysFriction.asymptoteSlip, wheelCollider.sidewaysFriction.asymptoteValue,
						wheelCollider.sidewaysFriction.stiffness, Physics.solverIterationCount);
					pushForce *= forceIncreaseFactor; // gently increase the force to keep traversing the friction curve.
					skipCount = skipCountMax; // skip next few readings which will suffer noise from force change.
				} else {
					outString = String.Format ("{0:F2},{1:F3},{2:F0},{3:F0},{4:F2},{5:F2},{6:F2},{7:F2},{8:F1},{9:smile:},,", 
						hit.sidewaysSlip, frictionCoeff, pushForce, hit.force, 
						wheelCollider.sidewaysFriction.extremumSlip, wheelCollider.sidewaysFriction.extremumValue,
						wheelCollider.sidewaysFriction.asymptoteSlip, wheelCollider.sidewaysFriction.asymptoteValue,
						wheelCollider.sidewaysFriction.stiffness, Physics.solverIterationCount);
				} // if()
				if (hit.sidewaysSlip < 2.0 * wheelCollider.sidewaysFriction.asymptoteSlip) {
					sw.WriteLine (outString); // write data to file.
					Debug.Log (outString);
				} else {
					// WARNING! This estimate of the correct value of stiffness only works with the current
					// incorrect PhysX implementation of the Wheel Friction Curve!!..
					referenceStiffness = hit.sidewaysSlip * wheelCollider.sidewaysFriction.asymptoteValue * wheelCollider.sidewaysFriction.stiffness / frictionCoeff; 
					Debug.Log (String.Format ("Finished! Reference Stiffness = {0:F0}; Friction Coeff. Extremum = {1:F2} [N/N] at {2:F2} [m/s]; ", 
					referenceStiffness, actualExtremumFrictionCoeff, actualExtremumSlip) + outString);
					// For best accuracy set stiffness = referenceStiffness and rerun the program!
					Debug.Break (); // stop.
				} // if()
			} else {
				skipCount--;
			} // if()
			
			sidewaysSlipOld = hit.sidewaysSlip;
			frictionCoeffOld = frictionCoeff;
		}
	} // FixedUpdate ()
} // FrictionTest


// Gnuplot Script.  Cut and Paste this into a file wfc-gnuplot.plt in the Unity Project directory.
/*
# Gnuplot plot file to plot Wheel Friction Curves. 
# See http://www.gnuplot.info/
# Usage:
# Start gnuplot, then at gnuplot prompt:
#  cd 'C:\Users\Neil\Documents\Unity\Wheel Friction Project'
#  load "wfc-gnuplot.plt"
# ======================================================================
# Set these parameters to match the values you used in Unity and this gnuplot script will 
# open the correct filename:
PhysxExtremumSlip   = 0.6
PhysxExtremumValue  = 1.2
PhysxAsymptoteSlip  = 2.0
PhysxAsymptoteValue = 0.2
PhysxStiffness      = 760
solverIterationCount = 6
# This value is internal to Unity:
refStiffness   = 760
# ----------------------------------------------------------------------
sfe = sprintf("fe(x) = x * PhysxExtremumValue * PhysxStiffness/%0.0f", refStiffness)
sfa = sprintf("fa(x) = x * PhysxAsymptoteValue * PhysxStiffness/%0.0f", refStiffness)
sLabel = sprintf("PhysxExtremumSlip    = %0.2f [m/s]\nPhysxExtremumValue = %0.2f [s/m]\nPhysxAsymptoteSlip    = %0.2f [m/s]\nPhysxAsymptoteValue = %0.2f [s/m]\nPhysxStiffness = %0.0f [-]\nPhysics.solverIterationCount = %d [-]\n%s\n%s", PhysxExtremumSlip, PhysxExtremumValue, PhysxAsymptoteSlip, PhysxAsymptoteValue, PhysxStiffness, solverIterationCount, sfe, sfa)
fileName = sprintf("wfc-%0.2f-%0.2f-%0.2f-%0.2f-%0.0f-%d",PhysxExtremumSlip, PhysxExtremumValue, PhysxAsymptoteSlip, PhysxAsymptoteValue, PhysxStiffness,solverIterationCount)
wfc = sprintf("%s.csv",fileName)
sGIFFilename = sprintf("%s.gif",fileName)
print "Attempting to read this csv file: ", wfc

# ======================================================================
# Here we show how a cspline could meet PhysX's design intention: Inputs are: 
# y0 = static friction coeff, (x1,y1) = Extremum Slip and Value; (x2,y2,m2) = Asymptote Slip, Value and Gradient. 
# Note: x0 = 0; m1 = 0; (x3,y3,m3) ensure straight line extrapolation after (x2,y2,m2).
## Inputs:
# y0 = static friction coeff:
y0 = 0.3
# (x1,y1) = Extremum Slip and Value:
x1 = 1.0
y1 = 1.05
# (x2,y2,m2) = Asymptote Slip, Value and Gradient:
x2 = 2.0
y2 = 0.8
m2 = 0.0
#
## Fixed or Derived Parameters.  Don't alter these:
x0 = 0.0
# m0: This is a reasonable estimate for the correct slope at (0,0):
m0 = 1.2*(y1-y0)/(x1-x0)
m1 = 0.0
x3 = 2.0*x2
y3 = y2 + m2 * (x3-x2)
m3 = m2
## cspline formula.  See http://en.wikipedia.org/wiki/Cspline
t(x,x0,x1) = (x-x0)/(x1-x0) 
h00(t) = (1. + 2.*t) * (1. - t)**2.0
h10(t) = t * (1. - t)**2.0
h01(t) = t*t * (3. - 2.*t)
h11(t) = t*t * (t - 1.)
# Note in gnuplot function args take precedence over globals with same name:
cs(x,x0,y0,m0,x1,y1,m1) = h00(t(x,x0,x1))*y0 + h10(t(x,x0,x1))*(x1-x0)*m0 + h01(t(x,x0,x1))*y1 + h11(t(x,x0,x1))*(x1-x0)*m1
cspline(x,y0,x1,y1,x2,y2,m2) = (x<x1)? cs(x,x0,y0,m0,x1,y1,m1) : (x<x2)? cs(x,x1,y1,m1,x2,y2,m2) : cs(x,x2,y2,m2,x3,y3,m3)

# ======================================================================
# Default terminal:
set terminal windows
reset
set size 1,1
# Create a parameter for wait time [s] between plots. -1 means wait for keypress.
wait=-1
set grid
# Don't show date/time
unset time
# Set no. of plot points for functions.  Default = 100
set samples 200

# Style when ploting points from a file:
set  style data linespoints
#set style data points

set autoscale xy
# ======================================================================
# ---- DO PLOT 1 ----
set title "PhysX/Unity Wheel Friction Curve"
set xlabel "Slip Speed [m/s]"
set ylabel "Friction Coefficient [N/N]"
set xrange[0:]
#set yrange[0:1.6]
set yrange[0:]
fe(x) = (x <= PhysxExtremumSlip)? x * PhysxExtremumValue  * (PhysxStiffness/refStiffness) : NaN
fa(x) = (x >= PhysxExtremumSlip)? x * PhysxAsymptoteValue * (PhysxStiffness/refStiffness) : NaN
set key at graph 0.90,0.97 right
set label sLabel at graph 0.98,0.35 right
# Input file contains comma-separated values fields
set datafile separator ","
#plot wfc lw 2, fe(x) lw 2, fa(x) lw 2, cspline(x,y0,x1,y1,x2,y2,m2)
plot wfc lw 2, fe(x) lw 2, fa(x) lw 2
pause wait "Press return to continue..."
# ----------------------------------------
# Generate a gif file of the above plot (default size for gif is 640x480)
set terminal gif font arial 8 size 500,375
set output sGIFFilename
replot
#pause 0 "Plot sGIFFilename  created!"

# Then reset for next plot:
set terminal windows
set size 1,1
set output
# ======================================================================
# ---- DO PLOT 2 ----
set title "Corrected cspline Wheel Friction Curve"
set xrange[0:1.0*x3]
unset label
plot cspline(x,y0,x1,y1,x2,y2,m2) lw 2
# ======================================================================
*/
1 Like

Wow! This is some great detective work. I found the wheel colliders to be less than accurate as well.

I’m gonna look into this at some point and try to make a more realistic wheel collider. Your script should make it a lot easier to make sure I’m heading in the right direction.

Thanks for your work.

is this problem still present on current release? 4.x ?
if this is the case, trying to understand the documentation is futile and frustrating to say the least.

even still present in unity 5.x…

Has anyone actually reported a bug with this?

I’m sorry for digging this up, but I’m looking into this as well. I’m trying to extract tire force at contact point of wheel collider, and is confused about whether wheelhit.force is the tire force or the vertical normal force.
Can someone tell me how to use the above code? There are many syntax error in the code (mostly in if statements) so I don’t know what to do to test.

I too just came about this thread because I’m very sick and tired of Untiy having the same broken wheelcolliders for a decade.
The commented code is indeed very messed up, did you ever clean it up and got it to work?
I’d rather not waste my time trying to decipher it if it doesnt work as advertised.