Unreal Dynamic Shaders - Specialization projects
The necessity of creating interactive visual effects in an Unreal Engine game resulted in this research, the work that followed and finally 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 module I had the opportunity to work as a Graphic Programmer on two game projects made in the Unreal Engine 5.3. The project that demanded the most work in terms of shader creation was first first know as “The Project Girl & Kitty”. Project Girl & Kitty Scene render
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:
- Project producing
- Creation of shaders and functions to replicate a watercolor style
- Creation of Landscape shaders, Virtual Texturing
- Creation of a stylized water shader
- Asset integration, colorization, test and review
- Creation of specific object’s shaders and code (Plant Growth, Glowing Outline, Vertex deformation)
- Graphic tools for the artists
In this blogpost, I am going to detail the work done towards the creation of the following interactive shaders:
Interactive class and dynamic materials
Before going in detail into the shader creation, the first thing to acknowledge is that in Unreal Engine, you can not modify objects materials in run-time since they are static. For that reason I had to code an “Interactable” component class so that every object that had it would override its materials and create a dynamic version of them.
Below you can see the Interactable.h
code related to the dynamic material creation: The Interactable.h
file:
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
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Interactable.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PROJECTGIRLANDKITTY_API UInteractable : public UActorComponent
{
GENERATED_BODY()
public:
UInteractable();
//...
protected:
virtual void BeginPlay() override;
private:
TArray<UMaterialInstanceDynamic*> DynMaterials;
//...
};
As you can see the DynMaterials
is an Array
that stores the materials created at run-time.
And the Interactable.cpp
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "Interactable.h"
UInteractable::UInteractable()
{
//...
}
void UInteractable::BeginPlay()
{
Super::BeginPlay();
TArray<UStaticMeshComponent*> Meshes;
GetOwner()->GetComponents<UStaticMeshComponent>(Meshes, true);
for (int32 i = 0; i < Meshes.Num(); i++)
{
UStaticMeshComponent* StaticMeshComponent = Meshes[i];
UMaterialInterface* Material = StaticMeshComponent->GetMaterial(0);
UMaterialInstanceDynamic* DynMaterial = UMaterialInstanceDynamic::Create(Material, GetOuter());
DynMaterials.Add(DynMaterial);
StaticMeshComponent->SetMaterial(0, DynMaterial);
}
//...
}
The code above takes all the Meshes
and creates a dynamic material instance for each of them and stores it in the DynMaterials
array. Thanks to this unreal component class, all exposed variables in the materials were accessible via code or blueprint.
Shaders
Outlining shader
Outline function
The necessity to have a dynamic outline, that reacted to the players position and rotation when he crossed interactable objects, brought me to create this “Coloured_Outline” Shader. Using the Fresnel
, Time
& Sine
Nodes I first created an “Emissive” effect.
The Enable_Outline
parameter (float) is used in the C++ code to activate it or not. I first tried using a Static bool
parameter but after research, since it is static, you can not modify it in code thus rendering it’s activation impossible.
The MF_Outline is the base function used to create the outline of objects. It colors every part of an object that has a normal direction, according to the camera’s position, with a value superior to the Outline_Thickness
.
C++ code
Once the shader done, I had to create the methods that would be used in game to enable and modify the outline. Below is the code related to the Outline of the “Interactable” objects:
Interactable.h
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
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PROJECTGIRLANDKITTY_API UInteractable : public UActorComponent
{
public:
//...
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Visuals|Outline", meta = (AllowPrivateAccess = "true"))
bool EnableOutline = false;
//...
private:
//...
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Visuals|Outline", meta = (AllowPrivateAccess = "true", ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0"))
float MaxOutlineThickness = 0.3f;
UPROPERTY(EditAnyWhere, Category = "Visuals|Outline", meta = (AllowPrivateAccess = "true"))
float OutlineThickness = 0.0f;
UPROPERTY(EditAnyWhere, Category = "Visuals|Outline", meta = (AllowPrivateAccess = "true"))
float CurrentOutlineValue = 0.0f;
//This float is used instead of a bool since materials only have static bools
float OutlineEnabled = 0.0f;
void SetOutline(float value);
//...
};
The public property of the EnableOutline
variable gave the access to its activation/deactivation from anywhere without having to worry about the fact that it was actually a float. The MaxOutlineThickness
was the only value that was exposed to modification on blueprints.
In the .cpp
file the modification of the outline values were done with a Conditional operator. When enabled/disabled with the EnableOutline
bool the OutlineThickness would be evaluated and gradually incremented/decremented by the DeltaTime
until it reached the MaxOutlineThickness
/0.0
value.
Interactable.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void UInteractable::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (EnableOutline)
{
OutlineThickness >= MaxOutlineThickness ? OutlineThickness = MaxOutlineThickness : OutlineThickness += DeltaTime;
OutlineEnabled = 1.0f;
}
else
{
OutlineThickness <= 0.0f ? OutlineThickness = 0.0f : OutlineThickness -= DeltaTime;
OutlineEnabled = 0.0f;
}
if (CurrentOutlineValue != OutlineThickness)
{
CurrentOutlineValue = OutlineThickness;
SetOutline(OutlineThickness);
}
}
Then, to avoid un-useful work every frame, the OutlineThickness
would be evaluated with the CurrentOutlineValue
and if it had changed, the SetOutline
method would be called and the values previously mentioned would be equalized.
1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// Sets the Outline Thickness
/// </summary>
/// <param name="Value">The value of the outline thickness</param>
void UInteractable::SetOutline(float value)
{
for (auto& DynMat : DynMaterials)
{
DynMat->SetScalarParameterValue("Outline_Thickness", value);
DynMat->SetScalarParameterValue("Enable_Outline", OutlineEnabled);
}
}
The SetOutline
method above gets the dynamic materials parameters by string and changes their values. In this case the Outline_Thickness
is changed with the value
parameter and the Enable_Outline
is set with the classes variable OutlineEnabled
.
Result
In the end, I also linked an Unreal Material Parameters Collection to it. The idea was to give the artist the possibility to change its color in a simple “Color palette” for that purpose
Distortion shader
Distortion function
The distortion function was a function that just took the Time
, Panner
and VertexNormalWS
nodes with a Normal map in a TextureParameter2D
node to deform the object.
The only specificity was to multiply the R
and G
channels by the Distortion_Power
, that was controlled in the C++ code, so that the effect could be enabled or not.
To use it I had to branch it to world position offset in the object’s material node.
C++ code
Similarly to the Outline shader, the distortion also required its variables and methods.
Below you can find the code related to the distortion in the .h
file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//...
//The rest of the Interactable.h file
//...
private:
bool tempDistortionState = false;
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Visuals|Distortion", meta = (AllowPrivateAccess = "true", ClampMin = "0.0", ClampMax = "100.0", UIMin = "0.0", UIMax = "100.0"))
float DistortionPower = 20.0f;
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Visuals|Distortion", meta = (AllowPrivateAccess = "true"))
float DistortionSpeed = 0.25f;
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Visuals|Distortion", meta = (AllowPrivateAccess = "true", ClampMin = "-1.0", ClampMax = "1.0", UIMin = "-1.0", UIMax = "1.0"))
float DistortionDirectionX = 0.0f;
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "Visuals|Distortion", meta = (AllowPrivateAccess = "true", ClampMin = "-1.0", ClampMax = "1.0", UIMin = "-1.0", UIMax = "1.0"))
float DistortionDirectionY = 0.03f;
//...
void SetDistortion(bool value);
void SetDistortionValues();
//...
and in the .cpp
file:
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
//...
void UInteractable::BeginPlay()
{
Super::BeginPlay();
//...
SetDistortionValues();
}
void UInteractable::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
//...
#if WITH_EDITOR
SetDistortionValues();
#endif
if (tempDistortionState != EnableDistortion)
{
SetDistortion(EnableDistortion);
tempDistortionState = EnableDistortion;
}
}
The method SetDistortionValues()
was used to set the materials values defined by blueprint, for that reason I used the #if WITH_EDITOR //code #endif
statement to be able to change them at run time in editor. The code would then be ignored in the built version.
1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// Sets the value of distortion for the Interactable Shaders
/// </summary>
void UInteractable::SetDistortionValues()
{
for (auto& DynMat : DynMaterials)
{
DynMat->SetScalarParameterValue("Distortion_Speed", DistortionSpeed);
DynMat->SetScalarParameterValue("Distortion_Power_X", DistortionDirectionX);
DynMat->SetScalarParameterValue("Distortion_Power_Y", DistortionDirectionY);
}
}
Below the SetDistortion(bool value)
method changed the state of activation of all dynamic material on the objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// Sets the state of the distortion effect
/// </summary>
/// <param name="value">True = Enabled</param>
void UInteractable::SetDistortion(bool value)
{
switch (value)
{
case true:
for (auto& DynMat : DynMaterials)
{
DynMat->SetScalarParameterValue("Distortion_Power", DistortionPower);
}
break;
case false:
for (auto& DynMat : DynMaterials)
{
DynMat->SetScalarParameterValue("Distortion_Power", 0.0f);
}
break;
}
}
Result
Here you can see the result after branching the material function to the materials and calling the methods with code/blueprints:
Fading shader
Fading function
The last dynamic shader I did was to be able to have magical plants growing when the player arrived in a defined area.
For the shader, I created a material function that took the V
Coordinates (Y axis) from the textures’ UV
(TextCoord[0]
) with the Mask
node and then applied a condition (if
node) to check if the coordinated where below a certain value.
Then the material function was linked to a material’s Opacity Mask. The material’s Blend Mode just had to be set to Masked
.
Blueprint code
As for the previous shaders, I had to create the code to activate the fading effect. The only difference was that, due to the diversity of possible plants, I had to manually select the meshes on which the effect has going to be applied. For that reason I created the same Begin()
function but in blueprint.
The Create Dynamic Fade Material
function replicated what had been done in the previous C++ codes using a Blueprint Function Library.
The next step was to code the detection. To be sure that only the player could activate it with the main character, I checked by tag if the colliding element was effectively the player.
I also exposed a variable of type Trigger Base
that gave the possibility to assign a trigger from outside the blueprint.
That gave the possibility to select a trigger directly in the editor, for multiple objects at the same time and not have to change a collider inside this blueprint.
Since this was a plant, the effect wouldn’t have been visually pleasing if every material was activated at the same time. For that reason I had to sequence the activation with this part of the node:
First of all, when the trigger was activated, the first element in my DynMats
array would update its Fade_Amount
value according to the Timeline.
Then, when finished, the Mat_Index
was incremented and the next material was updated with the same logic until there were no more material to update.
Lighting
The last element of the blueprint I coded was lighting and emissive. Inspired by the same process I made an array of lights
and set their intensity, with a few control variables, once all meshes had appeared.
Finally I set an emissive on the petals to correspond to the enlightenment.
Result
In the end, even though it took a certain amount of time and iteration, the effect looked really good but was also very easy to use and modify in the editor.
Conclusion
To conclude this blogpost I would say that, even though this work wasn’t really relative to Graphic programming, it has taught me a lot regarding Unreal Engine 5, its interface, the materials and the hierarchy behind them, the tools and possibilities available.
Since I was more used to Unity, having to use the system of dynamic materials was really complicated to understand and work with at the beginning but, in the end, I think that what I have learned was really valuable and has trained me to get used to a different interface and way of doing things. Overall it was really an interesting project to work on and I feel proud of what I was able to accomplish giving the fact that I had no knowledge of Unreal’s material system before.