Dynamic Environment Mapping
You are reading the XNA 3.1 version of this tutorial.
Hi and welcome back to my XNA Shader Programming tutorial. Last time we made a transmittance post process shader, making objects look transparent in a more proper way than normal alpha blending. Today we are going to build on Tutorial 13, so if you haven't yet done that one, now is the time. But, if you just want to learn dynamic environment mapping using a cube map, here is the place!
Dynamic Environment Mapping
First of all, what is a dynamic environment map? A dynamic environment map is a texture that represents the environment around a given mesh, and is generated each frame. The texture is a special kind of texture, a cube texture, containing six 2D textures:
As you can see in 15.1, the cube map is a "wrapped up" cube. Each side of the cube represents one picture of the environment. For each side of the cube, we need to set up a camera that looks in the right direction( along the positive X axis, negative X axis, positive Y axis ... ), and render the scene from the mesh that will have the environment mapping applied, without the mesh itself! This is because we want to render what will be reflected on the mesh, and not the insides of the mesh.
Once we got the cube map rendered, we can pass this into a shader that will use the environment cube map as a lookup table using a reflection vector. The reflection vector can be created like this (as we have seen in many of the previous tutorials):
R = 2 * N.L * N - L
Once we got the cube map rendered, we can pass this into a shader that will use the environment cube map as a lookup table using a reflection vector. The reflection vector can be created like this (as we have seen in many of the previous tutorials):
R = 2 * N.L * N - L
R is the reflection vector, L is the light direction and N is the Normal of the surface the light is reflected on.
Once we have the reflection vector, we can use this as a lookup texture into a cube map. The lookup in a cube map works by passing in a vector. The largest number in the vector will decide what face (one of the six textures) it will use, and the remaining two components will find what UV coordinate it will pick from the selected face.
In the real world, only 100% reflective objects will reflect all the light. Usually, lights get scattered and refracted inside the mesh, continuing it's journey inside the mesh until it finds a way out or is turned in to another for of energy. Today we are going to implement reflection and in the next tutorial I will implement refraction as well.
Once we have the reflection vector, we can use this as a lookup texture into a cube map. The lookup in a cube map works by passing in a vector. The largest number in the vector will decide what face (one of the six textures) it will use, and the remaining two components will find what UV coordinate it will pick from the selected face.
In the real world, only 100% reflective objects will reflect all the light. Usually, lights get scattered and refracted inside the mesh, continuing it's journey inside the mesh until it finds a way out or is turned in to another for of energy. Today we are going to implement reflection and in the next tutorial I will implement refraction as well.
Implementing the shader
Let's see how we can implement reflection, using a cube map, in a shader. The shader will need to have the cube texture, and amd calculate the reflection vector. Let's do this, by first declaring a global cube texture that will be set from the application:
texture ReflectionCubeMap; samplerCUBE ReflectionCubeMapSampler = sampler_state { texture = < |
We create a normal texture object, and a samplerCUBE for that texture. the samplerCUBE contains the ability to use a 3D vector as a texture coordinate instead of a 2D vector like we usually use.
Now, we got our cube texture, let's get the reflection vector and use that as a loopup vector in our samplerCUBE.
As you know, we already got the reflection vector in our specular shader, but just to remind you, I'll post the code:
Now, we got our cube texture, let's get the reflection vector and use that as a loopup vector in our samplerCUBE.
As you know, we already got the reflection vector in our specular shader, but just to remind you, I'll post the code:
float Diff = saturate(dot(L, N)); // Calculate reflection vector float3 Reflect = normalize(2 * Diff * N - L); |
Now, we use Reflect to look up a pixel in the cubemap:
float3 ReflectColor = texCUBE(ReflectionCubeMapSampler, Reflect); |
Thats it! If you want a 100% reflective object, just return ReflectColor from the pixel shader.
But we want some more, like ambient, diffuse and specular color. It's the same equation as in tutorial 3, specular mapping, but with the ReflectColor multiplied with ambient, diffuse and specular:
But we want some more, like ambient, diffuse and specular color. It's the same equation as in tutorial 3, specular mapping, but with the ReflectColor multiplied with ambient, diffuse and specular:
return Color*vAmbient*float4(ReflectColor,1) + Color*vDiffuseColor * Diff*float4(ReflectColor,1) + vSpecularColor * Specular*float4(ReflectColor,1); |
That's it for the environment mapping shader, using cube maps! Not very hard ey'??
Using the shader
To use the shader, we need to generate the cube map texture, render the scene in to it and pass it to the shader.
Luckily for us, XNA got support for Cube maps and have made them really simple to use!
Let's start by declaring a cube texture and a cube render target. The render target will be used to render our scene, and contains six render targets, one for each face of the cube. Also, we need to copy this, like before, into a texture so we can pass it to the shader.
Let's start by first declaring two global variables:
Luckily for us, XNA got support for Cube maps and have made them really simple to use!
Let's start by declaring a cube texture and a cube render target. The render target will be used to render our scene, and contains six render targets, one for each face of the cube. Also, we need to copy this, like before, into a texture so we can pass it to the shader.
Let's start by first declaring two global variables:
RenderTargetCube RefCubeMap; TextureCube EnvironmentMap; |
... and then initialize them:
RefCubeMap = new RenderTargetCube(this.GraphicsDevice, 256, 1, SurfaceFormat.Color); |
The RenderTargetCube function needs the graphics device object, the size of each texture (in this case, 256x256), number of levels and the surface format. As the scene is rendered six times pr. frame, we want to set the size of the cube map to as small as possible without loosing visual quality. Also, you only need to set the size of the texture for one of the sides, as the cube texture MUST be a square (..., 64x64, 128x128, 256x256 ...).
Next, we need to pass the texture to our shader:
Next, we need to pass the texture to our shader:
effect.Parameters["ReflectionCubeMap"].SetValue(EnvironmentMap); |
and finally, render the scene into the different sides of the cube render target, and copy these from the render target and into our environment texture, EnvironmentMap.
for (int i = 0; i < 6; i++) { // render the scene to all cubemap faces CubeMapFace cubeMapFace = (CubeMapFace)i; switch (cubeMapFace) { case CubeMapFace.NegativeX: { viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Left, Vector3.Up); break; } case CubeMapFace.NegativeY: { viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Down, Vector3.Forward); break; } case CubeMapFace.NegativeZ: { viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Backward, Vector3.Up); break; } case CubeMapFace.PositiveX: { viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Right, Vector3.Up); break; } case CubeMapFace.PositiveY: { viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Up, Vector3.Backward); break; } case CubeMapFace.PositiveZ: { viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Forward, Vector3.Up); break; } } effect.Parameters["matWorldViewProj"].SetValue(worldMatrix * viewMatrix * projMatrix); // Set the cubemap render target, using the selected face this.GraphicsDevice.SetRenderTarget(0, RefCubeMap, cubeMapFace); this.GraphicsDevice.Clear(Color.White); this.DrawScene(false); } graphics.GraphicsDevice.SetRenderTarget(0, null); this.EnvironmentMap = RefCubeMap.GetTexture(); |
The first thing we are doing is to make a loop, iterating through all of the six faces in the cube map ( See Fig 15.1 to see what face belongs to what number in the loop ). The face is stored in a CubeMapFace variable, that can contain a number [0,5].
Now, as we know what face we are currently on, we must set up the camera properly, making it look in the right direction before rendering the scene. We simply use the build in XNA function Matrix.CreateLookAt(Position, Target, Up). We know that the object is located at 0.0, and therefore can use the Vector.Up, Vector.Down++ to set the target so the camera is pointing the correct way.
Once this is done, we can render the scene. Notice that we have a boolean variable in our custom draw method. This indicates if we are drawing the transmitter or the environment. In the case of rendering the environment, we don't want to render the transmitter, but the environment around the transmitter. In the draw loop, i simply have a if statement that renders the transmitter if we pass true to our draw function, and the environment if we pass false to it.
Once we have rendered all six faces, we can restore the normal render target, and copy the cubemap we just generated into a texture.
That's it for this tutorial, hope you found this useful, and any feedback is appreciated!
Now, as we know what face we are currently on, we must set up the camera properly, making it look in the right direction before rendering the scene. We simply use the build in XNA function Matrix.CreateLookAt(Position, Target, Up). We know that the object is located at 0.0, and therefore can use the Vector.Up, Vector.Down++ to set the target so the camera is pointing the correct way.
Once this is done, we can render the scene. Notice that we have a boolean variable in our custom draw method. This indicates if we are drawing the transmitter or the environment. In the case of rendering the environment, we don't want to render the transmitter, but the environment around the transmitter. In the draw loop, i simply have a if statement that renders the transmitter if we pass true to our draw function, and the environment if we pass false to it.
Once we have rendered all six faces, we can restore the normal render target, and copy the cubemap we just generated into a texture.
That's it for this tutorial, hope you found this useful, and any feedback is appreciated!
NOTE:
You might have noticed that I have not used effect.commitChanges(); in this code. If you are rendering many objects using this shader, you should add this code in the pass.Begin() part so the changed will get affected in the current pass, and not in the next pass. This should be done if you set any shader parameters inside the pass.
You might have noticed that I have not used effect.commitChanges(); in this code. If you are rendering many objects using this shader, you should add this code in the pass.Begin() part so the changed will get affected in the current pass, and not in the next pass. This should be done if you set any shader parameters inside the pass.