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:
- DrawUserPrimitives: easy, but copies vertex data from the CPU to GPU every time you draw, which is wasteful if you will be drawing the same vertices many times.
- Use SetData to modify a VertexBuffer: avoids repeatedly copying the data, but SetData can cause pipeline stalls which prevent the GPU from running in parallel with the CPU.
- Use a DynamicVertexBuffer with SetDataOptions.NoOverwrite: avoids the pipeline stall, but only works if you are careful to never modify a vertex while the GPU could still be using it. This means you must not change a vertex for at least a couple of frames after the last time you drew it.
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:
- Ideally, the player is driving well, mostly on the road and without much skidding. In this case we can go a whole race without running out of skid objects, so we can keep skids around forever. If the player makes a small mistake on the first lap, the resulting skid should still be visible during their second and third laps.
- If the player drives on the grass for so long that we run out of skid objects, we should recycle the oldest one, reusing its memory.
- If the player keeps driving in circles on the grass, so they are constantly creating new skids while all the old ones are still visible, we should just stop creating skids once we run out of objects. We should never recycle a skid object that is currently visible on the screen! (because leaving out an effect that ideally ought to be there is less offensive than if an existing object suddenly disappeared).
Once we knew the right priorities, the implementation was simple:
- Compute a bounding box whenever a skid is created
- Each time we draw, test its bounding box against the view frustum
- Only draw skids that pass the bounding box test
- Store the time at which each skid was most recently drawn
- To create a new skid:
- Look for an object that is not already in use
- If they are all in use, pick the one that was drawn least recently
- If all the skids have been drawn within the last half second, give up: none are available so we have to skip creating this new skid
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.