Adding toon rendering to Unity's standard shaders

· by Steve · Read in about 3 min · (598 Words)

This week I wanted a toon-style non-photorealistic render, which is something I’ve done before but not for a while, and never in Unity. I’d been playing with the Standard Shader, the physically based pipeline which has support for quite a lot of good stuff like normal / specular / occlusion maps, and kinda just wanted that plus a toon ramp. I figured I’d check out what Unity already had first.

Turns out Unity do have a toon rendering example, but it’s a custom vertex/fragment shader which doesn’t include normal mapping or any other nice features of the standard shader; I’d have to add that back.

So I started looking at how you can customise the Standard Shader to add a toon mapping lighting ramp. The first thing I checked was custom lighting in surface shaders. That was pretty useful, but again it loses all of the standard shader lighting features if you use it; you have to implement all that again.

All I really wanted was to be able to post-process the specific lighting intensity calculation and make it quantised. So I rolled my sleeves up and read the Standard Shader code end-to-end to see what I could do.

Unity don’t really provide many entry points unfortunately. However, they do make reasonably frequent use of macros in their shaders, which are ripe for replacement through the shader preprocessor. The closest point I could find to the code I needed to alter was the definition of UNITY_BRDF_PBS:

half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);

At this point most of the required components have been extracted, and we just need to compute the lighting from it. We do still need to duplicate the internal implementations of UNITY_BRDF_PBS but at least we don’t have to write the whole thing from scratch or copy the entire shader code, with all of its alternate paths for various options. It’s not perfect, but it will do.

Unfortunately AFAICT in order to cover all the same cases as the standard shader, the actual Shaderlab definitions need to be replicated to insert my replacement, because there is no way to “extend” a material definition; you can copy a Pass verbatim, but you can’t copy it and prefix it with a new #define, I don’t think. So again, more duplication than I’d like, but less than the worst case.

I’ve put together the shader code and an example using the Ethan model from the Unity standard assets. Here’s a screenshot:

I’ve also published the code on GitHub. You’ll find all the shader code in Assets/Shaders, the files don’t rely on anything but Unity’s built in shaders, the other resources are just there to demonstrate it on a real model. The key files are:

  • SinbadToonSurface.shader: the Shaderlab definition of the shaders, which is a copy of Unity’s StandardSpecular.shader with some key added lines (see below)
  • SinbadPBSToonLighting.cginc: redefines UNITY_BRDF_PBS and has modified copies of the 3 BRDF functions Unity uses in its standard shaders
  • SinbadToon.cginc: the actual toon function

Although I had to copy the Shaderlab code (ugh), the only additions I needed to make were in the lit passes, to add these 2 lines before any use of UnityStandardCore[Forward].cginc:

    #define SINBAD_TOON_BANDS 3
    #include "SinbadPBSToonLighting.cginc"

The toon bands are procedural rather than based on a ramp texture (as I previously would have done it), so you can tweak the number of bands to suit. If you look inside SinbadToon.cginc you can also see there’s a tolerance argument you can use to change how sharply each band transitions to the next.

Hopefully it helps someone else!