Unreal Stylized Shaders - Specialization projects
Creating stylized shaders with a specific artistic direction in a student game project was the reason of the research and work exposed in this blogpost.
Contextualization
During the last year of bachelor’s degree in Game Programming at the SAE-Institute, the students of the Games Programming section had to create a game in collaboration with the Game Art and Audio Engineering sections. The purpose of the module was to simulate what was, for some, a first work experience in a professional-like environment.
For this project I had the opportunity to work as a Graphic Programmer on two game projects made in the Unreal Engine 5.3.
For both project we had a limited production time, roughly 8 months, with a team of 27 people.
The tasks I worked on were the following:
- Creation of shaders and functions to replicate a watercolor style
- Graphic tools for the artists
- Asset integration, colorization, test and review
- Creation of Landscape shaders, Virtual Texturing
- Creation of a stylized water shader
- Creation of specific objects shaders and code (Plant Growth, Glowing Outline, Vertex deformation)
The VR project, being PBR based, didn’t require specific shaders. For that reason, the focus will be emphasized on the research, iterations and global work done for the Girl & Kitty project.
The first elements to acknowledge is the art direction. One of the references given for the art style was “Dordogne”
The only problem was that compared to Dordogne, a game made with hand-painted textures by “Un je ne sais quoi” a professional french studio, we didn’t have as much time and would risk having incoherent texturing from the artists. For that reason a major part of the texturing was done with shaders.
Shaders
In this blogpost, I am going to detail the steps of the major task I have been doing during these projects as a graphic programmer.
In this section the following shaders will be exposed and detailed:
Multiple iterations were done before being validated by the “Art Director”, the head of the Game Art section.
Stepped cell shading
Cell shading function
Since a major part of stylized games use cell shading, that was the first shader I tried to produce. After multiple tests using sequenced unreal if
nodes, I finally created a custom HLSL shader using the Custom unreal node.
In this material function (MF_) I get the SkyAtmosphereLightDirection and do a dot product with the VertexNormalWS
(see Coordinates Expressions) to set a multiplication value of the BaseColor according to the position of the SkyLight.
The node requires to explicitly add the LightDirection
, Basecolor
and Steps
variables to the custom node.
To use this material function it is necessary to branch the function as shown below:
The variables to input are:
BaseColor
: The object’s color/textureExposure
: The value by which we want to multiply the final colors.Steps
: The amount of desired separations
HLSL code
The custom’s node internal code was the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
float3 col;
float T = 0;
int steps = Steps;
float Tstep = 1.0/steps;
for(int i = 0; i < steps; i++)
{
if(LightDirection < T)
{
col = BaseColor * T;
return col;
}
T += Tstep;
}
return BaseColor;
In this code, the float3
“col” is the color that is returned at every iteration of the for loop.
The T
variable is the current value of a step.
Tstep
is the value of one step, obtained by dividing 1 by the amount of steps that are input. LightDirection
is the value of the vector that is evaluated to define the limitation of colors.
The for
loop iterated until the amount of steps
is met. Its content evaluates the LightDirection
according to the current step T
. If it is lower, the returned color is equal to the BaseColor
multiplied by the step’s value. The current step’s value is then incremented by Tstep
. When the for
loop has ended the BaseColor
is returned for the “non-shaded” parts.
Result
The result of this node used with plane white (1.0, 1.0, 1.0, 1.0) and 3 steps is the following:
Watercolor shader
Watercolor function
This shader had the principle task of replicating a watercolor effect on the assets in local/world space. After multiple trials the final version of my shader was transformed in a material function to simplify it’s modification.
This function gave the user the ability to change the watercolor effect of each material instance that used this material function.
The different parameters are the following:
Metalness & Roughness
: Value between 0 and 1 to define these values.Enable_Texture
: Enables the possibility to input a texture asBaseColor
.Enable_Second_color
: Enables the possibility to have a second color channel.Second_Texture_Multiplier
: Changes the saturation value of the second texture
The Second_
values are used with a Noise material function to give the diffusion effect to objects.
Noise function
The use of a noise function was the key element to have a “spreading” effect of both colors declared before
The Noise parameters are the following:
Noise_Scale_X & _Y
: Used to scale the given noise.Black & White_Values
: Used to clamp the Black and White values of the given noise.Noise_Texture
: The Noise used to “spread” colors.Invert_Noise
: Inverts the black and white values of the Noise texture.
To use it in a Material, the MF_Noise
is given as an alpha to a Lerp
with the BaseColors
.
Result
The final result gives some sort of a color “spread” that is one of the key elements in the watercolor style.
Rim Shader
Since Dordogne has a specific outline on the characters that depends of the light source, I decided to create the same function in Unreal.
Rim Function
It was accomplished by using a Fresnel Node
and subtracting it with the inverse of the dot between the light source and the VertexNormalWS
(vertex normal world space) node.
To use it, connected it to either the BaseColor
or the Emissive Color
.
Result
The final result connected to the Emissive Color
of a plane white material was the following:
Landscape and virtual texturing
After discussion with a Game Artist friend participating in the project she pointed out the necessity of having a landscape material, that would blend vegetation and the ground seemingly.
My first step toward its creation was to read the Unreal Engine 5 documentation about Landscape.
Landscape material
After reading the documentation, I got the Landscape Coordinates
and broke them with the BreakOutFloat2Component
node and multiplied each output with Path_Tiling_X
and Path_Tiling_Y
(2 exposed floats) to then append them back together. That gave the possibility to scale the terrain’s texture.
The same logic was applied to the grass layer.
The Landscape Coordinates
, after modification, were used as input for UVs of the BaseColor
and the Normal
of each layer.
The exposed variables for the BaseColor
(_bc) were the following:
Tint
multiplied each channel (R,G,B,A) by the given values.Intensity
multiplied all channels uniformly.Contrast
took every channel and powered it by the given value.Saturation
used the Desaturation node. The input was inverted with the OneMinus node just for better understanding by the artists.
Finally the Saturate node was to clamp all values between 0
and 1
.
For the Normal
(_n), the only variable was the Intensity
(inverted again) that was connected to a FlattenNormal
node.
The output was then injected in a Make Material Attribute node.
Virtual texturing
After the “normal” texture treatment part each layer was connected to a Landscape Layer Blend node. That node gave the possibility to link each texture to a defined landscape layer. The Break Material Attributes node that followed was used to connect the material output to the master node but also to connect it simultaneously to the Runtime Virtual Texture Output.
That Runtime Virtual Texture Output
node took all the parameters from the Break Material Attributes
node except from the Normal
that was translated from Tangent Space to World Space and the WorldHeight
that took the Y
axis of the Absolute World Position
with a Mask
node.
Grass Material
To have and interactive grass effect I started by creating a material with all the basics that my previous materials contained and adding a wind “function” that took divers variables for wind boost, weight, speed, direction, and so on…
The specificity of this material is its transformation from Tangent Space
to World Space
and back again, integrating the terrain blending between.
For the blending, the material takes the Virtual Texture
’s parameters and will blend them according to the VirtualBlendHeight
and VirtualFalloff
parameters.
The last part of the material is just breaking every material attribute and re-assigning them to the master node.
Setup, integration and use
Once the Materials done I created a Landscape Material Instance, that had to be created per map, and Landscape Layers.
Then, I just followed the Unreal documentation to setup my maps.
From the Landscape creation and connecting the Layers
To the Runtime Virtual Texture Volume that was bound to the Landscape and finally painting the Map
Result
Thanks to all this process and the materials previously created the level artists were able to create landscapes and paint them with their desired texture, integrate foliage in the scene and modify at their blending at their convenience.
Water shader
One of the game mechanics was the ability to use a boat and navigate on water, for that reason I created a water material that you can overview below.
Water color
The base color component of it was a Lerp
between two exposed colors with a Depth Fade node in the Alpha
chanel. That enabled a transition between the lighter and darker parts of the water.
Shorelines
To simulate stylized shorelines a DistanceToNearestSurface
node was used with the AbsoluteWorldPosition
node so that the intersection of meshes would be detected and create lines on the water plane at a defined distance. A Time
node was then used to be able to change their position on the water plane.
So that the shoreline wouldn’t be static two identic distortion functions where created taking different noise textures and parameters, those parameters are self explanatory. The only specificity was the panner node that gave me the possibility to displace UV’s without creating a node or function for that.
Normal distortion
At that point the shorelines were acceptable but the water did not have any kind of movement to it, for that reason I created a distortion effect of the normal taking the previously generated noise from the shoreline distortion. Different parameters where exposed to be able to control the effect from material instances.
Tweaking iterations
Due to a lack of time, a big part of the shader was just iterations of multiple shoreline assembly until the result was pleasing. For that reason I am not going to go in depth in this part of the node. With the previous parts any replication can be adapted according to preferences.
The final part of the node was assembled with two Lerp
nodes that took in account the shorelines, one for the base color and the other for the opacity chanel with again a Depth Fade
node. The normal distortion was just directly connected to the master node.
Result
The shader probably could have been much better, taking the movement of objects in consideration per example, but the final result was nevertheless quite pleasing and satisfied the team that was working on the project.
Post Processing
For this part of the blog post, we are not going to go in depth trough the work process since it is not the intended focus point. Instead a simple overview of the final shader’s state will be exposed.
Kuwahara function
The idea of using a post processing shader came first of all from the necessity of having a screen space effect that would give a diffusion of color between objects in a scene. To accomplish that effect a simple shader using HLSL was created to obtain a Kuwahara filter effect in screen space.
The Unreal Engine material node looked like this:
HLSL code
The HLSL code was the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
float3 mean[4] =
{
{0, 0, 0},
{0, 0, 0},
{0, 0, 0},
{0, 0, 0}
};
float3 sigma[4] =
{
{0, 0, 0},
{0, 0, 0},
{0, 0, 0},
{0, 0, 0}
};
float2 offsets[4] =
{
{-RADIUS, -RADIUS},
{-RADIUS, 0},
{0, -RADIUS},
{0, 0};
}
float2 pos;
float3 col;
for(int i = 0; i < 4; i++){
for(int j = 0; j <= RADIUS; j++){
for(int k = 0; k <= RADIUS; k++){
pos = float2(j,k) + offsets[i];
float2 uvpos = UV + pos/VIEWSIZE;
col = SceneTextureLookup(uvpos, 14, false);
mean[i] += col;
sigma[i] += col * col;
}
}
}
float n = pow(RADIUS + 1, 2);
float sigma_f;
float min = 1;
for(int l = 0; l < 4; l++)
{
mean[l] /= n;
sigma[l] = abs(sigma[l] / n - mean[l] * mean[l]);
sigma_f = sigma[l].r + sigma[l].g + sigma[l].b;
if(sigma_f < min){
min = sigma_f;
col = mean[l];
}
}
return col;
Use
To use it, we just had to create a PostProcessVolume in the scene and plug it in “Rendering Features -> Post Process Materials -> Array”
Result
Here you can see the result of the post processing shader being used:
Final result
After a lot of research, watching youtube videos, reading the UnrealEngine 5 documentation, reading blogs; exchanges with colleges and teachers; multiples trials and errors, the final result looked pleasing. It probably did not really reproduce the desired watercolor effect, at least not as a hand made painting could have been done, but it certainly gave a good style to the environment and a real “vibe” to it.