Chapter 17. Antialiasing Procedural Textures

Jaggies, popping, sparkling, stair steps, strobing, and marching ants. They’re all names used to describe the anathema of computer graphics—ALIASING. Anyone who has used a computer has seen it. For still images, it’s not always that noticeable or objectionable. But as soon as you put an object in motion, the movement of the jagged edges catches your eye and distracts you. From the early days of computer graphics, the fight to eliminate these nasty artifacts has been called ANTIALIASING.

This chapter does not contain a thorough description of the causes of aliasing, nor the methods used to combat it. But it does introduce the reasons the problem occurs and the facilities within the OpenGL Shading Language for antialiasing. Armed with this knowledge, you should be well on your way to fighting the jaggies in your own shaders.

Sources of Aliasing

The human eye is extremely good at noticing edges. This is how we comprehend shape and form and how we recognize letters and words. Our eye is naturally good at it, and we spend our whole lives practicing it, so naturally it is something we do very, very well.

A computer display is limited in its capability to present an image. The display is made up of a finite number of discrete elements called pixels. At a given time, each pixel can produce only one color. This makes it impossible for a computer display to accurately represent detail that is smaller than one pixel in screen space, such as an edge.

When you combine these two things, the human eye’s ability to discern edges and the computer graphics display’s limitations in replicating them, you have a problem, and this problem is known as aliasing. In a nutshell, aliasing occurs when we try to reproduce a signal with an insufficient sampling frequency. With a computer graphics display, we’ll always have a fixed number of samples (pixels) with which to reconstruct our image, and this will always be insufficient to provide adequate sampling, so we will always have aliasing. We can reduce it to the point that it’s not noticeable, or we can transform it into some other problem that is less objectionable, like blurriness or noise.

The problem is illustrated in Figure 17.1. In this diagram, we show the results of trying to draw a gray object. The intended shape is shown in Figure 17.1 (A). The computer graphics display limits us to a discrete sampling grid. If we choose only one location within each grid square (usually the center) and determine the color to be used by sampling the desired image at that point, we see some apparent artifacts. This is called POINT SAMPLING and is illustrated in Figure 17.1 (B). The result is ugly aliasing artifacts for edges that don’t line up naturally with the sampling grid (see Figure 17.1 (C)). (The drawing is idealized because pixels on a standard CRT do not produce light in the shape of a square, but the artifacts are obvious even when the sampled points are reconstructed as overlapping circles on the computer display.)

Aliasing artifacts caused by point sampling. The gray region represents the shape of the object to be rendered (A). The computer graphics display presents us with a limited sampling grid (B). The result of choosing to draw or not draw gray at each pixel results in jaggies, or aliasing artifacts (C).

Figure 17.1. Aliasing artifacts caused by point sampling. The gray region represents the shape of the object to be rendered (A). The computer graphics display presents us with a limited sampling grid (B). The result of choosing to draw or not draw gray at each pixel results in jaggies, or aliasing artifacts (C).

Aliasing takes on other forms as well. If you are developing a sequence of images for an animation and you don’t properly sample objects that are in motion, you might notice TEMPORAL ALIASING. This is caused by objects that are moving too rapidly for the sampling frequency being used. Objects may appear to stutter as they move or blink on and off. The classic example of temporal aliasing comes from the movies: A vehicle (car, truck, or covered wagon) in motion is going forward, but the spokes of its wheels appear to be rotating backwards. This effect is caused when the sampling rate (movie frames per second) is too low relative to the motion of the wheel spokes. In reality, the wheel may be rotating two- and three-quarter revolutions per frame, but on film it looks like it’s rotating one-quarter revolution backwards each frame.

To render images that look truly realistic rather than computer generated, we need to develop techniques for overcoming the inherent limitations of the graphics display.

Avoiding Aliasing

One way to achieve good results without aliasing is to avoid situations in which aliasing occurs.

For instance, if you know that a particular object will always be a certain size in the final rendered image, you can design a shader that looks good while rendering that object at that size. This is the assumption behind some of the shaders presented previously in this book. The smoothstep, mix, and clamp functions are handy functions to use to avoid sharp transitions and to make a procedural texture look good at a particular scale.

Aliasing is often a problem when you are rendering an object at different sizes. Mipmap textures address this very issue, and you can do something similar with shaders. If you know that a particular object must appear at different sizes in the final rendering, you can design a shader for each different size. Each of these shaders would provide an appropriate level of detail and avoid aliasing for an object of that size. For this to work, the application must determine the approximate size of the final rendered object before it is drawn and then install the appropriate shader. In addition, if a continuous zoom (in or out) is applied to a single object, some “popping” will occur when the level of detail changes.

You can avoid aliasing in some situations by using a texture instead of computing something procedurally. This lets you take advantage of the FILTERING (i.e., antialiasing) support that is built into the texture-mapping hardware. However, there are issues with using stored textures as opposed to doing things procedurally, as discussed in Chapter 11.

Increasing Resolution

The effects of aliasing can be reduced through a brute force method called SUPERSAMPLING that performs sampling at several locations within a pixel and averages the result of those samples. This is exactly the approach supported in today’s graphics hardware with the multisample buffer. This method of antialiasing replaces a single point sampling operation with several, so it doesn’t actually eliminate aliasing, but it can reduce aliasing to the point that it is no longer objectionable. You may be able to ignore the issue of aliasing if your shaders will always be used in conjunction with a multisample buffer.

But this approach does use up hardware resources (graphics board memory for storing the multisample buffer), and even with hardware acceleration, it still may be slower than performing the antialiasing as part of the procedural texture-generation algorithm. And, because this approach doesn’t eliminate aliasing, your texture is still apt to exhibit signs of aliasing, albeit at a higher frequency than before.

Supersampling is illustrated in Figure 17.2. Each of the pixels is rendered by sampling at four locations rather than at one. The average of the four samples is used as the value for the pixel. This averaging provides a better result, but it is not sufficient to eliminate aliasing because high-frequency components can still be misrepresented.

Supersampling with four samples per pixel yields a better result, but aliasing artifacts are still present. The shape of the object to be rendered is shown in (A). Sampling occurs at four locations within each pixel as shown in (B). The results are averaged to produce the final pixel value as shown in (C). Some samples that are almost half covered were sampled with just one supersample point instead of two, and one pixel contains image data that was missed entirely, even with supersampling.

Figure 17.2. Supersampling with four samples per pixel yields a better result, but aliasing artifacts are still present. The shape of the object to be rendered is shown in (A). Sampling occurs at four locations within each pixel as shown in (B). The results are averaged to produce the final pixel value as shown in (C). Some samples that are almost half covered were sampled with just one supersample point instead of two, and one pixel contains image data that was missed entirely, even with supersampling.

Supersampling can also be implemented within a fragment shader. The code that is used to produce the fragment color can be constructed as a function, and this function can be called several times from within the main function of the fragment shader to sample the function at several discrete locations. The returned values can be averaged to create the final value for the fragment. Results are improved if the sample positions are varied stochastically rather than spaced on a regular grid. Supersampling within a fragment shader has the obvious downside of requiring N times as much processing per fragment, where N is the number of samples computed at each fragment.

There will be times when aliasing is unavoidable and supersampling is infeasible. If you want to perform procedural texturing and you want a single shader that is useful at a variety of scales, there’s little choice but to address the aliasing issue and take steps to counteract aliasing in your shaders.

Antialiased Stripe Example

Aliasing does not occur until we attempt to represent a continuous image in screen space. This conversion occurs during rasterization; therefore, our attempts to mitigate its effects always occur in the fragment shader. The OpenGL Shading Language has several functions for this purpose that are available only to fragment shaders. To help explain the motivation for some of the language facilities for filter estimation, we develop a “worst case” scenario—alternating black and white stripes drawn on a sphere. Developing a fragment shader that performs antialiasing enables us to further illustrate the aliasing problem and the methods for reducing aliasing artifacts. Bert Freudenberg developed the first version of the GLSL shaders discussed in this section during the process of creating the antialiased hatching shader described in Section 18.1.

Generating Stripes

The antialiasing fragment shader determines whether each fragment is to be drawn as white or black to create lines on the surface of an object. The first step is to determine the method to be used for drawing lines. We use a single parameter as the basis for our stripe pattern. For illustration, let’s assume that the parameter is the s coordinate of the object’s texture coordinate. We have the vertex shader pass this value to us as a floating-point varying variable named V, eventually giving us a method for creating vertical stripes on a sphere. Figure 17.3 (A) shows the result of using the s texture coordinate directly as the intensity (grayscale) value on the surface of the sphere. The viewing position is slightly above the sphere, so we are looking down at the “north pole.” The s texture coordinate starts off at 0 (black) and increases to 1 (white) as it goes around the sphere. The edge where black meets white can be seen at the pole, and it runs down the back side of the sphere. The front side of the sphere looks mostly gray, but increases from left to right.

Using the s texture coordinate to create stripes on a sphere. In (A), the s texture coordinate is used directly as the intensity (gray) value. In (B), a modulus function creates a sawtooth function. In (C), the absolute value function turns the sawtooth function into a triangle function. (Courtesy of Bert Freudenberg, University of Magdeburg, 2002)

Figure 17.3. Using the s texture coordinate to create stripes on a sphere. In (A), the s texture coordinate is used directly as the intensity (gray) value. In (B), a modulus function creates a sawtooth function. In (C), the absolute value function turns the sawtooth function into a triangle function. (Courtesy of Bert Freudenberg, University of Magdeburg, 2002)

We create a sawtooth wave by multiplying the s texture coordinate by 16 and taking the fractional part (see Figure 17.3 (B)). This causes the intensity value to start at 0, rise quickly to 1, and then drop back down to 0. (To get a feel for what a sawtooth wave looks like, see the illustrations for the built-in functions fract (refer to Figure 5.6) and mod (refer to Figure 5.7)). This sequence is repeated 16 times. The OpenGL shader code to implement this is

float sawtooth = fract(V * 16.0);

This isn’t quite the stripe pattern we’re after. To get closer, we employ the absolute value function (see Figure 17.3 (C)). By multiplying the value of sawtooth by 2 and subtracting 1, we get a function that varies from [–1,1]. Taking the absolute value of this function results in a function that goes from 1 down to 0 and then back to 1 (i.e., a triangle wave). The line of code to do this is

float triangle = abs(2.0 * sawtooth - 1.0);

A stripe pattern is starting to appear, but either it’s too blurry or our glasses need adjustment. We make the stripes pure black and white by using the step function. When we compare our triangle variable to 0.5, this function returns 0 whenever triangle is less than or equal to 0.5, and 1 whenever triangle is greater than 0.5. This could be written as

float square = step(0.5, triangle);

This effectively produces a square wave, and the result is illustrated in Figure 17.4 (A). We can modify the relative size of the alternating stripes by adjusting the threshold value provided in the step function.

Antialiasing the stripe pattern. We can see that the square wave produced by the step function produces aliasing artifacts (A). The smoothstep function with a fixed-width filter produces too much blurring near the equator but not enough at the pole (B). An adaptive approach provides reasonable antialiasing in both regions (C). (Courtesy of Bert Freudenberg, University of Magdeburg, 2002)

Figure 17.4. Antialiasing the stripe pattern. We can see that the square wave produced by the step function produces aliasing artifacts (A). The smoothstep function with a fixed-width filter produces too much blurring near the equator but not enough at the pole (B). An adaptive approach provides reasonable antialiasing in both regions (C). (Courtesy of Bert Freudenberg, University of Magdeburg, 2002)

Analytic Prefiltering

In Figure 17.4 (A), we see that the stripes are now distinct, but aliasing has reared its ugly head. The step function returns values that are either 0 or 1, with nothing in between, so the jagged edges in the transitions between white and black are easy to spot. They will not go away if we increase the resolution of the image; they’ll just be smaller. The problem is caused by the fact that the step function introduced an immediate transition from white to black or an edge with infinite frequency (see Figure 5.11). There is no way to sample this transition at a high enough frequency to eliminate the aliasing artifacts. To get good results, we need to take steps within our shader to remove such high frequencies.

A variety of antialiasing techniques rely on eliminating extremely high frequencies before sampling. This is called LOW-PASS FILTERING because low frequencies are passed through unmodified, whereas high frequencies are eliminated. The visual effect of low-pass filtering is that the resulting image is blurred.

To eliminate the high frequencies from the stripe pattern, we use the smoothstep function. We know that this function produces a smooth transition between white and black. It requires that we specify two edges, and a smooth transition occurs between those two edges. Figure 17.4 (B) illustrates the result from the following line of code:

float square = smoothstep(0.4, 0.6, triangle);

Adaptive Analytic Prefiltering

Analytic prefiltering produces acceptable results in some regions of the sphere but not in others. The size of the smoothing filter (0.2) is defined in parameter space. But the parameter does not vary at a constant rate in screen space. In this case, the s texture coordinate varies quite rapidly in screen space near the poles and less rapidly at the equator. Our fixed-width filter produces blurring across several pixels at the equator and very little effect at the poles. What we need is a way to determine the size of the smoothing filter adaptively so that transition can be appropriate at all scales in screen space. This requires a measurement of how rapidly the function we’re interested in is changing at a particular position in screen space.

Fortunately, the OpenGL Shading Language provides a built-in function that can give us the rate of change (derivative) of any parameter in screen space. The function dFdx gives the rate of change in screen coordinates in the x direction, and dFdy gives the rate of change in the y direction. Because these functions deal with screen space, they are available only in a fragment shader. These two functions can provide the information needed to compute a GRADIENT VECTOR for the position of interest.

Given a function f(x,y), the gradient of f at the position (x, y) is defined as the vector

Adaptive Analytic Prefiltering

In English, the gradient vector comprises the partial derivative of function f with respect to x (i.e., the measure of how rapidly f is changing in the x direction) and the partial derivative of the function f with respect to y (i.e., the measure of how rapidly f is changing in the y direction). The important properties of the gradient vector are that it points in the direction of the maximum rate of increase of the function f(x,y) (the gradient direction) and that the magnitude of this vector equals the maximum rate of increase of f(x,y) in the gradient direction. (These properties are useful for image processing too, as we see later.) The built-in functions dFdx and dFdy give us exactly what we need to define the gradient vector for functions used in fragment shaders.

The magnitude of the gradient vector for the function f(x,y) is commonly called the GRADIENT of the function f(x,y). It is defined as

mag[G[f(x,y)]] = sqrt((∂f/∂x)2 + (∂f/∂x)2)

In practice, it is not always necessary to perform the (possibly costly) square root operation. The gradient can be approximated with absolute values:

mag[G[f(x,y)]] ≅ abs(f(x,y) - f(x + 1, y)) + abs(f(x,y) - f(x,y + 1))

This is exactly what is returned by the built-in function fwidth. The sum of the absolute values is an upper bound on the width of the sampling filter needed to eliminate aliasing. If it is too large, the resulting image looks somewhat more blurry than it should, but this is usually acceptable.

The two methods of computing the gradient are compared in Figure 17.5. As you can see, there is little visible difference. Because the value of the gradient was quite small for the function being evaluated on this object, the values were scaled so that they would be visible.

Visualizing the gradient. In (A), the magnitude of the gradient vector is used as the intensity (gray) value. In (B), the gradient is approximated with absolute values. (Actual gradient values are scaled for visualization.) (Courtesy of Bert Freudenberg, University of Magdeburg, 2002)

Figure 17.5. Visualizing the gradient. In (A), the magnitude of the gradient vector is used as the intensity (gray) value. In (B), the gradient is approximated with absolute values. (Actual gradient values are scaled for visualization.) (Courtesy of Bert Freudenberg, University of Magdeburg, 2002)

To compute the actual gradient for a varying variable V within a fragment shader, we use

float width = length(vec2(dFdx(V), dFdy(V)));

To approximate it, we use the potentially higher performance calculation:

float width = fwidth(V);

We then use the filter width within our call to smoothstep as follows:

float edge   = width * 32.0;
float square = smoothstep(0.5 - edge, 0.5 + edge, triangle);

If we put this all together in a fragment shader, we get Listing 17.1.

Example 17.1. Fragment shader for adaptive analytic antialiasing

varying float V;                    // generic varying
varying float LightIntensity;

uniform float Frequency;            // Stripe frequency = 6

void main()
{
    float sawtooth = fract(V * Frequency);
    float triangle = abs(2.0 * sawtooth - 1.0);
    float dp = length(vec2(dFdx(V), dFdy(V)));
    float edge = dp * Frequency * 2.0;
    float square = smoothstep(0.5 - edge, 0.5 + edge, triangle);
    gl_FragColor = vec4(vec3(square), 1.0);
}

If we scale the frequency of our texture, we must also increase the filter width accordingly. After the value of the function is computed, it is replicated across the red, green, and blue components of a vec3 and used as the color of the fragment. The results of this adaptive antialiasing approach are shown in Figure 17.4 (C). The results are much more consistent across the surface of the sphere. A simple lighting computation is added, and the resulting shader is applied to the teapot in Figure 17.6.

Effect of adaptive analytical antialiasing on striped teapots. On the left, the teapot is drawn with no antialiasing. On the right, the adaptive antialiasing shader is used. A small portion of the striped surface is magnified 200% to make it easier to see the difference.

Figure 17.6. Effect of adaptive analytical antialiasing on striped teapots. On the left, the teapot is drawn with no antialiasing. On the right, the adaptive antialiasing shader is used. A small portion of the striped surface is magnified 200% to make it easier to see the difference.

This approach to antialiasing works well until the filter width gets larger than the frequency. This is the situation that occurs at the north pole of the sphere. The stripes very close to the pole are much thinner than one pixel, so no step function will produce the correct gray value here. In such regions, you need to switch to integration or frequency clamping, both of which are discussed in subsequent sections.

Analytic Integration

The weighted average of a function over a specified interval is called a CONVOLUTION. The values that do the weighting are called the CONVOLUTION KERNEL or the CONVOLUTION FILTER. In some cases, we can reduce or eliminate aliasing by determining the convolution of a function ahead of time and then sampling the convolved function rather than the original function. The convolution can be performed over a fixed interval in a computation that is equivalent to convolving the input function with a box filter. A box filter is far from ideal, but it is simple and easy to compute and often good enough.

This method corresponds to the notion of antialiasing by AREA SAMPLING. It is different from point sampling or supersampling in that we attempt to calculate the area of the object being rendered relative to the sampling region. Referring to Figure 17.2, if we used an area sampling technique, we would get more accurate values for each of the pixels, and we wouldn’t miss that pixel that just had a sliver of coverage.

In Advanced RenderMan: Creating CGI for Motion Pictures, Apodaca and Gritz (1999) explain how to perform analytic antialiasing of a periodic step function, sometimes called a PULSE TRAIN. Darwyn Peachey described how to apply this method to his procedural brick RenderMan shader in Texturing and Modeling: A Procedural Approach, and Dave Baldwin published a GLSL version of this shader in the original paper on the OpenGL Shading Language. We use this technique to analytically antialias the procedural brick GLSL shader we described back in Chapter 6. Recall that the simple brick example used the step function to produce the periodic brick pattern. The function that creates the brick pattern in the horizontal direction is illustrated in Figure 17.7. From 0 to BrickPct.x (the brick width fraction), the function is 1.0. At the value of BrickPct.x, there is an edge with infinite slope as the function drops to 0. At the value 1, the function jumps back up to 1.0, and the process is repeated for the next brick.

The periodic step function, or pulse train, that defines the horizontal component of the procedural brick texture

Figure 17.7. The periodic step function, or pulse train, that defines the horizontal component of the procedural brick texture

The key to antialiasing this function is to compute its integral, or accumulated, value. We have to consider the possibility that, in areas of high complexity, the filter width that is computed by fwidth will cover several of these pulses. By sampling the integral rather than the function itself, we get a properly weighted average and avoid the high frequencies caused by point sampling that would produce aliasing artifacts.

So what is the integral of this function? It is illustrated in Figure 17.8. From 0 to BrickPct.x, the function value is 1, so the integral increases with a slope of 1. From BrickPct.x to 1.0, the function has a value of 0, so the integral stays constant in this region. At 1, the function jumps back to 1.0, so the integral increases until the function reaches BrickPct.x + 1. At this point, the integral changes to a slope of 0 again, and this pattern of ramps and plateaus continues.

Periodic step function (pulse train) and its integral

Figure 17.8. Periodic step function (pulse train) and its integral

We perform antialiasing by determining the value of the integral over the area of the filter, and we do that by evaluating the integral at the edges of the filter and subtracting the two values. The integral for this function consists of two parts: the sum of the area for all the pulses that have been fully completed before the edge we are considering and the area of the possibly partially completed pulse for the edge we are considering.

For our procedural brick shader, we use the variable position.x as the basis for generating the pulse function in the horizontal direction. So the number of fully completed pulses is just floor(position.x). Because the height of each pulse is 1.0, the area of each fully completed pulse is just BrickPct.x. Multiplying floor(position.x) by BrickPct.x gives the area for all the fully completed pulses. The edge that we’re considering may be in the part of the function that is equal to 0, or it may be in the part of the function that is equal to 1. We can find out by computing fract(position.x) – (1.0 – BrickPct.x). If the result of this subtraction is less than 0, we were in the part of the function that returns 0, so nothing more needs to be done. But if the value is greater than zero, we are partway into a region of the function that is equal to 1. Because the height of the pulse is 1, the area of this partial pulse is fract(position.x) – (1.0 – BrickPct.x). Therefore, the second part of our integral is the expression max(fract(position.x) – (1.0 – BrickPct.x), 0.0).

We use this integral for both the horizontal and vertical components of our procedural brick pattern. Because the application knows the brick width and height fractions (BrickPct.x and BrickPct.y), it can easily compute 1.0 – BrickPct.x and 1.0 – BrickPct.y and provide them to our fragment shader as well. This keeps us from unnecessarily computing these values several times for every fragment that is rendered. We call these values the mortar percentage. Because we evaluate this expression twice with different arguments, we define it as a macro or a function for convenience:

#define Integral(x, p, notp) ((floor(x)*(p)) + max(fract(x)-(notp), 0.0))

The parameter p indicates the value that is part of the pulse (i.e., when the function is 1.0), and notp indicates the value that is not part of the pulse (i.e., when the function is 0). Using this macro, we can write the code to compute the value of the integral over the width of the filter as follows:

vec2 fw, useBrick;

fw = fwidth(position);

useBrick = (Integral(position + fw, BrickPct, MortarPct) -
            Integral(position, BrickPct, MortarPct)) / fw;

The result is divided by the area of the filter (a box filter is assumed in this case) to obtain the average value for the function in the selected interval.

Antialiased Brick Fragment Shader

Now we can put all this to work to build better bricks. We replace the simple point sampling technique used in the example in Chapter 6 with analytic integration. The resulting shader is shown in Listing 17.2. The difference between the aliased and antialiased brick shaders is shown in Color Plate 35.

Example 17.2. Source code for an antialiased brick fragment shader

uniform vec3  BrickColor, MortarColor;
uniform vec2  BrickSize;
uniform vec2  BrickPct;
uniform vec2  MortarPct;

varying vec2  MCposition;
varying float LightIntensity;

#define Integral(x, p, notp) ((floor(x)*(p)) + max(fract(x)-(notp), 0.0))

void main()
{
    vec2 position, fw, useBrick;
    vec3 color;

    // Determine position within the brick pattern
    position = MCposition / BrickSize;

    // Adjust every other row by an offset of half a brick
    if (fract(position.y * 0.5) > 0.5)
        position.x += 0.5;

    // Calculate filter size
    fw = fwidth(position);

    // Perform filtering by integrating the 2D pulse made by the
    // brick pattern over the filter width and height
    useBrick = (Integral(position + fw, BrickPct, MortarPct) -
                Integral(position, BrickPct, MortarPct)) / fw;

    // Determine final color
    color  = mix(MortarColor, BrickColor, useBrick.x * useBrick.y);
    color *= LightIntensity;
    gl_FragColor = vec4(color, 1.0);
}

Frequency Clamping

Certain functions do not have an analytic solution, or they are just too difficult to solve. If this is the case, you might try a technique called frequency clamping. In this technique, the average value of the function replaces the actual value of the function when the filter width is too large. This is convenient for functions such as sine and noise whose average is known.

Antialiased Checkerboard Fragment Shader

The checkerboard pattern is the standard measure of the quality of an antialiasing technique (see Figure 17.9). Larry Gritz wrote a checkerboard RenderMan shader that performs antialiasing by frequency sampling, and Dave Baldwin translated this shader to GLSL. Listing 17.3 shows a fragment shader that produces a procedurally generated, antialiased checkerboard pattern. The vertex shader transforms the vertex position and passes along the texture coordinate, nothing more. The application provides values for the two colors of the checkerboard pattern, the average of these two colors (the application can compute this and provide it through a uniform variable, rather than having the fragment shader compute it for every fragment), and the frequency of the checkerboard pattern.

Checkerboard pattern rendered with the antialiased checkerboard shader. On the left, the filter width is set to 0, so aliasing occurs. On the right, the filter width is computed using the fwidth function.

Figure 17.9. Checkerboard pattern rendered with the antialiased checkerboard shader. On the left, the filter width is set to 0, so aliasing occurs. On the right, the filter width is computed using the fwidth function.

The fragment shader computes the appropriate size of the filter and uses it to perform smooth interpolation between adjoining checkerboard squares. If the filter is too wide (i.e., the varying parameter is changing too quickly for proper filtering), the average color is substituted. Even though this fragment shader uses a conditional statement, care is taken to avoid aliasing. In the transition zone between the if clause and the else clause, a smooth interpolation is performed between the computed color and the average color.

Example 17.3. Source code for an antialiased checkerboard fragment shader

uniform vec3  Color1;
uniform vec3  Color2;
uniform vec3  AvgColor;
uniform float Frequency;

varying vec2  TexCoord;

void main()
{
    vec3 color;

    // Determine the width of the projection of one pixel into s-t space
    vec2 fw = fwidth(TexCoord);

    // Determine the amount of fuzziness
    vec2 fuzz = fw * Frequency * 2.0;

    float fuzzMax = max(fuzz.s, fuzz.t);

    // Determine the position in the checkerboard pattern
    vec2 checkPos = fract(TexCoord * Frequency);

    if (fuzzMax < 0.5)
    {

        // If the filter width is small enough, compute the pattern color
        vec2 p = smoothstep(vec2(0.5), fuzz + vec2(0.5), checkPos) +
                (1.0 - smoothstep(vec2(0.0), fuzz, checkPos));

        color = mix(Color1, Color2, p.x * p.y + (1.0 - p.x) * (1.0 - p.y));

        // Fade in the average color when we get close to the limit
        color = mix(color, AvgColor, smoothstep(0.125, 0.5, FuzzMax));
    }
    else
    {
        // Otherwise, use only the average color
        color = AvgColor;
    }

    gl_FragColor = vec4(color, 1.0);
}

Summary

With increased freedom comes increased responsibility. The OpenGL Shading Language permits the computation of procedural textures without restriction. It is quite easy to write a shader that exhibits unsightly aliasing artifacts (using a conditional or a step function is all it takes), and it can be difficult to eliminate these artifacts. After describing the aliasing problem in general terms, this chapter explored several options for antialiasing procedural textures. Facilities in the language, such as the built-in functions for smooth interpolation (smoothstep), for determining derivatives in screen space (dFdx, dFdy), and for estimating filter width (fwidth) can assist in the fight against jaggies. These functions were fundamental components of shaders that were presented to perform antialiasing by prefiltering, adaptive prefiltering, integration, and frequency clamping.

Further Information

Most signal processing and image processing books contain a discussion of the concepts of sampling, reconstruction, and aliasing. Books by Glassner, Wolberg, and Gonzalez and Woods can be consulted for additional information on these topics. Technical memos by Alvy Ray Smith address the issues of aliasing in computer graphics directly.

The book Advanced RenderMan: Creating CGI for Motion Pictures by Tony Apodaca and Larry Gritz (1999) contains a chapter that describes shader antialiasing in terms of the RenderMan shading language, and much of the discussion is germane to the OpenGL Shading Language as well. Darwyn Peachey has a similar discussion in Texturing & Modeling: A Procedural Approach, Third Edition by David Ebert et al. (2002).

Bert Freudenberg developed an OpenGL shader to do adaptive antialiasing and presented this work at the SIGGRAPH 2002 in San Antonio, Texas. In this chapter, I’ve recreated the images Bert used in his talk, but he deserves the credit for originally developing the images and the shaders to illustrate some of the topics I’ve covered. This subject is also covered in his Ph.D. thesis, Real-Time Stroke-based Halftoning.

  1. 3Dlabs developer Web site. http://developer.3dlabs.com/

  2. Apodaca, Anthony A., and Larry Gritz, Advanced RenderMan: Creating CGI for Motion Pictures, Morgan Kaufmann Publishers, San Francisco, 1999. http://www.renderman.org/RMR/Books/arman/materials.html

  3. Baldwin, Dave, OpenGL 2.0 Shading Language White Paper, Version 1.0, 3Dlabs, October, 2001.

  4. Cook, Robert L., Stochastic Sampling in Computer Graphics, ACM Transactions on Graphics, vol. 5, no. 1, pp. 51–72, January 1986.

  5. Crow, Franklin C., The Aliasing Problem in Computer-Generated Shaded Images, Communications of the ACM, 20(11), pp. 799–805, November 1977.

  6. Crow, Franklin C., Summed-Area Tables for Texture Mapping, Computer Graphics (SIGGRAPH ’84 Proceedings), pp. 207–212, July 1984.

  7. Dippé, Mark A. Z., and Erling Henry Wold, Antialiasing Through Stochastic Sampling, Computer Graphics (SIGGRAPH ’85 Proceedings), pp. 69–78, July 1985.

  8. Ebert, David S., John Hart, Bill Mark, F. Kenton Musgrave, Darwyn Peachey, Ken Perlin, and Steven Worley, Texturing and Modeling: A Procedural Approach, Third Edition, Morgan Kaufmann Publishers, San Francisco, 2002. http://www.texturingandmodeling.com

  9. Freudenberg, Bert, A Non-Photorealistic Fragment Shader in OpenGL 2.0, Presented at the SIGGRAPH 2002 Exhibition in San Antonio, July 2002. http://isgwww.cs.uni-magdeburg.de/~bert/publications

  10. Freudenberg, Bert, Real-Time Stroke-based Halftoning, Ph.D. thesis, University of Magdeburg, submitted in 2003.

  11. Glassner, Andrew S., Principles of Digital Image Synthesis, Vol. 1, Morgan Kaufmann Publishers, San Francisco, 1995.

  12. Glassner, Andrew S., Principles of Digital Image Synthesis, Vol. 2, Morgan Kaufmann Publishers, San Francisco, 1995.

  13. Gonzalez, Rafael C., and Richard E. Woods, Digital Image Processing, Second Edition, Prentice Hall, Upper Saddle River, New Jersey, 2002.

  14. Gritz, Larry, LGAntialiasedChecks.sl, RenderMan Repository Web site. http://www.renderman.org/RMR/Shaders/LGShaders/index.html

  15. Smith, Alvy Ray, Digital Filtering Tutorial for Computer Graphics, Lucasfilm Technical Memo 27, revised March 1983. http://www.alvyray.com/memos/default.htm

  16. Smith, Alvy Ray, Digital Filtering Tutorial, Part II, Lucasfilm Technical Memo 27, revised March 1983. http://www.alvyray.com/memos/default.htm

  17. Smith, Alvy Ray, A Pixel Is Not a Little Square, a Pixel Is Not a Little Square, a Pixel Is Not a Little Square! (And a Voxel Is Not a Little Cube), Technical Memo 6, Microsoft Research, July 1995. http://www.alvyray.com/memos/default.htm

  18. Wolberg, George, Digital Image Warping, Wiley-IEEE Press, 2002.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.210.151.5