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:
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:
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:
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:
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 theRW
prefix to the texture type
You also need to implement it in your C++:
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:
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:
#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:
[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:
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,
ExampleMaterialShaderParameters.VisBuffer64