I have always been drawn to games that experiment with non-standard art styles. I remember being amazed by the visuals of Borderlands, or the Ultimate Spider-Man on PS2. In recent times, this effect has been acquiring increasingly higher levels of complexity, blending with more advanced rendering systems. Examples of this are The Legend of Zelda: Breath of the Wild, or the latest Borderlands. Therefore, I wanted to create a version of this type of modern cel-shading in an engine like Unreal Engine, whose rendering is primarily based on the PBR model.

On the left, the scene without the shader. On the right, the scene with the shader activated.
Obtaining color and lighting information
To be able to get the lighting values, the resulting RGB map from the relationship between the render and the BaseColor buffer (which contains the color information of the objects without the lighting contribution) is obtained, given by dividing the first by the second. This map contains the lighting information of the scene.
However, working with RGB is complicated. For this reason, a conversion from RGB space to HSV space is performed, from which we can extract the Hue (color), Saturation (the amount of color) and the Value, which gives us the lighting value (also called brightness).

Lighting model
Luminosity values are presented on an exponential curve. That is, the lighting relationship between a pixel with a value of 100 and another of 200 is the same as between one of 200 and another of 400, although the scalar difference between the pixels is not the same. To convert it to linear, we have to apply a base-2 logarithmic function, which gives us a gradient that we can work with more easily. After working with linearly encoded luminosity, we transform it back to exponential.
As can be seen in the image below, lighting management on metallic surfaces is handled separately in order to provide greater control to the shader, since metallic surfaces usually have more variability in the lighting gradient, which introduces a greater number of bands than if the same configuration as non-metallic materials were used.

MF_GradientBanding
To discretize a gradient into several bands of the same value, I have created a Material Function that takes as input:
- The gradient, a continuous scalar field that we want to discretize.
- The Banding Amount, a scalar that controls the amount of bands to generate: if it is 1, it outputs one band for each unit increment of the gradient, if it is 0.5 it outputs one band every two increments, if it is 2 it outputs 2 bands per increment, etc.
- The Gradient Smoothness, another scalar that allows controlling the compression of the transition between the different bands. The closer it is to 0, the more abrupt the transition, while if it approaches 1 the transition is smoother, to the point that it stops being noticeable. This transition follows a smoothstep function, being able to choose between the one that comes by default or another custom one, MF_DynamicSCurve.

Hue management
The shader also allows applying banding to the Hue. Since this is ultimately just another scalar gradient when using HSV, the same discretization function can be applied, which allows us to obtain a stylistically more interesting result. By applying a Banding Amount of 12, for example, we will divide the entire Hue space into 12 unique colors, being able to choose how gradual the transition between these will be.

Metallic edge
To give a bit more personality to metallic objects, we apply a function that adds luminosity to the parts of the metallic surface that point upwards, and we decrease those that point downwards. All this clipped by a fresnel mask.
Note the use of the arcsine to obtain a more linear fresnel gradient from the dot product between the camera vector and the object’s normals.
