I've decided it was about time start writing something about Super Toy Cars. Something that could also be useful to other developers or students visiting this blog. I thought I'd rather start with something simple. I didn't want to embarrass myself on the first technical post :o). So I decided to write about something I had to implement recently for Super Toy Cars: fake car shadows.
|This image from RACE shows clearly the outlines of the fake shadows combined with dynamic shadows|
We are developing Super Toy Cars using Unity, which already provides nice dynamic shadows as part of the engine so, why would I want to implement fake shadows? Well, first and foremost for performance reasons. Then, again, considering we're currently experimenting with a 3rd person camera for the mobile platforms version (and maybe for the PC version too) this is a feature that may enhance the visual appearance of the game, and that works quite well with normal dynamic shadows. So, for those platforms where using 'real' dynamic shadows isn't an option for performance reasons, this is a good substitute, and for those were dynamic shadows can be used, it's still a good addition that enhances the overall look, in my opinion.
But, before going on, what do I mean by fake shadows? In racing games, particularly 10-15 years ago, when computers didn't provide enough power for rendering dynamic shadows, programmers had to use other types of tricks to try to simulate shadows. The static shadows (the shadows of objects that don't move and that are projected onto those same objects) can be faked using lightmaps. This is a very useful technique that dates back to the Quake times. Basically, you render polygons using 2 textures: the main texture for the polygon and another one defining the lighting. The techniques to calculate the lightmaps themselves are varied and quite complex, but luckily Unity supports this feature off the self and it's really easy to use.
|In this picture from Trackmania: Nations Forever I think the car to the left shows clearly the fake shadows, but the car to the right displays them too. Even in modern games this technique is still in use.|
Lightmaps can provide high quality shadows for the static part of your scenery, but most probably your game has some moving elements. In a racing game there may be others, but the cars are certainly going to be moving. We can't use lightmaps to fake their shadows so we will have to use something else. An alternative that kind of hooks in with what we were explaining before is drawing a polygon with the car shadows as with alpha right below the car. This kind of shadows are normally smooth and not really detailed, won't change based on the different source lights the object may be near to, and work only as long as the ground the car is moving on is more or less plane. In spite of all these issues, this technique fits very well in racing games where the car is normally moving very fast, so that a smooth shadow is likely to fit well, and ground is normally quite plane: cars don't fit well with stairs normally ;o).
This technique can be used to fake ambient occlusion below the car, when working with dynamic shadows. And since you can draw the shadow texture as you please it may get quite detailed and look very convincing. I believe many modern games use it, I'm sure Split/Second used it and I'm fairly sure the latest Trackmania game (released recently on Steam if you're interested) uses it too, with great results, I must say. It's very cheap in terms of CPU: 4 raycasts and rendering a quad with alpha. This shadow is also quick to render because, normally, it's almost parallel to the camera (assuming a 3rd person camera) and thus it takes very little space on the screen.
|In Outrun the shadow is presented as pixels black and grey under the car|
Obviously this technique has its limitations, although it's a very popular one and has been in use (in very similar terms) since the very early days of videogames. So games like Street Fighter 2, Outrun, King of Dragons, King of Fighters or Knights of the Round were using it 15-20 years ago, even without 3D, since this technique has used in 2D games that wanted to simulate some depth. Fighting games used more sophisticated versions of the technique that could work really well and give the appearance of real shadows (find reference).
|Fighting games have been using this technique for long. This is a picture from Street Fighter 2 in the SNES (1994)|
So, once explained, what it is and where it is used, let's get on with the implementation details. As I said at the beginning of the post, this is a very simple technique, easy to implement. Particularly in Unity, where you have all the tools necessary available off the self.
The fake shadow will be a game object with a MeshFilter component that we will modify dynamically to adapt to our needs. I think it's appropriate to place this object under the car in the scene hierarchy. it makes sense and it's kind of natural. In the case of Super Toy Cars, our vehicles have their position in the centre of the car, but at ground height (that is, where the height where the bottom of the wheels are on their default spring position). That's important to understand some of the calcs later.
|Super Toy Cars with no dynamic shadows and no fake shadows. The car looks like it's floating due to the lack of visual references to put it on the floor|
We create a new component, FakeCarShadow in our case, and we apply this component to the game object. In there we publish the extents of the shadow as a Vector3. The X and Z components will be the extents of the shadow from the centre, while the Y component will be how up will be the start of the ray we use to calculate the projected positions. We may define other parameters to control the shadow, such as the height over the ground where to place the vertices, the distance to start fading it, the distances to stop rendering fake shadows, etc.
As private variables we will store arrays with the vertices, colours, normals, tangents, UV coordinates, etc., that will be used to build the mesh for the fake shadow. We fill these arrays in the Start function with some default values, and then assign them to the mesh of the meshFilter. We should also assign the right material (a shader that allows us to use transparency and fade the texture with a colour or an alpha parameter) to the renderer. I think calling the MarkDynamic function of the mesh is reasonable too, since this mesh is potentially changing every tick.
|Super Toy Cars only with the fake shadows. This simple technique works greatly to improve the 3D spatial sensation and positioning. The car now appears to be on the ground, even with a very simple and inappropriate fake shadow as that.|
In the update we'll need to move the vertices to fit the car extents (the one we have defined as a parameter of the component). We need to have defined the cars in their own layer, since we want to project this shadow onto the track and thus ignore the car that projects it and actually any other cars for that matter. We'll need to do the raycast using a mask that excludes the cars (and any other objects we don't want to project on top of). We iterate over all the vertices doing a raycast to calculate the projection point of that vertex. The raycast needs be only so long since we won't project a shadow when really high. If the raycast returns a hit we use that point, otherwise we use the ending point and give it an alpha of 0 (transparent). Here's a code snippet doing that:
// go doing a raycast to see the ground position for the borders
for (int i = 0; i < car.wheels.Length; ++i)
// calculate the point where to start a raycast to find the ground
float xSign = ((i % 2) == 1) ? 1.0f : -1.0f;
float zSign = ((i / 2) == 1) ? -1.0f : 1.0f;
Vector3 initPosLocal = new Vector3(xSign * carExtents.x, carExtents.y, zSign * carExtents.z);
Vector3 raycastIni = transform.TransformPoint(initPosLocal);
// we need to perform a raycast to see where the ground is
if (Physics.Raycast(raycastIni, new Vector3(0.0f, -1.0f, 0.0f), out hit, maxDistance, layerMask))
// calculate the alpha which we use to reduce the opacity of the shadow
alpha = 1.0f - Mathf.Clamp01((hit.distance - minDistanceForFullOpacity) / (maxDistance - minDistanceForFullOpacity));
// now update the vertex position (bare in mind it's got to be in local coordinates)
groundPos = new Vector3(hit.point.x, hit.point.y + posAboveGround, hit.point.z);
// if there's no hit use a transparent color and move the point to the center
groundPos = raycastIni + new Vector3(0.0f, -maxDistance, 0.0f);
alpha = 0.0f;
verts[i] = transform.InverseTransformPoint(groundPos);
colors[i] = new Color(alpha, alpha, alpha, alpha);
// finally re-assign the arrays that may have changed
MeshFilter meshFilter = GetComponent<MeshFilter>();
Mesh mesh = meshFilter.mesh;
mesh.vertices = verts;
mesh.colors = colors;
This updates the vertices of the fake shadow geometry. Note that we're converting the local coordinates of the car extents to world coordinates before doing the raycast and then we convert back to local coordinates after calculating the final projected positions. This is important and may be very useful if doing optimisations.
In Super Toy Cars we do a check with the current camera to see if the car is behind the camera or too far away, and in those cases we don't render it (disable the renderer component in Unity). We actually calculate a 'strength' value that we use to fade off the shadows of cars that are far enough. That way as the car goes further from the camera the shadow fades out smoothly, which is a lot better than simply turning it on/off based on distance (which could ask cause some flickering).
On top of these optimisations we could have others to avoid doing the raycasts every tick, when appropriate. If the car shadow is an object under the car hierarchy, it means its vertices are local to the car position. Normally the car shadow is going to remain constant most of the time so we could do an optimisation, particularly for AI cars (which we will see smaller than ours) or any cars that are far enough, to update the vertices positions only once every X ticks. This could save us quite a few raycasts per tick and may be useful if that's proving to be time consuming.
I hope you found this post useful, amusing or, at least, entertaining. If you find it confusing or would like me to elaborate on something, feel free to write in the comments section or drop me an e-mail.