Working with HLSL in unreal





*The starting part of the post about Virtual Shader Source Path is no longer valid in UE 5+, the rest is valid except in SM6 (no global variables between custom nodes, there must be some workaround though).




The custom nodes are a good way to extent the functionality of the material editor in unreal. But if you want to add more than a couple lines of code it gets messy very fast.

So to solve this there is a way to use external files inside the node. So we just call the functions inside those files, making the HLSL edit, way more easy and structured. This files are the .usf or .ush files, in this case of using it with a custom node, you can use any of both (more info)

Today we are gonna make a simple game module to be able to access our .usf or .ush files from the custom node. In this case lets use .ush. This files will be stored in a folder called "Shaders" in the project root. (before the 4.21 version this was a feature out of the box, but since then It have to be added manually)




Adding the Shaders folder

Lets begin adding a directory by overriding the startup and shutdown module functions (this is a quick and fast way to add a very simple module). To do this lets just add in the "Project.h" the override and the functions to the "Project.cpp". I created a project named "CustomShader", this files are located in the solution " "Source>"Project" ", in my case "Source>CustomShader".


The .h file look like this:

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#pragma once
#include "CoreMinimal.h"

class FShaderLabModule : public IModuleInterface
{
public:
 virtual void StartupModule() override;
 virtual void ShutdownModule() override;
};


And the  .cpp look like this:


// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#include "CustomShader.h"
#include "Modules/ModuleManager.h"
#include "Misc/Paths.h"


void FShaderLabModule::StartupModule()
{
 FString ShaderDirectory = FPaths::Combine
(FPaths::ProjectDir(), TEXT("Shaders"));
 AddShaderSourceDirectoryMapping("/Project", ShaderDirectory);
}

void FShaderLabModule::ShutdownModule()
{
 ResetAllShaderSourceDirectoryMappings();
}

IMPLEMENT_PRIMARY_GAME_MODULE(FShaderLabModule, CustomShader, "CustomShader");





Finally lets add the RenderCore as a dependency to out project in the "Project.Build.cs"(CustomShader.Build.cs in my case) by adding "RenderCore".

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class CustomShader : ModuleRules
{
 public CustomShader(ReadOnlyTargetRules Target) : base(Target)
 {
  PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
 
  PublicDependencyModuleNames.AddRange(new string[] 
  { "Core", "RenderCore", "CoreUObject", "Engine", "InputCore" });

  PrivateDependencyModuleNames.AddRange(new string[] {  });
 }

}




To finish lets create a folder called "Shaders" inside the root directory of the project. Dont forget to compile!



This should be enough to make unreal know about the Shader folder. So lets try how .ush/.usf files work inside custom nodes.


Creating the HLSL file

Lets create a new .txt file in our new "Shaders" folder renaming it to to "myCustom.ush" (of course the name can be whatever you want). In my case I will work with notepad++ to edit the file, but you can use any text editor you like.


Lets keep it simple, in this case I made a multiplication function. The function and will be inside a struct wich we will use inside the custom node. Also use the #pragma once to avoid multiple includes in the same file.


#pragma once
struct CustomF 
{ 
 float A;
 float B;

 float Mult( float a, float b){
 return a*b;
 }
}


Using the HLSL file

Lets create a new unlit material and a custom node plugged into emissive so we see cleary what we are doing.




Now we only need to use it inside the custom node. To access it we just need to include the .ush/.usf in the shader and make a struct instance to use the variables and functions. Remember that you can use external variables by creating them in the external file and having the same name in the input of the custom node (like the A and B variables of this example).




You can also create your custom global material variables and pass them between nodes. This way you are no longer limited to a float4 output when working with custom nodes!
By default the custom node code is translated by the compiler as a function. Since the compiler inserts braces automatically, you can write code outside the custom node function by closing the function and reopening it. In this example we create a global struct Func and modify it in other custom nodes (remember to add the "return 0;" just to remember the compiler that it still a function!).

*If you are using UE 5+, global structs are const by default so you will have to create a variable inside the function you will use them in case you want to change struct values.

Custom node A:



    return 0;  } #pragma once struct Func {        float b;
    void F(){
    b = b/2.f;
    }
};

Func a;
void DummyFunc()  {


//--------------------------------


//SM6
    return 0;  } #pragma once struct Func {        float b; 
    void F(){     b = b/2.f;     } };
void DummyFunc()  {





Custom node B:


#pragma once

a.b = 500.f; a.F(); return 0;
//--------------------------------

//SM6
#pragma once

Func a; a.b = 500.f; a.F(); return 0;


Custom node C

//Not valid in SM6 (can only read local variables)
#pragma once
return a.b;


And here is the output:



If you don't know how to get a parameter in code, you can get code of any material in Window>ShaderCode>HLSL Code. This is the main way of getting all the code, but you can also check MaterialTemplate.h (Engine/Source/Shaders/Private/MaterialTemplate.ush) and the API documentation (despite being very lacking it can help). More info on custom node tricks here.



This is the base to start working on more complicated shaders without having to modify engine files. As we will see in future posts, this opens the gates for a lot of cool and easy to make materials. Hope you find It useful and see you in the next post!










Comments

  1. Hi. I am hitting a wall trying to replicate this in Unreal 5.3 is it only me or is this approach not working anymore?

    ReplyDelete
    Replies
    1. Hi Mike, looks like the virtual shader source path is not working. I assume its an issue with uE5 in general. I have searched a little but couldn't find clear documentation on the matter.

      About the rest of the post, after a quick test seems like still valid up to 5.3 but a little line of code. I will put a note about it at the top of the post.

      Thanks for the comment!

      Delete
    2. This comment has been removed by the author.

      Delete
    3. I still have an issue with the last example. In CustomC I cannot access "a.b"
      "use of undeclared identifier a"
      It seems it doesnt work, because the Func a is local to CustomExpression1, so CustomExpression2 cant access it. I seem to have the same issue in the raymarching example...I just tried to replicate it with this simple example fist.
      It would be great, if you have some insight to this, or if I am just missing someting.

      Delete
    4. True, after looking a little more into it, seems like its a SM6 problem specifically.

      Its setting the declared struct constant, and setting a value right after declaring it results in a "Shader minification failed" error. Not sure how to prevent it.
      This prevents the posibility of passing a struct between multiple custom nodes sadly.

      The reference link about the custom nodes was down, its now updated in waybackmachine(http://web.archive.org/web/20230314072439/http://blog.kiteandlightning.la/ue4-hlsl-shader-development-guide-notes-tips/)

      Delete
    5. Alrigh, in my case I can work around it. Thanks for looking into it and confirming!

      Delete

Post a Comment