Chapter 22. Vertex Shading: Do-It-Yourself Transform, Lighting, and Texgen

by Benjamin Lipchak

WHAT YOU'LL LEARN IN THIS CHAPTER:

  • How to perform per-vertex lighting

  • How to generate texture coordinates

  • How to calculate per-vertex fog

  • How to calculate per-vertex point size

  • How to squash and stretch objects

  • How to make realistic skin with vertex blending

This chapter is devoted to the application of vertex shaders. We covered the basic mechanics of high-level and low-level vertex shaders in the preceding two chapters, but at some point you have to put the textbook down and start learning by doing. Here, we introduce a handful of shaders that perform various real-world tasks. You are encouraged to use these shaders as a starting point for your own experimentation.

Getting Your Feet Wet

Every shader should at the very least output a clip-space position coordinate. Lighting and texture coordinate generation (texgen), the other operations typically performed in vertex shaders, may not be necessary. For example, if you're creating a depth texture and all you care about are the final depth values, you wouldn't waste instructions in your shader to output a color or texture coordinates. But one way or another, you always need to output a clip-space position for subsequent primitive assembly and rasterization to occur.

For your first sample shader, you'll perform the bare-bones vertex transformation that would occur automatically by fixed functionality if you weren't using a vertex shader. As an added bonus, you'll copy the incoming color into the outgoing color. Remember, anything that isn't output remains undefined. If you want that color to be available later in the pipeline, you have to copy it from input to output, even if the vertex shader doesn't need to change it in any way.

For each example shader, we'll provide both a high-level and a low-level version that perform equivalent operations. This way, you can learn both shader languages by comparison. Also, if only one of the two extensions is available on your OpenGL implementation, you won't miss out completely. Figure 22.1 shows the result of the simple shaders in Listings 22.1 and 22.2.

Example 22.1. Simple High-Level Vertex Shader

// simple.vs
//
// Generic vertex transformation,
// copy primary color

void main(void)
{
    // multiply object-space position by MVP matrix
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    // Copy the primary color
    gl_FrontColor = gl_Color;
}
This vertex shader transforms the position to clip space and copies the vertex's color from input to output.

Figure 22.1. This vertex shader transforms the position to clip space and copies the vertex's color from input to output.

Example 22.2. Simple Low-Level Vertex Shader

!!ARBvp1.0

# simple.vp
#
# Generic vertex transformation,
# copy primary color

ATTRIB iPos = vertex.position;       # input position
ATTRIB iPrC = vertex.color.primary;  # input primary color

OUTPUT oPos = result.position;       # output position
OUTPUT oPrC = result.color.primary;  # output primary color

PARAM mvp[4] = { state.matrix.mvp }; # modelview * projection matrix

TEMP clip;                           # temporary register

DP4 clip.x, iPos, mvp[0];            # multiply input position by MVP
DP4 clip.y, iPos, mvp[1];
DP4 clip.z, iPos, mvp[2];
DP4 clip.w, iPos, mvp[3];

MOV oPos, clip;                      # output clip-space coord

MOV oPrC, iPrC;                      # copy primary color in to out

END

Notice how much more compact and readable the high-level GLSL shader is versus the low-level GL_ARB_vertex_program shader. When explaining the shaders, we'll make reference to the GLSL version. You can compare the two to see how the high-level expressions are decomposed into their low-level counterparts. For example, the multiplication of the object-space vertex position (gl_Vertex) by the concatenation of the modelview and projection matrices (gl_ModelViewProjection) to get the clip-space vertex position (gl_Position) is implemented in the low-level shader by a series of four dot product (DP4) instructions.

Diffuse Lighting

Diffuse lighting takes into account the orientation of a surface relative to the direction of incoming light. The following is the equation for diffuse lighting:

  • Cdiff = max{N • L, 0} * Cmat * Cli

N is the vertex's unit normal, and L is the unit vector representing the direction from the vertex to the light source. Cmat is the color of the surface material, and Cli is the color of the light. Cdiff is the resulting diffuse color. Because the light in the example is white, you can omit that term, as it would be the same as multiplying by { 1,1,1,1} . Figure 22.2 shows the result from Listings 22.3 and 22.4, which implement the diffuse lighting equation.

Example 22.3. Diffuse Lighting High-Level Vertex Shader

// diffuse.vs
//
// Generic vertex transformation,
// diffuse lighting based on one
// white light

uniform vec3 lightPos0;

void main(void)
{
    // normal MVP transform
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    vec3 N = normalize(gl_NormalMatrix * gl_Normal);
    vec4 V = gl_ModelViewMatrix * gl_Vertex;
    vec3 L = normalize(lightPos0 - V.xyz);

    // output the diffuse color
    float NdotL = dot(N, L);
    gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));
}
This vertex shader computes diffuse lighting.

Figure 22.2. This vertex shader computes diffuse lighting.

Example 22.4. Diffuse Lighting Low-Level Vertex Shader

!!ARBvp1.0

# diffuse.vp
#
# Generic vertex transformation,
# diffuse lighting based on one
# white light

ATTRIB iPos = vertex.position;             # input position
ATTRIB iPrC = vertex.color.primary;        # input primary color
ATTRIB iNrm = vertex.normal;               # input normal

OUTPUT oPos = result.position;             # output position
OUTPUT oPrC = result.color.primary;        # output primary color

PARAM mvp[4] = { state.matrix.mvp };       # modelview * proj matrix
PARAM mv[4] =  { state.matrix.modelview }; # modelview matrix
# inverse transpose of modelview matrix:
PARAM mvIT[4] = { state.matrix.modelview.invtrans };

PARAM lightPos = program.local[0];         # light pos in eye space

TEMP N, V, L, NdotL;                       # temporary registers

DP4 oPos.x, iPos, mvp[0];                  # xform input pos by MVP
DP4 oPos.y, iPos, mvp[1];
DP4 oPos.z, iPos, mvp[2];
DP4 oPos.w, iPos, mvp[3];

DP4 V.x, iPos, mv[0];                      # xform input pos by MV
DP4 V.y, iPos, mv[1];
DP4 V.z, iPos, mv[2];
DP4 V.w, iPos, mv[3];

SUB L, lightPos, V;                        # vertex to light vector

DP3 N.x, iNrm, mvIT[0];                    # xform norm to eye space
DP3 N.y, iNrm, mvIT[1];
DP3 N.z, iNrm, mvIT[2];

DP3 N.w, N, N;                             # normalize normal
RSQ N.w, N.w;
MUL N, N, N.w;

DP3 L.w, L, L;                             # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

DP3 NdotL, N, L;                           # N . L
MAX NdotL, NdotL, 0.0;

MUL oPrC, iPrC, NdotL;                     # diffuse color

END

After computing the clip-space position as you did in the “simple” shader, the “diffuse” shader proceeds to transform the vertex position to eye space, too. All the lighting calculations are performed in eye space, so you need to transform the normal vector from object space to eye space as well. GLSL provides the gl_NormalMatrix built-in uniform matrix as a convenience for this purpose. It is simply the inverse transpose of the modelview matrix's upper-left 3×3 elements. The last vector you need to compute is the light vector, which is the direction from the vertex position to the light position, so you just subtract one from the other.

Both the normal and the light vectors must be unit vectors, so you normalize them before continuing. GLSL supplies a built-in function to perform this common task, but in the low-level shader, you have to do the normalization manually. Turning an arbitrary vector into a unit vector is reasonably simple. You have to scale the vector components by the inverse of the vector's length. A dot product operation (DP3) of the vector against itself returns the vector's length squared. A reciprocal square root operation (RSQ) turns that length squared into the inverse length scale factor. Then you just multiply (MUL) that scale factor by the original vector components to yield the unit vector.

The dot product of the two unit vectors, N and L, will be in the range [–1,1]. But because you're interested in the amount of diffuse lighting bouncing off the surface, having a negative contribution doesn't make sense. This is why you clamp the result of the dot product to the range [0,1] by using the max (GLSL) or MAX (low-level) operations. The diffuse lighting contribution can then be multiplied by the vertex's diffuse material color to obtain the final lit color.

Specular Lighting

Specular lighting also takes into account the orientation of a surface relative to the direction of incoming light. The following is the equation for specular lighting:

  • Cspec = max{N • H, 0} ^Sexp * Cmat * Cli

H is the unit vector representing the direction halfway between the light vector and the view vector, known as the half-angle vector. Sexp is the specular exponent, controlling the tightness of the specular highlight. Cspec is the resulting specular color. N, Cmat, and Cli are the same as for diffuse lighting. Because the light in the example is white, you can again omit that term. Figure 22.3 illustrates the output of Listings 22.5 and 22.6, which implement both diffuse and specular lighting equations.

Example 22.5. Diffuse and Specular Lighting High-Level Vertex Shader

// specular.vs
//
// Generic vertex transformation,
// diffuse and specular lighting
// based on one white light

uniform vec3 lightPos0;

void main(void)
{
    // normal MVP transform
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    vec3 N = normalize(gl_NormalMatrix * gl_Normal);
    vec4 V = gl_ModelViewMatrix * gl_Vertex;
    vec3 L = normalize(lightPos0 - V.xyz);
    vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
    const float specularExp = 128.0;

    // calculate diffuse lighting
    float NdotL = dot(N, L);
    vec4 diffuse = gl_Color * vec4(max(0.0, NdotL));

    // calculate specular lighting
    float NdotH = dot(N, H);
    vec4 specular = vec4(pow(max(0.0, NdotH), specularExp));

    // sum the diffuse and specular components
    gl_FrontColor = diffuse + specular;
}
This vertex shader computes diffuse and specular lighting.

Figure 22.3. This vertex shader computes diffuse and specular lighting.

Example 22.6. Diffuse and Specular Lighting Low-Level Vertex Shader

!!ARBvp1.0

# specular.vp
#
# Generic vertex transformation,
# diffuse and specular lighting
# based on one white light

ATTRIB iPos = vertex.position;             # input position
ATTRIB iPrC = vertex.color.primary;        # input primary color
ATTRIB iNrm = vertex.normal;               # input normal

OUTPUT oPos = result.position;             # output position
OUTPUT oPrC = result.color.primary;        # output primary color

PARAM mvp[4] = { state.matrix.mvp };       # modelview * proj matrix
PARAM mv[4] =  { state.matrix.modelview }; # modelview matrix
# inverse transpose of modelview matrix:
PARAM mvIT[4] = { state.matrix.modelview.invtrans };

PARAM lightPos = program.local[0];         # light pos in eye space

TEMP N, V, L, H, NdotL, NdotH;             # temporary registers
TEMP diffuse, specular;

DP4 oPos.x, iPos, mvp[0];                  # xform input pos by MVP
DP4 oPos.y, iPos, mvp[1];
DP4 oPos.z, iPos, mvp[2];
DP4 oPos.w, iPos, mvp[3];

DP4 V.x, iPos, mv[0];                      # xform input pos by MV
DP4 V.y, iPos, mv[1];
DP4 V.z, iPos, mv[2];
DP4 V.w, iPos, mv[3];

SUB L, lightPos, V;                        # vertex to light vector

DP3 N.x, iNrm, mvIT[0];                    # xform norm to eye space
DP3 N.y, iNrm, mvIT[1];
DP3 N.z, iNrm, mvIT[2];

DP3 N.w, N, N;                             # normalize normal
RSQ N.w, N.w;
MUL N, N, N.w;

DP3 L.w, L, L;                             # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                             # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                           # N . L
MAX NdotL, NdotL, 0.0;
MUL diffuse, iPrC, NdotL;

DP3 NdotH, N, H;                           # N • H
MAX NdotH, NdotH, 0.0;
POW specular, NdotH.x, 128.0.x;            # 128 is specular exponent

ADD oPrC, diffuse, specular;               # sum the colors

END

The light position is a constant vector passed into the shader from the application. This allows you to easily change the light position interactively without having to alter the shader. You can do this using the left- and right-arrow keys while running the VertexShaders example.

We used a hard-coded constant specular exponent of 128, which provides a nice, tight specular highlight. You can experiment with different values to find one you may prefer. Notice in the low-level shader how we supply the exponent as 128.0.x. We supplied it this way because a scalar value is expected, and the low-level grammar is not smart enough to realize that we're providing a scalar constant, so we have to provide the redundant suffix anyway.

Improved Specular Lighting

Specular highlights change rapidly over the surface of an object. Trying to compute them per-vertex and then interpolating the result across a triangle gives relatively poor results. Instead of a nice circular highlight, you end up with a muddy polygonal-shaped highlight.

One way you can improve the situation is to separate the diffuse lighting result from the specular lighting result, outputting one as the vertex's primary color and the other as the secondary color. By adding the diffuse and specular colors together, you effectively saturate the color (that is, exceed a value of 1.0) wherever a specular highlight appears. If you try to interpolate the sum of these colors, the saturation will more broadly affect the entire triangle. However, if you interpolate the two colors separately and then sum them per fragment, the saturation will occur only where desired, cleaning up some of the muddiness. This sum per fragment is achieved by simply enabling GL_COLOR_SUM. Here is the altered GLSL code for separating the two lit colors:

// put diffuse into primary color
float NdotL = dot(N, L);
gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));

// put specular into secondary color
float NdotH = dot(N, H);
gl_FrontSecondaryColor = vec4(pow(max(0.0, NdotH), specularExp));

The altered low-level vertex shader code is as follows:

OUTPUT oPrC = result.color.primary;        # output primary color
OUTPUT oScC = result.color.secondary;      # output secondary color

...

DP3 NdotL, N, L;                           # N . L
MAX NdotL, NdotL, 0.0;
MUL oPrC, iPrC, NdotL;                     # diffuse goes in primary

DP3 NdotH, N, H;                           # N • H
MAX NdotH, NdotH, 0.0;
POW oScC, NdotH.x, 128.0.x;                # spec goes in secondary

Separating the colors improves things a bit, but the root of the problem is the specular exponent. By raising the specular coefficient to a power, you have a value that wants to change much more rapidly than per-vertex interpolation allows. In fact, if your geometry is not tessellated finely enough, you may lose a specular highlight altogether.

An effective way to avoid this problem is to output just the specular coefficient (N • H), but wait and raise it to a power per fragment. This way, you can safely interpolate the more slowly changing (N • H). You're not employing fragment shaders yet, so how do you perform this power computation per fragment? All you have to do is set up a 1D texture with a table of s128 values and send (N • H) out of the vertex shader on a texture coordinate. This is considered custom texgen. Then you will use fixed functionality texture environment to add the specular color from the texture lookup to the interpolated diffuse color from the vertex shader.

The following is the GLSL code, again altered from the original specular lighting shader:

// put diffuse lighting result in primary color
float NdotL = dot(N, L);
gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));

// copy (N.H)*8-7 into texcoord
float NdotH = max(0.0, (dot(N, H) * 8.0) - 7.0);
gl_TexCoord[0] = vec4(NdotH, 0.0, 0.0, 1.0);

The altered low-level shader code is as follows:

OUTPUT oPrC = result.color.primary;        # output primary color
OUTPUT oTxC = result.texcoord[0];          # output texcoord 0

...

DP3 NdotL, N, L;                           # N . L
MAX NdotL, NdotL, 0.0;
MUL oPrC, iPrC, NdotL;                     # output diffuse

DP3 NdotH, N, H;                           # N • H
MAD NdotH.x, NdotH, 8.0, {-7.0};           # (N • H) * 8 - 7
MOV oTxC, {0.0, 0.0, 0.0, 1.0};            # init other components
MAX oTxC.x, NdotH, 0.0;                    # toss into texcoord 0

Here, the (N • H) has been clamped to the range [0,1]. But if you try raising most of that range to the power of 128, you'll get results so close to zero that they will correspond to texel values of zero. Only the upper 1/8 of (N • H) values will begin mapping to measurable texel values. To make economical use of the 1D texture, you can focus in on this upper 1/8 and fill the entire texture with values from this range, improving the resulting precision. This requires that you scale (N • H) by 8 and bias by –7 so that [0,1] maps to [–7,1]. By using the GL_CLAMP_TO_EDGE wrap mode, values in the range [–7,0] will be clamped to 0. Values in the range of interest, [0,1], will receive texel values between (7/8)128 and 1.

The specular contribution resulting from the texture lookup is added to the diffuse color output from the vertex shader using the GL_ADD texture environment function.

Figure 22.4 compares the three specular shaders to show the differences in quality. An even more precise method would be to output only the normal vector from the vertex shader and to encode a cube map texture so that at every N coordinate the resulting texel value is (N • H)128. We've left this as an exercise for you.

The per-vertex specular highlight is improved by using separate specular or a specular exponent texture.

Figure 22.4. The per-vertex specular highlight is improved by using separate specular or a specular exponent texture.

Now that you have a decent specular highlight, you can get a little fancier and take the one white light and replicate it into three colored lights. This activity involves performing the same computations, except now you have three different light positions and you have to take the light color into consideration.

As has been the case with all the lighting shaders, you can change the light positions in the sample by using the left- and right-arrow keys. Figure 22.5 shows the three lights in action, produced by Listings 22.7 and 22.8.

Example 22.7. Three Colored Lights High-Level Vertex Shader

// 3lights.vs
//
// Generic vertex transformation,
// 3 colored lights

uniform vec3 lightPos0;
uniform vec3 lightPos1;
uniform vec3 lightPos2;

varying vec4 gl_TexCoord[4];

void main(void)
{
    // normal MVP transform
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    vec3 N = normalize(gl_NormalMatrix * gl_Normal);
    vec4 V = gl_ModelViewMatrix * gl_Vertex;

    // Light colors
    vec4 lightCol[3];
    lightCol[0] = vec4(1.0, 0.25, 0.25, 1.0);
    lightCol[1] = vec4(0.25, 1.0, 0.25, 1.0);
    lightCol[2] = vec4(0.25, 0.25, 1.0, 1.0);

    // Light vectors
    vec3 L[3], H[3];
    L[0] = normalize(lightPos0 - V.xyz);
    L[1] = normalize(lightPos1 - V.xyz);
    L[2] = normalize(lightPos2 - V.xyz);

    gl_FrontColor = vec4(0.0);

    for (int i = 0; i < 3; i++)
    {
        // Half-angles
        H[i] = normalize(L[i] + vec3(0.0, 0.0, 1.0));

        // Accumulate the diffuse contributions
        gl_FrontColor += gl_Color * lightCol[i] * 
                         vec4(max(0.0, dot(N, L[i])));

        // Put N.H specular coefficients into texcoords
        gl_TexCoord[1+i] = vec4(max(0.0, dot(N, H[i]) * 8.0 - 7.0), 
                                0.0, 0.0, 1.0);
    }
}
Three lights are better than one, though it's hard to tell in black and white.

Figure 22.5. Three lights are better than one, though it's hard to tell in black and white.

Example 22.8. Three Colored Lights Low-Level Vertex Shader

!!ARBvp1.0

# 3lights.vp
#
# Generic vertex transformation,
# 3 colored lights

ATTRIB iPos = vertex.position;              # input position
ATTRIB iPrC = vertex.color.primary;         # input primary color
ATTRIB iNrm = vertex.normal;                # input normal

OUTPUT oPos = result.position;              # output position
OUTPUT oPrC = result.color.primary;         # output primary color
OUTPUT oTC0 = result.texcoord[1];           # output texcoord 1
OUTPUT oTC1 = result.texcoord[2];           # output texcoord 2
OUTPUT oTC2 = result.texcoord[3];           # output texcoord 3

PARAM mvp[4] = { state.matrix.mvp };        # modelview * proj mat
PARAM mv[4] =  { state.matrix.modelview };  # modelview matrix
# inverse transpose of modelview matrix:
PARAM mvIT[4] = { state.matrix.modelview.invtrans };

PARAM lightCol0 = { 1.0, 0.25, 0.25, 1.0 }; # light 0 color
PARAM lightCol1 = { 0.25, 1.0, 0.25, 1.0 }; # light 1 color
PARAM lightCol2 = { 0.25, 0.25, 1.0, 1.0 }; # light 2 color
PARAM lightPos0 = program.local[0];         # light pos 0 eye space
PARAM lightPos1 = program.local[1];         # light pos 1 eye space
PARAM lightPos2 = program.local[2];         # light pos 2 eye space

TEMP N, V, L, H, NdotL, NdotH, finalColor;  # temporary registers
ALIAS diffuse = NdotL;
ALIAS specular = NdotH;

DP4 oPos.x, iPos, mvp[0];                   # xform input pos by MVP
DP4 oPos.y, iPos, mvp[1];
DP4 oPos.z, iPos, mvp[2];
DP4 oPos.w, iPos, mvp[3];

DP4 V.x, iPos, mv[0];                       # xform input pos by MV
DP4 V.y, iPos, mv[1];
DP4 V.z, iPos, mv[2];
DP4 V.w, iPos, mv[3];

DP3 N.x, iNrm, mvIT[0];                     # xform norm to eye space
DP3 N.y, iNrm, mvIT[1];
DP3 N.z, iNrm, mvIT[2];

DP3 N.w, N, N;                              # normalize normal
RSQ N.w, N.w;
MUL N, N, N.w;

# LIGHT 0
SUB L, lightPos0, V;                        # vertex to light vector

DP3 L.w, L, L;                              # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                              # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                            # N . L0
MAX NdotL, NdotL, 0.0;
MUL diffuse, iPrC, NdotL;                   # priCol * N.L0

# priCol * lightCol0 * N.L0
MUL finalColor, diffuse, lightCol0;

DP3 NdotH, N, H;                            # N • H0
MAX NdotH, NdotH, 0.0;
MOV oTC0, {0.0, 0.0, 0.0, 1.0};
MAD oTC0.x, NdotH, 8, {-7};                 # NdotH * 8 - 7 to tc 0

# LIGHT 1
SUB L, lightPos1, V;                        # vertex to light vector

DP3 L.w, L, L;                              # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                              # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                            # N . L1
MAX NdotL, NdotL, 0.0;
MUL diffuse, iPrC, NdotL;                   # priCol * N.L1

# priCol * lightCol0 * N.L1
MAD finalColor, diffuse, lightCol1, finalColor;

DP3 NdotH, N, H;                            # N • H1
MAX NdotH, NdotH, 0.0;
MOV oTC1, {0.0, 0.0, 0.0, 1.0};
MAD oTC1.x, NdotH, 8, {-7};                 # NdotH * 8 - 7 to tc 1

# LIGHT 2
SUB L, lightPos2, V;                        # vertex to light vector

DP3 L.w, L, L;                              # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                              # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                            # N . L2
MAX NdotL, NdotL, 0.0;
MUL diffuse, iPrC, NdotL;                   # priCol * N.L2

# priCol * lightCol0 * N.L2
MAD oPrC, diffuse, lightCol2, finalColor;

DP3 NdotH, N, H;                            # N • H2
MAX NdotH, NdotH, 0.0;
MOV oTC2, {0.0, 0.0, 0.0, 1.0};
MAD oTC2.x, NdotH, 8, {-7};                 # NdotH * 8 - 7 to tc 2

END

Interesting to note in this sample is the use of a loop in the GLSL version. Loops are not available in low-level shaders, so we've “unrolled” the loops into a linear sequence; consequently, the code that would be in the loop is replicated three times, once for each light. Even though GLSL permits them, some OpenGL implementations may not support loops in hardware anyway. So if your shader is running really slow, it may be emulating your shader execution in software. Unrolling the loop in your GLSL shader could alleviate the problem, but at the expense of making your code less readable.

Per-Vertex Fog

Though fog is specified as a per-fragment rasterization stage that follows texturing, often implementations perform most of the necessary computation per-vertex and then interpolate the results across the primitive. This shortcut is sanctioned by the OpenGL specification because it improves performance with very little loss of image fidelity. The following is the equation for a second-order exponential fog factor, which controls the blending between the fog color and the unfogged fragment color:

  • ff = e^(–(d*fc)2)

In this equation, ff is the computed fog factor. d is the density constant that controls the “thickness” of the fog. fc is the fog coordinate, which is usually the distance from the vertex to the eye, or is approximated by the absolute value of the vertex position's Z component in eye space. In this chapter's sample shaders, you'll compute the actual distance, not an approximation.

In the first sample fog shader, you'll compute only the fog coordinate and leave it to fixed functionality to compute the fog factor and perform the blend. In the second sample, you'll compute the fog factor yourself within the vertex shader and also perform the blending per-vertex. Performing all these operations per-vertex instead of per-fragment is more efficient and provides acceptable results for most uses. Figure 22.6 illustrates the fogged scene, which is nearly identical for the two sample fog shaders in Listings 22.9 and 22.10.

Example 22.9. Fog Coordinate Generating High-Level Vertex Shader

// fogcoord.vs
//
// Generic vertex transformation,
// diffuse and specular lighting,
// per-vertex fogcoord

uniform vec3 lightPos0;

void main(void)
{
    // normal MVP transform
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    vec3 N = normalize(gl_NormalMatrix * gl_Normal);
    vec4 V = gl_ModelViewMatrix * gl_Vertex;
    vec3 L = normalize(lightPos0 - V.xyz);
    vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
    const float specularExp = 128.0;

    // calculate diffuse lighting
    float NdotL = dot(N, L);
    vec4 diffuse = gl_Color * vec4(max(0.0, NdotL));

    // calculate specular lighting
    float NdotH = dot(N, H);
    vec4 specular = vec4(pow(max(0.0, NdotH), specularExp));

    // calculate fog coordinate: distance from eye
    gl_FogFragCoord = length(V);

    // sum the diffuse and specular components
    gl_FrontColor = diffuse + specular;
}
Applying per-vertex fog using a vertex shader.

Figure 22.6. Applying per-vertex fog using a vertex shader.

Example 22.10. Fog Coordinate Generating Low-Level Vertex Shader

!!ARBvp1.0

# fogcoord.vs
#
# Generic vertex transformation,
# diffuse and specular lighting,
# per-vertex fogcoord

ATTRIB iPos = vertex.position;             # input position
ATTRIB iPrC = vertex.color.primary;        # input primary color
ATTRIB iNrm = vertex.normal;               # input normal

OUTPUT oPos = result.position;             # output position
OUTPUT oPrC = result.color.primary;        # output primary color
OUTPUT oFgC = result.fogcoord;             # output fog coordinate

PARAM mvp[4] = { state.matrix.mvp };       # modelview * proj matrix
PARAM mv[4] =  { state.matrix.modelview }; # modelview matrix
# inverse transpose of modelview matrix:
PARAM mvIT[4] = { state.matrix.modelview.invtrans };

PARAM lightPos = program.local[0];         # light pos in eye space
PARAM density = program.local[1];          # fog density

TEMP N, V, L, H, NdotL, NdotH;             # temporary registers
TEMP diffuse, specular, fogCoord;

DP4 oPos.x, iPos, mvp[0];                  # xform input pos by MVP
DP4 oPos.y, iPos, mvp[1];
DP4 oPos.z, iPos, mvp[2];
DP4 oPos.w, iPos, mvp[3];

DP4 V.x, iPos, mv[0];                      # xform input pos by MV
DP4 V.y, iPos, mv[1];
DP4 V.z, iPos, mv[2];
DP4 V.w, iPos, mv[3];

SUB L, lightPos, V;                        # vertex to light vector

DP3 N.x, iNrm, mvIT[0];                    # xform norm to eye space
DP3 N.y, iNrm, mvIT[1];
DP3 N.z, iNrm, mvIT[2];

DP3 N.w, N, N;                             # normalize normal
RSQ N.w, N.w;
MUL N, N, N.w;

DP3 L.w, L, L;                             # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                             # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                           # N . L
MAX NdotL, NdotL, 0.0;
MUL diffuse, iPrC, NdotL;

DP3 NdotH, N, H;                           # N • H
MAX NdotH, NdotH, 0.0;
POW specular, NdotH.x, 128.0.x;            # 128 is specular exponent

ADD oPrC, diffuse, specular;               # sum the colors

DP4 fogCoord.x, V, V;                      # fogCoord = |Ve|
RSQ fogCoord.x, fogCoord.x;
RCP oFgC.x, fogCoord.x;

END

The calculation to find the distance from the eye (0,0,0,1) to the vertex in eye space is trivial in GLSL. You need only call the built-in length function, passing in the vertex position vector as an argument. In the low-level shader, you must manually perform the same operation. First, you take the dot product of the vertex position against itself followed by the reciprocal square root, just as if you were normalizing the vector. But instead of multiplying this 1/length scale factor by the vector, you just take its reciprocal so the 1/length becomes the length, which is output directly as the vertex's fog coordinate.

The following is the altered GLSL code for performing the fog blend within the shader instead of in fixed functionality fragment processing:

uniform float density;

...

// calculate 2nd order exponential fog factor
const float e = 2.71828;
float fogFactor = (density * length(V));
fogFactor *= fogFactor;
fogFactor = clamp(pow(e, -fogFactor), 0.0, 1.0);

// sum the diffuse and specular components, then
// blend with the fog color based on fog factor
const vec4 fogColor = vec4(0.5, 0.8, 0.5, 1.0);
gl_FrontColor = mix(fogColor, clamp(diffuse + specular, 0.0, 1.0),
                    fogFactor);

The altered low-level shader code is as follows:

PARAM density = program.local[1];          # fog density
PARAM fogColor = {0.5, 0.8, 0.5, 1.0};     # fog color
PARAM e = {2.71828, 0, 0, 0};

TEMP diffuse, specular, fogFactor, litColor;

...

ADD litColor, diffuse, specular;           # sum the colors
MAX litColor, litColor, 0.0;               # clamp to [0,1]
MIN litColor, litColor, 1.0;

# fogFactor = clamp(e^(-(d*|Ve|)^2))
DP4 fogFactor.x, V, V;
POW fogFactor.x, fogFactor.x, 0.5.x;
MUL fogFactor.x, fogFactor.x, density.x;
MUL fogFactor.x, fogFactor.x, fogFactor.x;
POW fogFactor.x, e.x, -fogFactor.x;
MAX fogFactor.x, fogFactor.x, 0.0;         # clamp to [0,1]
MIN fogFactor.x, fogFactor.x, 1.0;

SUB litColor, litColor, fogColor;          # blend lit and fog colors
MAD oPrC, fogFactor.x, litColor, fogColor;

END

Per-Vertex Point Size

Applying fog attenuates object colors the farther away they are from the viewpoint. Similarly, you can attenuate point sizes so that points rendered close to the viewpoint are relatively large and points farther away diminish into nothing. Like fog, point attenuation is a useful visual cue for conveying perspective. The computation required is similar as well.

You compute the distance from the vertex to the eye exactly the same as you did for the fog coordinate. Then, to get a point size that falls off exponentially with distance, you square the distance, take its reciprocal, and multiply it by the constant 100,000. This constant is chosen specifically for this scene's geometry so that objects toward the back of the scene, as rendered from the initial camera position, are assigned point sizes of approximately 1, whereas points near the front are assigned point sizes of approximately 10.

In this sample application, you'll set the polygon mode for front- and back-facing polygons to GL_POINT so that all the objects in the scene are drawn with points. Also, you must enable GL_VERTEX_PROGRAM_POINT_SIZE_ARB so that the point sizes output from the vertex shader are substituted in place of the usual OpenGL point size. Figure 22.7 shows the result of Listings 22.11 and 22.12.

Example 22.11. Point Size Generating High-Level Vertex Shader

// ptsize.vs
//
// Generic vertex transformation,
// attenuated point size

void main(void)
{
    // normal MVP transform
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    vec4 V = gl_ModelViewMatrix * gl_Vertex;

    gl_FrontColor = gl_Color;

    // calculate point size based on distance from eye
    float ptSize = length(V);
    ptSize *= ptSize;
    gl_PointSize = 100000.0 / ptSize;
}
Per-vertex point size makes distant points smaller.

Figure 22.7. Per-vertex point size makes distant points smaller.

Example 22.12. Point Size Generating Low-Level Vertex Shader

!!ARBvp1.0

# ptsize.vs
#
# Generic vertex transformation,
# attenuated point size

ATTRIB iPos = vertex.position;             # input position
ATTRIB iPrC = vertex.color.primary;        # input primary color

OUTPUT oPos = result.position;             # output position
OUTPUT oPrC = result.color.primary;        # output primary color
OUTPUT oPtS = result.pointsize;            # output point size

PARAM mvp[4] = { state.matrix.mvp };       # modelview * proj matrix
PARAM mv[4] =  { state.matrix.modelview }; # modelview matrix

TEMP V, ptSize;                            # temporary registers

DP4 oPos.x, iPos, mvp[0];                  # xform input pos by MVP
DP4 oPos.y, iPos, mvp[1];
DP4 oPos.z, iPos, mvp[2];
DP4 oPos.w, iPos, mvp[3];

DP4 V.x, iPos, mv[0];                      # xform input pos by MV
DP4 V.y, iPos, mv[1];
DP4 V.z, iPos, mv[2];
DP4 V.w, iPos, mv[3];

MOV oPrC, iPrC;                            # copy color

DP4 ptSize.x, V, V;                        # ptSize = 100000 / |Ve|^2
RSQ ptSize.x, ptSize.x;
MUL ptSize.x, ptSize.x, ptSize.x;
MUL oPtS.x, ptSize.x, 100000;

END

Customized Vertex Transformation

You've already customized lighting, texture coordinate generation, and fog coordinate generation. But what about the vertex positions themselves? The next sample shader applies an additional transformation before transforming by the usual modelview/projection matrix.

Figure 22.8 shows the effects of scaling the object-space vertex position by a squash and stretch factor, which can be set independently for each axis, as in Listings 22.13 and 22.14.

Example 22.13. Squash and Stretch High-Level Vertex Shader

// stretch.vs
//
// Generic vertex transformation,
// followed by squash/stretch

uniform vec3 lightPos0;
uniform vec3 squashStretch;

void main(void)
{
    // normal MVP transform, followed by squash/stretch
    vec4 stretchedCoord = gl_Vertex;
    stretchedCoord.xyz *= squashStretch;
    gl_Position = gl_ModelViewProjectionMatrix * stretchedCoord;

    vec3 stretchedNormal = gl_Normal;
    stretchedNormal *= squashStretch;
    vec3 N = normalize(gl_NormalMatrix * stretchedNormal);
    vec4 V = gl_ModelViewMatrix * stretchedCoord;
    vec3 L = normalize(lightPos0 - V.xyz);
    vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));

    // put diffuse lighting result in primary color
    float NdotL = dot(N, L);
    gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));

    // copy (N.H)*8-7 into texcoord
    float NdotH = max(0.0, dot(N, H) * 8.0 - 7.0);
    gl_TexCoord[0] = vec4(NdotH, 0.0, 0.0, 1.0);
}
Squash and stretch effects customize the vertex transformation.

Figure 22.8. Squash and stretch effects customize the vertex transformation.

Example 22.14. Squash and Stretch Low-Level Vertex Shader

!!ARBvp1.0

# stretch.vs
#
# Generic vertex transformation,
# followed by squash/stretch

ATTRIB iPos = vertex.position;             # input position
ATTRIB iPrC = vertex.color.primary;        # input primary color
ATTRIB iNrm = vertex.normal;               # input normal

OUTPUT oPos = result.position;             # output position
OUTPUT oPrC = result.color.primary;        # output primary color
OUTPUT oTxC = result.texcoord[0];          # output texcoord 0

PARAM mvp[4] = { state.matrix.mvp };       # modelview * proj matrix
PARAM mv[4] =  { state.matrix.modelview }; # modelview matrix
# inverse transpose of modelview matrix:
PARAM mvIT[4] = { state.matrix.modelview.invtrans };

PARAM lightPos = program.local[0];         # light pos in eye space
PARAM squashStretch = program.local[1];    # stretch scale factors

TEMP N, V, L, H, NdotL, NdotH, ssV, ssN;   # temporary registers

MUL ssV, iPos, squashStretch;              # stretch obj-space vertex
MUL ssN, iNrm, squashStretch;              # stretch obj-space normal

DP4 oPos.x, ssV, mvp[0];                   # xform stretch pos by MVP
DP4 oPos.y, ssV, mvp[1];
DP4 oPos.z, ssV, mvp[2];
DP4 oPos.w, ssV, mvp[3];

DP4 V.x, ssV, mv[0];                       # xform stretch pos by MV
DP4 V.y, ssV, mv[1];
DP4 V.z, ssV, mv[2];
DP4 V.w, ssV, mv[3];

SUB L, lightPos, V;                        # vertex to light vector

DP3 N.x, ssN, mvIT[0];                     # xform stretched normal
DP3 N.y, ssN, mvIT[1];
DP3 N.z, ssN, mvIT[2];

DP3 N.w, N, N;                             # normalize normal
RSQ N.w, N.w;
MUL N, N, N.w;

DP3 L.w, L, L;                             # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                             # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                           # N . L
MAX NdotL, NdotL, 0.0;
MUL oPrC, iPrC, NdotL;                     # output diffuse

DP3 NdotH, N, H;                           # N • H
MAD NdotH.x, NdotH, 8.0, {-7.0};           # (N • H) * 8 - 7
MOV oTxC, {0.0, 0.0, 0.0, 1.0};
MAX oTxC.x, NdotH, 0.0;                    # toss into texcoord 0

END

Vertex Blending

Vertex blending is an interesting technique used for skeletal animation. Consider a simple model of an arm with an elbow joint. The forearm and bicep are each represented by a cylinder. When the arm is completely straight, all the “skin” is nicely connected together. But as soon as you bend the arm, as in Figure 22.9, the skin is disconnected and the realism is gone.

This simple elbow joint without vertex blending just begs for skin.

Figure 22.9. This simple elbow joint without vertex blending just begs for skin.

The way to fix this problem is to employ multiple modelview matrices when transforming each vertex. Both the forearm and bicep have their own modelview matrix already. The bicep's matrix would orient it relative to the torso if it were attached to a body, or in this case relative to the origin in object-space. The forearm's matrix orients it relative to the bicep. The key to vertex blending is to use a little of each matrix when transforming vertices close to a joint.

You can choose how close to the joint you want the multiple modelview matrices to have influence. We call this the region of influence. Vertices outside the region of influence do not require blending. For such a vertex, only the original modelview matrix associated with the object is used. However, vertices that do fall within the region of influence must transform the vertex twice: once with its own modelview matrix and once with the matrix belonging to the object on the other side of the joint. For this sample, you blend these two eye-space positions together to achieve the final eye-space position.

The amount of one eye-space position going into the mix versus the other is based on the vertex's blend weight. When drawing the glBegin/glEnd primitives, in addition to the usual normals, colors, and positions, you also specify a weight for each vertex. You use the glVertexAttrib1fARB function for specifying the weight. Vertices right at the edge of the joint receive weights of 0.5, effectively resulting in a 50% influence by each matrix. On the other extreme, vertices on the edge of the region of influence receive weights of 1.0, whereby the object's own matrix has 100% influence. Within the region of influence, weights vary from 1.0 to 0.5, and they can be assigned linearly with respect to the distance from the joint, or based on some higher-order function.

Any other computations dependent on the modelview matrix must also be blended. In the case of the sample shader, you also perform diffuse and specular lighting. This means the normal vector, which usually is transformed by the inverse transpose of the modelview matrix, now must also be transformed twice just like the vertex position. The two results are blended based on the same weights used for vertex position blending.

By using vertex blending, you can create life-like flexible skin on a skeleton structure that is easy to animate. Figure 22.10 shows the arm in its new Elastic Man form, thanks to a region of influence covering the entire arm. Listings 22.15 and 22.16 contain the vertex blending shader source.

Example 22.15. Vertex Blending High-Level Vertex Shader

// skinning.vs
//
// Perform vertex skinning by
// blending between two MV
// matrices

uniform vec3 lightPos;
uniform mat4 mv2;
uniform mat3 mv2IT;
attribute float weight;

void main(void)
{
    // compute each vertex influence
    vec4 V1 = gl_ModelViewMatrix * gl_Vertex;
    vec4 V2 = mv2 * gl_Vertex;
    vec4 V = (V1 * weight) + (V2 * (1.0 - weight));
    gl_Position = gl_ProjectionMatrix * V;

    // compute each normal influence
    vec3 N1 = gl_NormalMatrix * gl_Normal;
    vec3 N2 = mv2IT * gl_Normal;

    vec3 N = normalize((N1 * weight) + (N2 * (1.0 - weight)));
    vec3 L = normalize(lightPos - V.xyz);
    vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));

    // put diffuse lighting result in primary color
    float NdotL = dot(N, L);
    gl_FrontColor = 0.1 + gl_Color * vec4(max(0.0, NdotL));

    // copy (N.H)*8-7 into texcoord
    float NdotH = max(0.0, dot(N, H) * 8.0 - 7.0);
    gl_TexCoord[0] = vec4(NdotH, 0.0, 0.0, 1.0);
}
The stiff two-cylinder arm is now a fun, curvy, flexible object.

Figure 22.10. The stiff two-cylinder arm is now a fun, curvy, flexible object.

Example 22.16. Vertex Blending Low-Level Vertex Shader

!!ARBvp1.0

# skinning.vp
#
# Perform vertex skinning by
# blending between two MV
# matrices

ATTRIB iPos = vertex.position;              # input position
ATTRIB iPrC = vertex.color.primary;         # input primary color
ATTRIB iNrm = vertex.normal;                # input normal
ATTRIB iWeight = vertex.attrib[1];          # input weight

OUTPUT oPos = result.position;              # output position
OUTPUT oPrC = result.color.primary;         # output primary color
OUTPUT oTxC = result.texcoord[0];           # output texcoord 0

PARAM prj[4] = { state.matrix.projection }; # projection matrix
PARAM mv1[4] = { state.matrix.modelview };  # modelview matrix 1
PARAM mv2[4] = { state.matrix.program[0] }; # modelview matrix 2
# inverse transpose of modelview matrix:
PARAM mv1IT[4] = { state.matrix.modelview.invtrans };
PARAM mv2IT[4] = { state.matrix.program[0].invtrans };

PARAM lightPos = program.local[0];          # light pos in eye space

TEMP N1, N2, N, V1, V2, V;                  # temporary registers
TEMP L, H, NdotL, NdotH;

DP4 V1.x, iPos, mv1[0];                     # xform input pos by MV1
DP4 V1.y, iPos, mv1[1];
DP4 V1.z, iPos, mv1[2];
DP4 V1.w, iPos, mv1[3];

DP4 V2.x, iPos, mv2[0];                     # xform input pos by MV2
DP4 V2.y, iPos, mv2[1];
DP4 V2.z, iPos, mv2[2];
DP4 V2.w, iPos, mv2[3];

SUB V, V1, V2;                              # blend verts w/ weight
MAD V, V, iWeight.x, V2;

DP4 oPos.x, V, prj[0];                      # xform to clip space
DP4 oPos.y, V, prj[1];
DP4 oPos.z, V, prj[2];
DP4 oPos.w, V, prj[3];

SUB L, lightPos, V;                         # vertex to light vector

DP3 N1.x, iNrm, mv1IT[0];                   # xform norm to eye space
DP3 N1.y, iNrm, mv1IT[1];
DP3 N1.z, iNrm, mv1IT[2];

DP3 N2.x, iNrm, mv2IT[0];                   # xform norm to eye space
DP3 N2.y, iNrm, mv2IT[1];
DP3 N2.z, iNrm, mv2IT[2];

SUB N, N1, N2;                              # blend normals w/ weight
MAD N, N, iWeight.x, N2;

DP3 N.w, N, N;                              # normalize normal
RSQ N.w, N.w;
MUL N, N, N.w;

DP3 L.w, L, L;                              # normalize light vector
RSQ L.w, L.w;
MUL L, L, L.w;

ADD H.xyz, L, {0, 0, 1};

DP3 H.w, H, H;                              # normalize half-angle
RSQ H.w, H.w;
MUL H, H, H.w;

DP3 NdotL, N, L;                            # N . L
MAX NdotL, NdotL, 0.0;
MAD oPrC, iPrC, NdotL, 0.1;                 # output diffuse

DP3 NdotH, N, H;                            # N • H
MAD NdotH.x, NdotH, 8.0, {-7.0};            # (N • H) * 8 - 7
MOV oTxC, {0.0, 0.0, 0.0, 1.0};
MAX oTxC.x, NdotH, 0.0;                     # toss into texcoord 0

END

In this sample, you use built-in modelview matrix uniforms (GLSL) or parameters (low-level) to access the primary blend matrix. For the secondary matrix, you employ a user-defined uniform matrix (GLSL) or program matrix (low-level).

For normal transformation, you need the inverse transpose of each blend matrix. Although low-level shaders provide a simple way to access the inverse transpose of a matrix, high-level shaders do not. You continue to use the built-in gl_NormalMatrix for accessing the primary matrix's inverse transpose, but for the secondary matrix's inverse transpose, there is no shortcut. Instead, you manually compute the inverse of the second modelview matrix within the application and transpose it on the way into OpenGL when calling glUniformMatrix3fvARB.

Summary

This chapter provided various sample shaders as a jumping-off point for your own exploration of high- and low-level vertex shaders. Specifically, we provided examples of customized lighting, texture coordinate generation, fog, point size, and vertex transformation.

It is refreshing to give vertex shaders their moment in the spotlight. In reality, vertex shaders often play only supporting roles to their fragment shader counterparts, performing menial tasks such as preparing texture coordinates. Fragment shaders end up stealing the show. In the next chapter, we'll start by focusing solely on fragment shaders. Then in the stunning conclusion, we will see our vertex shader friends once again when we combine the two shaders and say goodbye to fixed functionality once and for all.

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

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