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#.
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 HighlightingVS_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.
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. |