step
Logically, step implements:
float step(float a, float x) {
return x >= a;
}
Which is, return a 0 value if x > a, otherwise return a 0 value.
Step is commonly used to avoid branching, when combined with lerp
.
float value;
if (a > 0.5) {
value = calcA();
}
else {
value = calcB();
}
Is better expressed in a shader as:
float value = lerp(calcA(), calcB(), step(x, 0.5));
To understand what is happening here, recall that the implementation of lerp
is effectively
a + w * (b-a)
, which means that the expansion of the statement is:
float value = a - wa + wb;
When w
is zero, the result is a
; when w
is one, the a
values cancel and the result is b
. This would give strange results if w
had any other value; but since step
restricts the output to strictly zero or one, it works.
Notice that for all shaders this calculates both the first and second value however; for simple operations this is more efficient as it avoids branching, but benchmarking is important; if all the fragments in a group avoid one of the branches, that branch is never evaluated.
The details of how this works are platform dependent, and it may be necessary to use #if
statements to optimize the shader for various platforms.
Read more about this topic GPU Gems 2,GPU Flow-Control Idioms.
Using #define to replace if statements
The readability of lerp(a, b, step(x, y))
can become troublesome over time.
Additionally, if the statement must be tweaked on multiple platforms maintaining the shader can become problematic.
By defining a macro for this:
#define IF(a, b, c) lerp(b, c, step((fixed) (a), 0));
The readability is greatly increased:
o.Albedo = IF(_Amount > 0.5, float4(1), float4(0));