I thought it would be a good idea to write up some of the basics of Toon Lighting for anybody that isn't familiar with it. There are heaps of ways to approach this, depending on your rendering pipeline and the style you're going for, this is just one simple way that I enjoy playing with.
The examples below are made in Amplify Shader Editor for Unity, but the basic ideas should be pretty universal. You can download all the shader examples separately, or a Unity Scene zip with everything included + examples, at the end of the writeup.
You can also find an index of previous Shaders I've made for Patreon at http://pucklovesgames.com/
N dot L
The first thing you'll want to control is the falloff of light on the surface of the model, posterising it into steps to make a clear delineation between lit and unlit.
Standard lighting is usually based something called N.L (N dot L), which is the dot product of the Normal Direction and the Light Direction (in world space). This gives a gradient that goes from 1 for surfaces facing directly towards the light, to -1 for surfaces facing directly away from the light, with 0 being right at the half way point where the shadow begins (the Shadow Terminator).

Multiply Floor Divide.
As soon as we have that gradient, we can do all sorts of interesting things to it. First we want to remap the Dot Product gradient to go from 0 to 1 instead of -1 to 1, so we Scale and Offset it by 0.5. Then you can create discreet steps in the gradient by Multiplying by X, then doing a Floor, then Dividing by X, where X is the amount of Steps you'd like.

e.g. Multiplying by 4 creates a smooth gradient of between 0 and 4, then when you use Floor, it discards the fractional component and creates 5 distinct steps (0, 1, 2, 3 and 4), then when you Divide by 4 again, it makes those steps fit within 0 to 1 again (0, 0.25, 0.5, 0.75 and 1).
SmoothStep

Another useful thing you can do, is use a SmoothStep to remap the gradient in order to position and scale the Shadow Terminator. Do this after you remap, but before you posterise the gradient.

Light Attenuation
This takes care of the Surface Shading of the model, but it doesn't yet take into account Cast Shadows, or the falloff of Point Lights. To accomodate these, you'll need to also multiply by Light Attenuation, which you'll probably want to do before you posterise the gradient.

One gotcha here that you have to keep in mind, is that both Shadows and Point Light Falloff are both stored in Light Attenuation, and there's no way to separate the two.

My usual lighting setup only does shadows on the main Directional Light (usually the sun), so I can work around this by splitting Light Attenuation between Directional Lights (Cast Shadows) and Point Lights (Point Light Falloff). You can do this with a World Space Light Position node, and using the Type output, which is 0 for Directional Lights and 1 for Point Lights. This lets me boost the fall off of point lights and allow them to show up better in toon lighting, and it will also be important later if we want to tint the shadows with a colour.
Multiply by Light Color and Material Colors.

Finally, multiply by the Light Colour, and any Textures or colours on the model, and pipe it in to Custom Lighting.
See EdLighting_ToonSteps.shader for an example.
So that's the basics of Toon Lighting, but what else can we do easily?
Rim Wrap.
Another nice effect of some Toon shaders I've seen, recently in Breath of the Wild, is having the light wrap around when the light source is behind the model, which creates a nice rim lighting effect

You can do this by getting the negated Dot Product of the View Direction and the Light Direction, which similar to N.L, creates a gradient from -1 to 1, but this time relative to whether the light is pointed towards the back or the front of the model. 1 being when the light is behind the model, -1 when the light is in front of the model.

Saturate that gradient (clamp it between 0-1, basically throwing away everything below 0) and then multiply that by Fresnel (which gives you a nice Rim mask based on the normals of the model). I also multiply in the Light Attenuation here separately, to prevent the Rim Light showing up in areas of Cast Shadow or at the extremities of a Point Light falloff.
You can then add this to the N.L * Light Attenuation combo gradient you made earlier, before it's posterised, to create a nice Rim Wrapping effect that is only noticeable when the light is behind the model.
See EdLighting_ToonStepsRimWrap.shader for an example.
Other Possibilities.
Some other cool things you can do is shift the Hue of the light at the edge of a point light falloff, to create some nice visual variation.

Instead of just multiplying by the Light Color after you have your posterised gradient, you can tweak the light colour based on the value of the steps, or anything else. You could even put a rainbow gradient in there if you want to override the colour of the light entirely.

See EdLighting_ToonStepsGradient.shader for an example.
You could also get creative with textures showing up only in the lit areas of the mode, by multiply the lighting gradient by any custom texture (screenspace, worldspace or uv mapped) or procedural pattern like this Halftone shader.


See EdLighting_ToonStepsHalftone.shader for an example.
In a previous post, I did a similar thing where I used a screenspace 'sketchy lines' texture in the unlit areas of the model. Patreons can check that out here.

Coloured Shadows.

You can also tint the shadows a particular colour, as long as you remember to separate the Directional Light Shadows out from the point light falloff as mentioned earlier.

See EdLighting_ColouredShadows.shader for an example.

If you're using Soft Shadows, you can even get a penumbra like edge on your shadows and tint it separately from the rest of the shadow to create some interesting stylised effects.

By remapping the combined N.L * Light Attenuation gradient into the -1 to 1 range, and then getting the Abs of that (the Absolute value means all negatives become positive), then One Minus-ing that value, we can get a mask of the edge of the shaded area.
See EdLighting_ColouredShadowsShadingEdges.shader for an example.

One final thing that Amplify Shader Editor makes easy is to add an outline, just pipe an outline node into the Local Vertex Offset input. You can get creative with the outline colour and size, by making the outline colour blend between the light colour and ambient colours for example, or by scaling the outline size relative to the light direction in transformed into view space.
Combining these different effects to create a more robust Toon Shading isn't that much more complicated, though it might require you to posterise different parts of the shader separately to have more control over the final look, rather than combining everything into one gradient. Just keep in mind the issues with Cast Shadows and Point Light falloff all being part of Light Attenuation, and also that each light that effects a mesh will additively combine with all the other lights, so if you only want a particular effect to apply once, it should be piped in to the Emission input, and not Custom Lighting.
It's easy to get carried away with adding more things, but generally I think it's a good idea to keep toon lighting as simple as possible for it to be able to hold up in different lighting conditions.
If you want to take your toon rendering further, post processing for outlines and other stylisation can often be a very useful tool. Also storing your toon ramp as a gradient texture (both lighting steps and also possibly colour information for material specific shading, like skin) and then piping your lighting info into the UV mapping of that can be a great way to get exactly the shading you're after.
Thanks for reading, have fun with making your own toon shaders, and let me know if you have any questions or suggestions.
-Ed
Jeff
2020-09-15 09:27:43 +0000 UTC