2. Per-Pixel Lighting
When a model has coarse tessellation,
performing the lighting calculations at the vertex level can result in
the loss of specular highlights and other detail, as shown in Figure 1.
One technique to counteract this unattractive
effect is per-pixel lighting; this is when most
(or all) of the lighting algorithm takes place in the fragment
shader.
Warning:
Shifting work from the vertex shader to the
pixel shader can often be detrimental to performance. I encourage you
to experiment with performance before you commit to a specific
technique.
The vertex shader becomes vastly simplified,
as shown in Example 6. It simply passes the
diffuse color and eye-space normal to the fragment shader.
Example 6. PixelLighting.vert
attribute vec4 Position; attribute vec3 Normal; attribute vec3 DiffuseMaterial;
uniform mat4 Projection; uniform mat4 Modelview; uniform mat3 NormalMatrix;
varying vec3 EyespaceNormal; varying vec3 Diffuse;
void main(void) { EyespaceNormal = NormalMatrix * Normal; Diffuse = DiffuseMaterial; gl_Position = Projection * Modelview * Position; }
|
The fragment shader now performs the burden
of the lighting math, as shown in Example 7.
The main distinction it has from its per-vertex counterpart is the presence of precision
specifiers throughout. We’re using lowp for colors,
mediump for the varying normal, and
highp for the internal math.
Example 7. PixelLighting.frag
varying mediump vec3 EyespaceNormal; varying lowp vec3 Diffuse;
uniform highp vec3 LightPosition; uniform highp vec3 AmbientMaterial; uniform highp vec3 SpecularMaterial; uniform highp float Shininess;
void main(void) { highp vec3 N = normalize(EyespaceNormal); highp vec3 L = normalize(LightPosition); highp vec3 E = vec3(0, 0, 1); highp vec3 H = normalize(L + E); highp float df = max(0.0, dot(N, L)); highp float sf = max(0.0, dot(N, H)); sf = pow(sf, Shininess);
lowp vec3 color = AmbientMaterial + df * Diffuse + sf * SpecularMaterial;
gl_FragColor = vec4(color, 1); }
|
Note:
To try these, you can replace the contents
of your existing .vert and
.frag files. Just be sure not to delete the first
line with STRINGIFY or the last line with the
closing parenthesis and semicolon.
Shifting work from the vertex shader to the
fragment shader was simple enough, but watch out: we’re dealing with the
normal vector in a sloppy way. OpenGL performs linear interpolation on
each component of each varying. Pragmatically speaking, simply renormalizing the
incoming vector is often good enough.
3. Toon Shading
Mimicking the built-in lighting functionality
in ES 1.1 gave us a fairly painless segue to the world of GLSL. We could
continue mimicking more and more ES 1.1 features, but that would get
tiresome. After all, we’re upgrading to ES 2.0 to enable
new effects, right? Let’s leverage shaders to
create a simple effect that would otherwise be difficult (if not
impossible) to achieve with ES 1.1.
Toon shading
(sometimes cel shading) achieves a cartoony
effect by limiting gradients to two or three distinct colors, as shown
in Figure 2.
Assuming you’re already using per-pixel
lighting, achieving this is actually incredibly simple; just add the
bold lines in Example 8.
Example 8. ToonShading.frag
varying mediump vec3 EyespaceNormal; varying lowp vec3 Diffuse;
uniform highp vec3 LightPosition; uniform highp vec3 AmbientMaterial; uniform highp vec3 SpecularMaterial; uniform highp float Shininess;
void main(void) { highp vec3 N = normalize(EyespaceNormal); highp vec3 L = normalize(LightPosition); highp vec3 E = vec3(0, 0, 1); highp vec3 H = normalize(L + E); highp float df = max(0.0, dot(N, L)); highp float sf = max(0.0, dot(N, H)); sf = pow(sf, Shininess);
if (df < 0.1) df = 0.0; else if (df < 0.3) df = 0.3; else if (df < 0.6) df = 0.6; else df = 1.0; sf = step(0.5, sf);
lowp vec3 color = AmbientMaterial + df * Diffuse + sf * SpecularMaterial;
gl_FragColor = vec4(color, 1); }
|