A helping hand for bedroom coders throughout the land.
Battle of Trafalgar for Windows Phone 7 - Part 2, Creating a Skybox

Anyone who, like me, has at some point cocked up their rendering code such that everything is rendered as a black silhouette, will appreciate that the default cornflower blue background does have some merit when you first start your XNA project! However, it is also something I want to replace pretty quickly.

In 3D apps, your replacement background generally needs to be based some sort of geometry onto which you can render images of the furthest visible extents of your world. Often people use spheres or hemispheres, in my case I went for a skybox - basically a cube. Here is a quick movie of the skybox drawable in action in phone compliant build:

http://www.youtube.com/v/g6m5P5kEXRU <p><a href="http://www.youtube.com/v/g6m5P5kEXRU">http://www.youtube.com/v/g6m5P5kEXRU</a></p>

In my previous implementations of this rendering element I have always used 3 bits.

  • A pre-created and appropriately UV mapped cube mesh
  • A skybox shader that, amongst other things, wraps a TextureCube image asset over the model.
  • An in-game drawable to call the shader on the cube model.

For the phone though we obviously can't use the shader option. Instead that in-game drawable is going to have to do everything using one of the built-in Reach effects. But which one?

Well the only Reach shader that supports a TextureCube is the EnvironmentMappedEffect, so initially I was drawn to that. However the reflection effect is not what we need here - in fact what we need is one of the simplest effects going really, just a simple texture mapped model with no dynamic lighting. So I've gone for the BasicEffect with all the lighting switched off.

This decision left me with a bit of a dilemma, because I would still like to use a TextureCube as the image asset so that any reflective objects could reflect a matching environment to the one shown in the Skybox by using the same asset. Luckily Charles Humphrey sent me a simple code snippet for stripping a TextureCube into an array of Texture2Ds (which the BasicEffect uses quite happily). It is clearly not the most efficient approach, because we are effectively duplicating the data, but it is extremely convenient to drop one TextureCube asset into your project to create both the skybox and matching reflections on the objects within it.

Next I decided that, since I was required to ditch the matching shader from the pipeline, I might as well ditch the need for pre-created geometry too. This is where the simplicity of a cube map came into its own as this can be trivially built at runtime.

OK - so the code for creating the geometry now looks like this:

Vector3[] normals =
{
    Vector3.Right,      // +X
    Vector3.Left,       // -X            
    Vector3.Up,         // +Y
    Vector3.Down,       // -Y
    Vector3.Backward,   // +Z
    Vector3.Forward     // -Z
};
 
Vector2[] tex_coord =
{               
    Vector2.One, Vector2.UnitY, Vector2.Zero, Vector2.UnitX, // +X                             
    Vector2.Zero, Vector2.UnitX, Vector2.One, Vector2.UnitY, // -X                  
    Vector2.Zero, Vector2.UnitX, Vector2.One, Vector2.UnitY, // +Y                    
    Vector2.Zero, Vector2.UnitX, Vector2.One, Vector2.UnitY, // -Y                     
    Vector2.UnitY, Vector2.Zero, Vector2.UnitX, Vector2.One, // +Z                 
    Vector2.UnitY, Vector2.Zero, Vector2.UnitX, Vector2.One, // -Z  
};
 
int index = 0;
// Create each face in turn.
foreach (Vector3 normal in normals)
{
    // Get two vectors perpendicular to the face normal and to each other.
    //Vector3 side1 = new Vector3(normal.Y, normal.Z, normal.X);
    Vector3 side1 = new Vector3(normal.Z, normal.X, normal.Y);
    Vector3 side2 = Vector3.Cross(normal, side1);
 
    // Six indices (two triangles) per face.
    AddIndex(CurrentVertex + 0);
    AddIndex(CurrentVertex + 1);
    AddIndex(CurrentVertex + 2);
 
    AddIndex(CurrentVertex + 0);
    AddIndex(CurrentVertex + 2);
    AddIndex(CurrentVertex + 3);
 
    // Four vertices per face.
    AddVertex((normal - side1 - side2) * size / 2, normal, tex_coord[index++]);
    AddVertex((normal - side1 + side2) * size / 2, normal, tex_coord[index++]);
    AddVertex((normal + side1 + side2) * size / 2, normal, tex_coord[index++]);
    AddVertex((normal + side1 - side2) * size / 2, normal, tex_coord[index++]);
}

and the texture array is created like this:

TextureCube tex_cube = Game.Content.Load<TextureCube>(m_texture_cube_asset_name);
Color[] pixel_array = new Color[tex_cube.Size * tex_cube.Size];
 
// Strip out the faces from the tex cube into a Texture2D array...
for (int s = 0; s < m_cube_faces.Length; s++)
{
    m_cube_faces[s] = new Texture2D(Game.GraphicsDevice, tex_cube.Size, tex_cube.Size, false, SurfaceFormat.Color);
    switch (s)
    {
        case 0:
            tex_cube.GetData<Color>(CubeMapFace.PositiveX, pixel_array);
            m_cube_faces[s].SetData<Color>(pixel_array);
            break;
        case 1:
            tex_cube.GetData<Color>(CubeMapFace.NegativeX, pixel_array);
            m_cube_faces[s].SetData<Color>(pixel_array);
            break;
        case 2:
            tex_cube.GetData<Color>(CubeMapFace.PositiveY, pixel_array);
            m_cube_faces[s].SetData<Color>(pixel_array);
            break;
        case 3:
            tex_cube.GetData<Color>(CubeMapFace.NegativeY, pixel_array);
            m_cube_faces[s].SetData<Color>(pixel_array);
            break;
        case 4:
            tex_cube.GetData<Color>(CubeMapFace.PositiveZ, pixel_array);
            m_cube_faces[s].SetData<Color>(pixel_array);
            break;
        case 5:
            tex_cube.GetData<Color>(CubeMapFace.NegativeZ, pixel_array);
            m_cube_faces[s].SetData<Color>(pixel_array);
            break;
    }
}

The trick to any sort of environment object is that it should never get any closer and it always remain behind the rest of your game geometry. The first part is dealt with in the DrawableGameComponent Update method:

public override void Update(GameTime gameTime)
{
    // Move the cube with the camera & apply any specified offset
    this.Position = m_camera.Position + this.PositionOffset;
    base.Update(gameTime);
}

Which simply moved the skybox with the camera.

The second aspect is dealt with by setting appropriate renderstates before drawing the skybox thus:

// no depth buffer of culling required
m_rasterizer_state.CullMode = CullMode.None;
m_depth_stencil.DepthBufferEnable = false;
GraphicsDevice device = basicEffect.GraphicsDevice;
            device.DepthStencilState = m_depth_stencil;
            device.RasterizerState = m_rasterizer_state;
            device.BlendState = m_blend_state;
            device.SamplerStates[0] = SamplerState.AnisotropicClamp;
 
            // Set our vertex declaration, vertex buffer, and index buffer.
            device.SetVertexBuffer(vertexBuffer);
            device.Indices = indexBuffer;
 
            // Step through the indexed mesh a face at a time changing the texture each time
            basicEffect.Texture = m_cube_faces[0];
            basicEffect.CurrentTechnique.Passes[0].Apply();
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Count, 0, 2);
 
            basicEffect.Texture = m_cube_faces[1];
            basicEffect.CurrentTechnique.Passes[0].Apply();
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Count, 6, 2);
 
            basicEffect.Texture = m_cube_faces[2];
            basicEffect.CurrentTechnique.Passes[0].Apply();
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Count, 12, 2);
 
            basicEffect.Texture = m_cube_faces[3];
            basicEffect.CurrentTechnique.Passes[0].Apply();
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Count, 18, 2);
 
            basicEffect.Texture = m_cube_faces[4];
            basicEffect.CurrentTechnique.Passes[0].Apply();
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Count, 24, 2);
 
            basicEffect.Texture = m_cube_faces[5];
            basicEffect.CurrentTechnique.Passes[0].Apply();
            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Count, 30, 2);

Not the most efficient, but it seems to work out OK.

The final thing to say is that, generally, you are going to want to render this object before any others so by default I set the DrawOrder property to be a very low number to ensure this happens.


Posted Thu, Mar 17 2011 2:48 PM by Edward Powell

Comments

Charles Humphrey wrote re: Battle of Trafalgar for Windows Phone 7 - Part 2, Creating a Skybox
on Thu, Mar 17 2011 2:57 PM

Cool, I think it needs seems.... lol

Binygal wrote re: Battle of Trafalgar for Windows Phone 7 - Part 2, Creating a Skybox
on Tue, Sep 20 2011 7:45 AM

Great post. can you upload the source for this example?