Home > TutorialsTechniques
4.5. Environmental Mapping 
In this tutorial you will learn how to use cube maps as environment maps to create sky spheres and simulate reflection.



Click here to go to the forum discussion of this tutorial


 

4.5.1. Prerequisites
default.jpg
Before you start this tutorial make sure that you have:
  • Installed XNA Game Studio 3.1
  • Downloaded the starter project from the downloads section
  • Successfully run the starter project
  • This tutorial starts from [3.2] so ideally you should have already finished it.
 

4.5.2. Game Assets 
 

4.5.3. The using statements
Besides the using statements from the starter project, this tutorial needs the following using statements:

using BetaCell.Environment.Animation;
using BetaCell.Environment.Mesh;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Content;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Dynamic.Skinned;
using BetaCell.Environment.Terrain;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Effects;


 

4.5.4. Game Attributes 
The attributes that come with the starter project are explained in tutorial section 1.1.4.

For this tutorial we are going to be needing the following attributes:

BCSuroundingBox surrounding;
TextureCube environment;
BCEnvMapFeeder envMapFeeder;


 

4.5.5. Cube Mapping & Sky Sphere
default.jpg
There are many ways to describe a texture, not only a flat 2D surface. Cube Mapping for example is a texture composed of 6 2D images that each represent the face of a cube. In the figure, you can see how this is achieved: In the lower left part you can see the unfolded faces of a cube, if you fold it, you will get the figure in the lower right part.

Environmental Mapping is a special case of Cube Mapping. An environmental map can be seen in the upper part of the figure.

Notice how the textures shown in the environmental map, that correspond to each of the faces of the cube, are continuous at the edges with the same index. That is, if you are enclosed in the cube, and you turn around, you won't see where one face starts or when it ends.

Suppose you are at the center of the cube looking at face 1 and you start to turn counter clock wise around the Y axis. Seeing the figure, it's obvious that you won't notice the transition between face 1 and face 2. What's not so obvious is that you won't notice the transition between faces 3 and 4 either. This is because the texture is designed so that their edges match.
default.jpg
Unlike 2D flat textures, cube maps texture coordinates are described by a 3D vector. The final color of the fragment is obtained by shooting a ray from the center of the cube map with the direction given by the 3D texture coordinate.

In the figure, we downscale the mapping to 2D. If we define a circle whose 3D texture coordinates are rays from the center to each of its vertices, we would end up with the cube map ported to the circle.

This is the principle behind the sky sphere that will be used in this tutorial, we enclose the user in a sphere mapped to an environmental map and update the sphere's position so that the center will always be at the position of the camera, hence, creating an effect of a background infinitely far away.

 

4.5.6. Reflections 
default.jpg
When using environment maps [4.4.5] simulating reflections is simple. To calculate the 3D cube map texture coordinare, we shoot a ray v from the position of the camera through the view vector (the direction we are looking at). The normal n of the surface where v hits is used to calculate the reflection vector r, which is used as the cube map texture coordinale.

BetaCell performs this calculation in the graphics hardware so the end result is very efficient.

 

Advertisement
 

4.5.7. The Initialize method 
We start the portions of code relevant to this tutorial by creating the cube map out of an environmental map:

environment = content.Load<TextureCube>("Content/gameAssets/grassenvmap1024");.

To prepare the mesh for the reflection effect explained in [4.4.6], we create an effect feeder:

envMapFeeder = new BCEnvMapFeeder(environment,
    new BCSampler("LINEAR", "LINEAR", "LINEAR", "4",
        "WRAP", "WRAP", "0xffffff"));


The first parameter is the cube map and the second is a sampler [2.3.5].

After that, we create a visitor to plug the reflection effect in the mesh:

BCDynamicEffectVisitor envSetter = new BCDynamicEffectVisitor(
                envMapFeeder.getFunction("Shader.Texture.EnvMap.Basic"),
                envMapFeeder, BCDynamicEffect.MAX_FUNCTIONS);


The first parameter is the reflection function obtained from the feeder, the second is the feeder itself and the third states that the reflection function should be the as function applied at the mesh [2.5.5].

 

4.5.8. The OnActivated method
we create the sky sphere defined in 4.4.5:

//Surrounding Box
surrounding = new BCSuroundingBox(device, camera, environment);

The first parameter is the device, the second is the camera (reason why this object isn't constructed in the initialize method) and the las one is the cube texture.

 

4.5.9. The Update method
Here we change how reflective is our mesh over time.

What you need to know is that the envMapFeeder.mirrorFactor variable should be in the range [0,1], being 0 completely opaque (no mirror) and 1 completely reflective.

double time = gameTime.TotalGameTime.TotalSeconds;

envMapFeeder.mirrorFactor = ((float)time / 10) % 2;
if (envMapFeeder.mirrorFactor > 1)
{
    envMapFeeder.mirrorFactor = 2 - envMapFeeder.mirrorFactor;
}

The code is just algorithmics that transition the mirror factor from 0 to 1 and from 1 to 0 every 2 seconds.

 

4.5.10. The Draw method
There's a major difference with the previous draw methods. Before this tutorial, the first operation was to clear the back buffer. In this tutorial, since the sky box is infinitely far away, drawing it effectively clears both the color and depth buffer.

surrounding.draw(gameTime, 0);

The second parameter is the value to clear the stencil buffer in case you need it.

After that the mesh is drawn:

evilMesh.Draw(gameTime);


 

4.5.11. Conclusion
This tutorial is another example of the benefits of using BetaCell on your XNA project. You added the reflection with a mesh visitor without rewriting your shaders.

Click here to go to the forum discussion of this tutorial
 
 

Advertisement
 

4.5.12. Complete source code
using System;
using System.Collections.Generic;
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.Net;
using Microsoft.Xna.Framework.Storage;

using BetaCell.Debug;
using BetaCell.Util.GlobalInfo.Content;
using BetaCell.Environment.Camera;
using BetaCell.Util.GlobalInfo;


using BetaCell.Environment.Animation;
using BetaCell.Environment.Mesh;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Content;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Dynamic.Skinned;
using BetaCell.Environment.Terrain;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Effects;

namespace Starter
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Game attributes
        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        ContentManager content;

        BCFirstPersonHumanCamera camera;
        //end game attributes

        //This tutorial's attributes
        BCMesh evilMesh;

        BCAnimationSetBase animationSet;

        BCAnimationBase idle;
        BCAnimationBase run;
        BCAnimationBase idleToRun;
        BCAnimationBase runToIdle;

        BCAnimationController controller;

        BCAddSequenceListener animationSequence;

        //----------------------------
        //Animation Control Attributes
        //----------------------------

        double timeToChange = 5;
        double lastTime = 0;
        bool isIdle = true;

        //----------------------------
        //Env Map Attributes
        //----------------------------

        BCSuroundingBox surrounding;
        TextureCube environment;
        BCEnvMapFeeder envMapFeeder;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            content = new ContentManager(Services);
#if(XBOX360)
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
#else
            graphics.PreferredBackBufferWidth = 852;
            graphics.PreferredBackBufferHeight = 480;
#endif
        }

        protected override void Initialize()
        {
            //base initialization
            device = graphics.GraphicsDevice;

            BCLogger.instance.init(device, content);
            BCInitializationManager.initialize(content);

            OnActivated(null, null);

            //-----
            //MESHES
            //-----

            evilMesh =
                BCMeshReader.readMesh("Content/gameAssets/evilAnimatedTexturedB", null, device, content);

            //-----
            //Light Effect
            //-----

            BCBasicLight light = new BCBasicLight();
            light.range = 3000;
            light.transform = Matrix.CreateTranslation(-1f, -1f, -1f);
            light.cubAtten = 0;
            light.constAtten = 1;
            light.linAtten = 0;
            light.useShadows = false;

            BCDynamicEffectVisitor lightSetter = new BCDynamicEffectVisitor(
                light.getFunction("Shader.Light.Classic.PixelDirectionLight"), light, 1
            );

            evilMesh.visit(lightSetter);

            //---------
            //ANIMATION
            //---------
            animationSet = content.Load<BCAnimationSetBase>("Content/gameAssets/evilAnimatedTexturedBAnim");

            animationSet.animations.TryGetValue("Idle", out idle);
            animationSet.animations.TryGetValue("Run", out run);
            animationSet.animations.TryGetValue("IdleToRun", out idleToRun);
            animationSet.animations.TryGetValue("RunToIdle", out runToIdle);

            controller = new BCAnimationController(evilMesh.skeleton.boneCount);

            evilMesh.controller = controller;

            BCSkinnedMeshFeeder skinnedFeeder = new BCSkinnedMeshFeeder();
            BCDynamicEffectVisitor skinnedSetter = new BCDynamicEffectVisitor(
                skinnedFeeder.getFunction("Actuator.Skinned.SkinnedMesh"),
                skinnedFeeder, 0
            );
            evilMesh.visit(skinnedSetter);

            animationSequence = new BCAddSequenceListener();
            animationSequence.setNextAnimation(null, idle);
            controller.addSingle(idle, animationSequence, 0);

            //---
            //Env Map Effect
            //---
            environment = content.Load<TextureCube>("Content/gameAssets/grassenvmap1024");
            envMapFeeder = new BCEnvMapFeeder(environment,
                new BCSampler("LINEAR", "LINEAR", "LINEAR", "4", "WRAP", "WRAP", "0xffffff"));
            BCDynamicEffectVisitor envSetter = new BCDynamicEffectVisitor(
                envMapFeeder.getFunction("Shader.Texture.EnvMap.Basic"), envMapFeeder,
                    BCDynamicEffect.MAX_FUNCTIONS);
            evilMesh.visit(envSetter);

            base.Initialize();
        }

        protected override void OnActivated(object sender, EventArgs args)
        {
            buildViewMatrix();
            base.OnActivated(sender, args);
        }

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(6f, 2, 6f);
            Vector3 look = new Vector3(-7, -3.0f, -7);
            look.Normalize();

            camera = new BCFirstPersonHumanCamera(look, pos,
                MathHelper.PiOver4,
                (float)this.Window.ClientBounds.Width / (float)this.Window.ClientBounds.Height,
                1f, 200);

            //Surrounding Box
            surrounding = new BCSuroundingBox(device, camera, environment);

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.VIEW_INDEX,
                camera.getViewMatrix(-1)
            );

            BCGlobalInfo.instance.setMatrix(
                BCGlobalInfo.PROJECTION_INDEX,
                camera.getProjectionMatrix(-1)
            );
        }

        protected override void Update(GameTime gameTime)
        {
            double time = gameTime.TotalGameTime.TotalSeconds;

            if (time - lastTime > timeToChange)
            {
                lastTime = time;
                if (isIdle)
                {
                    if (animationSequence.setNextAnimation(idleToRun, run))
                    {
                        isIdle = false;
                    }
                }
                else
                {
                    if (animationSequence.setNextAnimation(runToIdle, idle))
                    {
                        isIdle = true;
                    }
                }
            }

            envMapFeeder.mirrorFactor = ((float)time / 10) % 2;
            if (envMapFeeder.mirrorFactor > 1)
            {
                envMapFeeder.mirrorFactor = 2 - envMapFeeder.mirrorFactor;
            }

            controller.update(time);
        }

        protected override void Draw(GameTime gameTime)
        {
            surrounding.draw(gameTime, 0);

            evilMesh.Draw(gameTime);

            BCLogger.instance.printFPS(gameTime);
            BCLogger.instance.flush();
            base.Draw(gameTime);
        }
    }
}