Home > TutorialsTechniques
4.7. Water Effect on a flat surface 
In this tutorial you will combine the environmental map concept and the normal map concept to create a water effect.



Click here to go to the forum discussion of this tutorial


 

4.7.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
 

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

 

4.7.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;


 

4.7.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;
BCNormMapScroll normalMap;
BCMesh waterPlane;


 

Advertisement
 

4.7.5. The Initialize method 
We start the portions of code relevant to this tutorial by setting the shader model to 3.0, this is  because the effect is not supported for video cards of previous shader models:

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

We continue by creating the plane mesh and calculating it's tangent space as we did on [4.6.6]:

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

Then we create an effect feeder that scrolls a pair of normal maps over the mesh. The scrolling is similar to the one shown in [2.7.5] but each plane scrolls in a different direction. The final normal of a fragment is calculated given the combined normal of the two maps (we add the normals and divide them by 2); the maps are similar to the one seen in [4.6.5], this creates the illusion of wrinkles. After that we calculate the reflections using the newly obtained normal with the technique shown in [4.5.6].

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.05f, 0.01f);
normalMap.scroll1Velocity = new Vector2(0.04f, 0.02f);
normalMap.inverseNormalAttenuation = 10;


The following is the description of the parameters:
  1. The first Normal Map
  2. The sampler of the first normal map
  3. The scale of the first normal map
  4. The second Normal Map
  5. The sampler of the second normal map
  6. The scale of the second normal map
Then some attributes are set to achieve the desired effect:

normalMap.scroll0Velocity: The scroll velocity of the first normal map
normalMap.scroll1Velocity: The scroll velocity of the second normal map
inverseNormalAttenuation: Ths is the inverse attenuation of the normal map effect, as this value goes higher, the calculated normal from the normal map tends to be the normal of the face.

Then we create the normal map effect setter visitor:

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


Finally we apply an environmental mapping effect, this effect will use the normals of the normal maps and that's what creates the water illusion.
.

 

4.7.6. The Update method
Here we update the effect to scroll the maps and create the illusion

normalMap.update(gameTime);


 

4.7.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.7.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.

Click here to go to the forum discussion of this tutorial
 
 

Advertisement
 

4.7.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;

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;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            content = new ContentManager(Services);

#if(XBOX360)
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 768;
#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(
                10, 10, 10, 10, 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);

            //-----
            //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.05f, 0.01f);
            normalMap.scroll1Velocity = new Vector2(0.04f, 0.02f);
            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)
        {
            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);
        }
    }
}