TA logoThe Technical Artist
Home/Shaders/Unreal Custom Nodes
shaders - working
unreal
code meets graph

Custom Nodes & Material Functions

Workingtutorial~4 min readreviewed 2026-07-05

The Custom node is a text box that accepts HLSL and quietly becomes part of your material shader. Used well, it is a superpower. Used badly, it is an unreadable black box a colleague inherits. This is the sane version: small snippets, material functions, and master-material discipline.

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; the text box has the ergonomics of a postage stamp.

The Sharp Edges (Learn Them Before They Cut)

  • It blocks some optimizations. 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 math); native nodes for the trivial stuff.
  • Naming is your only documentation. 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.
  • One output only. Need several results? Pack them into a float4 like the dissolve does, or split into two Custom nodes. Packing is idiomatic; nobody will blink.

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 - a pre-submit regex validator keeps it honest.
  • 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 - thirty looks, one compile - 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. This is how a studio wakes up to six-hour shader compiles and a gigabyte of pipeline cache, and it lands on the TA's desk when it happens.

Budget rules: reach for a lerp with a scalar parameter when the cost of computing both paths is small (it usually is) - instances changing a scalar don't add permutations; save switches for genuinely heavyweight optional features (parallax, extra texture sets); and audit occasionally - Unreal's material stats list permutation counts, and the materials performance guide covers the wider budget. When the AD asks for "just one more toggle" on the master, this paragraph is your costed yes: "Sure - that doubles the shader count. Or it can be a scalar blend at about 4 instructions. Which would you like?"

From the trenches

I inherited a project whose "master material" had 23 static switches - over 8 million theoretical permutations, of which the build farm was dutifully compiling the ~40,000 combinations actually used. First shader build after a clean DDC took most of a night. We rebuilt it as four masters and lerp-blends, cut compile time to 40 minutes, and the visual diff was literally imperceptible. The artist who built the original wasn't wrong to want flexibility - nobody had ever told them switches multiply. Now you've been told.