Creating a Reflection Shader

Introduction

In this tutorial, we will create a shader that does reflection. Reflection in real life is a complicated situation. Rays of light bounce off of an object and into the camera. Or worse: they bounce off of multiple objects before finally getting to the camera. With reflection, you can see reflections of objects in other objects, or even reflections of part of an object in other parts of the same object. This takes quite a bit of computing power to do, and in a game, we don't really have the ability to do this. This idea is called ray tracing, and it hasn't really been done "live" in a game until just recently. We will use an older method of doing reflection called environment mapping.

Basically, to do environment mapping, we have a texture of the world around us, also called an environment map. This is where our skybox comes into play. We will use the stuff we did in the skybox shader to get going, and if you haven't done that tutorial yet, I'd suggest you go back to it. We will almost always want a skybox in a game where we are using environment mapping so that the object is reflecting stuff in the scene.

In this tutorial, we're going to create a completely new shader for reflection, but we'll just add our object to the XNA code that we created in the last tutorial.

Because environment mapping is a simplified version of full ray tracing or reflection, it will have a couple of limitations that may be noticeable (although your typical player won't notice it too much). First, the object will only reflect the skybox, not other objects in the scene. Second, along the same lines, you won't find reflections of the object itself either. There are some more advanced techniques that will let you deal with these problems, but for now, we're just interested in a simpler version of reflection. You may be surprised what you can accomplish with just this simple environment mapping shader.

screenshot1.png

The Reflection Shader

The basic idea of reflection is simple. The basic process that we will use is to take the vector that goes from the camera position to the object and reflect it off of the surface, based on the surface's normal. This will give us another vector in the scene that is pointing out to our skybox somewhere, and we will just look up where the reflected vector is pointing at and color our pixel the appropriate color.

Our shader is not too complicated—only a little bit more complicated than our skybox shader. Like with the skybox shader, we'll start from scratch and create each of the components of our effect file and discuss them. At the end, I've posted the entire effect file.

Go ahead and create a new effect file (I've put mine in a folder called "Effects" and called it "Reflection.fx") in your game's Content node. Once again, you can just delete everything that is in the file, because we'll start from scratch.

The Effect Parameters

The first thing we'll do is to add the necessary effect parameters. Most of these should look familiar to you, if you've already looked at the skybox tutorial. Add the following to your empty effect file:

float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 WorldInverseTranspose;
 
float4 TintColor = float4(1, 1, 1, 1);
float3 CameraPosition;
 
Texture SkyboxTexture; 
samplerCUBE SkyboxSampler = sampler_state 
{ 
   texture = <SkyboxTexture>; 
   magfilter = LINEAR; 
   minfilter = LINEAR; 
   mipfilter = LINEAR; 
   AddressU = Mirror;
   AddressV = Mirror; 
};

The first four are all ones that we've seen before. The next one, TintColor, is simply used to give the surface a tint color, but we'll just have it be white by default. You can come back and play around with this later, once the effect is working. The next one, CameraPosition, should be similar to what we saw in the skybox effect. This will just contain the position of the camera in the scene. The next one, SkyboxTexture, is the texture we are using for the skybox or the environment map. After that, we have our cube map sampler, which is the same as our skybox sampler from before.

The Input and Output Structures

The next thing we will need to create is our structs for our input to the vertex shader and the output to the vertex shader. The code for these is below:

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float4 Normal : NORMAL0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 Reflection : TEXCOORD0;
};

Notice that our input requires the vertex's position and normal. We need the position for obvious reasons, and the normal is required to calculate the reflection vector. Our output has the finished position, as well as the reflection texture coordinate, once again as a float3.

The Vertex Shader

The vertex shader is where we will do most of our work. The code for our vertex shader is below:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
 
    float4 VertexPosition = mul(input.Position, World);
    float3 ViewDirection = CameraPosition - VertexPosition;
 
    float3 Normal = normalize(mul(input.Normal, WorldInverseTranspose));
    output.Reflection = reflect(-normalize(ViewDirection), normalize(Normal));
 
    return output;
}

Here, we first calculate the vertex's final screen position, like usual. Next, we calculate the vertex's position in the world, so that we can calculate the view direction. In the next line, we calculate where the vertex's normal is, after the world transformation. In the last line before we return the final output value, we perform the reflection. Notice that the reflection is taken care of with another intrinsic function called reflect(). The math for reflection isn't horribly complicated, but it is nice that we don't have to worry about it ourselves. This function takes the view direction and reflects it off of the surface with a given surface normal. So we just give it the values that we just calculated and we're done. Notice, though that we've normalized the view and normal vectors here. This is important, so don't leave it out, or things won't look quite right.

Well, that's the hardest part, and now we're ready to move on to the pixel shader.

The Pixel Shader

The pixel shader is quite simple in comparison to the vertex shader. By the time we get to the pixel shader, we already know what direction the pixel is reflecting in, and we just need to take that direction and determine what color it should be, based on the texture. The code for our pixel shader should look something like this:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return TintColor * texCUBE(SkyboxSampler, normalize(input.Reflection));
}

We just look up the texture color in the skybox, and then factor in the tint color.

The Technique Code

The only thing that is left is to create the code for defining the technique, which is pretty simple:

technique Reflection
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Nothing here should be much of a surprise, so let's continue on!

The Assembled Shader Code

Like I promised, here is the entire shader code all assembled together:

float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 WorldInverseTranspose;
 
float4 TintColor = float4(1, 1, 1, 1);
float3 CameraPosition;
 
Texture SkyboxTexture; 
samplerCUBE SkyboxSampler = sampler_state 
{ 
   texture = <SkyboxTexture>; 
   magfilter = LINEAR; 
   minfilter = LINEAR; 
   mipfilter = LINEAR; 
   AddressU = Mirror; 
   AddressV = Mirror; 
};
 
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float4 Normal : NORMAL0;
};
 
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 Reflection : TEXCOORD0;
};
 
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
 
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);
 
    float4 VertexPosition = mul(input.Position, World);
    float3 ViewDirection = CameraPosition - VertexPosition;
 
    float3 Normal = normalize(mul(input.Normal, WorldInverseTranspose));
    output.Reflection = reflect(-normalize(ViewDirection), normalize(Normal));
 
    return output;
}
 
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return TintColor * texCUBE(SkyboxSampler, normalize(input.Reflection));
}
 
technique Reflection
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

The XNA Code

Probably by this point, you already have a pretty good idea about how to go about using this effect file, but just in case, I thought it would be a good idea to go through it just to be sure.

I'm just adding on to the source code that I provided at the end of the skybox tutorial.

The first thing that we'll need is a model to draw. I'm just using the UntexturedSphere model that is in the 3D Model Library, but feel free to use any model that you want. The model doesn't need to be textured, since we will be using our environment map/skybox for that. Of course, you could factor in the texture color of a model using the stuff we talked about back in the texturing tutorial and create a very cool effect, too.

Once you get your model ready to be loaded, we're ready to continue on. I've added the following three variables as instance variables to my class:

private Model model;
private Effect effect;
private TextureCube skyboxTexture;

The first will store the model itself, the second will store the reflection effect that we just created, and the skyboxTexture variable will store our skybox texture in a similar way to what we did in the Skybox class.

The next thing we need to do is to load these three variables in the LoadContent() method, which is done with this code:

skyboxTexture = Content.Load<TextureCube>("Skyboxes/Sunset");
model = Content.Load<Model>("Models/UntexturedSphere");
effect = Content.Load<Effect>("Effects/Reflection");

I'm just using the same skybox texture that I've used for my Skybox object. If you don't, it will look a little funny (reflecting the wrong sky). Notice that the type of this is TextureCube rather than Texture2D. The other two should be familiar to you by now.

Now all we need to do is draw our model with the correct effect. I've created a separate method to do this, which looks like this:

private void DrawModelWithEffect(Model model, Matrix world, Matrix view, Matrix projection)
{
    foreach (ModelMesh mesh in model.Meshes)
    {
        foreach (ModelMeshPart part in mesh.MeshParts)
        {
            part.Effect = effect;
            effect.Parameters["World"].SetValue(world * mesh.ParentBone.Transform);
            effect.Parameters["View"].SetValue(view);
            effect.Parameters["Projection"].SetValue(projection);
            effect.Parameters["SkyboxTexture"].SetValue(skyboxTexture);
            effect.Parameters["CameraPosition"].SetValue(this.cameraPosition);
            effect.Parameters["WorldInverseTranspose"].SetValue(
                                    Matrix.Transpose(Matrix.Invert(world * mesh.ParentBone.Transform)));
        }
        mesh.Draw();
    }
}

This, too, should probably be pretty familiar to you.

Lastly, we just need to call this method from your Draw() method, and we should be good to go. So add the following line of code to your Draw() method:

DrawModelWithEffect(model, world, view, projection);

My full main game code is below.

You should be able to run this now, and see your reflection working!

My complete code for this tutorial can be found here:

Reflection.zip

What's Next?

We've come a long way with our shaders. The next step might be to try out the refraction and simple glass shaders, as well as the toon shader and the post processing effects.


Troubleshooting.png Having problems with this tutorial? Try the troubleshooting page!