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:
- 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!
cos_alpha
andmaterialShininess
couldn't cause odd things to happen in yourpow()
operation. I don't think that using mediump there would slow you down too much.materialShininess
is it. Once that'smediump
, 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.