Saturday, April 30, 2011

Multipass lighting

Rendering a single light in a scene won't help us too much, we must be able to draw more than just one light, of course we could easily just create a pixel shader that takes into account more than one light, but this techniques will only work in fixed circumstances. Another approach is to use multipass lighting, this technique renders the scene more than once, each time adding the new light components into the mix. Multipassing can also be used for more advanced techniques so it is important to see a working example early.

 The starting source has nothing drawn except for the text, but it has keyboard input and light movement and generation already handled. You can download the start source from here.

What we will do is render each light desperately and add everything on the screen. This way we can control how many lights we render. But just adding the colors of each light with the shaders we have so far won't work, if we just add all the colors we will also add the ambient component time and time again! That's not going to be the desired effect, so we must draw the ambient component separately. Another thing to note is that we are going to use blending to combine the colors. If we just start adding over the background our objects will blend with the background or whatever we already have on the back buffer. So what we'll do is render the object opaque with only ambient and emissive light and then blend everything else over it. Because of this we need to separate our ambient and emissive terms in a separate pixel shader:

float4 PSAmbient(VertexShaderOutput input) : COLOR0
    return float4(Ka*globalAmbient + Ke,1) * tex2D(texSampler,input.Texture);

And of course delete them from any other shaders. Another thing that we can do is to remove all the already existing techniques in the shader and replace them with a single technique with multiple passes:

technique MultiPassLight
    pass Ambient
        VertexShader = compile vs_3_0 VertexShaderFunction();
        PixelShader = compile ps_3_0 PSAmbient();
    pass Directional
        PixelShader = compile ps_3_0 PSDirectionalLight();
    pass Point
        PixelShader = compile ps_3_0 PSPointLight();
    pass Spot
        PixelShader = compile ps_3_0 PSSpotLight();

As you can see I have only included the vertex shader only in the first pass as only this one changes it.

In the application we only need to change our draw function. First thing we have to do is make sure we have the right settings for rendering the ambient term. We have to make sure we are rendering opaque data and that the depth buffer is at default settings (it is changed by the sprite batch). Using the build-in states the lines of code are:

    GraphicsDevice.BlendState = BlendState.Opaque;
    GraphicsDevice.DepthStencilState = DepthStencilState.Default;

After that all we have to do is set our ambient pass and render the object to screen:

    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, number_of_vertices, 0, number_of_indices / 3);

We will now see our terrain appearing on screen with no lights at all (well... just ambient lighting).
Adding the other lights we will have to add everything onto the back buffer so we will have to set the blend state (again, using a build-in state):

    GraphicsDevice.BlendState = BlendState.Additive;

This way our colors will combine to give us the desired effect. Unfortunately we will have to draw the scene for every light (YUP!) so we will use foreach statements to seat each light and draw the scene using that light:

    foreach (SpotLight light in spotLights)
        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, number_of_vertices, 0, number_of_indices / 3);

Don't forget that you have to do this for every type of light and that point lights only have position while directional lights only have position.

You can download the final source form here and the result should be something along the lines of:


  1. Would normal mapping need to be a seperate pass as well?

    1. It does not have to be. It depends on how you design your rendering framework. Actually multipassing isn't a very efficient technique, it's just good to help you understand how the rendering pipeline works.