HLSL From Zero, For Artists
If you’ve built materials with nodes, many HLSL operations will already look familiar. Multiply and Lerp have direct code equivalents. This page works through the syntax one step at a time, using small effects you can compare with a node graph.
On this page
The Mental Model Floats & Vectors Swizzling The Five Functions UV Maths Is Motion Build: Dissolve Effect Where To Write ItThe Mental Model: One Tiny Program Per Pixel
A pixel shader is a short program that runs once for every pixel your object covers - hundreds of thousands of times per frame, all in parallel. It receives a few inputs (this pixel's UV, its normal, some textures) and must output one thing: a colour. For these examples, focus on how the inputs produce the output colour at each pixel.
The consequence that shapes everything: the program can't see neighbouring pixels, can't remember last frame, can't loop over the whole image. It only knows here, now. When shader code looks weird, it's usually this constraint doing the shaping.
Floats And Vectors (You Know These As Pins)
float roughness = 0.5; // one number (a gray pin) float2 uv = float2(0.25, 0.75); // two numbers (a UV pin) float3 color = float3(1.0, 0.4, 0.0); // three (a color pin - orange) float4 rgba = float4(color, 1.0); // four (color + alpha)
Maths works on all of them at once - color * 0.5 darkens all three channels, exactly like piping a colour through a Multiply node with 0.5. When you combine different sizes, HLSL is strict: float3 * float is fine (the float applies to each channel); float3 + float2 is a compile error, and the fix is deciding what you actually meant - which the node graph was silently deciding for you.
Swizzling: Select And Reorder Components
float3 c = tex.rgb; // grab three channels (a Mask node) float r = tex.r; // grab one (ComponentMask R) float3 g = tex.ggg; // one channel, three times (instant grayscale spread) float2 flipped = uv.yx; // swap! (rotate a gradient 90 deg) c.rb = c.br; // swap red and blue in place
Any combination of xyzw/rgba, any order, repeats allowed. What takes three Mask-and-Append nodes in a graph is four characters here. This is the first place code stops being "the hard way" and starts being the fast way.
Five Common Material Functions
| Function | What it does | Where you've used it as a node |
|---|---|---|
lerp(a, b, t) | Blend a->b by t | Every blend, layer, and mask combine you've ever made |
saturate(x) | Clamp to 0-1 | Clamp node - the "stop lighting from breaking" function |
dot(a, b) | How aligned two directions are | Fresnel, fake lighting, facing masks (maths sheet) |
frac(x) | Keep only the fraction - makes anything repeat | Tiling, stripes, scrolling that wraps |
smoothstep(e0, e1, x) | A soft threshold: 0 below e0, 1 above e1, silk between | Every soft mask edge; useful when you need a gradual transition |
Two worked one-liners to make them real. Fresnel - surfaces facing away from you glow at the rim:
float rim = pow(saturate(1.0 - dot(Normal, ViewDir)), 3.0); return lerp(baseColor, rimColor, rim);
Read it aloud: "how much does this pixel face the camera -> invert it -> sharpen with a power -> blend towards the rim colour." Stripes - frac makes anything periodic:
float stripes = step(0.5, frac(uv.x * 10.0)); // 10 hard bands across U float soft = smoothstep(0.4, 0.6, frac(uv.x * 10.0)); // same, soft edges
UV Maths Is Motion
UVs are just a float2 per pixel saying where to look in a texture. Add to them and the texture slides; multiply and it tiles; feed them through functions and it warps:
float2 scrolled = uv + float2(Time * 0.1, 0.0); // panner float2 tiled = uv * 4.0; // 4x4 tiling float2 distorted = uv + (noiseTex.rg - 0.5) * 0.05; // heat-haze wobble
That third line is the whole "distortion" family - water refraction, heat haze, flag ripple - in one move: look somewhere slightly wrong, on purpose. Feed it a texture from the noise generator and you've built it for real.
Build It: The Dissolve Effect
The classic portfolio piece, in eight lines you now fully understand:
float n = noiseTex.r; // 0-1 noise, tileable float cut = step(n, Progress); // 1 where dissolved away float edge = smoothstep(Progress - 0.06, Progress, n) * (1.0 - cut); // thin band at the frontier float3 color = baseColor + edge * edgeColor * 8.0; // overdrive it - bloom food float opacity = 1.0 - cut; // output: color -> Emissive/Base, opacity -> Opacity Mask
Slide Progress 0->1 and the noise eats the object, glowing at the boundary. Every part is one of the five functions. Try changing one input at a time and compare the result with the code.
Where To Actually Write It
Fastest path if you live in Unreal: the Custom node - paste the dissolve above, wire the inputs, iterate live. That workflow (plus material functions, plus keeping track of shader permutations) is the next page. For a pure sandbox with zero engine setup, Shadertoy in a browser tab is useful for practising these functions - just know it's GLSL there (vec3 for float3, mix for lerp; the thinking is identical).
