- Brick Shader Overview
- Vertex Shader
- Fragment Shader
- Observations
- Summary
- Further Information
6.3 Fragment Shader
The 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 in order 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 in a consistent way 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 we defined 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 (void) { vec3 color; vec2 position, useBrick;
In this shader, we'll 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 lines of code in our brick fragment shader will compute values for the local vec2 variable position:
position = MCposition / BrickSize;
This statement divides the fragment's x position in modeling coordinates by the column width and the y position in modeling coordinates by the 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'll use a conditional to determine whether the fragment is in a row of bricks that is offset:
if (fract(position.y * 0.5) > 0.5) position.x += 0.5;
The "brick row number" (position.y) is multiplied by 0.5, and the result is compared against 0.5. Half the time (or every other row) this comparison will be 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. Following this, we need to compute the fragment's location within the current brick:
position = fract(position);
This computation provides us with the vertical and horizontal position within a single brick. This will be used as the basis for determining whether to use the brick color or the mortar color.
Next 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'll 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 will cause the final answer to be 0; otherwise, it will be 1.0, and the brick color will be used.
The step function can be used to achieve the desired effect. It 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 or equal to 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 is used to produce a pattern of pulses (i.e., a square wave) where the function starts off at 0 and rises to 1 when the threshold is reached. We can get a function that starts off 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 these two lines, 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 will produce 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 will vary from [0,1). The variable BrickPct is a uniform variable, so its value will be constant across the primitive. This means that the value of useBrick.x will be 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 using position.y and BrickPct.y to compute the value for useBrick.y. By multiplying useBrick.x and useBrick.y together, we can get a value of 0 or 1.0 that will let 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.3.
Figure 6.3. 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 they 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. The built-in function mix is used 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 will choose the brick color only if both values are 1.0; otherwise, we will 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.
Listing 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(void) { 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 attributes, 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 will be shown in Section 7.11, after the OpenGL Shading Language API has been presented. The result of rendering some simple objects with these shaders is shown in Figure 6.4. A color version of the result is shown in Color Plate 25.
Figure 6.4. A flat polygon, a sphere, and a torus rendered with the brick shaders