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.

4.7 (23) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

unreal engine shaders

Next, go into the Shaders folder and create a new file. Name it Gaussian.usf. This is your shader file.

unreal engine shaders

Note: Shader files must have the .usf or .ush extension.

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.

unreal engine shaders

Now that everything is set up for easy shader development, it’s time to create a Gaussian blur.

Note: Since this is not a Gaussian blur tutorial, I won’t spend too much time explaining it. If you’d like to learn more, check out Gaussian Smoothing and Calculating Gaussian Kernels.

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.

unreal engine shaders

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.

unreal engine shaders

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:

unreal engine shaders

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.

Note: Notice that the final brace is missing in the input code. This is important since the compiler inserts a brace at the end. If you leave in the brace, you will end up with two braces and receive an error.

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:

unreal engine shaders

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.

unreal engine shaders

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.

unreal engine shaders

Next, create a Multiply and connect everything like so:

unreal engine shaders

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:

  1. Calculate the relative offset of the sample pixel and convert it into UV space
  2. Sample the scene texture (Post Process Input 0 in this case) using the offset
  3. 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 normalize x and y to this range.
  4. Add the weighted color to PixelSum
  5. 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.

unreal engine shaders

Note: Sometimes the Apply button will be disabled. Simply make a dummy change (such as moving a node) and it will reactivate.
Tommy Tran

Contributors

Tommy Tran

Author

Over 300 content creators. Join our team.