Skip to content
The Technical ArtistArt. Code. Pipelines. Games.
Home/Shaders/Unreal Custom Nodes
shaders - working
unreal
code meets graph

Custom Nodes & Material Functions

Workingtutorial~4 min read

Unreal’s Custom node lets you add HLSL to a material graph. It’s useful for some operations, but it also needs clear inputs, documentation, and performance checks. This page covers the node, reusable material functions, and the effect of static switches on shader permutations.

On this page The Custom Node Its Sharp Edges Material Functions Master Material Design The Permutation Budget

The Custom Node In Ninety Seconds

Add a Custom node, and in the Details panel: write your HLSL in Code, declare Inputs (each becomes a pin), set the Output Type. The code's final line must return something matching that type. The dissolve from the previous page, wired for real:

Custom node - inputs: Noise (float), Progress (float), EdgeColor (float3)
float cut = step(Noise, Progress);
float edge = smoothstep(Progress - 0.06, Progress, Noise) * (1.0 - cut);
// pack: rgb = emissive edge glow, a = opacity mask
return float4(edge * EdgeColor * 8.0, 1.0 - cut);

Output Type CMOT Float4, then split downstream: rgb -> Emissive, a -> Opacity Mask. Iteration is live - edit code, the preview recompiles. For anything longer than ~20 lines, write in a real editor and paste; a separate editor makes longer code easier to read and review.

Limitations To Check

  • It blocks some optimisations. The material compiler can't fold constants through your black box the way it folds native nodes. Rule: Custom nodes for logic that's awkward as nodes (loops, many-step maths); native nodes for the trivial stuff.
  • Document the node and its inputs. The node shows "Custom" and nothing else. Name the node itself (CU_DissolveEdge), name inputs like parameters, and leave a // one-line what & why at the top of the code. The colleague who inherits it may be you at year's end.
  • Texture inputs come as objects. Sampling inside a Custom node needs the texture object and sampler: Tex.SampleLevel(TexSampler, UV, 0) - most of the time it's cleaner to sample with a native TextureSample node and pipe the resulting float4 in.
  • Returning several values. You can pack related values into a float4, as the dissolve example does, or configure Additional Outputs on the Custom expression. Choose the interface that makes the material easiest to follow.

Material Functions: Your Studio's Standard Library

A Material Function is a reusable subgraph with defined inputs/outputs - the shader equivalent of a Python function, and the difference between a shader library and forty materials with copy-pasted fresnel that all drifted apart. The discipline that makes them work:

  • One job per function. MF_WorldAlignedNoise, MF_DetectUpFacing, MF_TriplanarSample - if the name needs "And", split it.
  • Defaults on every input (Preview Value) so the function previews sensibly and casual users can ignore optional pins.
  • A naming home: /Game/Materials/Functions/MF_*, enforced like any convention (the Naming Forge can write the validator).
  • Wrap your Custom nodes in functions. The HLSL lives in exactly one place; the graph-facing world sees a clean, documented node. This single habit removes most of the Custom node's maintenance tax.

Master Material Design

The architecture pattern from the interview bank, in practice: a handful of master materials (opaque-standard, foliage, glass, VFX) with parameters, and every asset material an instance overriding textures and scalars. Instances share the master's compiled shader - different parameter values can reuse a compiled shader where the static settings match - and artists get sliders instead of graphs. Design the parameter surface like tool UX, because it is: group parameters (Base / Wear / Advanced), name them in artist ("Dirt Amount", not "NoiseRemapMax"), clamp ranges to values that can't look broken, and keep the count under ~15 visible - past that, add a second master, not a second page of sliders.

The Permutation Budget (Why Switches Aren't Free)

A Static Switch parameter doesn't branch at runtime - it compiles both versions of the shader, and every combination multiplies: five independent switches = 2^5 = 32 shaders from one master. Ten = 1,024. That growth can increase compilation time and cache size. Check the combinations actually used by the project before adding another switch.

Compare a static switch with a scalar-controlled blend before adding an option. A switch can add compiled variants; a blend may evaluate both paths at runtime. Measure the relevant cost and explain the trade-off. The materials performance guide covers the wider performance checks.

A practical example

For example, 23 independent static switches allow over 8 million theoretical combinations. The project may use far fewer, so the theoretical count alone doesn’t tell you the build cost. Audit the combinations in use, split unrelated features into separate masters where that helps, and compare compile times and rendered results before adopting the change.