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
GDSim
Driving simulator prototype made with Godot
Status | Prototype |
Author | Wolfe |
Genre | Simulation, Racing |
Tags | 3D, Driving, gamepad, Local multiplayer, Physics |
Languages | English |
Accessibility | Configurable controls |
More posts
- 10,000 Downloads! Thank you so much!Apr 14, 2024
- Costly 3D Mistakes to Avoid (Postmortem)Mar 09, 2024
- Tires at Rest - A Deceptive SolutionFeb 07, 2024
- Workshop V - Start Your Engines (Mirror)Feb 06, 2024
- Workshop IV - Rays Go Round - Adding Things Up (Mirror)Feb 06, 2024
- Workshop III - Rays Go Round - Lateral Force (Mirror)Feb 06, 2024
- Workshop II - Suspending a RigidBody, Part 2 (Mirror)Feb 06, 2024
- Workshop I - Suspending a RigidBody (Mirror)Feb 06, 2024
- SERIES: Driving Simulator Workshop (Mirror)Feb 06, 2024
Comments
Log in with itch.io to leave a comment.
Hello!
Thank you for the great work, I have followed the original thread on gtplanet, and I am really glad that the project continued, and gained some following!
I have a question regarding the implementation of the brush model. Are you using multiple "bristles", and if so, could you elaborate a bit on the implementation?
Thanks in advance!
With this tutorial, only one "bristle" is being simulated. Casting a sphere provides the rounded shape, but there is only one tire calculation per collision, trading accuracy for efficiency.
Thanks for your comment!
I see, makes sense.
Thank you for the clarification..