5

Apple's Best Practices for OpenGL ES recommend against branching on results calculated in a fragment shader. But Phong shading generally involves skipping the specular term when the light source is on the "wrong" side of the surface, for which the straightforward approach is to dot the unit normal direction N and light direction L and check for a positive result.

I attempted to do this without a branch in my shader: instead of using an if statement, I do all the calculations for the specular term and then give it a coefficient which is 1.0 if dot(N, L) is greater than zero and 0.0 otherwise. (I achieve this using the builtin step() function. Combining max() and sign() produces the same results but is said to be a bit slower.)

However, this appeared to lead to odd, device- and/or iOS version-specific, results:

with branchwithout branch

  • On my iPhone 4 running iOS 6.0, the version with the branch has a wide specular highlight (left image); without the branch I see a narrow specular highlight (right image), despite the "shininess" exponent remaining the same.
  • On the iOS 6.0 Simulator, I see the second image with both versions of the shader.
  • On my iPad (original 2010 model, stuck at iOS 5.1), I see the first image with both versions of the shader.

Clearly, it's not the branching or lack thereof that's the problem. (The right-hand image is the "correct" rendering, by the way.)

Here's my fragment shader:

precision mediump float;

uniform lowp vec3 ambientLight;
uniform lowp vec3 light0Color;
uniform lowp vec3 materialAmbient;
uniform lowp vec3 materialDiffuse;
uniform lowp vec3 materialSpecular;
uniform lowp float materialShininess;

// input variables from vertex shader (in view coordinates)
varying vec3 NormDir;
varying vec3 ViewDir;
varying vec3 LightDir;

void main (void)
{
    vec3 L = normalize(LightDir);
    vec3 N = normalize(NormDir);
    vec3 E = normalize(ViewDir);

    lowp vec3 ambient = ambientLight * materialAmbient;

    float cos_theta = dot(L, N);

    lowp vec3 diffuse = materialDiffuse * light0Color * max(0.0, cos_theta);

    lowp vec3 specular = vec3(0.0, 0.0, 0.0);
//  if (cos_theta > 0.0) {
        lowp vec3 R = reflect(-L, N);
        lowp vec3 V = normalize(-E);
        float cos_alpha = dot(R, V);
        specular =  step(0.0, cos_theta) *  materialSpecular * light0Color * pow(max(0.0, cos_alpha), materialShininess);
        //          ~~~~~~~~~~~~~~~~~~~~~~
        //          should produce the same results as the commented branch, right?
//  }
    gl_FragColor = vec4(ambient + diffuse + specular, 1.0);
}

I welcome further suggestions for improving this shader's performance on iOS hardware, too!

5
  • 6
    Could this be a precision artifact? I know that I've seen significant differences in the way that lowp values are rounded between the various iOS devices. In particular, I wonder if the use of lowp for some of the values leading to cos_alpha and materialShininess couldn't cause odd things to happen in your pow() operation. I don't think that using mediump there would slow you down too much.
    – Brad Larson
    Commented Oct 4, 2012 at 17:31
  • Both approaches are not completely equivalent. step(0.0, cos_theta) is equivalent to "if(cos_theta >= 0)", as step() returns 1 when cos_theta = 0 because (0.0 < 0 ) is false. but i don't think that's making the difference anyway.
    – aacotroneo
    Commented Dec 20, 2012 at 23:14
  • 4
    It took awhile for me to find time to experiment with this some more, but @BradLarson is right: the precision on materialShininess is it. Once that's mediump, the device/OS inconsistencies go away and all render the "correct" way (right-hand image). (Really, the branch/no-branch stuff turned out to be a red herring.) Post as an answer and I'll accept.
    – rickster
    Commented Dec 20, 2012 at 23:45
  • @BradLarson If you don't want to post it as answer i can do it too! ;-)
    – Felix K.
    Commented Dec 23, 2012 at 18:46
  • 1
    @BradLarson You should really post this as an answer, it is one of those things that is quite difficult to learn from theory alone.
    – damix911
    Commented Feb 9, 2013 at 22:48

1 Answer 1

1

As noted in @BradLarson's comment, the lowp qualifier on materialShininess turned out to be the problem; with that set to mediump instead, it renders correctly (right-hand image) on all devices and OS versions I have on hand, regardless of whether the branch or no-branch (with step) version of the shader is used.

(Using lowp versus mediump for the inputs R and V from which cos_alpha is calculated doesn't make any visible difference, which makes sense: those are normalized vectors, so their components have magnitudes in the range 0.0 to 1.0. That's the same range as color components, for which lowp seems to be intended.)

Not the answer you're looking for? Browse other questions tagged or ask your own question.