Shawn Hargreaves Blog
When a bike drove off the road, or braked so hard it lost traction, we wanted to leave a visible skidmark. We could reuse existing collision data for this, as the physics engine had already computed the contact point between the wheel and ground. We created a triangle strip joining a series of wheel locations from previous frames, and rendered it using depth bias to avoid z-fighting where the generated triangles did not exactly match the underlying ground geometry.
Using premultiplied alpha, we made a texture that was dark on one edge but had an additive glint on the other, which gave a 3D embossed look so the skid appeared to be slightly sunken. This fake shading didn’t match the light direction of the rest of the scene, but it was good enough to tie the skids in with the underlying surface:
The challenge was how to efficiently manage the dynamic skid geometry. Dynamic geometry is always a fun problem. There are basically three options:
Skidmarks are an append-only data structure. As the bike moved, we added triangles to the end of the skid, but never changed existing vertices. This seems like a perfect fit for NoOverwrite.
But you know it can’t be that easy, right? :-)
If we added new vertices every frame, we would soon end up with a crazy high triangle count. To keep within sane limits, we only wanted to add triangles at periodic intervals, after the bike had moved a significant distance or changed direction. But if if we delayed adding to the skid, there would be an ugly gap where the skid had not yet caught up to the latest wheel position. To avoid this gap while outputting less than one triangle per frame, we would have to modify existing vertices each time the bike moved, which would break the NoOverwrite rules, requiring us to use regular SetData and stall the GPU pipeline.
We settled on a hybrid approach. The main skidmark was only extended at periodic intervals, using NoOverwrite with an append-only triangle strip. To fill the gap between the head of the skid and the current wheel position, we added a separate single quad via DrawUserPrimitives.
Are we done yet? Of course not :-)
One of the unique challenges of console development is that consoles are not particularly forgiving if you run out of memory. Unlike a PC, there is no virtual memory to take up the slack if you use too much.
MotoGP used all the memory in the machine. On the biggest track with the biggest combination of bikes in the most complicated game mode, we had less than one kilobyte spare. To work reliably in such an environment, our memory usage had to be 100% deterministic. We could not allow any code that dynamically allocated memory while the game was running, because then it would be impossible to know for sure if some unfortunate combination of events might cause us to run out.
To make its memory usage consistent and predictable, the skidmark system allocated a fixed amount of memory at startup. It created a single large vertex buffer, then divided this up into a number of fixed size individual skid objects. When a bike started to skid, we would scan this list, looking for an object that was not already in use. If a skid lasted too long to fit in a single fixed size object, we would switch to a second object, continuing from where the first one left off.
But what if we run out of skids? We only have a fixed number of these objects, so something has to give if the player keeps skidding for ever. To design the right behavior, it is interesting to consider the relative priority of different scenarios:
Once we knew the right priorities, the implementation was simple:
This design guaranteed that the skid system could never overflow memory, and also limited the number of triangles it could use. We knew that no matter what the player did, there would never be more skids than we could afford to draw at a good framerate.
Thanks for these - they really are interesting.
Testing the limits of this would be pretty fun. "OK guys, we need 16 people doing doughnuts at the same time. And, go!"
Re running out of skids/driving in circles on the grass: could also have faded out old skids gradually over time, so that once completely gone they could then be re-used? That way it isn't so noticeable when skids that were previously there are gone, and you can still have skids immediately generated where the tires are currently.
Fading out skids over time would be a good way to recycle even visible ones without any ugly vanishing problems, but it wouldn't achieve our #1 goal of skidmarks being able to last for an entire race in the most common case where the player isn't skidding too much and therefore we never run out.
If you had a game where the common case was that players were skidding a lot and so you did run out of these objects, fading them out over time would definitely be a good idea.