I have a fragment shader, when I've carefully managed to remove most branching decisions, as I have found out through research here that they are bad.

But I have one function that I just can't work out how to do it without them.

The function takes in a HSV vector, and 'expands' it. By that I mean any Value (V of HSV) from 0.0 to 0.5 gets expanded to 0.0 to 1.0. Any Value from 0.5 to 1.0 gets de-saturated, so that it becomes more white. At V=1.0 any input colour is white.

Here is my working function. Can anyone help me de-'if' it?

//expand doubles the level up to 0.5=> 1.0
//for over 0.5 then the saturation is reduced from where it is now (V=0.5) to 0.0 (v=1.0)
vec3 expand() {
    vec3 expanded;
    float newSaturation;
float newValue;

//HSV = rgb2hsv(inputColor);
if (HSV.z < 0.5) { //x = Hue,y = Sat,z = Val ,w= bugger all
    newValue = HSV.z * 2.0; //double value 0.0 to 0.5 => 0.0 to 1.0
    //no need to touch saturation
    newSaturation = HSV.y;
} else {
    newValue = 1.0; //value is max
    //need to de-saturate proportional to level over 0.5 : (1.0 - ((v - 0.5) * 2)) * s
    newSaturation = (1.0 - ((HSV.z - 0.5) * 2.0)) * HSV.y;

expanded = hsv2rgb(vec3(HSV.x, newSaturation, newValue));
return expanded;
  • \$\begingroup\$ It's probably possible to remove the branches using some clever math, but branching on modern hardware isn't as bad as it used to be. The hit you take in performance, of course, depends. The shader compiler might go so far as to optimize out the ifs for you. I would profile the shader before deciding whether to eliminate branches. \$\endgroup\$ Nov 14 '13 at 20:57
  • \$\begingroup\$ Yeah, modern hardware has less trouble with if-statements. Even older hardware didn't have a problem with the statements themselves. The problem comes down to the length of the statement clauses. The harder will always run both branches masking the results of one. If the branches are short then no big deal but if they're long or complex then you end up always paying their price even though they may not be used for a specific fragment. "Short" is in terms of instructions, not lines, of course. \$\endgroup\$ Nov 14 '13 at 22:16

You can do it easily with a couple of saturates:

newValue = saturate(HSV.z * 2.0); //double value 0.0 to 0.5 => 0.0 to 1.0
newSaturation = (1.0 - (saturate(HSV.z - 0.5) * 2.0)) * HSV.y;

The first line is relatively simple to understand. Saturate will clamp the result to the 0-1 range so values bigger than 0.5 will end up as 1.0 which is exactly what you get in the second half of the conditional.

You can also see that for the case of HSV.z < 0.5 the expression (1.0 - (saturate(HSV.z - 0.5) * 2.0)) will always end up as 1.0, which won't change HSV.y when multiplied by it.

  • \$\begingroup\$ Thank you! I probably should have mentioned that this was for Android OpenGL ES2.0. There is no saturate function, but a bit of Googleing uncovered that I may be able to replace 'saturate(x)' with 'clamp(x, 0.0, 1.0)', so I've replaced my function with: newValue = clamp(HSV.z * 2.0, 0.0, 1.0); newSaturation = (1.0 - (clamp(HSV.z - 0.5, 0.0, 1.0) * 2.0)) * HSV.y; This seems to work! Thanks again! \$\endgroup\$
    – user39022
    Nov 15 '13 at 10:11

For PC and console GPUs, branches aren't bad; they can often improve performance. Typically, for a simple branch like this, the hardware will just run both sides for each pixel and then pick the result. Unless your shader is ALU bound (most are texture-bound), you will see absolutely zero performance from removing branches.

For a more complex/longer branch, or a branch which samples textures, the compiler may choose to use a dynamic branch. The way GPUs work is that they run many (as many as 32-64) shader cores at once, running the same instruction on different data. This group is called a "Warp" (NVIDIA) or "Wavefront" (AMD). If any of the fragments in a wavefront want to take that path, then every single fragment in that wavefront takes the branch. So if one pixel out of those 64 needs the then part, and the other 63 need the else part, both sides will still be executed. Same thing happens with (non-unrolled) loops. If most pixels want to execute a loop 3 times, but there's one that needs to execute it 8 times, the hardware will execute the loop 8 times for every single pixel in that wavefront.

The best practice for PC/console applications is simply to write the shaders clearly and let the compiler and user-mode driver optimize it. Then, you can use a profiler (such as GPUPerfStudio) to help find out what needs to be optimized. Sometimes this will include removing branches, but in certain cases it could be better to add them, especially if you have large clumps of pixels where much of the shader is unused (for example, smooth shadows -- most pixels are either entirely shadowed or entirely un-shadowed, so you only want to do complex smoothing at the shadow edges).

Mobile GPUs work very differently, so if you're designing your game for phones or tablets, you might want to look into best practices for those things.

  • \$\begingroup\$ Thanks. I'm a little confused, though. I should mention that this is for Android OpendGL ES2.0. Does that compiler optimize? I replaced my rgb2hsv / hsv2rgb functions with non-if versions and the performance sky-rocketed! Here's the app (an Xmas Wallpaper), if anyone is interested in that sort of thing... play.google.com/store/apps/… \$\endgroup\$
    – user39022
    Nov 15 '13 at 10:22
  • \$\begingroup\$ If you're ALU bound, the performance difference might also come from the fact that functions like clamp() and saturate() have hardware-backing (and have been optimized well by the gpu implementors) and may take many cycles less than manually emulating them using software shader code. \$\endgroup\$
    – TravisG
    Nov 15 '13 at 13:04
  • \$\begingroup\$ My optimization also depends on knowing the range of values for the variables. My code will give different answers if HSV.z is negative for example. The compiler wouldn't apply the same optimization for that reason. \$\endgroup\$
    – Adam
    Nov 15 '13 at 14:41
  • \$\begingroup\$ Thanks Adam. If I understand you correctly, there is no need to worry about that. My adjusted version of your code slots in perfectly in this case. I'm really happy with my shader. \$\endgroup\$
    – user39022
    Nov 15 '13 at 19:40
  • \$\begingroup\$ @user39022 - My answer is more for desktop GPUs. GPUs in mobile devices are quite different beasts. I will update the answer to reflect this. \$\endgroup\$ Nov 16 '13 at 0:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .