5. Implementing Outline, Glow, and Shadow Effects
Using shaders with distance fields can also
achieve a variety of special effects, as shown in Figure 4. In the interest of brevity, I won’t go
into too much detail here; much like the smoothing example from the
previous section, all these effects rely on using
smoothstep and various offsets from the
distance=0 boundary. They also make use of a GLSL
function called mix; here’s its
declaration:
float mix(float x, float y, float a)
You probably already guessed that this
function performs linear interpolation between its first two
arguments:
mix(x, y, a) = x * (1 - a) + y * a
See Example 4 for an “übershader” that can
produce any of the aforementioned distance field effects, depending on
how the application sets up the uniforms. If you’re trying to run this
shader on the simulator, you’ll need to remove the top line and replace
the fwidth function with a constant.
Example 4. Distance field übershader
#extension GL_OES_standard_derivatives : enable
varying mediump vec2 TextureCoord;
uniform sampler2D DistanceField; uniform mediump vec3 OutlineColor; uniform mediump vec3 GlyphColor; uniform mediump vec3 GlowColor;
uniform bool Outline; uniform bool Glow; uniform bool Shadow;
const mediump vec2 ShadowOffset = vec2(0.005, 0.01); const mediump vec3 ShadowColor = vec3(0.0, 0.0, 0.125); const mediump float SmoothCenter = 0.5; const mediump float OutlineCenter = 0.4; const mediump float GlowBoundary = 1.0;
void main(void) { mediump vec4 color = texture2D(DistanceField, TextureCoord); mediump float distance = color.a; mediump float smoothWidth = fwidth(distance); mediump float alpha; mediump vec3 rgb;
if (Outline) { mediump float mu = smoothstep(OutlineCenter - smoothWidth, OutlineCenter + smoothWidth, distance); alpha = smoothstep(SmoothCenter - smoothWidth, SmoothCenter + smoothWidth, distance) rgb = mix(GlyphColor, OutlineColor, mu); }
if (Glow) { mediump float mu = smoothstep(SmoothCenter - smoothWidth, SmoothCenter + smoothWidth, distance); rgb = mix(GlyphColor, GlowColor, mu); alpha = smoothstep(SmoothCenter, GlowBoundary, sqrt(distance)); }
if (Shadow) { mediump float distance2 = texture2D(DistanceField, TextureCoord + ShadowOffset).a; mediump float s = smoothstep(SmoothCenter - smoothWidth, SmoothCenter + smoothWidth, distance2); mediump float v = smoothstep(SmoothCenter - smoothWidth, SmoothCenter + smoothWidth, distance); // If s is 0, then we're inside the shadow; // if it's 1, then we're outside the shadow. // // If v is 0, then we're inside the vector; // if it's 1, then we're outside the vector. // Totally inside the vector (i.e., inside the glyph): if (v == 0.0) { rgb = GlyphColor; alpha = 0.0; } // On a nonshadowed vector edge: else if (s == 1.0 && v != 1.0) { rgb = GlyphColor; alpha = v; }
// Totally inside the shadow: else if (s == 0.0 && v == 1.0) { rgb = ShadowColor; alpha = 0.0; }
// On a shadowed vector edge: else if (s == 0.0) { rgb = mix(GlyphColor, ShadowColor, v); alpha = 0.0; }
// On the shadow's outside edge: else { rgb = mix(GlyphColor, ShadowColor, v); alpha = s; } }
gl_FragColor = vec4(rgb, alpha); }
|
The shadow effect in Example 4 deserves further explanation. It
applies anti-aliasing to the transition not only between the vector and
the background but also between the shadow and the background and
between the vector and the shadow. The shader pulls this off by deciding
which of the following five regions the pixel falls into (see Figure 7):
Completely within the vector
On a vector edge that’s not
shadowed
Completely within the shadow
On a vector edge that’s shadowed
On the shadow’s outside edge