Tutorial 4: Using Materials and Lights

The Lights tutorial project adds lights and materials to create more realism in Microsoft Direct3D objects. Each object in the scene will be lit based on the location and type of lights that are used. Materials describe how polygons reflect ambient and diffuse light, how their specular highlights appear, and whether the polygons appear to emit light.

Path

Source location: (SDK root)\Samples\Managed\Direct3D\Tutorials\Tutorial4

Procedure

Note: For information about initializing Direct3D, handling Microsoft Windows messages, rendering, or shutting down, see Tutorial 1: Creating a Device.

Tutorial 3: Using Matrices transformed object vertices in 3-D. This tutorial adds to the Tutorial 3 code to create a material and a light.

Initializing a Depth Stencil

This project also adds to the Tutorial 3 initialization an initialization procedure to allow use of a z-buffer (depth buffer) and a depth stencil, as shown in the following code fragment. Depth stencils enable applications to mask sections of the rendered image so that they are not displayed. Here the depth stencil is first enabled, and then the format is set to a 16-bit z-buffer depth.

          [C#]
          public bool InitializeGraphics()
{
    .
    .
    .
    // Turn on a depth stencil
    presentParams.EnableAutoDepthStencil = true;
    
    // Set the stencil format
    presentParams.AutoDepthStencilFormat = DepthFormat.D16;
    .
    .
    .
}

Intializing the Vertex Buffer and Render States

One of the requirements of using lights is that each surface has a normal vector. This project therefore uses a different custom vertex type than the previous tutorials, the CustomVertex.PositionNormal structure, which includes a 3-D position and a surface normal that is used internally by Direct3D for lighting calculations.

          [C#]
          public void OnCreateDevice(object sender, EventArgs e)
{
    Device dev = (Device)sender;

    // Now create the vertex buffer
    vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionNormal),
                                    100,
                                    dev,
                                    Usage.WriteOnly,
                                    CustomVertex.PositionNormal.Format,
                                    Pool.Default);
    vertexBuffer.Created +=
                    new System.EventHandler(this.OnCreateVertexBuffer);
    this.OnCreateVertexBuffer(vertexBuffer, null);
}

The sample code also enables use of a z-buffer (depth buffer), for efficiently storing scene geometry, and enables Direct3D lighting with RenderStateManager properties as follows.

          [C#]
          public void OnResetDevice(object sender, EventArgs e)
{
    Device dev = (Device)sender;
    
    // Turn off culling, so the user sees the front and back of the triangle
    device.RenderState.CullMode = Cull.None;
    
    // Turn on the z-buffer
    device.RenderState.ZBufferEnable = true;
    device.RenderState.Lighting = true;    // Make sure lighting is enabled
}

Creating the Cylinder Object

At device initialization (OnCreateDevice shown above), the application-defined OnCreateVertexBuffer method is called to create a cylinder object. A vertex buffer is initialized that stores the points of the cylinder, and then the position and normal of each point on the cylinder are loaded into the vertex buffer, as shown in the following sample code.

          [C#]
          public void OnCreateVertexBuffer(object sender, EventArgs e)
{
    VertexBuffer vb = (VertexBuffer)sender;
    // Create and lock a vertex buffer (which will return the structures)
    CustomVertex.PositionNormal[] verts =
                    (CustomVertex.PositionNormal[])vb.Lock(0,0);
                    
    for (int i = 0; i < 50; i++)
    {
        // Fill up the structs
        float theta = (float)(2 * Math.PI * i) / 49;
        verts[2 * i].Position = new Vector3(
                (float)Math.Sin(theta), -1, (float)Math.Cos(theta));
        verts[2 * i].Normal = new Vector3(
                (float)Math.Sin(theta), 0, (float)Math.Cos(theta));
        verts[2 * i + 1].Position = new Vector3(
                (float)Math.Sin(theta), 1, (float)Math.Cos(theta));
        verts[2 * i + 1].Normal = new Vector3(
                (float)Math.Sin(theta), 0, (float)Math.Cos(theta));
    }
    // Unlock (and copy) the data
    vb.Unlock();
}

The cylinder is rotated using the world transformation matrix in the SetupMatrices private method, as in Tutorial 3.

Creating a Material

A material defines the color that is reflected off the surface of a geometric object when a light hits it. The following code fragment uses the Material structure to create a material that is white in color. The diffuse and ambient color properties for the material are set to white. After this call is made, every primitive will be rendered with this material until the color properties are set to another value.

          [C#]
          private void SetupLights()
{
    System.Drawing.Color col = System.Drawing.Color.White;
    
    // Set up a material. The material here just has the diffuse and ambient
    // colors set to white. Note that only one material can be used at a time.
    Direct3D.Material mtrl = new Direct3D.Material();
    mtrl.Diffuse = col;
    mtrl.Ambient = col;
    device.Material = mtrl;
    .
    .
    .
}

Creating a Light

There are three types of lights available in Direct3D: point lights, directional lights, and spotlights. This tutorial project creates a directional light, which is a light that shines in one direction, and it oscillates the direction of the light. The following code fragment uses a Light object to create a directional light with a dark turquoise color. All objects in the scene are also illuminated with a low level of monochromatic (gray) ambient lighting.

          [C#]
          private void SetupLights()
{
    .
    .
    .
    // Set up a colored directional light, with an oscillating direction.
    // Note that many lights may be active at a time (but each one slows down
    // the rendering of the scene). However, here just one is used.
    device.Lights[0].Type = LightType.Directional;
    device.Lights[0].Diffuse = System.Drawing.Color.DarkTurquoise;
    device.Lights[0].Direction = new Vector3(
                    (float)Math.Cos(Environment.TickCount / 250.0f),
                    1.0f,
                    (float)Math.Sin(Environment.TickCount / 250.0f));

    device.Lights[0].Enabled = true; // Turn it on
    
    // Finally, turn on some ambient light.
    // Ambient light is light that scatters and lights all objects evenly.
    device.RenderState.Ambient = System.Drawing.Color.FromArgb(0x202020);