Home > TutorialsTechniques
4.6. Normal Mapping 
In this tutorial you will learn how to apply normal mapping using BetaCell.



Click here to go to the forum discussion of this tutorial


 

4.6.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.6.2. Game Assets 
Game assets [2.3.2] for this tutorial:
As you will see in [4.6.5], in order to apply the normals in the normal map to the mesh, the tangent space of the needs to be calculated. In order to do that:
  1. Right click evilAnimatedTexturedB.bcm
  2. Click properties
  3. Set the 'Content Processor' to 'BetaCell_mesh_processor'
  4. Expand the attributes of the processor in the '+' sign
  5. Set 'calculate normals of mesh?' and 'calculate tangent space of mesh?' to true

 

4.6.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.Mesh.Util;
using BetaCell.Environment.Texture.Feeder;


 

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

There are no attributes other than the ones in [3.2.4].

 

4.6.5. Normal Mapping
default.jpg
Section 1 of the figure shows a flat surface, if it's illuminated by a direct light, every part of it will have the same color. Section 2, on the other hand, shows a bumpy surface; if it's illuminated by a direct light, the parts that face the light appear lighter and the ones that oppose the light appear darker.

This effect is desired when you are modeling surfaces that aren't smooth like a rock or sand. Section 3 shows the normals of the faces  of the  bumpy surface. These normals determine how dark is the surface when a light illuminates it.

Now, if what determines how the surface is lit are the normals, why bother creating the complex geometry of a rock? We can save both content creation and GPU work by setting the flat surface's normals to the ones of the bumpy surface as shown in section 4 and let the illumination algorithm lit the flat surface as shown in section 5.

default.jpg
The surface in section 1 starts with an inclination with a normal like the one in the left part of section 2a, then it has a declination with a normal like the one in the right part of section 2a, then it repeats this pattern so the normals also repeat.

A normal is defined in the [-1,1]  interval but an image can only store values in the [0,255] interval, the intervals are shown in section 2b. The conversion of the values in 2a can be seen in 2d, what it means is that 0.9 in the [-1,1] interval is 243 in the [0,255] interval.

The normals in the [0,255] interval are in 'texture space'. In 2c, you can see the texture space versions of the normals in section 2a, for example, the left normal of 2a is [-0.44,0.90] which in texture space is [72,243] which is the color shown in the left part of 2c. Notice that since we are downscaling the problem to 2D, the blue component of the color remains 0.
 

Section 3 of the figure above shows the normal map in colors that represents the bumpy surface of section 1.

default.jpg
Suppose we want to build a bumpy box like the one shown in 4, for that we would need to apply the normal map shown in 1 to the box shown in 2.

The result should be 3 but thinking like a computer, we are declaring to just replace the normals and this simple replacement can be seen in 5 (if you don't believe, go through each of the colors in 5 and draw the normal corresponding to the map in 1)

Normal mapping is done at a pixel level. to decide the final color of a pixel in the screen, the normal for the fragment is obtained from the map and lighting calculations are performed with that normal.

The trick is to 'fix' the normal before the lighting calculations to obtain 3 instead of 5.

default.jpg
To fix the normals we perform a change of frame. A very famous 2D frame is the coordinate system where the horizontal axis is the [1,0] vector and the vertical one is the [0,1] vector, this frame is represented as A in the figure.

In general, a 2D vector can be described as amounts in a frame's horizontal and vertical vectors. For example [0.44,0.9] is [0.44,0.9] in frame A.

Another frame is the one shown in B, where the horizontal and vertical vectors aren't the standard axis but arbitrary vectors. In it, the vertical axis is [0.71,0.71] and the horizontal one is [0.71,-0.71].

In frame B, vector [0.44,9.9] is [0.95,0.33].

default.jpg
The solution to the problem is to find a frame for each face, this frame is known as the tangent space of the face. Note in the figure that the box has 4 faces and that each has it's own tangent space, then we convert the normals in the map from texture space to the tangent space of the face before applying lighting calculations.

BetaCell has routines that calculate the tangent space of the faces of a mesh so that you don't have to mess with this.

 

Advertisement
 

4.6.6. The Initialize method 
We start by creating an effect part that alters the normals of a mesh given a normal map, that's done with the following call:

BCNormMap normalMap = new BCNormMap(
    content.Load<Texture2D>("Content/gameAssets/normalMap"),
    new Vector2(5, 5),
    new BCSampler(
        "LINEAR", "LINEAR", "LINEAR", "4", "WRAP", "WRAP", "0xffffff"
    )
);

The parameters of the constructor are:
  1. The normal map
  2. The scale of the normal map
  3. A sampler for the normal map
Then we create the visitor that applies the normal map effect:

BCDynamicEffectVisitor normalMapSetter = new BCDynamicEffectVisitor(
    normalMap.getFunction("Actuator.NormalMap"),
    normalMap,
    0
);


A relevant change with respect to tutorial [3.2] is the construction of the skinned effect feeder:

BCDynamicEffectVisitor skinnedSetter = new BCDynamicEffectVisitor(
    skinnedFeeder.getFunction("Actuator.Skinned.SkinnedMeshNorMap"),
    skinnedFeeder, 0
);
 
We use the SKINNED NORMAL MAP function instead of the SKINNED function because we need to update the tangent space of the faces of the mesh when it is animated. 

 

4.6.7. The Draw method
Nothing new, just draw the mesh:

evilMesh.Draw(gameTime);

 

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

Click here to go to the forum discussion of this tutorial
 
 

Advertisement
 

4.6.9. 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.Light.NormalMap;
using BetaCell.Environment.Texture.Feeder;

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;

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

            BCGlobalInfo.instance.VS_VERSION = "vs_3_0";
            BCGlobalInfo.instance.PS_VERSION = "ps_3_0";

            OnActivated(null, null);

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

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

            //-----
            //Normal Map Effect
            //-----

            BCNormMap normalMap = new BCNormMap(
                content.Load<Texture2D>("Content/gameAssets/normalMap"),
                new Vector2(5, 5),
                new BCSampler("LINEAR", "LINEAR", "LINEAR", "4", "WRAP", "WRAP", "0xffffff")
            );

            BCDynamicEffectVisitor normalMapSetter = new BCDynamicEffectVisitor(
                normalMap.getFunction("Actuator.NormalMap"),
                normalMap,
                0
            );

            //-----
            //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
            );

            //---------
            //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.SkinnedMeshNorMap"),
                skinnedFeeder, 0
            );

            evilMesh.visit(skinnedSetter);
            evilMesh.visit(normalMapSetter);
            evilMesh.visit(lightSetter);

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

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

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

            controller.update(time);
        }

        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);

            evilMesh.Draw(gameTime);

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