8 min read

Skyboxes - Part 3

Using a Skybox in XNA

Now that we’ve got our skybox effect ready, we’ll move on to our XNA code. We will first create a class to handle the skybox stuff, and then use it in our game.

The Skybox Class

For simplicity, I’m just going to start by giving you the code for my skybox class. Create a new file (I’ve called mine “Skybox.cs”) and add this code to it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace Skyboxes
{
    /// <summary>
    /// Handles all of the aspects of working with a skybox.
    /// </summary>
    public class Skybox
    {
        /// <summary>
        /// The skybox model, which will just be a cube
        /// </summary>
        private Model skyBox;

        /// <summary>
        /// The actual skybox texture
        /// </summary>
        private TextureCube skyBoxTexture;

        /// <summary>
        /// The effect file that the skybox will use to render
        /// </summary>
        private Effect skyBoxEffect;

        /// <summary>
        /// The size of the cube, used so that we can resize the box
        /// for different sized environments.
        /// </summary>
        private float size = 50f;

        /// <summary>
        /// Creates a new skybox
        /// </summary>
        /// <param name="skyboxTexture">the name of the skybox texture to use</param>
        public Skybox(string skyboxTexture, ContentManager Content)
        {
            skyBox = Content.Load<Model>("Skyboxes/cube");
            skyBoxTexture = Content.Load<TextureCube>(skyboxTexture);
            skyBoxEffect = Content.Load<Effect>("Skyboxes/Skybox");
        }

        /// <summary>
        /// Does the actual drawing of the skybox with our skybox effect.
        /// There is no world matrix, because we're assuming the skybox won't
        /// be moved around.  The size of the skybox can be changed with the size
        /// variable.
        /// </summary>
        /// <param name="view">The view matrix for the effect</param>
        /// <param name="projection">The projection matrix for the effect</param>
        /// <param name="cameraPosition">The position of the camera</param>
        public void Draw(Matrix view, Matrix projection, Vector3 cameraPosition)
        {
            // Go through each pass in the effect, but we know there is only one...
            foreach (EffectPass pass in skyBoxEffect.CurrentTechnique.Passes)
            {
                // Draw all of the components of the mesh, but we know the cube really
                // only has one mesh
                foreach (ModelMesh mesh in skyBox.Meshes)
                {
                    // Assign the appropriate values to each of the parameters
                    foreach (ModelMeshPart part in mesh.MeshParts)
                    {
                        part.Effect = skyBoxEffect;
                        part.Effect.Parameters["World"].SetValue(
                            Matrix.CreateScale(size) * Matrix.CreateTranslation(cameraPosition));
                        part.Effect.Parameters["View"].SetValue(view);
                        part.Effect.Parameters["Projection"].SetValue(projection);
                        part.Effect.Parameters["SkyBoxTexture"].SetValue(skyBoxTexture);
                        part.Effect.Parameters["CameraPosition"].SetValue(cameraPosition);
                    }

                    // Draw the mesh with the skybox effect
                    mesh.Draw();
                }
            }
        }
    }
}

I think the comments in the file should explain the code pretty well, but let’s take a moment to look at this code to be sure. Basically, all the skybox class needs to do is load in three components: the box model, the skybox effect file, and whichever skybox texture the user wants. There are variables to store each of these so they can be used when drawing, and additionally, this code will load the needed stuff in the constructor.

The Draw() method is fairly similar to what we have done before with our other effects. Notice that our world matrix for drawing is done a little differently. In the Skybox class we have a size variable. This determines how big the skybox should be. When we draw, our world matrix will scale the box by this size, and also transform it by the camera position. This is done so that the skybox is centered around the camera, so that as you move through your scene, the skybox stays still, relative to the viewer. This will give the player the impression that it is very far away.

The Main Game Code

Now that we’ve got our skybox class created, we just need to add a few things to our main game class to use it. Chances are, you know how you want to use your skybox already, but just to be complete, I’ll explain what I’ve done to use my skybox in a simple XNA game. I have the following variables as instance variables at the top of my main game class:

Skybox skybox;
Matrix world = Matrix.Identity;
Matrix view = Matrix.CreateLookAt(new Vector3(20, 0, 0), new Vector3(0, 0, 0), Vector3.UnitY);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 600f, 0.1f, 100f);
Vector3 cameraPosition;
float angle = 0;
float distance = 20;

I have an instance of my Skybox class and in addition to that, I have my basic world, view, and projection matrices. I am also storing the camera position here because we will be updating that in the Update() method and using it in the Draw() method. I have decided to have my camera slowly spin around a center point to really see the skybox, so I’ve added in an angle and a distance for the camera to circle with. In a minute in the Update() method, I’ll increment the angle slightly at every update and recalculate my camera position and view matrices accordingly.

The next thing we need to do is create our skybox. So in the LoadContent() method, I’ve added the following line to load my skybox:

skybox = new Skybox("Skyboxes/Sunset", Content);

That is all you need to actually load the skybox. Of course, make sure you use the name of your skybox image here. Mine was the [*http://rbwhitaker.wikidot.com/local–files/texture-library/Sunset.zip Sunset one], so that’s what I’ve put here.

Now like I said before, in the Update() method, I’ve incremented the angle, and recalculated the camera location and view matrices with the code below, but in your game, you’ll just do whatever you need to do. Just make sure in the end, you have the view matrix and camera location that you want.

angle += 0.002f;
cameraPosition = distance * new Vector3((float)Math.Sin(angle), 0, (float)Math.Cos(angle));
view = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 0), Vector3.UnitY);

All that is left now is to draw our skybox, which can be done with just a few lines of code. So in your Draw() method, add the following code:

graphics.GraphicsDevice.RasterizerState.CullMode = CullMode.CullClockwiseFace;
skybox.Draw(view, projection, cameraPosition);
graphics.GraphicsDevice.RasterizerState.CullMode = CullMode.CullCounterClockwiseFace;

Obviously, the core of this code is the skybox.Draw() command. But notice that we are also changing the CullMode render state. This particular cube model has the surfaces of the cube facing outwards. The graphics card will usually determine whether or not a triangle is facing the camera and ignore (or “cull”) triangles that are facing away from the camera, to save itself a lot of work By setting the cull mode to CullMode.None', we are saying “draw only the backward triangles”. It is important to do this because otherwise all of the faces will be culled and you won’t see anything! Alternatively, you could create a different cube model that has the surfaces facing inward. After our skybox is done being drawn, we switch the cull mode back to the standard mode so that other things that we draw will work correctly.

Handling the No Skybox Problem

I’m going to head off one concern/problem that people will often have when they run their program. You run your game, and all you see is a blank screen where your skybox should be. (Everything else in your game may look right.)

If this happens, don’t panic! There’s a perfectly logical explanation. Depending on the format that you saved your skybox in (or the format of one that you downloaded, either from me, or anywhere else on the Internet) and some of the properties that you set in XNA. Game Studio, you may have a problem where nothing shows up.

If you see this problem, there’s an easy fix for it.

  1. Right-click on the skybox image that you are working with and open up the Properties.
  2. Click on the little arrow by Content Processor to open up the group.
  3. Change Premultiply Alpha to false. We don't want to premultiply alpha with this shader.

Viewing Your Skybox

At this point, we are ready to run our game, and we should see the skybox being drawn. Remember that you may need to change the size of your skybox to prevent it from being beyond the far clipping plane and being cut out, but still large enough that the rest of your scene doesn’t run into it.

When you run it, you should see something like the following:

My entire main game code can be found below, and below you can also download my entire solution if you want to play around with it.

Skybox.zip
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace Shaders
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Effect effect;

        Skybox skybox;
        Matrix world = Matrix.Identity;
        Matrix view = Matrix.CreateLookAt(new Vector3(20, 0, 0), new Vector3(0, 0, 0), Vector3.UnitY);
        Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 600f, 0.1f, 100f);
        Vector3 cameraPosition;
        float angle = 0;
        float distance = 20;

        Vector3 viewVector;

        Model model;
        Texture2D texture;
        private Texture2D normalMap;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            model = Content.Load<Model>("Models/Helicopter");
            effect = Content.Load<Effect>("Effects/NormalMap");
            texture = Content.Load<Texture2D>("Textures/HelicopterTexture");
            normalMap = Content.Load<Texture2D>("Textures/HelicopterNormalMap");

            skybox = new Skybox("Skyboxes/Sunset", Content);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            cameraPosition = distance * new Vector3((float)Math.Sin(angle), 0, (float)Math.Cos(angle));
            Vector3 cameraTarget = new Vector3(0, 0, 0);
            viewVector = Vector3.Transform(cameraTarget - cameraPosition, Matrix.CreateRotationY(0));
            viewVector.Normalize();

            angle += 0.002f;
            view = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 0), Vector3.UnitY);

            base.Update(gameTime);
        }
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            RasterizerState originalRasterizerState = graphics.GraphicsDevice.RasterizerState;
            RasterizerState rasterizerState = new RasterizerState();
            rasterizerState.CullMode = CullMode.None;
            graphics.GraphicsDevice.RasterizerState = rasterizerState;

            skybox.Draw(view, projection, cameraPosition);

            graphics.GraphicsDevice.RasterizerState = originalRasterizerState;

            DrawModelWithEffect(model, world, view, projection);

            base.Draw(gameTime);
        }

        private void DrawModel(Model model, Matrix world, Matrix view, Matrix projection)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    effect.World = world * mesh.ParentBone.Transform;
                    effect.View = view;
                    effect.Projection = projection;
                }
                mesh.Draw();
            }
        }

        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["ViewVector"].SetValue(viewVector);
                    effect.Parameters["ModelTexture"].SetValue(texture);
                    effect.Parameters["NormalMap"].SetValue(normalMap);

                    Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform * world));
                    effect.Parameters["WorldInverseTranspose"].SetValue(worldInverseTransposeMatrix);
                }
                mesh.Draw();
            }
        }
    }
}