DirectX10 Tutorial 8: Lighting Theory and HLSL
August 30, 2010 6 Comments
This tutorial will deal with the basic scene lighting. It will cover the basic Phong and Blinn-Phong reflection models and the per-vertex (Gouraud) and per-pixel shading models (Phong). The Blinn-Phong reflection model is used in the openGL fixed-function pipeline (and as far as I know also used in DX9 but I’m not 100% sure). Modern games don’t really used these shading models any more as they are very expensive especially when there are numerous objects and light sources in a scene and much more efficient techniques such as deferred shading are pretty much the industry standard at the moment in regards to scene lighting and shadowing. Even though the shading models may have changed and aren’t really all that relevant, the reflection models explained here are still in use.
Lighting Theory
The first thing that needs to be covered is what’s the difference between a reflection model and a shading model, a reflection model is a mathematical model (representation) of how a surface reflects any light rays hitting the surface (incident rays), it takes in a light ray and a surface and returns the final light reflection at the surface of the incident ray. The shading model determines how a primitive’s surface is colored (shaded), if it is done per vertex, then the colors are set at each vertex and the final surface color will be a linear interpolation of the vertex colors. If it is done per pixel, then the color is set separately at each pixel on a surface, this allows for much higher detail and is how texturing works.
The difference between the two shading models is when the lighting intensity (brightness) values are calculated, per-vertex or per-pixel. Per vertex is obviously faster since the lighting calculations are only done 3 times per primitive while with per-pixel they are done as many times as the number of pixels covered by that surface.
I’m not going to go into too much detail regarding lighting theory and I am only going to cover the essentials, the first thing that we need for our scene lighting is a light source, there are various types of light sources with various properties but the one thing they all have in common is their intensity. The intensity of a light source can be thought of as its brightness, this brightness value ranges from 0 to 1 and is separate for each of the primary RGB colors. This brightness is also the lights color, as if a light emits brighter red light rays than green or blue, it will appear redder. Now in most graphics book the term light intensity and light color are used interchangeably but I will stick to the term intensity.
There are several types of light sources, the most common ones are:
- Directional lights – defined by a direction and a light intensity, the light direction is the same at any point on any surface, this occurs when you have a light source that is infinitely far away (like the sun).
- Point Lights – these lights have both an intensity and a position, they also exhibit attenuation (the light intensity decreases with distance). Light direction and attenuation has to be calculated before being able to perform the lighting calculations.
- Spot Lights – These lights are point lights that only emit light in a directed cone, there are various ways to define spotlights (google them) but at the most basic level they have a position, intensity and a cone direction.
Now we need to move onto the surfaces, when light hits a surface it gets reflected in two ways:
- Diffuse reflection: the first reflection is when light enters the surface, splits up and interacts with the particles in the surface and finally gets reflected back out the surface (the blue rays in the figure). The exiting light rays get reflected in various directions. In computer graphics we think of all surfaces as perfectly diffuse, what this means is that diffuse light gets reflected outwards in all direction equally. This is necessary so that we can use Lambert’s law of reflection.
- Specular Reflection: When a light ray hits a surface, part of the light ray enters the surface, there it is modified by the surface (certain light colors are absorbed – I’m not going to into too much detail on light waves and their properties) and is reflected as diffuse light. Any light that hasn’t entered the surface is reflected immediately at the surface in the exact opposite direction; this is termed the specular (mirror-like) reflection. The specular light is reflected in an arc, the width of which is determined by the smoothness of the surface, the smoother the surface the narrower the beam. The direction of the arc is almost the exact reflection of the incoming beam around the surface normal (i.e. the perfected reflection of a vector around the normal). It is important to note that specular light is view dependent; it is only visible if the viewer is within the reflected arc (i.e. the viewer picks up the reflected light). Since diffuse light is reflected in all directions equally it is view independent. When the angle between the specular reflection and the view vectors is very small, then the intensity of the specular light is very high and viewer will see a small round section that is mainly the color of the light (since the light is directly reflected off the surface and doesn’t get modified by it), this is called a specular highlight.
It is also important to note that the lighting we are going to do is termed local lighting as it light each object in the scene on its own and doesn’t take into consideration any secondary reflections of light, obstructions and so on. There is another factor to be taken into account when calculating the light intensity at a surface.
- Ambient Reflection: In real life objects are not only lit by the primary light sources but by the reflection of light from other object. Since we cannot calculate all the reflections in the scene the simplest method to correct (fake) this is to add in an equal amount of ambient light reflection to every object in the scene. This is a quick and dirty hack but works visually.
Every surface is made of some material and each material reflects light differently, for example think of how metal objects are shiny and wooden objects are matte. We need to have a way to specify material parameters that can control how a surface reflects light. These material parameters are called reflectivity constants and they control the intensity of the various reflections. Each surfaces has three reflectivity constants: Ka (ambient), Ks(specular), Kd(diffuse) and a fourth parameter called the surface shininess (a), this surface shininess value controls how large the specular highlights are. These reflectivity constants can either be either single float values (defining how a surface reflect all light) or a triplet of floats (defining how a surface reflects each color of light separately).
Figure 2 shows all the necessary vectors needed at any point to work out the light intensity at that point. It is crucial to note that all the vectors are unit vectors (length of 1) and that they originate from the point on the surface and not from their source!!!
I mentioned that we think of surfaces in computer graphics as perfectly diffuse so we can use lamberts law, lamberts law states that the diffuse reflection’s intensity is proportional to the cosine of the angle (theta) between the incoming light ray (L) and the normal of the surface (N). Since both the light (L) and the normal (N) vector are both unit vectors then the cosine of the angle between them can be represented by their dot product.
NOTE: it is absolutely critical to note that the dot product of two vectors can be negative, so it must be clamped to the range [0-1]. To do this in HLSL use the saturate function.
So for diffuse light the light intensity at a point is:
N.L * light source intensity
Now we add in the surface reflectivity constant and the final diffuse reflection value (Id) is:
Id = Kd * N.L * light source intensity
The specular reflection is a bit trickier, but we know that it is dependent on the view (V) and the reflection vector (R). The closer the two vectors are together the stronger the intensity, once again we use the cosine (can you figure out why?) of the angle between them (phi) which once again since they are unit vectors is the dot product between them (R.V).
Now we also need to take into account how smooth a surface is, the smoother the surface the tighter the arc of reflected light, so that means the that the highlight will only be visible when the F value is extremely small. An easy way to simulate this is to add an exponent to the intensity, since light intensities range from 0-1, the higher the exponent the smaller the final specular intensity and the smoother the surface. This means that only really high values of R.V will be seen, and these high values will only occur when R and V are really close together.
So the final specular reflection (Is) becomes:
Is = Ks * exp(R.V, a) * light source intensity
Ambient light (Id) is simply the product of the ambient light value and the surface’s ambient light co-efficient.
Ia = Ka * Ambient light intensity.
The final light intensity (I) at the surface is the sum of Ia, Id and Is:
I = Ia + Is + Id
The above equation is the Phong reflection model, there is a second lighting model called Blinn-Phong which was popular due to it cheaper computational cost. The difference between the two is only in the specular term. To calculate the reflection of the incoming ray is quite expensive and so a method was proposed to use the half angle between the light and the view instead of the reflected vector. This half angle is given by:
H = ||L+V||
And the specular term changes to:
Is = Ks * exp(N.H, a) * light source intensity
It is important to note that the a value needs to modified to produce similar results, this is due to the fact that the value produced by N.H will be significantly smaller than that produced by R.V (I’m not going into detail regarding this but it’s pretty simple to see why).
Implementing the lighting equations
We can implement the lighting equations either per pixel or per vertex, at either stage we need the following information for any point:
- The normal to the surface at that point
- The material properties (Ka, Ks, Kd & a)
- The light vector from the surface (either the negation of the light direction or the light position – point position)
- The view vector (camera position – point position)
It is very, very important for all the vectors to all be in the same space, be it object, world or view space. The easiest is to define your light in world space and then simply used the world space normal (vertex normal multiplied with the world matrix).
For either type of lighting the lighting equation remains the same and you can write a small HLSL function to take care of it for you, I have also created two helper structs to make the code look neater:
struct DirectionalLight { float4 color; float3 dir; }; struct Material { float Ka, Kd, Ks, A; };
Now the HLSL lighting functions:
//-------------------------------------------------------------------------------------- // Phong Lighting Reflection Model //-------------------------------------------------------------------------------------- float4 calcPhongLighting( Material M, float4 LColor, float3 N, float3 L, float3 V, float3 R ) { float4 Ia = M.Ka * ambientLight; float4 Id = M.Kd * saturate( dot(N,L) ); float4 Is = M.Ks * pow( saturate(dot(R,V)), M.A ); return Ia + (Id + Is) * LColor; }
or for Blinn-Phong
//-------------------------------------------------------------------------------------- // Blinn-Phong Lighting Reflection Model //-------------------------------------------------------------------------------------- float4 calcBlinnPhongLighting( Material M, float4 LColor, float3 N, float3 L, float3 H ) { float4 Ia = M.Ka * ambientLight; float4 Id = M.Kd * saturate( dot(N,L) ); float4 Is = M.Ks * pow( saturate(dot(N,H)), M.A ); return Ia + (Id + Is) * LColor; }
Implementing the lighting equations – Per Vertex – Gouraud Shading
For per-vertex lighting the operation is simple: we calculate the light intensity at a vertex and store it as the vertex color. During rasterization the lighting will be interpolated across the surface just like the color would have been. This unfortunately produces blockiness especially on specular highlights and on sparse meshes.
An example Phong Reflection Model Gouraud vertex and pixel shader follows:
//-------------------------------------------------------------------------------------- // PER VERTEX LIGHTING - PHONG //-------------------------------------------------------------------------------------- PS_INPUT_PV VS_VERTEX_LIGHTING_PHONG( VS_INPUT input ) { PS_INPUT_PV output; //transform position to clip space input.p = mul( input.p, World ); output.p = mul( input.p, View ); output.p = mul( output.p, Projection ); //set texture coords output.t = input.t; //calculate lighting vectors float3 N = normalize( mul( input.n, (float3x3) World) ); float3 V = normalize( eye - (float3) input.p ); //DONOT USE -light.dir since the reflection returns a ray from the surface float3 R = reflect( light.dir, N); //calculate per vertex lighting intensity and interpolate it like a color output.i = calcPhongLighting( material, light.color, N, -light.dir, V, R); return output; } float4 PS_VERTEX_LIGHTING_PHONG( PS_INPUT_PV input ) : SV_Target { return input.i; }
and for Blinn-Phong
//-------------------------------------------------------------------------------------- // PER VERTEX LIGHTING - BLINN-PHONG //-------------------------------------------------------------------------------------- PS_INPUT_PV VS_VERTEX_LIGHTING_BLINNPHONG( VS_INPUT input ) { PS_INPUT_PV output; //transform position to clip space input.p = mul( input.p, World ); output.p = mul( input.p, View ); output.p = mul( output.p, Projection ); //set texture coords output.t = input.t; //calculate lighting float3 N = normalize( mul( input.n, (float3x3) World) ); float3 V = normalize( eye - (float3) input.p ); float3 H = normalize( -light.dir + V ); //calculate per vertex lighting intensity and interpolate it like a color output.i = calcBlinnPhongLighting( material, light.color, N, -light.dir, H); return output; } float4 PS_VERTEX_LIGHTING_BLINNPHONG( PS_INPUT_PV input ) : SV_Target { return input.i; }
Implementing the lighting equations – Per Pixel – Phong Shading
Per-pixel shading is a little more complicated and expensive, since the lighting calculations get performed per pixel but the results are significantly improved, removing all blockiness and offering smooth specular highlights. The trick is that since the calculations are done per pixel, we need the necessary vectors at each pixels, so it is to calculate the necessary vectors needed at each vertex and have them interpolated across the surface. Interpolation is not guaranteed to return unit vectors so it is necessary to normalize them again in the pixel shader.
NOTE: When you wish to have values interpolated per pixel you need to ensure that you tag the variables with the correct HLSL sematics as only variables with pixel shader input semantics like TEXCOORD & COLOR will be interpolated!
An example Phong Reflection Model Phong vertex and pixel shader follows:
//-------------------------------------------------------------------------------------- // PER PIXEL LIGHTING //-------------------------------------------------------------------------------------- PS_INPUT_PP_PHONG VS_PIXEL_LIGHTING_PHONG( VS_INPUT input ) { PS_INPUT_PP_PHONG output; //transform position to clip space - keep worldspace position output.wp = mul( input.p, World ); output.p = mul( output.wp, View ); output.p = mul( output.p, Projection ); //set texture coords output.t = input.t; //set required lighting vectors for interpolation output.n = normalize( mul(input.n, (float3x3)World) ); } float4 PS_PIXEL_LIGHTING_PHONG( PS_INPUT_PP_PHONG input ) : SV_Target { //calculate lighting vectors - renormalize vectors input.n = normalize( input.n ); float3 V = normalize( eye - (float3) input.wp ); //DONOT USE -light.dir since the reflection returns a ray from the surface float3 R = reflect( light.dir, input.n); //calculate lighting return calcPhongLighting( material, light.color, input.n, -light.dir, V, R ); }
for Blinn-Phong:
//-------------------------------------------------------------------------------------- // PER PIXEL LIGHTING //-------------------------------------------------------------------------------------- PS_INPUT_PP_BLINNPHONG VS_PIXEL_LIGHTING_BLINNPHONG( VS_INPUT input ) { PS_INPUT_PP_BLINNPHONG output; //set position into clip space input.p = mul( input.p, World ); output.p = mul( input.p, View ); output.p = mul( output.p, Projection ); //set texture coords output.t = input.t; //set required lighting vectors for interpolation float3 V = normalize( eye - (float3) input.p ); output.n = normalize( mul(input.n, (float3x3)World) ); output.h = normalize( -light.dir + V ); return output; } float4 PS_PIXEL_LIGHTING_BLINNPHONG( PS_INPUT_PP_BLINNPHONG input ) : SV_Target { //renormalize interpolated vectors input.n = normalize( input.n ); input.h = normalize( input.h ); //calculate lighting return I = calcBlinnPhongLighting( material, light.color, input.n, -light.dir, input.h ); }
Lighting Example Project
I’ve written a very basic example program to demonstrate the various lighting models, it’s feature a very, very basic heightmap terrain for which I manually calculate the normals. The mesh is very sparse and the heightmap isn’t exactly amazing either so please excuse the quality of the terrain. I’m using a directional light for the example since it is the easiest one to demonstrate and not confused people.
Please feel free to play around with the light direction and material properties just to figure out what they do! You can find them in the dxManager.cpp file at line 361. The link to the example project can be found at the end of the tutorial!
//CREATE LIGHTS AND MATERIAL //-------------------------------------------------------------------------------- ambientLight = D3DXVECTOR4(1.0f,1.0f,1.0f,1.0f); //set directional light - MAKE sure light direction is a unit vector directionalLight.color = D3DXVECTOR4(1.0f,1.0f,1.0f,1.0f); directionalLight.direction = D3DXVECTOR3(1,-1,0); D3DXVec3Normalize(&directionalLight.direction, &directionalLight.direction); material.ambient = 0.1f; material.diffuse = 0.5f; material.specular = 0.5f; material.shininess = 30;
Differences between Per-pixel and Per-vertex Shading
For the most part the two lighting schemes look the same, the major difference between the two is in the specular lighting and especially in the quality of the specular highlights. Per-pixel smooths out the specular highlights and this greatly improves the visual quality especially on curved or spherical objects.
Lighting and Texturing
To enable texturing in conjunction with lighting all we do is simple multiply the sampled texel color with the intensity of the lighting at that point. Only a single line in our pixel shaders change:
return calcPhongLighting( material, light.color, input.n, -light.dir, V, R );
becomes
float4 I = calcPhongLighting( material, light.color, input.n, -light.dir, V, R ); return I * colorMap.Sample(linearSampler, input.t);
And there we have it, hopefully this tutorial hasn’t been to complicated!
Source Code for Lighting Example: tutorial8.zip
Pingback: graphic cards: computer graphics card doesn’t work?
thanks for the tutorial.
i got a question ! how can i load a mesh in to my dx10 project.
i found some loader that loads in dx9 then converts it to dX10. i want just dx10 mesh loader!
if you got any sample or know how to do it ! please make a tutorial about it .
Thanks :)
Pingback: DirectX10 Tutorial 10: Shadow Mapping « Taking Initiative: Bobby Anguelov's Blog
Thanks for tutorials they are really great !
I have a question about light. What I probably do wrong that in result it looks like that ?
http://fotoo.pl/zdjecia/files/2011-06/4eb8b966.png
On this screen is BlinnPhong technique of light. Can I fix that or it should looks like that ?
PS. Sorry for my english but it’s not my native language. :)
I found the problem. What a stupid mistake. I have ‘Shininess’ set as int not as float what causing all of problem. :)
Thanks for tutorial and source code :)
Thanks for the tutorial, but how do I implement the FreeImage.lib so I can run the source code?