HLSL for Unreal Engine materials

Unreal materials are built from nodes, but the Custom node lets you write HLSL directly. That is the most powerful tool in the Material editor, and the one most tutorials skip. Practise the HLSL here, and ShaderBattle wraps your functions in a struct so that a whole shader fits inside one Custom node.

Things to know about HLSL

  • The Custom node holds a function body, not a whole file, so helper functions live inside a struct.
  • Inputs such as UV and Time are added in the node's inputs list.
  • Unreal's texture coordinates start at the top-left; the export flips y so lessons match.
  • Set the Output Type to CMOT Float 4 and connect the result to Emissive Color.

Cheat sheet

IdeaWrite
Vectorsfloat2 float3 float4
Declare a valuefloat d = length(p);
A functionfloat circle(float2 p, float r) { ... }
Matricesfloat2x2, multiply with mul(M, v)
Blendlerp(a, b, t)
Fractional partfrac(x)
Clamp to 0..1saturate(x)
Remainderfmod(a, b) (keeps the sign of a)
Choosec ? a : b
Angle of a vectoratan2(y, x)
Time in secondstime, or View.GameTime
Pixel positionuv (0..1, bottom-left)
Width / heightaspect
Entry pointfloat4 frag(float2 uv) { return …; }

Handy built-ins: sin cos abs sign floor ceil frac step smoothstep clamp min max pow exp sqrt length distance dot cross normalize reflect

Gotchas

  • Every pixel runs on its own, with no memory of other pixels.
  • Declare a function above the place you use it.
  • Prefer 2.0 over 2 in maths. This site fixes most cases for you; real engines may not.
  • Loops need a small fixed count: for (int i = 0; i < 5; i++).
  • In Unreal the export wraps your functions in a struct so a single Custom node can hold them.

Example: a glowing ring

The same shader as it is written for Unreal:

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0);
    float d = length(p);
    float m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
    float3 bg = float3(0.08, 0.08, 0.12);
    float3 fg = float3(0.4, 0.9, 1.0);
    return float4(lerp(bg, fg, m), 1.0);
}

Exporting to Unreal

In a Material, add a Custom node. Output Type: CMOT Float 4. Add two inputs named UV (from a TexCoord node) and Time (from a Time node). Paste this as the Code and connect the output to Emissive Color (Unlit) or Base Color. The struct lets your helper functions live inside one Custom node.

// ShaderBattle: Glowing ring
struct ShaderBattle
{
    float time;
    float aspect;

    float4 frag(float2 uv)
    {
        float2 p = (uv - 0.5) * float2(aspect, 1.0);
        float d = length(p);
        float m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
        float3 bg = float3(0.08, 0.08, 0.12);
        float3 fg = float3(0.4, 0.9, 1.0);
        return float4(lerp(bg, fg, m), 1.0);
    }
};

ShaderBattle sb;
sb.time = Time;
sb.aspect = 1.0;   // a square plane; use your mesh's width / height otherwise
return sb.frag(float2(UV.x, 1.0 - UV.y));

Questions

Can I use multiple functions in a Custom node?

Not directly, which is why the export puts them in a struct and calls the main one from the node's code.

Other languages

Unity (Built-in) · Unity (URP) · Shadertoy / WebGL · OpenGL (LWJGL, raylib) · three.js · GameMaker · LÖVE (Lua) · Godot · Apple (iOS, macOS) · WebGPU / Bevy