← Project Library

Dynamic Branch Material Node

Dynamic Branch Material Node

Project: AAA title Engine: Unreal Engine 5 Language: C++ / HLSL

On a recent AAA production I implemented and refined a Dynamic Branch material node for Unreal Engine 5. The node lets material authors gate expensive shader work behind a real runtime branch, without leaving the normal material graph workflow.

My focus was making the feature actually usable for technical artists: predictable generated HLSL, Material Attributes support, readable shader output, and material stats that reflect the compiled shader instead of inflated translator-side estimates.

Dynamic Branch node in the material graph The node in the material graph: a condition pair and explicit True/False return inputs.

Dynamic Branch Translation

The node translates its graph inputs into explicit HLSL branch scopes:

[branch] if (Condition != 0.0f)
{
    Result = TrueValue;
}
else
{
    Result = FalseValue;
}

For scalar and vector outputs, each branch compiles in its own scoped body, so temporary shader chunks generated in one branch can't leak into the sibling branch.

Material Attributes Support

Material Attributes were the tricky part. The naive approach compiles the full branch graph once per requested attribute, which explodes the HLSL and makes the shader look far more complex than it actually is.

Instead, the translator emits a single shared FMaterialAttributes branch body and reads individual attributes from that shared result:

FMaterialAttributes DynamicBranch1 = (FMaterialAttributes)0.0f;

[branch] if (Condition != 0.0f)
{
    DynamicBranch1.BaseColor = ...;
    DynamicBranch1.Roughness = ...;
    DynamicBranch1.Normal = ...;
}
else
{
    DynamicBranch1.BaseColor = ...;
    DynamicBranch1.Roughness = ...;
    DynamicBranch1.Normal = ...;
}

return DynamicBranch1.BaseColor;

This keeps the generated shader finite, readable, and in line with what the author sees in the graph.

Dynamic Branch details panel The details panel: the branch condition is plain HLSL, the inputs are named like any custom expression, and the output type switches to Material Attributes.

Scoped Code Reuse

Unreal's material translator reuses generated code chunks aggressively. That's normally what you want, but dynamic branches add a stricter rule: chunks generated inside the true branch must not be reused in the false branch, and vice versa.

I added branch-scope-aware chunk reuse, so local temporaries can still be shared within a branch while staying isolated from the sibling scope.

Material Stats Accuracy

Even after the HLSL was deduplicated, the material editor kept reporting inflated texture and LWC operation estimates. The shader itself was fine, but the translator-side counters still incremented during the repeated attribute compiles.

I fixed this by snapshotting the estimate counters around each attribute compile in the aggregate path: if a pass only reuses existing code and adds no new shader body, the deltas are rolled back. The reported stats now match the generated HLSL, which gives artists a realistic picture of shader cost.

Constraints

  • Compiler integration: The work lives inside Unreal's material translator, so it had to preserve existing compiler behavior, shader frequency rules, and code generation assumptions.
  • Narrow footprint: Engine changes were kept minimal and marked with local project markers, with no new shader permutations.
  • Scoped attribute support: The Material Attributes path was limited to the legacy pixel-input attributes that make sense for this workflow.

Outcome

Technical artists can now control expensive material work directly in the graph, without falling back to custom shader-only workflows. I handled the port, the compiler integration, Material Attributes support, shader-output debugging, and the stats investigation. Most of that time was spent tracing Unreal's HLSL translation path and iterating against the real generated shader output until it matched what the graph promised.