step, lerp and smoothstep: the three functions behind most shader effects
Open a few shaders at random and you will find the same three functions again and again: step, lerp (called mix in GLSL, Metal and WGSL) and smoothstep. Between them they cover hard edges, blends and soft edges, which is most of what a 2D effect needs. Learn these three well and a lot of shader code stops looking like magic.
step: a hard edge
step(edge, x) returns 0 when x is below edge and 1 when it is at or above it. That is all it does, and it is surprisingly useful. It turns any smooth number into an on/off mask:
float4 frag(float2 uv)
{
float left = step(uv.x, 0.5); // 1 on the left half, 0 on the right
return float4(left, left, left, 1.0);
}
Watch the argument order. step(0.5, uv.x) is 0 on the left and 1 on the right; step(uv.x, 0.5) is the other way round. When the result looks mirrored, swap the two arguments before you do anything else.
lerp: a blend between two things
lerp(a, b, t) blends from a to b. At t = 0 you get a, at t = 1 you get b, and halfway you get the average. The formula is a + (b - a) * t. It works on numbers, on vectors, and on colors:
float3 sky = float3(0.1, 0.3, 0.8); float3 horizon = float3(1.0, 0.7, 0.4); float3 color = lerp(horizon, sky, uv.y); // orange at the bottom, blue at the top
The third argument is the "how much" control. Feed it a gradient and you get a smooth blend. Feed it a hard 0-or-1 mask and you get a choice between the two. That is how a shader does "if" without branching:
float3 color = lerp(float3(0.1, 0.1, 0.1), float3(1.0, 0.8, 0.0), step(uv.x, 0.5));
Notice what is happening: step produces the mask and lerp uses it to pick a color. You will write that pair constantly.
smoothstep: a soft edge
smoothstep(e0, e1, x) is 0 below e0, 1 above e1, and eases smoothly between them. Inside the range it computes t = saturate((x - e0) / (e1 - e0)) and returns t * t * (3 - 2 * t), an S-shaped curve that starts and ends flat. That flat start and end is what makes the transition look soft rather than like a straight ramp.
float4 frag(float2 uv)
{
float d = length(uv - 0.5);
float edge = smoothstep(0.30, 0.25, d); // see the note below about reversed edges
return float4(edge, edge, edge, 1.0);
}
Use it wherever step would leave jagged, aliased edges. A circle drawn with step has staircase edges; the same circle drawn with a narrow smoothstep across its rim looks clean.
A mistake to avoid: reversed edges
Many shading languages leave smoothstep undefined when e0 is larger than e1. Some compilers accept it and give the reversed curve, others do not, and the result can differ between your laptop and a phone. The safe way to flip a smoothstep is to subtract it from one:
float inside = 1.0 - smoothstep(0.25, 0.30, d);
This works everywhere, so make it your habit.
Putting them together
Here are the three working as a team to draw a soft-edged disc over a gradient background:
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0);
float d = length(p);
float3 background = lerp(float3(0.05, 0.05, 0.15), float3(0.15, 0.1, 0.3), uv.y);
float3 disc = float3(1.0, 0.75, 0.2);
float mask = 1.0 - smoothstep(0.24, 0.26, d); // soft edge, about 0.02 wide
float3 color = lerp(background, disc, mask);
return float4(color, 1.0);
}
The disc mask is a number between 0 and 1 at every pixel. lerp uses that number to blend the disc color over the background. Change the numbers in smoothstep to move the edge or make it softer: (0.10, 0.40, d) gives a big glowing falloff, and (0.25, 0.2501, d) is almost a hard edge.
Stripes and bands from two steps
Subtract one step from another and you get a band:
float band = step(0.3, uv.x) - step(0.6, uv.x); // 1 only where 0.3 <= uv.x < 0.6
Combine that with frac (which repeats a value) and you have repeating stripes, a checkerboard, a grid: Repeating: floor and frac shows how.
Quick reference
- step(edge, x): 0 or 1. A hard mask.
- lerp(a, b, t): a blend.
tof 0 givesa, 1 givesb. Calledmixelsewhere. - smoothstep(e0, e1, x): an eased 0-to-1 ramp, for soft edges. Keep
e0 < e1. - Mask with
steporsmoothstep, then blend withlerp.
Practise them
The lessons step() and lerp() and smoothstep(): soft edges each ask you to match a target picture with just these functions, and the challenge list has dozens that use them as building blocks.
Keep reading
- What is a fragment shader? A plain explanation with code
- HLSL vs GLSL vs WGSL vs Metal: shader languages compared
- How to learn shader programming: a step-by-step roadmap
- What is ray marching? Distance fields explained simply
- Noise in shaders: value noise, fBm and what they are for
- How to make an ocean shader: waves, Fresnel and foam
- Why is my circle an oval? UV coordinates and aspect ratio in shaders
- Signed distance functions explained: drawing shapes with math
- Porting a Shadertoy shader to Unity (GLSL to HLSL): a conversion guide
- How to debug a shader when you cannot print anything
- Why is my shader slow? A practical guide to shader performance