Yeah, it’s almost never a branch.
AFAIK this was true for all GLES 2.0 hardware as they had a hardware level step() function. Also, they don’t support branches, at all, so an if was never a branch. On some devices an if would automatically get compiled into a lerp for you.
On modern GPUs & shader compilers, step(y, x) produces identical compiled shader code as x >= y ? 1.0 : 0.0 or float val = 0.0; if (x >= y) val = 1.0;.
This (usually) compiles to a single ge (Greater than or Equal comparison) instruction. Which isn’t a branch. It’s just picking between two already calculated values.
On modern GPUs, even ones that support branches, the vast majority of if statements end up as a comparison instruction where “both sides” always get calculated, and then the result of one “side” is thrown out.
You might think using a real branch would solve this, but you’d be wrong. Branches still do “both sides” of the calculation. At least most of the time. GPUs run groups of pixels at a time, usually in 8x8 or similar sized blocks. These are called “waves” or “warps” depending on the GPU. If any pixel in the warp needs one side of a branch, all pixels in the warp pay the cost of calculating that side of the branch. So if not all pixels within a warp do the same branch, all pixels are now paying the cost of both sides of the branch, plus the additional cost of the branch instruction itself. If all pixels within a warp are only doing one side of the branch, than they only pay the cost of that one side, plus the cost of the branch instruction. The main issue is the branch instruction is itself not free, so the work being skipped needs to be significantly more costly than the branch instruction is adding. And most of the time the shader compiler will guess that it won’t be, and will compile the shader to not use a branch.
If which side of the branch can be guaranteed before the shader runs, like specifically if the value being compared is a material property against a constant or another material property, then the cost of the branch instruction is much less. Basically free at that point, and you really do only pay the cost of one side of the branch. This is basically the only time a shader compiler will for sure actually use a branch.
But here’s the last kicker. If the shader compiler thinks it’ll be faster, sometimes that step might be turned into a real branch. Because as I eluded to above, the compiler doesn’t differentiate between a step, a ternary (comp ? a : b), or an if.