4 min read

Skyboxes - Part 2

The Skybox Shader

In the previous section we talked about how to create a cubemap that can be used for a skybox. In this second part, we will look at how to make a shader for a skybox. You’ll see that this shader isn’t too difficult to make, and when we get done in the next section, you’ll see that it produces pretty good results.

Create the Effect File

The first thing we need to do is to create the effect file. If you’ve been following the tutorials from one to the next, you are probably in the habit of just continuing on with the existing effect, but this time, we’re going to do a complete rewrite. Skyboxes are significantly different from what we’ve done in the past.

So start with a new effect file as we’ve done in the past. I’ve called mine “Skybox.fx”. You don’t need any of the stuff that XNA automatically generates for you, so feel free to delete everything in the file, because we’ll build ours from scratch. (Or if you really want, you can try to merge the code below with the existing effect file.)

The Effect Parameters

Our first step is to add the needed effect parameters. Below is the HLSL code we need to create the necessary effect parameters:

float4x4 World;
float4x4 View;
float4x4 Projection;

float3 CameraPosition;

Texture SkyBoxTexture;
samplerCUBE SkyBoxSampler = sampler_state
{
   texture = <SkyBoxTexture>;
   magfilter = LINEAR;
   minfilter = LINEAR;
   mipfilter = LINEAR;
   AddressU = Mirror;
   AddressV = Mirror;
};

Notice that we have the usual World, View, and Projection matrices. Additionally, we have added a parameter called CameraPosition, which will be important because everything will be in relation to where the camera is located at.

Below that we have the only real new contribution. We have our normal texture object which will store the skybox texture. Below that, we have a sampler, which is not too different from the texture sampler we used before in the texturing shader. The real difference is that this sampler is of type samplerCUBE instead of sampler2D. We will use this to pull out the correct value in the skybox, depending on what direction we are looking in.

Input and Output Structures

The next thing we need to add structs for the input to the vertex shader, as well as the output of the vertex shader (which is the same type as the input to the pixel shader). Both of these structs are pretty simple. The code below is what we’ll use:

struct VertexShaderInput
{
    float4 Position : POSITION0;
};

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

As far as the input goes, all we really care about is the position of the vertex. The output additionally has a texture coordinate that will be used to look up the appropriate value in the skybox cubemap. Notice, though, that the texture coordinate is a float3.

The Vertex Shader

The next step is to create our vertex shader. In the vertex shader, we are simply going to calculate the new transformed position of the vertices, like we have done plenty of times before, and additionally compute the texture coordinate for the skybox. The texture coordinate is determined by finding the vector between the camera position and the vertex’s position, which is calculated simply by subtracting the vertex’s position from the camera’s position.

The code below is all we need for our vertex shader:

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);
    output.TextureCoordinate = VertexPosition - CameraPosition;

    return output;
}

The Pixel Shader

The pixel shader will have the responsibility of looking up the texture coordinate in the skybox of each pixel that we draw. This is a pretty simple calculation, and our pixel shader is really only one line long:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return texCUBE(SkyBoxSampler, normalize(input.TextureCoordinate));
}

The Technique

The last component that we need to create is the technique itself. The technique is pretty simple, since it will only require one pass, and all we need to do is set the appropriate vertex and pixel shaders:

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

Everything Together

Before continuing on, I just wanted to put the entire shader in here. This is just assembling the parts of the shader that we talked about in the previous sections on this page.

float4x4 World;
float4x4 View;
float4x4 Projection;

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;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 TextureCoordinate : 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);
    output.TextureCoordinate = VertexPosition - CameraPosition;

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return texCUBE(SkyBoxSampler, normalize(input.TextureCoordinate));
}

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

With all of this done, we are ready to move on to the XNA game code in the next section, where we will create a separate class to handle our skybox.