Notifications
Article
Recreating a Zelda-inspired toon shading in ShaderGraph
Updated 6 days ago
2.9 K
0
Tips on how to recreate the iconic look in Unity's node-based shader editor
Some time ago I shared on Twitter and here my toon shader which is trying to replicate the look of the characters in Nintendo's game Zelda: Breath of the Wild. Since many people on Twitter asked for the shader itself or an image of the graph, I wanted to share a short explanation together with the actual thing.
Before we start, a few things to consider: this is a shader created entirely in ShaderGraph, Unity's shader editor. I'm also using the new Scriptable Rendering Pipelines (SRPs), specifically the Lightweight Rendering Pipeline (LWRP). Since at the time of writing both the LWRP and ShaderGraph are quite new, they have a few limitations. I tried to work around them with the tricks I explain below.
Oh and by the way, this shader wouldn't have been possible without the help of LWRP wizard Andre McGrail, and ShaderGraph mastermind Matt Dean.

The goal

I wanted to achieve a 2-level toon shader, with hard light/shadows. I wanted to support specular lighting, in two ways: a simple patch of colour for the hair, while on skin I wanted it to be defined by brush strokes. You can see the difference in this image, where Zelda's dress show the characteristic brush strokes, while the specular on her hair is just a hard blob of brighter colour.
I also wanted to replicate the rim lighting you see when looking against the sun, and the almost-white rim you can see when the light is at an angle (see above, her ear, fingers and right arm).
Finally, I wanted to have support for specular, normal and emission maps.

The flow of the graph

Here you can find a full-sized image of the graph, so you can zoom in and see all the details of the nodes and the Properties.
Below instead is an overview of the flow of the graph, with all the key sections highlighted as blocks based on their function:
Note: the TangentToWorld node you see on the left is not a custom node, but rather a SubGraph. I made it because at the time there was a bug in the Transform node, so I had to reimplement the conversion (from tangent space to world space) with a SubGraph. It looks like this:
If it works for you, you can just use a Transform node instead.
Back to the graph. As you can see, the graph goes from left to right, beginning with some work on the normals from the geometry, which get blended with normals coming from the Normal map (Normals block, in purple). Together, they define the directionality of the surface and are key to calculate the lighting.
They flow into the yellow part (Lighting), where I used a custom node made by me to get lighting data from the main Directional in the scene. I have written an extensive article on writing custom nodes. Also, you can grab my node from Gist.
To create the toon shading, I calculate the Dot Product of two vectors: the light direction and the normal. This mask, which looks black and white with a hard edge, is obtained through the use of a Smoothstep node. In addition to being the actual light/shadow of the model, it's also used to mask the rim highlight and the specular. In fact, you can see three branches coming out of the Smoothstep node in the centre.

Specular highlights and the paint brush effect

At the bottom in the blue block called Specular, I obtain the the half vector between view and the light directions, and I use it to calculate a mask for the specular. In this phase I use (very loosely) the Blinn-Phong shading model, which is extremely simple. For more info on the Blinn-Phong model, see this.
My implementation of this model is not the most perfect and has a few instances where it doesn't look the best (it depends on the light and view angle), but it's good enough for the purposes of this demo. Feel free to swap it with your own!
Then, I either use this mask to crop a screen-space texture to create the effect of paint brush dabs (top half, Paint brush), or I just use it as it is for the patch-shaped specular for the hair parts (bottom half, Patch). To switch between the two, I have exposed a Property called UseSpecularDabs, which I verify through a branch node.
In this part it was key to support a specular map too, so that metallic things could be more shiny that, for instance, skin or wood.

Creating interesting contrast with the rim effect

Back to the top, in the cyan box called Rim highlights. Here I was lazy and I used a pre-made Fresnel Effect node, but I still filter it by using the Dot Product between the light and the view directions. This gives me a rim that only appears when the object is seen against the light. You know when you look at an object against the sun, or against a sunset? They are completely dark. With this addition instead, your characters have a colourful silhouette even if they shouldn't. It's a fake effect but it makes them much more interesting. The result:
If you notice, the Fresnel gets stepped twice. Why?
The bottom one is an effect that can appear in dark areas and overwrite them, creating the "sunset" effect I mentioned earlier. In this case, brightness is not artificially enhanced, and you just get the colour as if that surface was fully hit by the sun. You can see this effect in the image above on the sides of the body and especially on the silhouette of the face.
The top one instead represents the white outline, seen at an angle, which only appears on areas which are already lit. That's why it's multiplied by the Smoothstep representing the toon (not visible in this image, check the graph), so it doesn't appear in dark areas. The fact that the Step function is offset by 0.2 means that this effect will appear only very close to the edge of the shape, allowing the two effects to live together and overlap. You can see the effect very clearly in the image above on the girl's glove, which is almost white.

Merging it all together - The Master Node

The rest of the graph is pretty trivial. You can see how the different component flow into what I call the "backbone" of the shader, represented by a red line, which eventually connects to the Master Node (the last one on the right). There's only one final "trick" I used.
ShaderGraph as of now supports two types of Master Nodes: PBR and Unlit. Ideally for a toon shader I would use the Unlit one, since I am calculating the colours myself. But because I wanted shadows, and unlit shaders don't get shadows by default, I had to use the PBR node. In the future I might change this as (maybe) Unity introduces new types of Master Nodes.
The PBR (Physically-Based Rendering) Master Node gives you a pre-made lighting model that's very good for realistic materials. However, I don't need that model: I only need shadows! Also because if I were to use the Albedo, the light would be multiplied on top of the colours I already calculate, and I don't want that. I want to control the colours myself in the graph.
As such, I don't use the Albedo at all and I set it to black, so that the material doesn't get any of the "PBR-yness". So how do I drive the colour? Through the Emission. This poses an additional problem: the Emission slot needs to act as Albedo, Specular, Metallicness and Emission at the same time.
Emission is the trickiest one: I need to calculate all my shader as if it was Albedo, then at the very end add the emission value on top (whether it's a single value or a texture), and then scale it somehow so that parts which should not be emissive don't get picked up, for instance, by a Bloom screen filter. It's a tricky balance and it's not perfect, but it gives me the results I wanted: toony colours, emission support, and self-shadowing.

Conclusions

All in all, I'm happy of where this experiment has gone. I hope you enjoy the shader and this article. If you have comments and questions, post them here or reach me on Twitter.
You might be wondering where you download the graph. You can't. I decided not to share the ShaderGraph file on purpose: if I did, you could just slap it into your game and learn nothing. Instead, by following the image linked above and recreating it by hand, you will learn so much more about ShaderGraph and why things work the way they do. For your convenience, here's the image linked again.
Enjoy!

Ciro Continisio
Technical Evangelist - Educator
6
Comments