Skip to content

Making Material Shaders

This overview is based on this example plugin: https://github.com/Phyronnaz/MaterialShaderExample

Material Shaders are a special kind of shaders in Unreal Engine: they are made of a main .usf file which is then compiled for every material in your game.

This is useful to easily interface with artists & tech artists.

Initial setup

When making custom shaders in Unreal, I highly recommend setting r.ShaderDevelopmentMode=1 in your ConsoleVariables.ini.

TIP

If you don't know where that is, use Rider and search anywhere (magnifying glass top right)

To ensure your shaders will be properly registered, make sure your module loading phase is set to PostConfigInit like done here.

Material shader

The main material shader class is FExampleMaterialShader. It's fairly straightforward:

ExampleMaterialShader.h

cpp
class FExampleMaterialShader : public FMaterialShader
{
public:
	DECLARE_SHADER_TYPE(FExampleMaterialShader, Material);

	FExampleMaterialShader() = default;
	explicit FExampleMaterialShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer);

	static bool ShouldCompilePermutation(const FMaterialShaderPermutationParameters& Parameters);

	static void ModifyCompilationEnvironment(
		const FMaterialShaderPermutationParameters& Parameters,
		FShaderCompilerEnvironment& OutEnvironment);
};

You can find the implementation of the two functions in ExampleMaterialShader.cpp.

Make sure to register it:

cpp
IMPLEMENT_MATERIAL_SHADER_TYPE(
	,
	FExampleMaterialShader,
	TEXT("/Plugin/MaterialShaderExample/MaterialShaderExample.usf"),
	TEXT("Main"),
	SF_Compute);

To be able to use the /Plugin/MaterialShaderExample virtual path, you need to do the following in your StartupModule:

cpp
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin("MaterialShaderExample");
check(Plugin);

const FString Path = FPaths::ConvertRelativePathToFull(Plugin->GetBaseDir() / "Shaders");

AddShaderSourceDirectoryMapping(TEXT("/Plugin/MaterialShaderExample"), Path);

Shader parameters

This is how we send data to the shaders:

ExampleMaterialShader.h

cpp
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FExampleMaterialShaderParameters, )
	SHADER_PARAMETER(uint32, ShadingBinToReplace)
	SHADER_PARAMETER_SCALAR_ARRAY(uint32, MaterialIndexToShadingBin, [16])
	SHADER_PARAMETER_RDG_TEXTURE(Texture2D<UlongType>, VisBuffer64)
	SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D<uint>, ShadingMask)
END_GLOBAL_SHADER_PARAMETER_STRUCT()

A few notes here:

  • _RDG is for textures registered using the render graph builder. This is the modern way of doing things, and most textures should be passed like this.
  • _UAV makes the texture writable, make sure to also add the RW prefix to the texture type

You also need to implement it in your C++:

ExampleMaterialShader.cpp

cpp
IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FExampleMaterialShaderParameters, "ExampleMaterialShaderParameters");

The second parameter here is the name of the parameters in HLSL - you'll access them like this: ExampleMaterialShaderParameters.VisBuffer64.

Calling the material shader

This is done here: ExampleSceneViewExtension.cpp

The gist of it is this:

cpp
const FMaterialRenderProxy* MaterialRenderProxy = MaterialInterface->GetRenderProxy();
if (!ensure(MaterialRenderProxy))
{
    return;
}
const FMaterial& Material = MaterialRenderProxy->GetMaterialWithFallback(View.GetFeatureLevel(), MaterialRenderProxy);

const FMaterialShaderMap* MaterialShaderMap = Material.GetRenderingThreadShaderMap();
if (!ensure(MaterialShaderMap))
{
    return;
}

const TShaderRef<FMaterialShader> Shader = MaterialShaderMap->GetShader<FExampleMaterialShader>();
if (!ensure(Shader.IsValid()))
{
    return;
}

FRHIComputeShader* ComputeShader = Shader.GetComputeShader();
if (!ensure(ComputeShader))
{
    return;
}

TUniformBufferRef<FExampleMaterialShaderParameters> ExampleParameters;
{
    FExampleMaterialShaderParameters Parameters;
    // Set parameters
    // ...
    ExampleParameters = CreateUniformBufferImmediate(Parameters, UniformBuffer_SingleFrame);
}

FMeshDrawShaderBindings ShaderBindings;

FMeshProcessorShaders MeshProcessorShaders;
MeshProcessorShaders.ComputeShader = Shader;
ShaderBindings.Initialize(MeshProcessorShaders);

FMeshDrawSingleShaderBindings SingleShaderBindings = ShaderBindings.GetSingleShaderBindings(SF_Compute);
SingleShaderBindings.Add(Shader->GetUniformBufferParameter<FExampleMaterialShaderParameters>(), ExampleParameters);

Shader->GetShaderBindings(
    View.Family->Scene,
    View.GetFeatureLevel(),
    *MaterialRenderProxy,
    Material,
    SingleShaderBindings);

SetComputePipelineState(RHICmdList, ComputeShader);

ShaderBindings.SetOnCommandList(RHICmdList, ComputeShader);

RHICmdList.DispatchComputeShader(
    FMath::DivideAndRoundUp(Extent.X, 8),
    FMath::DivideAndRoundUp(Extent.Y, 8),
    1);

Shader

This is the actual shader file. It's located here: MaterialShaderExample.usf

You should start with this:

hlsl
#include "/Engine/Private/Common.ush"
#include "/Engine/Generated/Material.ush"

Generated/Material.ush is the header generated by Unreal from the material graph.

You then need to declare your main function:

hlsl
[numthreads(8, 8, 1)]
void Main(const uint2 DispatchThreadId : SV_DispatchThreadID)
{

note that numthreads matches the value in DivideAndRoundUp above. The function name itself matches the name passed to IMPLEMENT_MATERIAL_SHADER_TYPE.

You can then query your material graph like this:

hlsl
FVertexFactoryInterpolantsVSToPS Interpolants = (FVertexFactoryInterpolantsVSToPS)0;

// Setup the pixel parameters
FMaterialPixelParameters Parameters = FetchNaniteMaterialPixelParameters(
    PrimitiveData,
    InstanceData,
    InstanceDynamicData,
    NaniteView,
    Tri,
    Cluster,
    Barycentrics,
    Interpolants,
    SvPosition);

FPixelMaterialInputs PixelMaterialInputs;

// Run the material graph
CalcMaterialParametersEx(
    Parameters,
    PixelMaterialInputs,
    SvPosition,
    Parameters.ScreenPosition,
    true,
    Parameters.WorldPosition_CamRelative,
    Parameters.WorldPosition_NoOffsets_CamRelative);

// Get the un-clamped base color from the material
const float3 Color = GetMaterialBaseColorRaw(PixelMaterialInputs);

Similar functions can be used for normal, roughness etc - GetMaterialRoughness, GetMaterialNormal etc.

Like exampled before, you can access shader parameters using ExampleMaterialShaderParameters: for example,

hlsl
ExampleMaterialShaderParameters.VisBuffer64