Tuesday, May 3, 2011

Creating a Sky Box

If your game is going to have a part that you play outside you will also need a sky. One of the most simple ways to get this is by simply using a cube map to create a sky box. What you will do is create a cube map that has all 6 faces mapped to a cube surrounding the player. Simple huh? Well it is and you should get results like this one:

Screenshot from the finished tutorial

Backreferences:
 You can download the starting source from here. It already has movement implemented the camera movement, directional keys to rotate it and Page Up/page Down to go forward/backward. When starting the application rotate the camera and then go back, this will allow you to see that the red thing around you is actually a cube. The cube vertices are extremely simple, just the position, so I had to create a new vertex declaration for them.

First thing to do to get from a cube to a sky box is to load the sky box. For this you will have to create a new TextureCube object and load it into the program, also we will set it as a parameter for the sky effect, even if the parameter is not already defined.


        TextureCube skyTex;
....
        protected override void LoadContent()
        {
 
            skyEffect = Content.Load<Effect>("SkyEffect");
 
            skyTex = Content.Load<TextureCube>("SkyBoxTex");
 
            skyEffect.Parameters["tex"].SetValue(skyTex);
 
            ....
        }

 As always the .... represent code that's in the source but not relevant here.
Now we have to go into the effect file and add the new texture and a sampler for it. because we are using a cube map we are going to have a TextureCube instead of the normal Texture2D we have used so far:


TextureCube tex;
sampler cubeSampler = sampler_state {
 texture = <tex>;
 AddressU = CLAMP;
 AddressV = CLAMP;
};

The clamp address modes aren't really required so you can remove them but if you see the edges of the cube you may want to put them back, those edges become visible because it reads too much texture and wraps around to the pixels on the other side, so the edges don't seamlessly connect anymore. The next thing to do if change our vertex output structure to accept a texture coordinate with 3 float values, we will later use this to access sample the cube map in the pixel shader:

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 PosTex   : TEXCOORD0;
};

 Because we have a axis aligned cube that will never rotate, we can just use the interpolated position of those vertices (without any other transformations) as our look-up vector. The problem comes that we need the SkyBox must always be further than any other objects. We could scale it so much to make sure it goes behind everything else or we could render it with a depth test as Never (always fails so it will be drawn only where there are no other objects). But a more elegant solution is to observe how the transformations happen. After the positions are interpolated the GPU divides them by the W component. After this divide all visible objects will have a Z value of 0 to 1 where 1 is the furthest possible. This is exactly what we need for our sky box, to make sure that the z/w divide will result in 0 we will set z to be equal to w using a simple swizzle. The resulting vertex shader is:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;
    output.Position = mul(input.Position, WVP).xyww;
    output.PosTex = input.Position;
 
    return output;
}

Now we only have to sample the texture in the pixel shader, for this we will use the texCUBE intrinsic function (first parameter is the sampler, second a float3 look-up):


float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return texCUBE(cubeSampler,input.PosTex);
}

If you run the program now it will look OK, if you rotate the camera it will still look OK, but if you move the illusion breaks, this is because you should never get any closer of further from the sky box, it's supposed to represent the horizon not something close. To fix this we must make sure that the sky box is always centered around us, easy fix, just make sure that the SkyWorld is a translation of our position (change it whenever we move):

    if (currentKeys.IsKeyDown(Keys.PageUp))
    {
        UpdateView(1);
        SkyWorld = Matrix.CreateTranslation(position);
    }
    if (currentKeys.IsKeyDown(Keys.PageDown))
    {
        UpdateView(-1);
        SkyWorld = Matrix.CreateTranslation(position);
    }

That's it, you should have a functional sky box. If it looks too 'boxy' just replace the box with a sphere, or ellipsoid or hemisphere (if you know you will always have land). This will improve the visual aspect of the sky. You can find the finished source code here and the result should look like this:

3 comments:

  1. Hi When I try to ran I get just window with red color.
    why?

    ReplyDelete
    Replies
    1. That is the starting project at the top of the tutorial, the finished project is at the bottom of the page.

      Here it is again:

      http://dl.dropbox.com/u/27752872/ILSSkyBoxComplete.rar

      Delete