Workshop VI - Brushing Up and Misc. Details (Mirror)


This post is a mirror of a message board post I wrote in 2021, 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. 🙂

Brushing Up and Misc. Details

This is the last main entry for "workshop" posts for now, sharing the formula for the "Brush" tire model, and some extras.

In part III, I provided the equation and some information for "Pacejka's Magic Tire Formula", or just Pacejka for short. It is a common model used in the tire manufacturing and motorsports industries, and capable of great sophistication, but it is an unwieldy model that requires many parameters based on curve-fitting with empirical data (for any one given tire compound) to make the most of it. Personally, I find most (consumer videogame) sims that are documented as using a Pacejka tire model to be lacking when the tires are pushed to their limits. I once considered that to be a fault of the model, but now I understand it probably has more to do with a lack of data.

Eventually, I uncovered the math for an alternative -- the "Brush" tire formula. The Brush model is another common and respected model, dating back to tire research in the mid-20th-Century. It conceptualizes a tire as a round brush with a series of bristles. When subjected to slip, the tire's "bristles" deflect; in short, the behavior of the tire is derived from a mathematical representation of these bristles and their stiffness. Unlike Pacejka's data-based parameters, the Brush model is a theoretical model that can simulate a range of states and tire compounds with fewer parameters. It is not cutting-edge, but it is effective.

Credit goes to this example written for MATLAB software, which demonstrates the structure of implementing the Brush formula in programming. I have rewritten and personalized my own form of the model.

The Brush tire model requires just a few variables:

  • mu - Coefficient of friction (example: 1.0)
  • con_patch - Length of the contact patch (example: around 0.15 to 0.25 meters)
  • tire_stiffness - Affects the tire's behavior in maintaining grip and how suddenly it begins slipping. Low values result in more of a "greasy" behavior, suitable for low-traction surfaces (example: 5 to 10)

Like for Pacejka, we need the current slip angle and slip ratio as explained in part III. There is also a final "stiffness" value for the model, combining the tire compound's stiffness value and contact_patch area, and the generic equation for friction, using our suspension y_force value:

    # 500000 because this line is multiplied by 0.5, while stiffness values are actually in the millions
    # tire_stiffness is a small value just for convenience
    var stiffness = 500000 * tire_stiffness * pow(con_patch, 2)
    var friction = mu * y_force

Now for the formula:

    # "Brush" tire formula
    var deflect = sqrt(pow(stiffness * slip.y, 2) + pow(stiffness * tan(slip.x), 2))
    if deflect == 0:  return Vector2.ZERO
    else:
        var vector = Vector2.ZERO
        var crit_length = friction * (1 - slip.y) * con_patch / (2 * deflect)
        if crit_length >= con_patch:
            vector.y = stiffness * -slip.y / (1 - slip.y)
            vector.x = stiffness * tan(slip.x) / (1 - slip.y)
        else:
            var brushy = (1 - friction * (1 - slip.y) / (4 * deflect)) / deflect
            vector.y = friction * stiffness * -slip.y * brushy
            vector.x = friction * stiffness * tan(slip.x) * brushy
        return vector

The chunk I've named "deflect" is akin to the hypotenuse of a triangle between the longitudinal slip value and the tangent of the lateral slip value, both multiplied by the brush's stiffness. If there is no deflection, there is no force to exert. If there is some deflection, the formula goes into one of two branches based on how much the tire has deflected. The variable named "brushy" is just another chunk of the formula, for convenience.

That's all there is to it, or at least as much as I have the knowledge to explain. So I am going to include a few extras/revisions:

Reading what kind of surface the tire is on

My method so far is to split surfaces into separate StaticBody nodes, and to assign those surfaces into groups ("tarmac", "grass", etc.) that are read by the RayCast, like this:

    if $Ray.is_colliding():
        if $Ray.get_collider().get_groups().size() > 0:
            surface = $Ray.get_collider().get_groups()[0]

Multiply by spring_length or not?

Currently, I have decided not to multiply y_force by the length of the spring. This is not how springs work, but it means you can adjust the ride height of a vehicle without having to adjust the spring stiffness to match, which seems like a worthwhile convenience to me.

I believe the damping (bump/rebound) calculation should also have included the spring_length in the first place, if you are interested in using real-world bump/rebound values (spring_length has no other effect).

Anti-roll/sway/stabilizer bar

This requires a couple parts, but if you have the SpringArm nodes return their "compress" value after calculating suspension forces, you can use an array to collect and distribute the compressions of each corner to calculate the force of an anti-roll/sway/stabilizer bar, with a stiffness value (anti_roll) and the compression value from the opposite side (opp_comp):

    ## Main script, attached to the RigidBody ##
    # Update wheels, distributing compression lengths to apply anti-roll bar
    # Use last frame's values to update opposite compressions in sync
    var prev_comp = susp_comp
    susp_comp[0] = wheel[0].update_forces(prev_comp[1], delta)
    susp_comp[1] = wheel[1].update_forces(prev_comp[0], delta)
    susp_comp[2] = wheel[2].update_forces(prev_comp[3], delta)
    susp_comp[3] = wheel[3].update_forces(prev_comp[2], delta)
    ## SpringArm script ##
    if compress > 0:
        y_force += anti_roll * (compress - opp_comp)

Fuel consumption with BSFC

Lastly for now, my new fuel consumption method. This one is actually quite simple once figured out:

const PETROL_KG_L = 0.7489  # kg per liter of petrol
const NM_2_KW = 9549  # power = torque * rpm / [const]
func consume_fuel(delta):  # Brake Specific Fuel Consumption; kg per kilowatt-hour
   # 2nd gen Prius = 0.225 BSFC
   # thirsty turbo = up to 0.320 BSFC
   var fuel_burned = engine_bsfc * torque_out * rpm * delta / (3600 * PETROL_KG_L * NM_2_KW)
   fuel -= fuel_burned

I hope this will serve as a handy resource for anyone looking to develop their own simulation. 🙂 As I said before, I have tried to keep the number of details relatively lean to make the guides easier to follow, piece by piece, but this is the whole essential principle of what I have built, and simulators like it.

Get GDSim

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.