4. Smoothing and Derivatives
The first distance field effect that I want
to cover is smoothing, as shown in the leftmost panel in Figure 4.
Go back and take another look at the big
stair steps in the left-most panel in Figure 1;
they correspond to the texels in the source image. Alpha testing with a
distance field fixed this up (rightmost panel), but it still exhibits
pixel-level aliasing. This is because the rasterized pixels are always
either fully lit or discarded; there are no shades of gray. We can fix
this up with a fragment shader.
Before diving into the shader code, let’s
take a look at GLSL’s smoothstep function. Here’s the
declaration:
float smoothstep(float edge0, float edge1, float x)
smoothstep returns 0.0 if
x is less than or equal to edge0
and returns 1.0 if x is greater than or equal to
edge1. If x is between these two
values, then it interpolates between 0 and 1. Calling
smoothstep is equivalent to the following:
float t = clamp ((x – edge0) / (edge1 – edge0), 0.0, 1.0);
return t * t * (3.0 – 2.0 * t);
To see how smoothstep
comes in handy for smoothing, visualize two new boundary lines in the
distance field: one at edge0 (a deflated version of
the glyph), the other at edge1 (an inflated version
of the glyph). See Figure 5; the middle
line is the region where distance = 0.
Alpha should be opaque at
edge0 and transparent at edge1. To
achieve smoothing, the fragment shader needs to create an alpha ramp
between these two boundaries. Example 2
shows an implementation.
Example 2. Naive fragment shader for distance field smoothing
varying mediump vec2 TextureCoord;
uniform sampler2D DistanceField; uniform mediump vec3 GlyphColor;
const mediump float SmoothCenter = 0.5; const mediump float SmoothWidth = 0.04;
void main(void) { mediump vec4 color = texture2D(DistanceField, TextureCoord); mediump float distance = color.a; mediump float alpha = smoothstep(SmoothCenter - SmoothWidth, SmoothCenter + SmoothWidth, distance); gl_FragColor = vec4(GlyphColor, alpha); }
|
The fragment shader in Example 6 is fairly easy to understand, but
unfortunately it suffers from a fundamental flaw. The value of
SmoothWidth is always the same, regardless of how
much the glyph is magnified. As a result, anti-aliasing is too blurry
when the camera is near the texture (Figure 7-10), and it’s ineffective when the camera is
very far away.
Fortunately, the iPhone supports a fragment
shader extension to help out with this. Unfortunately, it’s not
supported in the simulator at the time of this writing.
The name of this extension is
OES_standard_derivatives. That’s right,
“derivatives.” Don’t run in fear if this conjures up images of a brutal
calculus professor! It’s not as bad as it sounds. The extension simply
adds three new functions to GLSL:float dFdx(float f);
float dFdy(float f);
float fwidth(float f)
These functions are available only to the
fragment shader. They return a value proportional to the rate of change
of the argument when compared to neighboring pixels. The
dFdx function returns a rate of change along the
x-axis; the dFdy function returns a rate of change
along the y-axis. The fwidth function provides a
convenient way of combining the two values:
fwidth(f) = abs(dFdx(f)) + abs(dFdy(f))
In our case, when the camera is far away, the
rate of change in the on-screen distance field is greater than when the
camera is close-up. To achieve consistent anti-aliasing, we’ll simply
use a larger filter width when the camera is far away. See Example 3 for a new version of the fragment shader
that uses derivatives.
Example 3. Corrected fragment shader for distance field smoothing
#extension GL_OES_standard_derivatives : enable
varying mediump vec2 TextureCoord;
uniform sampler2D DistanceField;
uniform mediump vec3 GlyphColor;
const mediump float SmoothCenter = 0.5;
void main(void)
{
mediump vec4 color = texture2D(DistanceField, TextureCoord);
mediump float distance = color.a;
mediump float smoothWidth = fwidth(distance);
mediump float alpha = smoothstep(SmoothCenter - smoothWidth,
SmoothCenter + smoothWidth, distance);
gl_FragColor = vec4(GlyphColor, alpha);
}