In Part 7 of this series, weā€™re going to talk a little more about lighting. We talked about ambient, diffuse, and specular lighting in the previous part, but we can go beyond those basic lighting types!

Cel Shading and Fresnel Effect finished shaders.

Check out this tutorial over on YouTube too!

Fresnel Lighting

One kind of lighting Iā€™m a big fan of is called Fresnel, named after a guy called Augustin-Jean Fresnel who did a whole bunch of optics-related stuff, including inventing the Fresnel lens. Obviously. Put simply, the Fresnel effect is the principle by which objects become more reflective when you view them at really shallow viewing angles. Unityā€™s Lit Shader Graph does implement this already, and itā€™ll manifest on a sphere mesh as a highlight around the edges. Itā€™s actually a type of specular reflection so itā€™s impacted by the smoothness of the material - higher smoothness means more Fresnel reflections. In this screenshot, the left hand side of the sphere (which is ostensibly in darkness) still has a thin strip of lighting at the extreme edges.

Lit shader with visible Fresnel light.

However, for a moment, letā€™s separate the idea of the Fresnel effect in the physical world from the general idea of reflections that get stronger when viewed at shallower angles. If you want to add Fresnel light to your object with zero regard for real-world physical accuracy, we can do that with Unityā€™s built-in Fresnel Effect node.

Shader Graph's Fresnel Effect node.

You can use this even within an Unlit graph, which is what Iā€™m going to do - letā€™s right-click in the Project View and go to Create -> Shader Graph -> URP -> Unlit Shader Graph to create a new Unlit graph, which Iā€™m going to name ā€œFresnelHighlightā€. I started by quickly wiring up a Base Color and Base Texture like youā€™ve seen a couple of times now.

Base Color and Base Texture properties and nodes.

If we go ahead and add a Fresnel Effect node to the graph, weā€™ll see three inputs: a normal vector and a view vector, which in most cases we can just leave alone, plus a Power value. If we increase the power, the ā€˜edgesā€™ as it were of the Fresnel get thinner, and vice versa. You probably shouldnā€™t go below 0 but it wonā€™t actually break anything. The output is a floating-point number between 0 and 1.

Different Fresnel power values.

Iā€™m going to use this node to add a highlight effect to my object. For that, Iā€™ll add two properties to the graph: one is going to be a Float property named Fresnel Power. If I click on it and go to the Node Settings, Iā€™ll also change the Mode to Slider, leave the minimum value as 0, and set the maximum to something like 20. I will also change the default to 1 rather than 0 so that the Fresnel light doesnā€™t cover the entire surface of the object by default.

Fresnel Power graph property.

The second will be a Color property named Fresnel Color. This time, in the Node Settings, I will change the Mode to HDR. Weā€™ve used HDR colors before in previous parts of this tutorial series, but to elaborate a bit further, it stands for ā€œHigh Dynamic Rangeā€ which in this context means we can force colors to use values beyond the normal range (which in shaders is 0 to 1 for each color channel value). We do that through the use of an extra Intensity option. What this actually does under the hood is multiply each of the red, green, and blue color channels by 2 to the power of the Intensity value, so if the Intensity is zero, we multiply by 2 to the power of 0, which is 1, which is the same as a regular, non-HDR color.

Fresnel Color graph property.

You might also notice that closing and reopening an HDR color picker might change the RGB and Intensity values because now there are multiple combinations of these values that resolve to the same color - itā€™s not a bug, I promise! The screenshot below shows two versions of the same color - you can do the math yourself to verify!

HDR color picker values changing.

Anyway, we can drag the Fresnel Power onto the graph and slot it into the Fresnel Effect nodeā€™s Power input, then we can take the output from the Fresnel Effect node and Multiply it with the Fresnel Color property, effectively giving us an HDR-enabled Fresnel amount. If we choose to use a high-intensity color, then this amounts to a bright glow that will appear around the object.

Final Fresnel color value.

We can simply add this to the existing Base Color nodes and output the result to the Base Color output to complete our graph. Remember to hit Save Asset so that your changes get saved.

Adding the Fresnel and Base colors.

In the Scene View, we can apply the shader to a sphere mesh and the Fresnel acts like a highlight, as intended. This is a really cheap way to bring attention to objects, and you might have seen this approach in games before! However, it only really works properly on spherical and curved objects - objects with flat faces, like cubes, donā€™t really get a ā€˜highlightā€™ effect from this shader.

Completed Fresnel shader result.


Subscribe to my Patreon for perks including early access, your name in the credits of my videos, and bonus access to several premium shader packs!

Patreon banner.


Cel Shading

Weā€™ve just dipped our toes into the idea that we can use lighting for non-realistic purposes, so letā€™s dive even further into that concept. With a Lit shader, we can supply the physical properties of the object, but what Unity chooses to do with that data is a black box - itā€™ll just spit out some lighting and we have no control over what itā€™s doing, at least not in Shader Graph. That gives us limited ability to create non-photorealistic objects, often abbreviated as NPR for non-photorealistic rendering. Not to be confused with National Public Radio, of course.

What we could do instead is use an Unlit shader and calculate the lighting ourselves. This is obviously more involved than just using a Lit shader, but we have total control over the resultant light. Iā€™m going to create a very basic cel-shaded effect. With cel shading, light does not fall off smoothly across the object; instead, there is a hard cutoff between lit and unlit areas of the object. That means we have to do the lighting calculation ourselves and implement a threshold.

Iā€™ll create a new Unlit graph via Create -> Shader Graph -> URP -> Unlit Shader Graph, like before, and name it ā€œCelShadedā€. I will once again start with Base Color and Base Texture properties wired up like this:

Base Color and Base Texture properties and nodes.

Next, letā€™s recap from Part 6 how diffuse light works. Itā€™s inversely proportional to the size of the angle between the normal vector, which faces outwards perpendicular to the surface of the object, and the light vector, which faces in the direction of the light. For a directional light, you already have a direction. For a point light, the vector is between the surface and the point lightā€™s position in the world. However, in my shader, Iā€™m only going to account for the singular main directional light in the scene, as itā€™s usually the one light that contributes the most to objects. We can model this relationship using the vector dot product - the amount of diffuse light is simply n dot l, as the dot product decreases as the angle gets larger, which is what we want.

Modelling the amount of diffuse lighting mathematically.

On the graph, we can get information from the sceneā€™s main directional light by using the Main Light Direction node. This is a relatively new node, and itā€™s one that Iā€™m extremely happy to see implemented in Shader Graph by default! This currently points from the light origin to the surface so weā€™ll have to Negate it so that it instead points from the surface to the light origin, then we can take the Dot product of that Negate node and a Normal Vector node. This collection of nodes is doing the basic diffuse lighting calculation.

Calculating diffuse light inside the shader.

Next, weā€™ll deal with the thresholding stage which is crucial to the cel-shaded look. Thereā€™s two ways we could do this.

The first involves the Step node. This node takes two inputs called In and Edge. Essentially, if your In input is below the Edge input, then the node outputs 0, or black. Otherwise, it outputs 1, or white. Itā€™s named as such because in math, this is known as a step function. Simple!

Step node thresholding.

The other way instead uses a node called Smoothstep. It does just exactly it sounds like it does: whereas the Step node introduces a hard cutoff where the output suddenly changes from 0 to 1, Smoothstep has a sort of ā€˜buffer zoneā€™ where the output values smoothly transition from 0 to 1. So with Smoothstep, you provide two Edge values. If In is below Edge1, the output is 0. If In is above Edge2, the output is 1. And if In is between Edge1 and Edge2, then the output will be something between 0 and 1. This node is great if you want to avoid the razor-sharp cutoff you get with Step. In my graph, Iā€™m gonna go with Smoothstep.

Smoothstep node thresholding.

Since it takes two threshold inputs, Iā€™m going to add a Vector2 property to my graph called Cutoff Thresholds. The first component will be used for Edge1, and the second will be used for Edge2. We can go ahead and set that up on the graph using a Split node to separate out the two components of the Cutoff Thresholds vector.

Cutoff thresholds for the Smoothstep node.

Currently, the values output by the Smoothstep range from 0 to 1. To use this as a lighting value, usually you just multiply it with the Base Color or whatever youā€™re applying the light to. However, weā€™re going to get some very dark areas on the object if we do that (i.e., the unlit side of the object will appear completely black, which is probably not quite what you want), so Iā€™m going to control the lower threshold with a new Float property called Ambient Light Strength, which I will make into a slider between 0 and 1.

Ambient Light Strength property.

I want to remap the [0 to 1] range to instead be [Ambient Light Strength to 1], and since weā€™re starting off with a 0 to 1 range, the easiest way to do that is with a Lerp node. Letā€™s put the Smoothstep output into the T slot, then the Ambient Light Strength into the A slot, and hard-code 1 into the B slot.

Applying ambient light to the Smoothstep output.

This gives us a final light value, then we can multiply it with the base color and texture values we started with and output to Base Color, and thatā€™s the graph complete, so letā€™s hit Save Asset.

Applying cel-shading to the base color.

In the Scene View, we can apply the material to our object and weā€™ll see that the light does not smoothly fall off as it curves round the surface, like before, but instead has a hard cutoff. Here, Iā€™m using Cutoff Threshold values of -0.02 and 0.02, so we get the cutoff halfway around the object, with a very small amount of blending to help soften the edge a little bit.

Cel-shading with a smooth cutoff.

You can also just set these values to be equal, and you will still get a hard cutoff if you want.

Cel-shading with a hard cutoff.

Weā€™ve only implemented diffuse light so thereā€™s no specular highlight either - so thereā€™s a challenge for you if you want to have a go at adding the specular highlights using what youā€™ve learned in this and the previous part. Until next time, have fun making shaders!


Subscribe to my Patreon for perks including early access, your name in the credits of my videos, and bonus access to several premium shader packs!

Patreon banner.

Acknowledgements

Special thanks to my Patreon backers for May 2024!

Leonard Rhys Veale-Chan Verisutha Jack Dixon Morrie Mr.FoxQC Adam Meyer Alexis Lessard claudio croci Jun Lukas Schneider Muhammad Azman Olly J Paul Froggatt Will Poillion Zachary Alstadt ęŗ 刘