Is uber shader with if statements still bad practice?

Back in the day, if statements for expensive routines or texture lookups were considered bad practice because some GPUs would execute both code paths. I’ve switched over to clustered forward rendering, which uses a ton of conditional statements and variable-length for loops, so I was wondering if this is still the case.

Right now I have a sort of uber shader using macros like this:

    outcolor *= texture(texture0, texcoords0);

With all the variations of what textures and settings may be present, that gives me about 64 possible ways a shader can be loaded. This gets ugly when I try to support user-made custom shaders and shader reloading while the game is running.

Am I just making things needlessly complicated? Is this okay to do nowadays?:

if (texturePresent[0] > 0) outcolor *= texture(texture0, texcoords0);

Modern hardware uses actual branches for conditions which are dynamically uniform, i.e. where the result is the same for all shader invocations which are executed in parallel.

On hardware lacking branch instructions, the driver would typically convert uniform if/else conditions to #if/#else, compiling separate variants of the shader for each combination and selecting the appropriate variant for each draw call. So it shouldn’t be necessary to manually create and select variants, although collating draw calls which use the same variant may be advantageous.

Non-uniform control flow (i.e. where the condition can vary between shader invocations executed in parallel) still requires executing both branches on all hardware.

That’s not quite correct for modern hardware. If the condition is the same for all invocations in a particular wavefront (even if it could be different, the condition doesn’t have to be different all the time), then the hardware can execute a single branch just fine. And if it isn’t the same, then whether both branches are executed will depend on the compiler. If the two branches are long, it may be cheaper to break up the wavefront dynamically than to execute both.

But overall, either way there will be pain.

The condition is a boolean uniform that is set once before all objects using this material are drawn.

Minimum hardware support is OpenGL 4.0.