Wednesday, April 27, 2011

Implementing Basic Lights

Rendering lights is one of the most important aspects of 3D graphics. Lights give an object shape and volume, the increase the depth of objects and induce very realistic details into the final details. Learning to render even the most basic lights is important.

Simple directional light

Remember that backrefences are recursive!

The source code for this tutorial is here. It has a lot of things implemented: vertex and index buffers, matrices, vectors (including rotations) to be used for the lighting calculations and of course user input ( left-right keys to rotate the world and numpad 1 2 3 to change the selected type of light). All the shader parameters that we will require are already set to some values. In the shader we have all required parameters, 3 techniques (1 vertex shader and 3 pixel shaders) a texture sampler with 16x AF and the structures we will use. Note that the vertex output struct returns the position twice, second time using a texture coordinate semantic, this is required as we can not access variables with a position semantic from the pixel shader.

First of all the light calculations must be made in the same coordinate space. Local space gives us the advantage of not having to multiply each position and normal with the wold matrix, instead we just multiply the light direction with the inverse transform of the object once on the CPU. Your GPU may be faster that your CPU, but it's not thousands of times faster. You can see this in the draw function implementation. Notice that I use Vector3.Transform for positions and Vector3.TransformNormal for directions, this is because directions should NOT be affected by transformations that have a translation and TrasformNormal ignores translations.

Now I want you to take a look at the Vertex Shader, it looks a little like this:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
    VertexShaderOutput output;
    output.Position = mul(input.Position, WVP);
    output.Normal = input.Normal;
    output.Texture = input.Texture;
    output.PositionO =;
    return output;

I have separated the line of interest from the rest of the code. As you can see I'm copying the input position to the output. The .xyz is a swizzle, that means that I'm only accessing the x, y, and z components (so I'm not reading w). Swizzles and write masks are a very useful component of HLSL, they are used for easy vector acces and can be used in a lot of differ ways. For example:

    float4 a = float4(1,2,3,4);
    float4 b = float4(5,6,7,8);
    a.yxw = b.xzz;

At the end of these few line of code 'a' will have the following components (x to w): 7, 5, 3, 7. This allows for very short typing of code (especially if you combine this with all the other intrinsic capabilities of HLSL). Also these type of vector operations are implemented directly into the GPU so they are very fast.

Now we only have to implement the pixel shaders. There are 3 pixel shaders because we have 3 different types of light: directional light, point light and spot light. Directional light is used to render distant light sources like the sun, because the light source is so far away the rays seems to fall parallel to the ground, point lights are used to draw omnidirectional lights that are close to the scene. For example a light bulb would be rendered by a point light. Spot light are used to render cone shaped lights like a street post or flashlight. I will explain the implementation of the directional light and changes necessary for the other light types.

Our light model has 4 components: emission, ambient, diffuse and specular. The first one, emission is very simple, it's simply equal to our emission factor Ke:

    float3 emissive = Ke;

The next component, the ambient light is just as simple, it's our ambient factor (Ka) multiplied by the ambient color (globalAmbient):

    float3 ambient = Ka*globalAmbient;

The diffuse light is just a little more complicated, we have to get the direction of the light in this point (actually the normalized vector from the point to the light), because we have a directional light we just negate and normalize it. After that we can have to calculate the the diffuse factor based on Lambert's cosine law and multiply the result with the color of the light (componentwize multiplication) and by the diffuse factor (Kd):

    float3 L = normalize(-lightDirection);
    float diffuseLight = max(dot(input.Normal,L), 0);
    float3 diffuse = Kd*lightColor*diffuseLight;

Now we need the add the calculations for the specular light. To be able to calculate the specular light we need the normalized vector that is half way between the normalized vector from the point to the eye and the normalized vector from the point to the light. We already have the second vector, to get the first one we need to subtract the points position from the eye position and normalize it. After that we add these 2 vectors together and normalize them to get the half way vector. We then do a dot product between the half vector and the normal and raise the result to the specularPower. We check to see if we have diffuseLight (and set specular light to 0 if we don't). Afterward we multiply by the light color and specular factor (Ks):

    float3 V = normalize(eyePosition - input.PositionO);
    float3 H = normalize(L + V);
    float specularLight = pow(dot(input.Normal,H),specularPower);
    if(diffuseLight<=0) specularLight=0;
    float3 specular = Ks * lightColor * specularLight;

All our light components are calculated so we only have to add them together and multiply the color with result:

    float3 light = emissive + ambient + diffuse + specular;
    color.rgb *= light;

Our directional light shader is now finished.

Rendering a point light is very similar to rendering a directional light. The only difference between the 2 is how we calculate the light direction vector (L in our code).The directional light had the same direction for any point in the scene but for the point light it's different. To calculate we simply subtract the current points position from the light position:

    float3 L = normalize(lightPosition - input.PositionO);

Spot lights are point lights but with a restricted angle, forming a cone. We want our spot light to be bright near the center of the cone, and become more and more dull towards the sides, very abruptly changing to darkness. This is obvious as a flashlight shines brightest when it's light hits the surface perpendicularly, becoming very clear that we must take into account the angle between the light direction in that point (our L vector) and where the light is pointed. We had a similar effect when we calculated the specular light, we raised to a power so we could get a restricted angle. The spot factor we are going to obtain should only affect the diffuse and specular term and not the emission and ambient terms as these are independent of lights. Considering all of these we will have to add one line into the shader and change the way the light factors are summed:

    float spotScale = pow(max(dot(L,-lightDirection),0),spotPower);
    float3 light = emissive + ambient + (diffuse + specular)*spotScale;

Now we have a full shader with all 3 basic types of light. You can download the finished code here. The final solution should look something like this:

No comments:

Post a Comment