- Brick Shader Overview
- Vertex Shader
- Fragment Shader
- Observations
- Summary
- Further Information
6.3 Fragment Shader
The typical purpose of a fragment shader is to compute the color to be applied to a fragment or to compute the depth value for the fragment or both. In this case (and indeed with most fragment shaders), we’re concerned only about the color of the fragment. We’re perfectly happy using the depth value that’s been computed by the OpenGL rasterization stage. Therefore, the entire purpose of this shader is to compute the color of the current fragment.
Our brick fragment shader starts off by defining a few more uniform variables than did the vertex shader. The brick pattern that will be rendered on our geometry is parameterized to make it easier to modify. The parameters that are constant across an entire primitive can be stored as uniform variables and initialized (and later modified) by the application. This makes it easy to expose these controls to the end user for modification through user interface elements such as sliders and color pickers. The brick fragment shader uses the parameters that are illustrated in Figure 6.1. These are defined as uniform variables as follows:
uniform vec3 BrickColor, MortarColor; uniform vec2 BrickSize; uniform vec2 BrickPct;
We want our brick pattern to be applied consistently to our geometry in order to have the object look the same no matter where it is placed in the scene or how it is rotated. The key to determining the placement of the brick pattern is the modeling coordinate position that is computed by the vertex shader and passed in the varying variable MCposition:
varying vec2 MCposition;
This variable was computed at each vertex by the vertex shader in the previous section, and it is interpolated across the primitive and made available to the fragment shader at each fragment location. Our fragment shader can use this information to determine where the fragment location is in relation to the algorithmically defined brick pattern. The other varying variable that is provided as input to the fragment shader is defined as follows:
varying float LightIntensity;
This varying variable contains the interpolated value for the light intensity that we computed at each vertex in our vertex shader. Note that both of the varying variables in our fragment shader are defined with the same type that was used to define them in our vertex shader. A link error would be generated if this were not the case.
With our uniform and varying variables defined, we can begin with the actual code for the brick fragment shader:
void main() { vec3 color; vec2 position, useBrick;
In this shader, we do things more like we would in C and define all our local variables before they’re used at the beginning of our main function. In some cases, this can make the code a little cleaner or easier to read, but it is mostly a matter of personal preference and coding style. The first actual line of code in our brick fragment shader computes values for the local vec2 variable position:
position = MCposition / BrickSize;
This statement divides the fragment’s x position in modeling coordinates by the brick column width and the y position in modeling coordinates by the brick row height. This gives us a “brick row number” (position.y) and a “brick number” within that row (position.x). Keep in mind that these are signed, floating-point values, so it is perfectly reasonable to have negative row and brick numbers as a result of this computation.
Next, we use a conditional to determine whether the fragment is in a row of bricks that is offset (see Figure 6.3):
if (fract(position.y * 0.5) > 0.5) position.x += 0.5;
Figure 6.3 A graph of the function fract(position.y * 0.5) shows how the even/odd row determination is made. The result of this function is compared against 0.5. If the value is greater than 0.5, a value of 0.5 is added to position.x; otherwise, nothing is added. The result is that rows whose integer values are 1, 3, 5, . . ., are shifted half a brick position to the right.
The “brick row number” (position.y) is multiplied by 0.5, the integer part is dropped by the fract function, and the result is compared to 0.5. Half the time (or every other row), this comparison is true, and the “brick number” value (position.x) is incremented by 0.5 to offset the entire row by half the width of a brick. This is illustrated by the graph in Figure 6.3.
Following this, we compute the fragment’s location within the current brick:
position = fract(position);
This computation gives us the vertical and horizontal position within a single brick. This position serves as the basis for determining whether to use the brick color or the mortar color.
Figure 6.4 shows how we might visualize the results of the fragment shader to this point. If we were to apply this shader to a square with modeling coordinates of (–1.0, –1.0) at the lower-left corner and (1.0, 1.0) at the upper right, our partially completed shader would show the beginnings of the brick pattern we’re after. Because the overall width of the square is 2.0 units in modeling coordinates, our division of MCposition.x by BrickSize.x gives us 2.0 / 0.3 or roughly six and two-thirds bricks across, as we see in Figure 6.4. Similarly the division of MCposition.y by BrickSize.y gives us 2.0 / 0.15 or roughly thirteen and two-thirds rows of bricks from top to bottom. For this illustration, we shaded each fragment by summing the fractional part of position.x and position.y, multiplying the result by 0.5, and then storing this value in the red, green, and blue components of gl_FragColor.
Figure 6.4 Intermediate results of brick fragment shader
To complete our brick shader, we need a function that gives us a value of 1.0 when the brick color should be used and 0 when the mortar color should be used. If we can achieve this, we can end up with a simple way to choose the appropriate color. We know that we’re working with a horizontal component of the brick texture function and a vertical component. If we can create the desired function for the horizontal component and the desired function for the vertical component, we can just multiply the two values together to get our final answer. If the result of either of the individual functions is 0 (mortar color), the multiplication causes the final answer to be 0; otherwise, it is 1.0, and the brick color is used.
We use the step function to achieve the desired effect. The step function takes two arguments, an edge (or threshold) and a parameter to test against that edge. If the value of the parameter to be tested is less than the edge value, the function returns 0; otherwise, it returns 1.0. (Refer to Figure 5.11 for a graph of this function). In typical use, the step function produces a pattern of pulses (i.e., a square wave) whereby the function starts at 0 and rises to 1.0 when the threshold is reached. We can get a function that starts at 1.0 and drops to 0 just by reversing the order of the two arguments provided to this function:
useBrick = step(position, BrickPct);
In this line of code, we compute two values that tell us whether we are in the brick or in the mortar in the horizontal direction (useBrick.x) and in the vertical direction (useBrick.y). The built-in function step produces a value of 0 when BrickPct.x < position.x and a value of 1.0 when BrickPct.x >= position.x. Because of the fract function, we know that position.x varies from (0,1). The variable BrickPct is a uniform variable, so its value is constant across the primitive. This means that the value of useBrick.x is 1.0 when the brick color should be used and 0 when the mortar color should be used as we move horizontally. The same thing is done in the vertical direction, with position.y and BrickPct.y computing the value for useBrick.y. By multiplying useBrick.x by useBrick.y, we can get a value of 0 or 1.0 that lets us select the appropriate color for the fragment. The periodic step function for the horizontal component of the brick pattern is illustrated in Figure 6.5.
Figure 6.5 The periodic step function that produces the horizontal component of the procedural brick pattern
The values of BrickPct.x and BrickPct.y can be computed by the application to give a uniform mortar width in both directions based on the ratio of column width to row height, or the values can be chosen arbitrarily to give a mortar appearance that looks right.
All that remains is to compute our final color value and store it in the special variable gl_FragColor:
color = mix(MortarColor, BrickColor, useBrick.x * useBrick.y); color *= LightIntensity; gl_FragColor = vec4(color, 1.0); }
Here we compute the color of the fragment and store it in the local variable color. We use the built-in function mix to choose the brick color or the mortar color, depending on the value of useBrick.x * useBrick.y. Because useBrick.x and useBrick.y can have values of only 0 (mortar) or 1.0 (brick), we choose the brick color only if both values are 1.0; otherwise, we choose the mortar color.
The resulting value is then multiplied by the light intensity, and that result is stored in the local variable color. This local variable is a vec3, and gl_FragColor is defined as a vec4, so we create our final color value by using a constructor to add a fourth component (alpha) equal to 1.0 and assign the result to the built-in variable gl_FragColor.
The source code for the complete fragment shader is shown in Listing 6.2.
Example 6.2. Source code for brick fragment shader
uniform vec3 BrickColor, MortarColor; uniform vec2 BrickSize; uniform vec2 BrickPct; varying vec2 MCposition; varying float LightIntensity; void main() { vec3 color; vec2 position, useBrick; position = MCposition / BrickSize; if (fract(position.y * 0.5) > 0.5) position.x += 0.5; position = fract(position); useBrick = step(position, BrickPct); color = mix(MortarColor, BrickColor, useBrick.x * useBrick.y); color *= LightIntensity; gl_FragColor = vec4(color, 1.0); }
When comparing this shader to the vertex shader in the previous example, we notice one of the key features of the OpenGL Shading Language, namely, that the language used to write these two shaders is almost identical. Both shaders have a main function, some uniform variables, and some local variables; expressions are the same; built-in functions are called in the same way; constructors are used in the same way; and so on. The only perceptible differences exhibited by these two shaders are (A) the vertex shader accesses built-in attribute variables, such as gl_Vertex and gl_Normal, (B) the vertex shader writes to the built-in variable gl_Position, whereas the fragment shader writes to the built-in variable gl_FragColor, and (C) the varying variables are written by the vertex shader and are read by the fragment shader.
The application code to create and use these shaders is shown in Section 7.13, after the OpenGL Shading Language API has been presented. The result of rendering some simple objects with these shaders is shown in Figure 6.6. A color version of the result is shown in Color Plate 35.
Figure 6.6 A flat polygon, a sphere, and a torus rendered with the brick shaders