Home > TutorialsTechniques
4.10. Displacement Mapping 
In this tutorial you will combine the environmental mapping, the normal mapping and displacement mapping  to create a water effect.



Click here to go to the forum discussion of this tutorial

 

4.10.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 [4.7] so ideally you should have already finished it.
 

4.10.2. Game Assets 
Game assets [2.3.2] for this tutorial:

 

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

using BetaCell.Environment.Terrain;
using BetaCell.Environment.Light.Special;
using BetaCell.Environment.Mesh;
using BetaCell.Util.Procedural;
using BetaCell.Common.Vertex;
using BetaCell.Environment.Mesh.Util;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Environment.Light;
using BetaCell.Util.RenderStates;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Dynamic.Waves;


 

4.10.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:

BCDisplacementWavesFeeder waves;

 

Advertisement
 

4.10.5. The Initialize method 
Displacement mapping is a technique similar to height mapping [3.3.5] but the positions of the vertices are updated in the vertex shader (the graphics card displaces the vertices).

The idea is to have a pair height maps that alter the vertices and scroll them in different directions to create wrinkles.

We start the portions of code relevant to this tutorial by loading the displacement maps:

Texture2D wave0D = content.Load<Texture2D>("Content/gameAssets/wave0D");
Texture2D wave1D = content.Load<Texture2D>("Content/gameAssets/wave1D");

After that we create the displacement map feeder:

waves = new BCDisplacementWavesFeeder(
    wave0D, wave1D,
    new BCSampler("POINT", "POINT", "POINT", "4", "WRAP", "WRAP", "0xffffff"),
    new BCSampler("POINT", "POINT", "POINT", "4", "WRAP", "WRAP", "0xffffff"),
    new Vector2(20f, 20f), 0.1f, 128.0f,
    new Vector2(0.05f, 0.01f),
    new Vector2(0.04f, 0.02f)
);


The parameters are as follows:
  1. The first displacement map
  2. The second displacement map
  3. The sampler of the first displacement map
  4. The sampler of the second displacement map
  5. A vector whose X component states a height scale to be applied to the heights sampled from the first displacement map. The Y component states the same about the second displacement map
  6. The texture scale to sample the displacement maps
  7. The size in pixels of each of the sides of the displacement map
  8. The velocity vector that governs the movement of the first displacement map. 
  9. The velocity vector that governs the movement of the second displacement map.
The last step is to create an effect visitor to plug displacement mapping to our plane:

BCDynamicEffectVisitor displacementSetter = new BCDynamicEffectVisitor(
           waves.getFunction("Actuator.Waves.DisplacementSquared"), waves, 0);
waterPlane.visit(displacementSetter)
;

Also, the speed of the normal map scrolling effect is increased:

normalMap.scroll0Velocity = new Vector2(0.5f, 0.1f);
normalMap.scroll1Velocity = new Vector2(0.4f, 0.2f);


 

4.10.6. The Update method
Here we update the effect to scroll the displacement maps and create the water illusion

waves.update((float)gameTime.ElapsedGameTime.TotalSeconds);

 

4.10.7. The Draw method
First we draw the environment as shown in [4.5.10]:

surrounding.draw(gameTime, 0);

Since the water is going to be transparent, we create a transparency render strategy [2.9.5]:

BCTransparencyRenderStrategy makeTransparent =
    new BCTransparencyRenderStrategy();


And draw the plane using the strategy

makeTransparent.push(device);
waterPlane.Draw(gameTime);
makeTransparent.pop(device);


 

4.10.8. Conclusion
This tutorial is a good example of encapsulation; with little knowledge about the inner workings of the effect, you were able to create a water simulation that includes displacement mapping.

Click here to go to the forum discussion of this tutorial

 

Advertisement
 

4.10.9. Complete source code
using System;
using System.Collections.Generic;
using System.Threading;

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.Terrain;
using BetaCell.Environment.Mesh;
using BetaCell.Util.Procedural;
using BetaCell.Common.Vertex;
using BetaCell.Environment.Mesh.Util;
using BetaCell.Environment;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Environment.Light;
using BetaCell.Util.RenderStates;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Light.NormalMap;
using BetaCell.Effects;
using BetaCell.Dynamic.Waves;

namespace Starter
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        //Game attributes
        GraphicsDeviceManager graphics;
        GraphicsDevice device;
        ContentManager content;

        BCFirstPersonHumanCamera camera;
        //end game attributes

        BCSuroundingBox surrounding;
        TextureCube environment;
        BCNormMapScroll normalMap;
        BCMesh waterPlane;

        BCDisplacementWavesFeeder waves;

        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()
        {
            device = graphics.GraphicsDevice;
            BCLogger.instance.init(device, content);
            BCInitializationManager.initialize(content);

            OnActivated(null, null);

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

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

            waterPlane = ProceduralModelers.PLANE_MODELER.createPlane(
                50, 50, 2, 2, new Vector3(0, 0, 0),
                new BCVertexPosNorTexContainer(), 1);
            BCTangentSpaceProcessor.calculateTangentSpace(waterPlane);
            BCMeshGraphicUtil.init(waterPlane, device);

            BCColorSet colorSet = new BCColorSet(new Color(0, 0, 0, 192));
            BCDynamicEffectVisitor colorSetter =
                new BCDynamicEffectVisitor(colorSet.getFunction("Shader.ColorSet"), colorSet, 0);
            waterPlane.visit(colorSetter);

            //-----
            //Displacement Mapping
            //-----

            Texture2D wave0D = content.Load<Texture2D>("Content/gameAssets/wave0D");
            Texture2D wave1D = content.Load<Texture2D>("Content/gameAssets/wave1D");

            waves = new BCDisplacementWavesFeeder(
                wave0D, wave1D,
                new BCSampler("POINT", "POINT", "POINT", "4", "WRAP", "WRAP", "0xffffff"),
                new BCSampler("POINT", "POINT", "POINT", "4", "WRAP", "WRAP", "0xffffff"),
                new Vector2(20f, 20f), 0.1f, 128.0f,
                new Vector2(0.05f, 0.01f),
                new Vector2(0.04f, 0.02f)
            );
            BCDynamicEffectVisitor displacementSetter = new BCDynamicEffectVisitor(
                       waves.getFunction("Actuator.Waves.DisplacementSquared"), waves, 0);
            waterPlane.visit(displacementSetter);

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

            normalMap = new BCNormMapScroll(
                content.Load<Texture2D>("Content/gameAssets/wave0"),
                new BCSampler("LINEAR", "LINEAR", "LINEAR", "4", "WRAP", "WRAP", "0xffffff"),
                new Vector2(2.5f, 2.5f),
                content.Load<Texture2D>("Content/gameAssets/wave1"),
                new BCSampler("LINEAR", "LINEAR", "LINEAR", "4", "WRAP", "WRAP", "0xffffff"),
                new Vector2(2.5f, 2.5f)
            );
            normalMap.scroll0Velocity = new Vector2(0.5f, 0.1f);
            normalMap.scroll1Velocity = new Vector2(0.4f, 0.2f);
            normalMap.inverseNormalAttenuation = 10;

            BCDynamicEffectVisitor normalMapSetter = new BCDynamicEffectVisitor(
                normalMap.getFunction("Actuator.NormalMapScroll"), normalMap, 0
            );
            waterPlane.visit(normalMapSetter);

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

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

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

            //-----
            //Environmental map
            //-----
            BCEnvMapFeeder envMapFeeder = new BCEnvMapFeeder(
                environment,
                new BCSampler("LINEAR", "LINEAR", "LINEAR", "4", "WRAP", "WRAP", "0xffffff")
            );
            envMapFeeder.chaosFactor = 0.1f;
            envMapFeeder.mirrorFactor = 0.5f;

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

            waterPlane.visit(envSetter);

            base.Initialize();
        }

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

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(30, 10, 30);
            Vector3 look = new Vector3(-3, -1, -3);
            look.Normalize();

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

            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)
        {
            waves.update((float)gameTime.ElapsedGameTime.TotalSeconds);
            normalMap.update(gameTime);
            moveCamera(gameTime);
        }

        private void moveCamera(GameTime gameTime)
        {

            float df = 0;
            float ds = 0;
            float dy = 0;
            float pitch = 0;
            float angleY = 0;

            float speed = 5f;
#if (XBOX360)
            GamePadState gamepad = GamePad.GetState(PlayerIndex.One);

            df = gamepad.ThumbSticks.Left.Y;
            ds = gamepad.ThumbSticks.Left.X;
            pitch = -gamepad.ThumbSticks.Right.Y * 0.05f;
            angleY = gamepad.ThumbSticks.Right.X * 0.05f;

            if (gamepad.IsButtonDown(Buttons.RightShoulder))
            {
                dy += 0.25f;
            }
            if (gamepad.IsButtonDown(Buttons.LeftShoulder))
            {
                dy -= 0.25f;
            }

            speed = new Vector3(df, ds, dy).Length() * 10;
#else

            KeyboardState keys = Keyboard.GetState();
            MouseState mouse = Mouse.GetState();

            if (keys.IsKeyDown(Keys.W))
            {
                df += 1;
            }
            if (keys.IsKeyDown(Keys.S))
            {
                df -= 1;
            }
            if (keys.IsKeyDown(Keys.D))
            {
                ds += 1;
            }
            if (keys.IsKeyDown(Keys.A))
            {
                ds -= 1;
            }
            if (keys.IsKeyDown(Keys.Q))
            {
                dy += 1;
            }
            if (keys.IsKeyDown(Keys.E))
            {
                dy -= 1;
            }
            speed = 5f;
            if (keys.IsKeyDown(Keys.LeftControl))
            {
                speed = 10f;
            }

            angleY = ((float)this.Window.ClientBounds.Width / 2.0f - (float)mouse.X) * -0.05f;
            pitch = ((float)this.Window.ClientBounds.Height / 2.0f - (float)mouse.Y) * -0.05f;

            Mouse.SetPosition(this.Window.ClientBounds.Width / 2, this.Window.ClientBounds.Height / 2);

#endif
            camera.update(df, dy, ds, pitch, angleY, speed, (float)gameTime.ElapsedGameTime.TotalSeconds);

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

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

            BCTransparencyRenderStrategy makeTransparent = new BCTransparencyRenderStrategy(false);

            makeTransparent.push(device);
            waterPlane.Draw(gameTime);
            makeTransparent.pop(device);

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