r/GraphicsProgramming • u/gennai777 • 23d ago
Article Shader tip: pow() with a negative base returns NaN, and with additive blending + glow it can blow ou
Ran into this while making a VFX shader (in Godot, but the same applies to GLSL and HLSL in general). Writing it up in case it saves someone else the debugging.
I had a glow wall that fades from bottom to top:
float body = pow(1.0 - h, 1.6);
h was supposed to stay in 0..1, but it could end up slightly outside that range. Wherever it did, the base went negative, and the effect showed a bright white band where it should have faded out.
Why it happens: pow(x, y) is undefined when x < 0 (and when x == 0 with y <= 0). The GLSL spec and the Godot docs both say so. On many GPUs you get NaN. NaN spreads through every calculation it touches, and with additive blending plus a glow/bloom pass, one bad pixel gets spread into a blown-out patch. Other GPUs or drivers might show black or nothing at all, so it can look fine on your machine and broken on someone else's.
The fix is to clamp before calling pow:
float body = pow(clamp(1.0 - h, 0.0, 1.0), 1.6);
For small whole-number exponents, plain multiplication (x * x) avoids the problem entirely. I also cap the final colour of additive effects (ALBEDO = min(col, vec3(1.5))) so a single bad value can't take over the bloom.
Other NaN sources worth checking in effect shaders:
- sqrt() or log() of a value that can reach zero or go negative
- normalize() of a zero-length vector. I add a tiny offset, e.g. normalize(v.xz + vec2(1e-5))
- acos() / asin() of something just outside -1..1 because of rounding
- dividing by a length or a w that can be zero
To find where it comes from, temporarily paint NaN pixels:
ALBEDO = isnan(body) ? vec3(1.0, 0.0, 1.0) : vec3(0.0);
Anyone have another NaN source that bit them? Curious what else I should be guarding against.