Workshop III - Rays Go Round - Lateral Force (Mirror)


This post is a mirror of a message board post I wrote in 2020, reposted as-is. There are no errors I'm aware of, but it would be different if I wrote it all again. I'm copying it here for preservation and because it makes sense. 🙂

Rays Go Round - Lateral Force

Now that we have a suspension, it's time to simulate some rubber beneath it. Before you can do that, there is some information to keep track of:

  1. The wheel's "local" velocity
  2. The wheel's "Z velocity", which is just the forward/back component of its local velocity (I have reasons to store this separately)
  3. What I call the wheel's "planar vector" -- a convenient normalized/unit vector that excludes vertical motion
  4. The wheel's last position, because the SpringArm is not a physics object and Godot does not calculate its velocity automatically

To get these, we do a little bit of vector math. If you're lost, don't feel bad -- I had to reacquaint myself. 😆 This "cheat sheet" might help. I've collected the variables like this:

    var local_vel = global_transform.basis.xform_inv((global_transform.origin - prev_pos) / delta)
    z_vel = -local_vel.y
    var planar_vect = Vector2(local_vel.x, local_vel.y).normalized()
    prev_pos = global_transform.origin

Note that the 'Z' velocity is the 'Y' component of the full vector. That's one reason I give it another variable -- for clarity -- and a consequence of rotating the SpringArm node, as I mentioned in part one. Negative Z is forward in Godot, so we pull it from negative Y here.

As I've mentioned, I have a "brush" tire formula I'm using, but I think it would be more informative to start with a simplified Pacejka formula first -- which is what I started out with. The Pacejka formula can be easily plotted to a graph, which will help explain how tires work and what we're tinkering with.

"Pacejka?" -- The Pacejka formula is an equation concocted to fit empirical data collected from testing real tires. It is named for Hans Pacejka, one of the authors of the formula. A sim developer called Edy, who sells a physics package in the Unity asset store, provides a helpful page going into depth on the 1994 version of the Pacejka formula as well as an overview of the model. In short, it is an effective but unwieldy formula to use, with little guidance on good parameters.

To make use of any tire formula -- it doesn't have to be Pacejka or the brush formula -- we have a bit more necessary information to collect: the tire's slip angle and slip ratio.

Slip Angle

I suppose [you're probably] familiar with slip angle, but don't worry if you're not -- it is the angle between the direction the tire is pointing and the direction it's rolling. I have another link here for reference. 🙂

There are multiple ways to derive the slip angle. My method makes use of the "planar vector" from the beginning. I've called this variable "x_slip" to denote lateral slip:

var x_slip = asin(clamp(-planar_vect.x, -1, 1))

That's all. The arcsine of the (negative) x component of the vector, with a check to make sure its floating point value doesn't slightly exceed -1 or 1, which is undefined in an arcsine function (oops).

Slip Ratio

The slip ratio compares the angular velocity of the wheel (I call it "spin" for short) with how quickly the road is passing by beneath it -- to put it simply, wheelspin or locking the wheels. There are multiple definitions for the slip ratio; I'm using one where locking the brakes equates to -1 if you're moving forward, and +1 if you're moving backward. This time we have to avert dividing by zero:

var z_slip = 0
if not z_vel == 0:  z_slip = (spin * radius - z_vel) / abs(z_vel)

"Spin" must be tracked manually -- in fact, doing so is more useful than using a built-in angular velocity value on a physics object -- and it is modified elsewhere. See section IV below for calculating it.

Simplified Pacejka

Now it's time for the formula. You can actually get a half-decent approximation by skipping most of the parameters. The formula goes like this in English:

Force = Load * Peak * sin(Shape * arctan(Stiffness * Slip - Curvature * (Stiffness * Slip - arctan(Stiffness * Slip))))

...where "load" is the acting weight on the tire from the car (our "y_force" from part one), in Newtons, giving a force in Newtons (with the lateral slip angle in radians).

This is where it becomes handy to plot the formula to a graph (and where things start to get complicated). I used a nifty graphing calculator available on Desmos.com. (There, I've used the standard coefficient labels because of how the calculator works.)

  • Peak (D) = Simply determines the peak of the curve; comparable to coefficient of friction, I think? (example: 1)
  • Shape (C) = Affects the shape of the curve in the middle, after it has passed the peak (sources say ~1.35 for lateral force, ~1.65 for longitudinal force)
  • Stiffness (B) = Affects the slope of the curve around the origin (example: 10)
  • Curvature (E) = Affects the height of the tail of the curve, together with "shape" (example: 0, going negative from there)

If you haven't seen this curve before, it is a reasonably accurate approximation of the characteristics of a tire. As slip increases, there is an amount of slip that will provide the most force. To us drivers, generally speaking, if the curve is pointy and peaky, it will make for a "twitchy" tire that suddenly breaks away. If it is round and broad, the tire will be more forgiving. The lower the tail, the less traction you have once you've lost grip. The higher the peak, the more grip you get...at the peak.

I used Godot's "export" variables to make sliders for the parameters that can be modified in the Godot editor window while testing ("Always on top" in the window settings for rendering is nice). Something like this -- I honestly don't know if it is better to have separated values for all four coefficients:

export(float, 0, 2, 0.05) var peak = 1
export(float, 1, 2.5, 0.05) var x_shape = 1.35
export(float, 1, 2.5, 0.05) var z_shape = 1.65
export(float, 0, 20, 0.1) var stiff = 10
export(float, -10, 0, 0.05) var curve = 0

Then I created a method just for the formula:

func pacejka(slip, t_shape):
    return y_force * peak * sin(t_shape * atan(stiff * slip - curve * (stiff * slip - atan(stiff * slip))))

I have to use "t_shape" instead of just "shape" ('t' for tire) because SpringArm already has a "shape" property.

Now there's enough in place to apply some force via the tires. I figure it makes sense to implement the lateral force first -- add it where we applied "y_force" before. (Note: this example will use the X basis; I'll show how I rotated the surface normal when I bring things around to the brush formula, because I'm drawing from different stages of progress and don't want to mix up +/-):

x_force = pacejka(x_slip, x_shape)
z_force = pacejka(z_slip, z_shape)
if $RayCast.is_colliding():  # Check helps eliminate force spikes when tumbling
   car.add_force(global_transform.basis.x * x_force, contact)
   car.add_force(normal * y_force, contact)
   car.add_force(global_transform.basis.x * z_force, contact) ## See note in post below

This should be enough to shove the car around and let grip align it with its wheels (if the car spins like a top, chuck a negative sign in there). Without longitudinal force, it will basically act like it is always freewheeling. Otherwise, it will act like the brakes are all locked (so far). There's more to do before the longitudinal force can be properly implemented. That was one hard part to figure out, and this post is long enough.

⏭️ Rays Go Round - Adding Things Up ⏭️

Get GDSim

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.