An Illustrative Shader for Unity


A huge amount of effort has been made in 3D video games to produce “realistic” looking graphics. The idea is that you should feel like you’re playing a movie and the rendering should look like it was filmed with a camera. While there are some exceptions, other artistic styles — such as trying to look like a cartoon, a painting, a drawing, etc. — used to be largely ignored, or at least restricted to 2D games. Thankfully, this is changing. Especially so in the indy crowd, but also in big budget games like Team Fortress 2. This is great for graphics programmers to explore different rendering methods and lets a shader become part of an overall creative goal instead of a simulation of a camera.

For my game Space Rocks, I decided that I wanted it to look like the cover of a 60’s science fiction novel: illustrated, and somewhat cartoonish, but at the same time not cell shaded and outlined like a comic book or a Disney cartoon. As I mentioned, Valve accomplished something like this in TF2. What’s more they published a very good paper on their work.

This is a tutorial on how to implement this kind of shader in Unity3D. The complete shader source is attached at the end of the document. If you’ve never done any shader before, you might want to find try a few simpler tutorials first. However if you know a bit about shading languages, but are perhaps not familiar with Unity, this should give you a good introduction. The complete source is given at the end of this page.

Paperwork: the bits that make up a Unity shader

We will be making a Unity3D “Surface Shader”. This isn’t so much a shader as a template from which Unity will create numerous vertex and fragment shaders for different rendering paths. To create a surface shader we specify a lighting model (in this case a custom one called LightingNPR) and a function that prepares the surface for lighting, surf. We will use a normal map (bump map) though it should be removed where it is not needed. Here’s our starting point:

The surf function is pretty standard, but we need to fill in LightingNPR .

The basics: Lambert’s model

Even if you have never heard of “Lambertian” shading, you’ve undoubtedly seen this if ever touched shaders before. It is the basis of almost all shaders and is used to compute diffuse illumination, \(I_d\), by taking the cosine of the angle between the surface’s normal vector, \(\hat{\mathbf{n}}\) and a unit vector towards the light \(\hat{\mathbf{l}}\). This makes sense because when the surface directly faces the light this angle will be zero and it’s cosine 1; if the surface is at right angle to the light then the cosine will be 0. This cosine is trivial to compute as it is simply the dot product of the two vectors: \[I_d = \max\{\hat{\mathbf{n}}\cdot\hat{\mathbf{l}}, 0\}\] Let’s add this to our lighting function, and include the surface colour, light colour, and attenuation:

This produces a smooth, realistic, and rather boring approximation of a matte surface, as illustrated by Suzanne the Blender Monkey:

Lambertian Diffuse

Lambertian Diffuse

Closer to illustaion: Gooch shading

Lambert’s model is used to directly compute the brightness of a surface. However, Gooch observed that artists often use other cues in addition to just light and dark to show illumination and shading. Specifically, warm colours (red, yellows, and oranges) show illumination and cool colours (blues, teals, and purples) show shading. We can incorporate these principles very easily using a light ramp: a one dimensional colour gradient imported into our shader as a texture.

We still compute Lambert’s diffuse term, but instead of using it as a measure of lightness, we use it as a position in the light ramp:

When we combine this with a ramp from a dark, cool colour to a warm, bright colour, we get a result much closer to an artistic rendering:

Simple Gooch cool/warm light ramp

Simple Gooch cool/warm light ramp

Simple Gooch cool/warm light ramp

This is an incredibly versatile tool for both illustrative and photorealistic shading. It can be used to produce some very unusual highlights:

An unusual light ramp

An unusual light ramp

An unusual light ramp

And it can even be used to create a cell-shaded effect if the texture contains only two colours and uses no filtering:

A two-pixel light ramp used for cell shading

A two-pixel light ramp used for cell shading

Controlling the transition: warped Lambert

The light ramp gives us excellent control, but for that final touch we want some finer control over how the shader traverses the ramp. To do this, we add a scale \(\alpha\), bias \(\beta\), and exponent \(\gamma\) to Lambert’s term: \[I_d = \left(\alpha(\max\{\hat{\mathbf{n}}\cdot\hat{\mathbf{l}},0\}) + \beta\right)^\gamma\]

This is called the “warped” diffuse model. By varying the scale, bias, and exponent parameters we can control the size of the shaded area, the total amount of difference between dark and light, and the sharpness of transition:

Scale: 0.5, bias: 0.5, exponent: 2

Scale: 0.5, bias: 0.6, exponent: 4

Defining edges

The shader still lacks some indication of the object’s edge. A common way to make the edges stand out more is to add a solid outline, but this clashes somewhat with the smoothness of the shading so far. Instead, we will use rim lighting. Similarly to Lambert’s, we compute this by looking at the cosine of the angle between the surface normal and the camera. Again, we can calculate the cosine using a dot product:

\[I_r = 1 – \max\{\hat{\mathbf{v}}\cdot\hat{\mathbf{n}},0\}\]

This will produce a smooth transition from the edge of the object to it’s face, regardless of the camera’s orientation. In order to make the transition a little sharper, we raise the whole thing to a power:

\[I_r = \left(1 – \max\{\hat{\mathbf{v}}\cdot\hat{\mathbf{n}},0\}\right)^q\]

This gives a little more definition to the object:

Rim lighting applied

Rim lighting applied

Rim lighting only

Rim lighting only

By varying the colour we can also produce interesting effects such as the alien glow on the asteroids in Space Rocks:

Space Rocks

Some Space Rocks with coloured rim lights to create an alien-looking effect.

Adding shininess

For shiny, glossy objects, we need a specular highlight. This is used to show a “reflection” of the light source in the surface. We will use Phong’s model as it is simple and widely used. Blinn’s model could be used and offers slightly better performance under some circumstances. To compute Phong’s specular highlight we need the vector \(\mathbf{\hat{r}}\) of the light direction reflected about the surface normal. This can be computed as \(\mathbf{\hat{r}} = 2(\mathbf{\hat{l}}\cdot\mathbf{\hat{n}})\mathbf{\hat{n}} – \mathbf{\hat{l}}\). However, Cg has a built-int reflect function. We compute the final highlight as the angle between the relfected vector and the camera. We raise the result to a “shininess” power to sharpen it (sometimes called the “Phong exponent”): \[I_s = (\hat{\mathbf{r}}\cdot\hat{\mathbf{v}})^p\]

Rim lighting and specular

Rim lighting and specular only. The diffuse colour has been changed from other images to accommodate the increase in brightness added by other light terms.

Rim lighting and specular only

Rim lighting and specular only

Results

This shader is incredibly versatile. It easily accommodated the visual style I needed for Space Rocks and seems quite versatile. It can, of course, also be used to render in TF2’s style.

Spaceship

Our intrepid hero’s spacecraft.

A sinister alien

A sinister alien.

Complete Shader