Rendering Environment Maps

Environment mapping is a technique that can be used to simulate a highly reflective surface. You may see this effect in racing games, where a bright shiny car "reflects" the clouds above it, or where a "new" ice hockey rink seems to reflect the players as they skate on it. A common way to implement these environment maps is to use a cube texture (a six-sided texture), and we will show this technique now.

Before we begin looking at this code specifically, you will once again need to create a new project and get it ready for coding (including adding the references, the device variable, and setting the window style). We will also need two of the meshes that ship with the DirectX SDK, the car model and the skybox2 model (and associated textures). Once that has been done, add the following variable declarations:

 private Mesh skybox = null; private Material[] skyboxMaterials; private Texture[] skyboxTextures; private Mesh car = null; private Material[] carMaterials; private Texture[] carTextures; private CubeTexture environment = null; private RenderToEnvironmentMap rte = null; private const int CubeMapSize = 128; private readonly Matrix ViewMatrix = Matrix.Translation(0.0f, 0.0f, 13.0f); 

Here we've declared the two meshes we will need to draw, the sky box (our "environment"), and the object we want to reflect this environment on[md]in our case, the car. We also need to have the cube texture that will store the environment and the helper class for rendering the environment map.

Since not all cards support cube textures, we will need to detect whether your card supports these before we can continue with the example. You will need a new graphics initialization method to handle this. Use the one found in Listing 10.7.

Listing 10.7 Initializing Your Device for Environment Mapping
 public bool InitializeGraphics() {     // Set our presentation parameters     PresentParameters presentParams = new PresentParameters();     presentParams.Windowed = true;     presentParams.SwapEffect = SwapEffect.Discard;     presentParams.AutoDepthStencilFormat = DepthFormat.D16;     presentParams.EnableAutoDepthStencil = true;     // Create our device     device = new Device(0, DeviceType.Hardware, this,          CreateFlags.SoftwareVertexProcessing, presentParams);     device.DeviceReset +=new EventHandler(OnDeviceReset);     OnDeviceReset(device, null);     // Do we support cube maps?     if (!device.DeviceCaps.TextureCaps.SupportsCubeMap)         return false;     // Load our meshes     skybox =  LoadMesh(@"..\..\skybox2.x", ref skyboxMaterials,            ref skyboxTextures);     car =  LoadMesh(@"..\..\car.x", ref carMaterials, ref carTextures);     return true; } 

The only major difference between this and our others is the fact that it returns a Boolean value identifying whether it succeeded or not. It hooks the device reset event and uses a new LoadMesh method as well. Before we look at those methods, though, we will need to update our main method to handle the return value of this method:

 static void Main() {     using (Form1 frm = new Form1())     {         // Show our form and initialize our graphics engine         frm.Show();         if (!frm.InitializeGraphics())         {             MessageBox.Show("Your card does not support cube maps.");             frm.Close();         }         else         {             Application.Run(frm);         }     } } 

You can see there is nothing unusual here. Now, let's look at our mesh loading method. Again, it's similar to our previous methods. After loading the mesh, materials and textures, it ensures that the data controls normal information (and adds it if not). It then returns the resulting mesh. Use the code found in Listing 10.8.

Listing 10.8 Loading a Mesh with Normal Data
 private Mesh LoadMesh(string file,ref Material[] meshMaterials,ref Texture[] meshTextures) {     ExtendedMaterial[] mtrl;     // Load our mesh     Mesh mesh = Mesh.FromFile(file, MeshFlags.Managed, device, out mtrl);     // If we have any materials, store them     if ((mtrl != null) && (mtrl.Length > 0))     {         meshMaterials = new Material[mtrl.Length];         meshTextures = new Texture[mtrl.Length];         // Store each material and texture         for (int i = 0; i < mtrl.Length; i++)         {             meshMaterials[i] = mtrl[i].Material3D;             if ((mtrl[i].TextureFilename != null) &&                (mtrl[i].TextureFilename != string.Empty))             {                 // We have a texture, try to load it                 meshTextures[i] = TextureLoader.FromFile(device, @"..\..\" +                      mtrl[i].TextureFilename);             }         }     }     if ((mesh.VertexFormat & VertexFormats.Normal) != VertexFormats.Normal)     {         // We must have normals for our patch meshes         Mesh tempMesh = mesh.Clone(mesh.Options.Value,             mesh.VertexFormat | VertexFormats.Normal, device);         tempMesh.ComputeNormals();         mesh.Dispose();         mesh = tempMesh;     }     return mesh; } 

Now, much like our last example, we will need to create a render target surface to hold the environment map. We will do this in our reset event handler method:

 private void OnDeviceReset(object sender, EventArgs e) {     Device dev = (Device)sender;     rte = new RenderToEnvironmentMap(dev, CubeMapSize, 1,         Format.X8R8G8B8, true, DepthFormat.D16);     environment = new CubeTexture(dev, CubeMapSize, 1,         Usage.RenderTarget, Format.X8R8G8B8, Pool.Default); } 

Here we've created both our helper class and our cube texture. The larger the size you pass in (in our case the CubeMapSize constant), the more detailed the environment map is. In many cases, it may be more efficient for you to store the environment map statically, in which case you could simply load the cube texture from a file (or some other data source). However, we want to show the environment map being created dynamically based on the rendered scene, thus we create our render target textures.

Much like our last example where we rendered our scene into a single texture, this example will also require multiple renderings of our scene. In this case though, we will need to render it a total of six extra times (once for each face of our cube map). Add the method in Listing 10.9 to your application to handle rendering to the environment map.

Listing 10.9 Rendering Your Environment Map
 private void RenderSceneIntoEnvMap() {     // Set the projection matrix for a field of view of 90 degrees     Matrix matProj;     matProj = Matrix.PerspectiveFovLH((float)Math.PI * 0.5f, 1.0f,          0.5f, 1000.0f);     // Get the current view matrix, to concat it with the cubemap view vectors     Matrix matViewDir = ViewMatrix;     matViewDir.M41 = 0.0f; matViewDir.M42 = 0.0f; matViewDir.M43 = 0.0f;     // Render the six cube faces into the environment map     if (environment != null)         rte.BeginCube(environment);     for (int i = 0; i < 6; i++)     {         rte.Face((CubeMapFace) i , 1);         // Set the view transform for this cubemap surface         Matrix matView = Matrix.Multiply(matViewDir,                GetCubeMapViewMatrix((CubeMapFace) i));         // Render the scene (except for the teapot)         RenderScene(matView, matProj, false);     }     rte.End(1); } 

There's quite a bit going on in this method. First, we create a projection matrix with a field of view of 90 degrees, since the angles between each cube face happen to be 90 degrees. We then get the view matrix we've stored for this application and modify the last row so that we can combine this with the view matrices for each face.

Next, we call the BeginCube method on our helper class, letting it know we will be creating an environmental cube map. There are also other methods this helper class has that allow different types of environment maps to be created, including BeginHemisphere, BeginParabolic (which each use two surfaces, one for positive z, the other for negative z), and BeginSphere (which uses a single surface).

With the environment map ready to be rendered, we then start a loop for each face in our cube map. For each face, we first call the Face method, which is analogous to the BeginScene method we've used for our device, and texture renders. It signals that the last "face" has been completed, and a new one is ready to be rendered. We then combine the current view matrix with one we retrieve from the GetCubeMapViewMatrix method. The definition for this method is found in Listing 10.10.

Listing 10.10 Getting the View Matrix
 private Matrix GetCubeMapViewMatrix(CubeMapFace face) {     Vector3 vEyePt = new Vector3(0.0f, 0.0f, 0.0f);     Vector3 vLookDir = new Vector3();     Vector3 vUpDir = new Vector3();     switch (face)     {         case CubeMapFace.PositiveX:             vLookDir = new Vector3(1.0f, 0.0f, 0.0f);             vUpDir   = new Vector3(0.0f, 1.0f, 0.0f);             break;         case CubeMapFace.NegativeX:             vLookDir = new Vector3(-1.0f, 0.0f, 0.0f);             vUpDir   = new Vector3(0.0f, 1.0f, 0.0f);             break;         case CubeMapFace.PositiveY:             vLookDir = new Vector3(0.0f, 1.0f, 0.0f);             vUpDir   = new Vector3(0.0f, 0.0f,-1.0f);             break;         case CubeMapFace.NegativeY:             vLookDir = new Vector3(0.0f,-1.0f, 0.0f);             vUpDir   = new Vector3(0.0f, 0.0f, 1.0f);             break;         case CubeMapFace.PositiveZ:             vLookDir = new Vector3(0.0f, 0.0f, 1.0f);             vUpDir   = new Vector3(0.0f, 1.0f, 0.0f);             break;         case CubeMapFace.NegativeZ:             vLookDir = new Vector3(0.0f, 0.0f,-1.0f);             vUpDir   = new Vector3(0.0f, 1.0f, 0.0f);             break;     }     // Set the view transform for this cubemap surface     Matrix matView = Matrix.LookAtLH(vEyePt, vLookDir, vUpDir);     return matView; } 

As you can see, we simply modify the view parameters based on which face we are looking at, and return the view matrix based on these vectors. Finally, after we've done all that, we render the entire scene without the shiny car (the false parameter to the RenderScene method). There's a lot going on in this method as well. Add the code found in Listing 10.11.

Listing 10.11 Rendering Your Full Scene
 private void RenderScene(Matrix View, Matrix Project, bool shouldRenderCar) {     // Render the skybox first     device.Transform.World = Matrix.Scaling(10.0f, 10.0f, 10.0f);     Matrix matView = View;     matView.M41 = matView.M42 = matView.M43 = 0.0f;     device.Transform.View = matView;     device.Transform.Projection = Project;     device.TextureState[0].ColorArgument1 = TextureArgument.TextureColor;     device.TextureState[0].ColorOperation = TextureOperation.SelectArg1;     device.SamplerState[0].MinFilter = TextureFilter.Linear;     device.SamplerState[0].MagFilter = TextureFilter.Linear;     device.SamplerState[0].AddressU = TextureAddress.Mirror;     device.SamplerState[0].AddressV = TextureAddress.Mirror;     // Always pass Z-test, so we can avoid clearing color and depth buffers     device.RenderState.ZBufferFunction = Compare.Always;     DrawSkyBox();     device.RenderState.ZBufferFunction = Compare.LessEqual;     // Render the shiny car     if (shouldRenderCar)     {         // Render the car         device.Transform.View = View;         device.Transform.Projection = Project;         using (VertexBuffer vb = car.VertexBuffer)         {             using (IndexBuffer ib = car.IndexBuffer)             {                 // Set the stream source                 device.SetStreamSource(0, vb, 0,                     VertexInformation.GetFormatSize(car.VertexFormat));                 // And the vertex format and indices                 device.VertexFormat = car.VertexFormat;                 device.Indices = ib;                 device.SetTexture(0, environment);                 device.SamplerState[0].MinFilter = TextureFilter.Linear;                 device.SamplerState[0].MagFilter = TextureFilter.Linear;                 device.SamplerState[0].AddressU = TextureAddress.Clamp;                 device.SamplerState[0].AddressV = TextureAddress.Clamp;                 device.SamplerState[0].AddressW = TextureAddress.Clamp;                 device.TextureState[0].ColorOperation =                     TextureOperation.SelectArg1;                 device.TextureState[0].ColorArgument1 =                      TextureArgument.TextureColor;                 device.TextureState[0].TextureCoordinateIndex =                     (int)TextureCoordinateIndex.CameraSpaceReflectionVector;                 device.TextureState[0].TextureTransform =                      TextureTransform.Count3;                 device.Transform.World = Matrix.RotationYawPitchRoll(                     angle / (float)Math.PI, angle / (float)Math.PI * 2.0f,                     angle / (float)Math.PI / 4.0f);                 angle += 0.01f;                 device.DrawIndexedPrimitives(PrimitiveType.TriangleList,                     0, 0, car.NumberVertices, 0, car.NumberFaces);             }         }     } } 

Now that's a method. The first thing we will want to do is render the skybox. Since we know the skybox is a little smaller than we will need initially, we will scale it to 10 times its size. We then set the texture and sampler states necessary to render the skybox textures (feel free to look at MSDN help for further explanations on these states).

If you'll notice, we never cleared the device. Since we don't want to call this for every face we render, before we render the sky box (which we know will have the farthest depth of anything in our scene), we set the render state so that the depth buffer will always pass while the skybox is rendering. With that out of the way, we render the skybox and switch the depth buffer function back to normal. The skybox rendering method should look familiar:

 private void DrawSkyBox() {     for (int i = 0; i < skyboxMaterials.Length; i++)     {         device.Material = skyboxMaterials[i];         device.SetTexture(0, skyboxTextures[i]);         skybox.DrawSubset(i);     } } 

With the skybox now rendered, we need to decide if we'll be rendering the car as well, based on the passed in parameter. If we do, then we need to reset the view and projection transforms. Now, since we want to control the automatic texture coordinate generation here, we will not use the DrawSubset method on our mesh. Instead, we will use the vertex buffer and index buffer directly, and call DrawIndexedPrimitives on our device instead. Once we've got these two buffers, we need to ensure that our device is ready to render the mesh.

First, we set the stream source to the vertex buffer from our mesh. We set the vertex format to the correct format based on our mesh as well, and set the index buffer to the indices property. Lastly, we set the cube texture into the first stage before we set up our remaining state and draw our primitives.

The really important items here are these lines:

 device.TextureState[0].TextureCoordinateIndex =     (int)TextureCoordinateIndex.CameraSpaceReflectionVector; device.TextureState[0].TextureTransform = TextureTransform.Count3; 

Since we don't have texture coordinates defined in our car mesh (and actually, even if we did), the first line says that rather than using any defined texture coordinates, instead use the reflection vector (transformed into camera space) as the texture coordinates. These are automatically generated in the fixed function pipeline based on the vertex position and normal, which is why we needed to ensure that our mesh had normals. Lastly, since we are using a 3D cube map, the texture transform tells the texture to expect 3D texture coordinates.

With the texture coordinates out of the way, we can then update our world transform (by just using a crazy rotation algorithm) and make our call to draw our primitives. Our application is almost ready for running, but first we need to add our rendering method:

 protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) {     RenderSceneIntoEnvMap();     device.BeginScene();     RenderScene(ViewMatrix, Matrix.PerspectiveFovLH((float)Math.PI / 4,                this.Width / this.Height, 1.0f, 10000.0f), true);     device.EndScene();     device.Present();     this.Invalidate(); } 

For each render, first we render our environment map, and then we actually begin our "real" scene, and render it once more, this time including the car. With this, the application is ready for prime time.

After running the application, you should see a shiny reflective car spinning around the sky, much like in Figure 10.4.

Figure 10.4. A car with an environment map

graphics/10fig04.jpg



Managed DirectX 9 Graphics and Game Programming, Kick Start
Managed DirectX 9 Kick Start: Graphics and Game Programming
ISBN: B003D7JUW6
EAN: N/A
Year: 2002
Pages: 180
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net