XNA Shader Programming – Tutorial 15, Dynamic environment mapping

XNA Shader Programming
Tutorial 15, Dynamic environment mapping

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 havent 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:


                                                               Fig 15.1
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 envirnoment 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


                                                               Fig 15.2
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 untill 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 = <ReflectionCubeMap>;    
};
 
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:

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:

return Color*vAmbient*float4(ReflectColor,1) + Color*vDiffuseColor * Diff*float4(ReflectColor,1) + vSpecularColor * Specular*float4(ReflectColor,1);

Thats if 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:

RenderTargetCube RefCubeMap;
TextureCube EnvironmentMap;

.. and then initialise 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, 256×256 ), 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 ( …, 64×64, 128×128, 256×256 … ).

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 containg 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 environmen. 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 usefull, and any feedback is apprechiated!

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 paramteres inside the pass.

YouTube – XNA Shader programming, Tutorial 15 – Dynamic environment mapping
  

Download: Executable + Source

Advertisements
This entry was posted in XNA Shader Tutorial. Bookmark the permalink.

8 Responses to XNA Shader Programming – Tutorial 15, Dynamic environment mapping

  1. Unknown says:

    This blog is very informative. I love blogging when it is for or about something I believe in. I also read news blogs often and find that it makes me feel more intelligent every time I read them. I also feel like I am a pretty good person who tries to treat others with respect, no matter what their opinion is. There are some real haters out there. Thanks again. I learned a few things about blogging. I will definitely put your site on my speed dial.Thanks http://www.vanjobb.huhttp://www.vanjobb.hu/mobilinternet.php?11http://www.vanjobb.hu/Bankbetetkamatok.php?22http://www.vanjobb.hu/nevnapokdatumszerint.php?39http://www.vanjobb.hu/nevnapoknaptar.php?41http://www.vanjobb.hu/nevnapok.php?40

  2. laptopchina.com says:

    very good!

  3. josafassj says:

    Have you ported this code to XNA 4? I’ve tried but it renders a white screen.

  4. SpyderTL+ says:

    You may have your +Z and -Z Matrices swapped. If +X is to the right, then -Z should be Vector3.Forward, and +Z should be backward, unless I’m missing something.

  5. SpyderTL+ says:

    Well, after actually trying it, it turns out your matrices are correct, although I’m not sure why. Maybe someone can explain it to me.

  6. AmazingFactory says:

    DirectX is hard coded to Left Handed coordinate system when sampling cube maps (so when you turn your head from right to forward your cube sampling vector will change from (1, 0, 0) to (0, 0, 1) instead of (0, 0, -1) in a right handed system. That is why he is rendering the Forward view to PositiveZ. But then you need to flip the right handed sampling vector Z component: texCUBE(s, t * float3(1, 1, -1));

  7. Franc says:

    Hi,
    is there a version for xna 4.0.. got some problems translating it from 3.0 to 4.0.. (kind of new to this 3d shading and stuff)

    with regards…

  8. felipe says:

    using xna 4.0 I make the following changes:

    renderTarget = new RenderTargetCube(
    GraphicsDevice,
    256, //this can be changed
    true,
    GraphicsDevice.DisplayMode.Format,
    DepthFormat.Depth16); //this can be changed
    TextureCube scene = new TextureCube(
    GraphicsDevice,
    256, //use whatever you like
    true,
    GraphicsDevice.DisplayMode.Format);

    and then for each face I did like the following:

    /// Render bottom
    GraphicsDevice.SetRenderTarget(renderTarget, CubeMapFace.PositiveY);
    camera.SetBottomViewRender(new Vector3(0, -18, 0));
    DrawAllGameObjects();

    and after each face was drawn, i wrapped it up like this

    GraphicsDevice.SetRenderTarget(null);
    scene = (TextureCube)renderTarget;

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s