Unreal Engine 4 Custom Shaders Tutorial
In this Unreal Engine 4 tutorial, you will learn how to create custom shaders using HLSL By Tommy Tran.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Unreal Engine 4 Custom Shaders Tutorial
25 mins
- Getting Started
- Creating a Custom Node
- Converting Material Nodes to HLSL
- Using the SceneTextureLookup Function
- Using External Shader Files
- Creating a Gaussian Blur
- Creating the Radius Parameter
- Creating Global Functions
- Creating the Gaussian Function
- Sampling Multiple Pixels
- Limitations
- Rendering Access
- Engine Version Compatibility
- Optimization
- Where to Go From Here?
Using External Shader Files
First, you need to create a Shaders folder. Unreal will look in this folder when you use the #include
directive in a Custom node.
Open the project folder and create a new folder named Shaders. The project folder should now look something like this:
Next, go into the Shaders folder and create a new file. Name it Gaussian.usf. This is your shader file.
Open Gaussian.usf in a text editor and insert the code below. Make sure to save the file after every change.
return SceneTextureLookup(GetDefaultSceneTextureUV(Parameters, 2), 2, false);
This is the same code as before but will output Diffuse Color instead.
To make Unreal detect the new folder and shaders, you need to restart the editor. Once you have restarted, make sure you are in the GaussianBlur map. Afterwards, reopen PP_GaussianBlur and replace the code in Gaussian Blur with the following:
#include "/Project/Gaussian.usf"
return 1;
Now when you compile, the compiler will replace the first line with the contents of Gaussian.usf. Note that you do not need to replace Project
with your project name.
Click Apply and then go back to the main editor. You will now see the diffuse colors instead of world normals.
Now that everything is set up for easy shader development, it’s time to create a Gaussian blur.
Creating a Gaussian Blur
Just like in the toon outlines tutorial, this effect uses convolution. The final output is the average of all pixels in the kernel.
In a typical box blur, each pixel has the same weight. This results in artifacts at wider blurs. A Gaussian blur avoids this by decreasing the pixel’s weight as it gets further away from the center. This gives more importance to the center pixels.
Convolution using material nodes is not ideal due to the number of samples required. For example, in a 5×5 kernel, you would need 25 samples. Double the dimensions to a 10×10 kernel and that increases to 100 samples! At that point, your node graph would look like a bowl of spaghetti.
This is where the Custom node comes in. Using it, you can write a small for
loop that samples each pixel in the kernel. The first step is to set up a parameter to control the sample radius.
Creating the Radius Parameter
First, go back to the material editor and create a new ScalarParameter named Radius. Set its default value to 1.
The radius determines how much to blur the image.
Next, create a new input for Gaussian Blur and name it Radius. Afterwards, create a Round node and connect everything like so:
The Round is to ensure the kernel dimensions are always whole numbers.
Now it’s time to start coding! Since you need to calculate the Gaussian twice for each pixel (vertical and horizontal offsets), it’s a good idea to turn it into a function.
When using the Custom node, you cannot create functions in the standard way. This is because the compiler copy-pastes your code into a function. Since you cannot define functions within a function, you will receive an error.
Luckily, you can take advantage of this copy-paste behavior to create global functions.
Creating Global Functions
As stated above, the compiler will literally copy-paste the text in a Custom node into a function. So if you have the following:
return 1;
The compiler will paste it into a CustomExpressionX function. It doesn’t even indent it!
MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters)
{
return 1;
}
Look what happens if you use this code instead:
return 1;
}
float MyGlobalVariable;
int MyGlobalFunction(int x)
{
return x;
The generated HLSL now becomes this:
MaterialFloat3 CustomExpression0(FMaterialPixelParameters Parameters)
{
return 1;
}
float MyGlobalVariable;
int MyGlobalFunction(int x)
{
return x;
}
As you can see, MyGlobalVariable
and MyGlobalFunction()
are not contained within a function. This makes them global and means you can use them anywhere.
Now let’s use this behavior to create the Gaussian function.
Creating the Gaussian Function
The function for a simplified Gaussian in one dimension is:
This results in a bell curve that accepts an input ranging from approximately -1 to 1. It will then output a value from 0 to 1.
For this tutorial, you will put the Gaussian function into a separate Custom node. Create a new Custom node and name it Global.
Afterwards, replace the text in Code with the following:
return 1;
}
float Calculate1DGaussian(float x)
{
return exp(-0.5 * pow(3.141 * (x), 2));
Calculate1DGaussian()
is the simplified 1D Gaussian in code form.
To make this function available, you need to use Global somewhere in the material graph. The easiest way to do this is to simply multiply Global with the first node in the graph. This ensures the global functions are defined before you use them in other Custom nodes.
First, set the Output Type of Global to CMOT Float 4. You need to do this because you will be multiplying with SceneTexture which is a float4.
Next, create a Multiply and connect everything like so:
Click Apply to compile. Now, any subsequent Custom nodes can use the functions defined within Global.
The next step is to create a for
loop to sample each pixel in the kernel.
Sampling Multiple Pixels
Open Gaussian.usf and replace the code with the following:
static const int SceneTextureId = 14;
float2 TexelSize = View.ViewSizeAndInvSize.zw;
float2 UV = GetDefaultSceneTextureUV(Parameters, SceneTextureId);
float3 PixelSum = float3(0, 0, 0);
float WeightSum = 0;
Here is what each variable is for:
- SceneTextureId: Holds the index of the scene texture you want to sample. This is so you don’t have to hard code the index into the function calls. In this case, the index is for Post Process Input 0.
- TexelSize: Holds the size of a texel. Used to convert offsets into UV space.
- UV: The UV for the current pixel
- PixelSum: Used to accumulate the color of each pixel in the kernel
- WeightSum: Used to accumulate the weight of each pixel in the kernel
Next, you need to create two for
loops. One for the vertical offsets and one for the horizontal. Add the following below the variable list:
for (int x = -Radius; x <= Radius; x++)
{
for (int y = -Radius; y <= Radius; y++)
{
}
}
Conceptually, this will create a grid centered on the current pixel. The dimensions are given by 2r + 1. For example, if the radius is 2, the dimensions would be (2 * 2 + 1) by (2 * 2 + 1) or 5×5.
Next, you need to accumulate the pixel colors and weights. To do this, add the following inside the inner for
loop:
float2 Offset = UV + float2(x, y) * TexelSize;
float3 PixelColor = SceneTextureLookup(Offset, SceneTextureId, 0).rgb;
float Weight = Calculate1DGaussian(x / Radius) * Calculate1DGaussian(y / Radius);
PixelSum += PixelColor * Weight;
WeightSum += Weight;
Here is what each line does:
- Calculate the relative offset of the sample pixel and convert it into UV space
- Sample the scene texture (Post Process Input 0 in this case) using the offset
- Calculate the weight for the sampled pixel. To calculate a 2D Gaussian, all you need to do is multiply two 1D Gaussians together. The reason you need to divide by
Radius
is because the simplified Gaussian expects a value from -1 to 1. This division will normalizex
andy
to this range. - Add the weighted color to
PixelSum
- Add the weight to
WeightSum
Finally, you need to calculate the result which is the weighted average. To do this, add the following at the end of the file (outside the for
loops):
return PixelSum / WeightSum;
That’s it for the Gaussian blur! Close Gaussian.usf and then go back to the material editor. Click Apply and then close PP_GaussianBlur. Use PPI_Blur to test out different blur radiuses.