by Mike Weiblen
DIFFRACTION is the effect of light bending around a sharp edge. A device called a DIFFRACTION GRATING leverages that effect to efficiently split white light into the rainbow of its constituent colors. Jos Stam described how to approximate this effect, first with assembly language shaders (in a SIGGRAPH '99 paper) and then with Cg (in an article in the book GPU Gems). Let's see how we can approximate the behavior of a diffraction grating with an OpenGL shader.
First, let's quickly review the wave theory of light and diffraction gratings. One way of describing the behavior of visible light is as waves of electromagnetic radiation. The distance between crests of those waves is called the wavelength, usually represented by the Greek letter lambda (l).
The wavelength is what determines the color we perceive when the light hits the sensing cells on the retina of the eye. The human eye is sensitive to the range of wavelengths beginning from about 400 nanometers (nm) for deep violet, up to about 700nm for dark red. Within that range are what humans perceive as all the colors of the rainbow.
A diffraction grating is a tool for separating light based on its wavelength, similar in effect to a prism but using diffraction rather than refraction. Diffraction gratings typically are very closely spaced parallel lines in an opaque or reflective material. They were originally made with a mechanical engine that precisely scribed parallel lines onto the surface of a mirror. Modern gratings are usually created with photographic processes.
The lines of a grating have a spacing roughly on the order of the wavelengths of visible light. Because of the difference in path length when white light is reflected from adjacent mirrored lines, the different wavelengths of reflected light interfere and reinforce or cancel, depending on whether the waves constructively or destructively interfere.
For a given wavelength, if the path length of light reflecting from two adjacent lines differs by an integer number of wavelengths (meaning that the crests of the waves reflected from each line coincide), that color of light constructively interferes and reinforces in intensity. If the path difference is an integer number of wavelengths plus half a wavelength (meaning that crests of waves from one line coincide with troughs from the other line), those waves destructively interfere and extinguish at that wavelength. That interference condition varies according to the wavelength of the light, the spacing of the grating lines, and the angle of the light's path (both incident and reflected) with respect to the grating surface. Because of that interference, white light breaks into its component colors as the light source and eyepoint move with respect to the diffracting surface.
Everyday examples of diffraction gratings include compact discs, novelty "holographic" gift-wrapping papers, and the rainbow logos on modern credit cards used to discourage counterfeiting.
While everyone is familiar with the overall physical appearance of a CD, let's look at the microscopic characteristics that make it a functional diffraction grating. A CD consists of one long spiral of microscopic pits embossed onto one side of a sheet of mirrored plastic. The dimensions of those pits is on the order of several hundred nanometers, or the same order of magnitude as the wavelengths of visible light. The track pitch of the spiral of pits (i.e., the spacing between each winding of the spiral) is nominally 1600 nanometers. The range of those dimensions, being so close to visible wavelengths, is what gives a CD its rainbow light-splitting qualities.
Our diffraction shader computes two independent output color components per vertex:
We can do this computation just by using a vertex shader. No fragment processing beyond typical OpenGL fixed functionality is necessary. Therefore, we write this vertex shader to take advantage of OpenGL's capability to combine programmable and fixed functionality processing. The vertex shader writes to the built-in varying variable gl_FrontColor and the special output variable gl_Position, and no programmable fragment processing is necessary.
The code for the diffraction vertex shader is shown in Listing 14.5. To render the diffraction effect, the shader requires the application to send a normal and tangent for each vertex. For this shader, the tangent is defined to be parallel to the orientation of the simulated grating lines. In the case of a compact disc (which has a spiral of pits on its mirrored surface), the close spacing of that spiral creates a diffraction grating of basically concentric circles, so the tangent is tangent to those circles.
Since the shader uses wavelength to compute the constructive interference, we need to convert wavelength to OpenGL's RGB representation of color. We use the function lambda2rgb, which approximates the conversion of wavelength to RGB by using a bump function. We begin the conversion by mapping the range of visible wavelengths to a normalized 0.0 to 1.0 range. From that normalized wavelength, we create a vec3 by subtracting an offset for each of the red/green/blue bands. Then for each color component, we compute the contribution with the bump expression 1 cx2 clamped to the range of [0, 1]. The c term controls the width of the bump and is selected for best appearance by allowing the bumps to overlap somewhat, approximating a relatively smooth rainbow spread. This bump function is quick and easy to implement, but we could use another approach to the wavelength-to-RGB conversion, for example, using the normalized wavelength to index into a lookup table or using a 1D texture, which would be tuned for enhanced spectral qualities.
More than one wavelength can satisfy the constructive interference condition at a vertex for a given set of lighting and viewing conditions, so the shader must accumulate the contribution from each of those wavelengths. Using the condition that constructive interference occurs at path differences of integer wavelength, the shader iterates over those integers to determine the reinforced wavelength. That wavelength is converted to an RGB value by the lambda2rgb function and accumulated in diffColor.
A specular glint of HighlightColor is reflected from the grating lines in the region where diffractive interference does not occur. The SurfaceRoughness term controls the width of that highlight to approximate the scattering of light from the microscopic pits.
The final steps of the shader consist of the typical vertex transformation to compute gl_Position and the summing of the lighting contributions to determine gl_FrontColor. The diffAtten term attenuates the diffraction color slightly to prevent the colors from being too intensely garish.
A simplification we made in this shader is this: Rather than attempt to represent the spectral composition of the HighlightColor light source, we assume the incident light source is a flat spectrum of white light.
Being solely a vertex shader, the coloring is computed only at vertices. Since diffraction gratings can produce dramatic changes in color for a small displacement, there is an opportunity for artifacts caused by insufficient tesselation. Depending on the choice of performance trade-offs, this shader could easily be ported to a fragment shader if per-pixel shading is preferred.
Results from the diffraction shader are shown in Figure 14.2 and Color Plate 17.
Figure 14.2. The diffraction shader simulates the look of a vinyl phonograph record (3Dlabs, Inc.)
Listing 14.5. Vertex shader for diffraction effect