Now that we have an easy way of running shaders on our meshes, I thought it would be a good time to implement a simple lighting shader so we can start making the objects in our world look a little more like they belong there.
Step 1: Temporary Tiger Modifications
To start out, I am going to add a temporary line of code in our HMMesh class to change our tiger into the simple DirectX Teapot. I am doing this mostly because playing with this lighting shader will make more sense if at first we don’t have to worry about texturing the model. Here is the temorary code:
// Directly beneath the myMesh = Mesh.FromFile() line myMesh = Mesh.Teapot(myDevice);
Step 2: Baby Steps, Adding to our current shader
Now that we have a lovely little teapot mesh being rendered on the screen with a grossly stretched and deformed tiger texture on its surface, it seems time to get down to making our lighting work. The lighting model that we will be implementing is called Phong Shading (named after its creator, Bui Tuong Phong), and it consists of three separate lighting components that will be added together to get our final light values on the mesh. From here on out this tutorial will be broken into sections for each of the three types of lighting.
Component 1: Ambient Lighting
Ambient lighting is the type of light that brightens everything in the scene evenly. It usually comes from a very bright, very far away source like the sun. Because everything gets lit evenly, it is the simplest type of lighting to implement. The modifications to our shader script will be very minimal to accomplish this simple task. Before we simply modify the shader though, let’s make a copy of it and rename it to TransformPhong.fx so we can keep all our our different rendering types in their own separate files for ease of use later. Now, the only thing we need to change about this shader is in the Texture() function (which I am going to rename Phong in this script). Here is what the new pixel shader section will look like:
sampler TextureSampler; float4 Ambient = float4(0.25, 0.25, 0.25, 1.0); struct PS_INPUT { float2 Texcoord : TEXCOORD0; }; float4 Phong(PS_INPUT Input) : COLOR0{ return saturate(Ambient); }; technique TransformPhong { pass P0 { VertexShader = compile vs_2_0 Transform(); PixelShader = compile ps_2_0 Phong(); } }
The float4 values were chosen as 0.25 simply because we only want our ambient light to brighten the model a little bit so we have room to add in other light components later. If you run the demo now, you will see a nice little gray Teapot in the middle of the screen. There is one thing about this type of lighting that has always bothered me slightly, and that is the fact that none of the contours of the object are visible at all. Even though it is supposed to be an object with even lighting on all sides, in the real world we are still able to distinguish edges on objects like this, so I am going to add an edge component to the final color to help out with this.
The edge component will require a few more changes and a bit more math than the simple ambient lighting addition, but it won’t be anything major. To start off I will explain the property of vectors that we are going to be taking advantage of to do this and all of our other lighting calculations, that of the dot product.
The dot product of a vector is defined like this:
V.W = ||V|| * ||W|| * cos(theta) where theta is the angle between the two vectors
Now since most of our lighting calculations will require that we get the angle between two vectors (the view and the light direction, or the light direction and a surface normal) or a representation of the angle in cosine form, we can manipulate this equation to give ourselves a simple way of obtaining this value using the built in shader functions normalize() and dot():
V.W = ||V|| * ||W|| * cos(theta) normalize both vectors, making ||V|| and ||W|| = 1: V.W = 1 * 1 * cos(theta) = cos(theta) V.W = cos(theta) Now, to retrieve the (cosine of the) angle between two of our vectors in a shader: float3 V = normalize(vector1); float3 W = normalize(vector2); float cosangle = dot(V, W);
The two vectors that we will be using to calculate our edge component will be the view vector, which is projected from the camera’s position to the position of the mesh being drawn, and the normal that we calculated for each vertex in the mesh when we first wrote the HMMesh class. We will need to pass the camera’s position to the shader in the same way that we are already passing the WorldViewProj matrix, but to do so, we will first need to add a property to the camera class in Vector4 format so we can use the SetValue function to pass it along. Here is the new property in the camera class.
public Vector4 Position4 { get { return new Vector4(position.X, position.Y, position.Z, 0); } }
We will not only need to be passing the camera position to the shader, but the World matrix as well so that we can transform the positions and the input vectors into the objects world space. Using the new camera property in the ShaderRender function, we pass the camera’s position and our World matrix along like this:
// In the ShaderRender function myShader.MyEffect.SetValue(”EyePosition”, myCamera.Position4); myShader.MyEffect.SetValue(”World”, matWorld); // In the TransformPhong shader file float4×4 World; float3 EyePosition;
Now that we have our EyePosition going into our shader, we need to normalize it and pass it through to the pixel shader so we can add it into our final lighting calculation there. Here are the modifications to the shader:
// In the VS_INPUT struct float3 Normal : NORMAL0; // In the VS_OUTPUT struct float3 Normal : TEXCOORD1; float3 ViewDirection : TEXCOORD2; // In the Transform function float3 ObjectPosition = mul(Input.Position, World); Output.Normal = mul(Input.Normal, World); Output.ViewDirection = EyePosition - ObjectPosition; // In the PS_INPUT struct float3 Normal : TEXCOORD1; float3 ViewDirection : TEXCOORD2; // In the Phong function float3 Normal = normalize(Input.Normal); float3 ViewDirection = normalize(Input.ViewDirection); float EdgeComponent = dot(Normal, ViewDirection); float4 TotalAmbient = Ambient * EdgeComponent; return saturate(TotalAmbient);
Rendering our teapots now will give us an image that looks much more true to life because all of the edges are now visible. Now that we have an ambiently lit set of teapots, we should add in our second component of Phong lighting, the Diffuse component.
Component 2: Diffuse Lighting
Diffuse lighitng is the type of lighting that is affected by the direction of a light source, and its position relative to the object being lit. For this type of lighting calculation, we will need to pass the light position the same way we passed our camera position along to the shader. Here is the code for that:
// In the ShaderRender function myShader.MyEffect.SetValue(”LightPosition”, new Vector4(0.0f, 850.0f, 1000.0f, 0.0f)); // Beneath our float4 Ambient value float4 Diffuse = float4(0.80, 0.80, 0.80, 1.0); // In the VS_OUTPUT struct float3 LightDirection : TEXCOORD3; // In the Transform function Output.LightDirection = LightPosition - ObjectPosition; // In the PS_INPUT struct float3 LightDirection : TEXCOORD3; // In the Phong function float3 LightDirection = normalize(Input.LightDirection); float NDotL = dot(Normal, LightDirection); // The cos(angle) between the normal and light direction float4 TotalDiffuse = saturate(Diffuse * NDotL); return saturate(TotalAmbient + TotalDiffuse);
Now when we render our scene we will see the teapots looking like they are being lit by the sun in our skybox. This is close to the final realistic lighting that we have been working towards. The last component of our Phong lighting model is called the Specular component. It is usually used for simulate a shiny plastic or matallic object, or anything else with a smooth reflective surface.
Component 3: Specular Lighting
Luckily, the specular component of light is calcuated based on all of the values that we have already passed into our shader and requries very little change to our pixel shader code. One thing that we will pass however is the specular power value, which will affect how shiny the surface looks, and how large the reflective areas appear. Here are the final changes to include specular lighting in our model:
// In the ShaderRender function myShader.MyEffect.SetValue(”SpecularPower”, 16); // Under our float4 Ambient and Diffuse values float4 Specular = float4(0.50, 0.50, 0.50, 1.0); float SecularPower; // In the Phong function // These values are based directly from the Phong function defined in this form: // R = 2.0 * N * N.L - L // RDotV = R.V // S = (R.V)^N * SIntensity float3 Reflection = normalize((2.0 * Normal * NDotL) - LightDirection); float RDotV = max(0.0, dot(Reflection, ViewDirection)); float4 TotalSpecular = saturate(Specular * pow(RDotV, SpecularPower)); return saturate(TotalAmbient + TotalDiffuse + TotalSpecular);
We use the max() function to make sure the values on the unlit side of the object don’t get brightened by the specular component incorrectly. With the addition of our Specular component the Phong lighting model is complete. Here is the final shader file in its complete form.
float4×4 WorldViewProj; float4×4 World; float3 EyePosition; float3 LightPosition; struct VS_INPUT { float4 Position : POSITION0; float2 Texcoord : TEXCOORD0; float3 Normal : NORMAL0; }; struct VS_OUTPUT { float4 Position : POSITION0; float2 Texcoord : TEXCOORD0; float3 Normal : TEXCOORD1; float3 ViewDirection : TEXCOORD2; float3 LightDirection : TEXCOORD3; }; VS_OUTPUT Transform(VS_INPUT Input){ VS_OUTPUT Output; Output.Position = mul(Input.Position, WorldViewProj); Output.Texcoord = Input.Texcoord; float3 ObjectPosition = mul(Input.Position, World); Output.Normal = mul(Input.Normal, World); Output.ViewDirection = EyePosition - ObjectPosition; Output.LightDirection = LightPosition - ObjectPosition; return Output; } float4 Ambient = float4(0.25, 0.25, 0.25, 1.0); float4 Diffuse = float4(0.80, 0.80, 0.80, 1.0); float4 Specular = float4(0.50, 0.50, 0.50, 1.0); float SpecularPower; struct PS_INPUT { float2 Texcoord : TEXCOORD0; float3 Normal : TEXCOORD1; float3 ViewDirection : TEXCOORD2; float3 LightDirection : TEXCOORD3; }; float4 Phong(PS_INPUT Input) : COLOR0{ float3 Normal = normalize(Input.Normal); float3 ViewDirection = normalize(Input.ViewDirection); float3 LightDirection = normalize(Input.LightDirection); float EdgeComponent = dot(Normal, ViewDirection); float4 TotalAmbient = saturate(Ambient * EdgeComponent); float NDotL = dot(Normal, LightDirection); float4 TotalDiffuse = saturate(Diffuse * NDotL); float3 Reflection = normalize((2.0 * Normal * NDotL) - LightDirection); float RDotV = max(0.0, dot(Reflection, ViewDirection)); float4 TotalSpecular = saturate(Specular * pow(RDotV, SpecularPower)); return saturate(TotalAmbient + TotalDiffuse + TotalSpecular); }; technique TransformPhong { pass P0 { VertexShader = compile vs_2_0 Transform(); PixelShader = compile ps_2_0 Phong(); } }
Now to adjust the brightness and the effectivness of each component of the light all you need to do is change the float4 Ambient, Diffuse, and Specular values, and to change the reflectivity of the surface to simply change the SpecularPower variable. With this completed lighting shader, there is only one thing left we should probably add here, and that is the texture of the mesh. The texture color will be calculated the same way we did in the original shader file that we have been adding to for this tutorial. To keep our library of shaders growing, I am going to copy the final version of the TransformPhong.fx file and name the new one TransformTexturePhong.fx. The only change between this and the TransformPhong shader is shown below:
float4 TextureColor = tex2D(TextureSampler, Input.Texcoord); return TextureColor * saturate(TotalAmbient + TotalDiffuse + TotalSpecular); // Changes in the HMDemo class HMShader shader1 = new HMShader(”TransformPhong”, @”../../../Shaders/TransformPhong.fx”, demo1.MyDevice); HMShader shader2 = new HMShader(”TransformTexturePhong”, @”../../../Shaders/TransformTexturePhong.fx”, demo1.MyDevice); demo1.MyScene.AddShader(shader1); demo1.MyScene.AddShader(shader2); mesh1.SetShader(”TransformPhong”); mesh2.SetShader(”TransformTexturePhong”);
I also took out the line that changed our tigers to teapots now that we have the texturing and lighting both working together. So there you have it, a full working Phong lighting model with textured meshes. The next step in the shader implementation process is to set our engine up to allow multiple lights with alpha blending. This will be the topic of the next two tutorials coming soon.
Happy Shading!