Adding Specular Highlights

Another type of lighting that hasn't been discussed yet is specular highlighting. Specular highlighting makes an object appear "shiny" and much more realistic when being rendered. It's possible to use the lighting objects in the fixed-function pipeline to enable specular highlights; however, these highlights will be calculated per-vertex. This example will show how to accomplish this with the programmable pipeline instead.

Like the other examples in this chapter, this one will be based on an earlier section of code; in this case, the lit cylinder example you wrote in the previous chapter. This code already handles the diffuse lighting case, which should stay in this updated example to show the differences between the two lighting models.

While the cylinder object will show off the features that this example is trying to, a better object to use is the teapot. Change your main code from loading a cylinder to loading a teapot:

 // Load our mesh mesh = Mesh.Teapot(device); 

Since this example will also be showing multiple different effects and allowing you to switch between them, you will want to have something to notify the user of the current state. You should declare a font variable to allow the user to be notified during the rendered scene:

 // Out font private Direct3D.Font font = null; 

You may as well initialize the font variable directly after your teapot has been created as well:

 //Create our font font = new Direct3D.Font(device, new System.Drawing.Font("Arial", 12.0f)); 

With these things out of the way, you should now add the constants and variables you will need into your shader code:

 float4x4 WorldViewProj : WORLDVIEWPROJECTION; float4x4 WorldMatrix : WORLD; float4 DiffuseDirection; float4 EyeLocation; // Color constants const float4 MetallicColor = { 0.8f, 0.8f, 0.8f, 1.0f }; const float4 AmbientColor = { 0.05f, 0.05f, 0.05f, 1.0f }; 

The combined world, view, and projection matrix variable has been used in each example for transforming the vertices. The single world matrix will once again be used to transform the normals of each vertex. The diffuse direction member will allow the application to define the direction of the light, rather than having it hard coded in the shader like before. The last variable is the location of the eye. Specular highlights are calculated by determining a reflection between the normal and the eye.

You'll also notice there are two constants declared. Since metal is normally shiny, the example will use a color that resembles a metallic material for our diffuse light. An ambient color is also declared, although for this example it is essentially a non-factor. It's only been included for completeness of the lighting mathematic formulas.

For this example, the per-vertex lighting output structure only needs to be concerned with the position and color of each vertex, so declare the structure as follows:

 struct VS_OUTPUT_PER_VERTEX {     float4 Position : POSITION;     float4 Color : COLOR0; }; 

Before the shader code for the specular highlighting, you need to update our diffuse lighting shader. There will be a separate shader for each lighting model. Replace your diffuse lighting shader with the following:

 VS_OUTPUT_PER_VERTEX TransformDiffuse(     float4 inputPosition : POSITION,     float3 inputNormal : NORMAL,     uniform bool metallic     ) {     //Declare our output structure     VS_OUTPUT_PER_VERTEX Out = (VS_OUTPUT_PER_VERTEX)0;     // Transform our position     Out.Position = mul(inputPosition, WorldViewProj);     // Transform the normals into the world matrix and normalize them     float3 Normal = normalize(mul(inputNormal, WorldMatrix));     // Make our diffuse color metallic for now     float4 diffuseColor = MetallicColor;     if(!metallic)         diffuseColor.rgb = sin(Normal + inputPosition);     // Store our diffuse component     float4 diffuse = saturate(dot(DiffuseDirection, Normal));     // Return the combined color     Out.Color = AmbientColor + diffuseColor * diffuse;     return Out; } 

You'll notice there's a new input in this shader, a Boolean value declared with the "uniform" attribute. The uniform modifier is used to tell Direct3D that this variable can be treated as a constant, and cannot be changed during a draw call.

Once the shader actually starts, things appear pretty similar. First, the position is transformed, and then the normal is transformed. The diffuse color is then set to the constant metallic color that was declared earlier.

The next statement is something that hasn't been discussed before: namely, flow control. HLSL supports numerous flow control mechanisms in your shader code, including if statements like above, as well as loops such as the do loop, the while loop, and the for loop. Each of the flow control mechanisms has syntax similar to the equivalent statements in C#.

SHOP TALK: UNDERSTANDING FLOW CONTROL ON OLDER SHADER MODELS

Older shader models do not natively support flow control, or branching. In order to facilitate this, when generating the actual shader code, the HLSL compiler may actually unwind a loop, or execute all branches of if statements. This is something you need to be aware of, particularly if you are already writing a complex shader that is stretching the limits of the instruction count. Take a simple loop such as

 for(int i = 0; i<10; i++) {     pos.y += (float)i; } 

Even though there is only a single statement in the line, this shader code once unwound would take up a whopping 20 instructions (at least). When your instruction count limit is 12 (like for pixel shader 1.1), this simple loop wouldn't be able to be compiled.

If the metallic variable is true, then the metallic color is kept for the light. However, if it is false, the color is switched to an "animating" color much like the first example in this chapter. Finally, the color returned is based on the directional light formula that's been used numerous times.

With the two different color types that can be used in this shader, you will need to add two techniques:

 technique TransformSpecularPerVertexMetallic {     pass P0     {         // shaders         VertexShader = compile vs_1_1 TransformSpecular(true);         PixelShader  = NULL;     } } technique TransformSpecularPerVertexColorful {     pass P0     {         // shaders         VertexShader = compile vs_1_1 TransformSpecular(false);         PixelShader  = NULL;     } } 

You should notice that the only difference between these two techniques (other than the name) is the value that is passed into the shader. Since the technique names have changed, you'll need to update your main code to handle the new technique:

 effect.Technique = "TransformDiffusePerVertexMetallic"; 

With the diffuse lighting with branching statements out of the way, you should now add a new shader to deal with specular highlights. Add the method found in Listing 12.2 to your HLSL code.

Listing 12.2 Transform and Use Specular Highlighting
 VS_OUTPUT_PER_VERTEX TransformSpecular(     float4 inputPosition : POSITION,     float3 inputNormal : NORMAL,     uniform bool metallic     ) {     //Declare our output structure     VS_OUTPUT_PER_VERTEX Out = (VS_OUTPUT_PER_VERTEX)0;     // Transform our position     Out.Position = mul(inputPosition, WorldViewProj);     // Transform the normals into the world matrix and normalize them     float3 Normal = normalize(mul(inputNormal, WorldMatrix));     // Make our diffuse color metallic for now     float4 diffuseColor = MetallicColor;     // Normalize the world position of the vertex     float3 worldPosition = normalize(mul(inputPosition, WorldMatrix));     // Store the eye vector     float3 eye = EyeLocation - worldPosition;     // Normalize our vectors     float3 normal = normalize(Normal);     float3 light = normalize(DiffuseDirection);     float3 eyeDirection = normalize(eye);     if(!metallic)          diffuseColor.rgb = cos(normal + eye);     // Store our diffuse component     float4 diffuse = saturate(dot(light, normal));     // Calculate specular component     float3 reflection = normalize(2 * diffuse * normal - light);     float4 specular = pow(saturate(dot(reflection, eyeDirection)), 8);     // Return the combined color     Out.Color = AmbientColor + diffuseColor * diffuse + specular;     return Out; } 

This is by far the largest shader this book has covered thus far. It starts out just like the last one by transforming the position and the normal, and then by setting the diffuse color to the metallic color constant. From here on, it begins to change, though. First, the position of each vertex in the world is stored. This is done since the eye position is already in world space, while the vertex is in model space. Since the eye direction will be used to calculate the specular highlights, each member needs to be in the same coordinate system.

Next, each of the vectors are normalized, which will clamp the unit length to 1.0f. Now the Boolean variable can be checked, and the color updated based on the newly normalized vector to simulate the random animating color. The diffuse component is stored using the same formula as always. Finally, the specular component is calculated. See the DirectX SDK docs on the mathematics of lighting for more information on this formula.

Once the light components have been calculated, the shader returns the combined color using the same mathematics of lighting formulas. With the specular shader now written, you can add the following techniques to allow access to it:

 technique TransformSpecularPerVertexMetallic {     pass P0     {         // shaders         VertexShader = compile vs_1_1 TransformSpecular(true);         PixelShader  = NULL;     } } technique TransformSpecularPerVertexColorful {     pass P0     {         // shaders         VertexShader = compile vs_1_1 TransformSpecular(false);         PixelShader  = NULL;     } } 

Before running the application once more, you should update your main code to use the specular highlight shader:

 effect.Technique = "TransformSpecularPerVertexMetallic"; 

You should now see your teapot appear shiny as it spins around. See Figure 12.1.

Figure 12.1. Teapot using per-vertex specular highlights.

graphics/12fig01.jpg

SHOP TALK: UPDATING TO USE PER-PIXEL SPECULAR HIGHLIGHTS

As you can see, the teapot looks much more realistic with the shiny surface formed by the specular highlights. You can obviously tell the lighting calculations are performed per vertex. The lighting doesn't appear to smoothly light the entire curved surface of the teapot. Since the shader written for this application was a vertex shader, this is to be expected. In order to get an even more realistic effect, why not have the light be calculated per pixel rather than per vertex?

Since this lighting calculation requires more instructions than allowed for pixel shader 1.x, you will need to ensure that your application supports at least pixel shader version 2.0. You can update your main code that checks this logic as follows:

 if ((hardware.VertexShaderVersion >= new Version(1, 1)) &&     (hardware.PixelShaderVersion >= new Version(2, 0))) 

You will still need a vertex shader to transform your coordinates, as well as to pass on the data needed to your pixel shader. Add the shader programs shown in Listing 12.3:

Listing 12.3 Shader Programs

[View full width]

 struct VS_OUTPUT_PER_VERTEX_PER_PIXEL {     float4 Position : POSITION;     float3 LightDirection : TEXCOORD0;     float3 Normal : TEXCOORD1;     float3 EyeWorld : TEXCOORD2; }; // Transform our coordinates into world space VS_OUTPUT_PER_VERTEX_PER_PIXEL Transform(     float4 inputPosition : POSITION,     float3 inputNormal : NORMAL     ) {     //Declare our output structure     VS_OUTPUT_PER_VERTEX_PER_PIXEL Out =  graphics/ccc.gif(VS_OUTPUT_PER_VERTEX_PER_PIXEL)0;     // Transform our position     Out.Position = mul(inputPosition, WorldViewProj);     // Store our light direction     Out.LightDirection = DiffuseDirection;     // Transform the normals into the world matrix and normalize them     Out.Normal = normalize(mul(inputNormal, WorldMatrix));     // Normalize the world position of the vertex     float3 worldPosition = normalize(mul(inputPosition, graphics/ccc.gif WorldMatrix));     // Store the eye vector     Out.EyeWorld = EyeLocation - worldPosition;     return Out; } float4 ColorSpecular(  float3 lightDirection : TEXCOORD0,  float3 normal : TEXCOORD1,  float3 eye : TEXCOORD2,  uniform bool metallic) : COLOR0 {     // Make our diffuse color metallic for now     float4 diffuseColor = MetallicColor;     if(!metallic)         diffuseColor.rgb = cos(normal + eye);     // Normalize our vectors     float3 normalized = normalize(normal);     float3 light = normalize(lightDirection);     float3 eyeDirection = normalize(eye);     // Store our diffuse component     float4 diffuse = saturate(dot(light, normalized));     // Calculate specular component     float3 reflection = normalize(2 * diffuse * normalized - light);     float4 specular = pow(saturate(dot(reflection, eyeDirection)) graphics/ccc.gif, 8);     // Return the combined color     return AmbientColor + diffuseColor * diffuse + specular; }; 

The code here is similar to the per vertex version. The vertex shader does the transformation and stores the required data for the pixel shader to use later. The pixel shader in turn does all of the math calculations to determine the resulting color. Since the pixel shader is run per pixel rather than per vertex, this effect will be much more fluid and realistic. Add the following technique to your shader file:

 technique TransformSpecularPerPixelMetallic {     pass P0     {         // shaders         VertexShader = compile vs_1_1 Transform();         PixelShader  = compile ps_2_0 ColorSpecular(true);     } } 

Switching your application to use this technique instead will produce a much more realistic result. See Figure 12.2.

Figure 12.2. Teapot with per-pixel specular highlights.

graphics/12fig02.jpg

The sample included on the CD allows you to seamlessly switch between the per-vertex and per-pixel versions of this application, along with switching between the metallic and colorful versions.



Managed DirectX 9 Graphics and Game Programming, Kick Start
Managed DirectX 9 Kick Start: Graphics and Game Programming
ISBN: B003D7JUW6
EAN: N/A
Year: 2002
Pages: 180
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net