Home > TutorialsMeshes and I/O
3.3. Terrain Basics 
In this tutorial you will learn how to import a height map made with an open tool called HME and walk over it using  BetaCell.



Click here to go to the forum discussion of this tutorial


 

3.3.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
  • For a better understanding, is
  •  preferred to have finished the previous tutorials.
  • If you want to make your own terrain, look for HME under the downloads section. 
 

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

3.3.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;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Environment.Mesh;
using BetaCell.Environment;
using BetaCell.Effects.Composer;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Texture;
using BetaCell.Effects;
using BetaCell.Common.Vertex;


 

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

BCTerrain floor;

 

3.3.5. Importing the terrain 

default.jpg
The main strength of HME is also one of it's points of improvement; it's very simple. In it, you can create a map and save it in either the hmp format (which is understood by BetaCell) or a bmp image.

If you want, you can create your own map, save it as hmp and add it to the game assets of this tutorial; then load it and see what happens. If right now you are not into creating the map, just use the one in the 3.2.2.

When working with height maps we start by a plane divided in rows and columns like the one shown in the upper left part of the figure, at that point, its a flat surface.

To apply the height, we define a map (you can think of it as an image) where each entry (pixel) is assigned to a vertex in the plane.

We can see such map in the upper right corner of the image; each pixel is a height, the darker the lower. It's important to clarify that the image is zoomed; each square is a pixel.

To map the image to the plane, we assign each vertex a pixel that states it's height. Now this is very important: Count the number of columns in the plane, it has 4 columns, count the number of columns of the height map, it has 5 columns. This is very important to notice because we are working with the vertices of the plane, not the faces, if you count the vertices in the x axis, it will be 5; the same as the number of columns in the map.

In the lower left part of the figure, we can see the mapping between the plane and the image in a pixel to vertex fashion, the final result can be seen in the lower right part of the figure, notice that the darker parts end up lower than the brighter parts.
 
 

Advertisement
 

3.3.6. The Initialize method 

Once the terrain is created, it will contain a mesh that represents it's geometry; so we will need visitors for that mesh.

The initialize method begins by creating the visitors; a special visitor is the one for the channel blend effect which is explained in 2.5.

The interesting part of the method is the part where the terrain is loaded

floor = new BCTerrain(device, "Content/gameAssets/hm_128",
                1.0f, 1.0f, 128, 128,
0.8f, 0, 1, 2, null,
                new BCVertexPosNorTexContainer(), content);

BCMeshGraphicUtil.init(floor.terrainMesh, device);

The above method loads the terrain, the first argument is the device, the second is the name of the height map file, the third is the length of a square in the original plane [1.2.4][3.2.5], the fourth is the depth of a square in the original plane.

The fifth argument is the NUMBER OF VERTICES in the x axis NOT THE NUMBER OF COLUMNS, if you want to create a map like the one in the figure of [3.2.5]  this argument should be 5 not 4, the sixth argument is the number of vertices in the z axis, much like the fourth argument but in a different axis.

The eight argument is a value by which the heights of the plane are multiplied, think of it as a modulator, the ninth argument is a negative offset; the lowest value that a map can have is 0 (black) so if you want to have negative heights, you need to set this offset.

The tenth argument is the texture scale [2.4.5] the eleventh argument is a value used to smoother the terrain, it works by setting the height of a vertex as the average of the vertices around it; if the value is 1, then the average is done with the immediate neighbors, if it's 2, the average is done with the immediate and their neighbors, and so on. As you can see this is a very expensive operation.

The twelve th argument is a height modifier that's out of the scope of this tutorial, you can leave it as null, the thirteenth argument is the vertex container [1.3.4] and the last one is the content manager.

After the terrain is loaded, the terrain's mesh must be decorated bu the visitors. notice that our terrain (floor) isn't the mesh itself, the mesh is floor.terrainMesh.

floor.terrainMesh.visit(blendMapTex);
floor.terrainMesh.visit(grassTex);
floor.terrainMesh.visit(roadTex);
floor.terrainMesh.visit(stoneTex);

floor.terrainMesh.visit(materialVisitor);
floor.terrainMesh.visit(colorSetter);
floor.terrainMesh.visit(lightSetter);
floor.terrainMesh.visit(textureEffectSetter);


 

 3.3.7. The OnActivated method
There's an important part in the buildViewMatrix() method which is called in the OnActivated method. Is the following line:

camera.attachWalkable(floor, 2);

In it, we state that the camera should be attached 2 units above our terrain so that we can walk on it.
 

3.3.8. The Update method
Now we will discuss the camera movement.

First we declare some variables to hold the user's input:

float df = 0; //Forward
float ds = 0; //Sides
float dy = 0; //Up
float pitch = 0;//Looking up
float angleY = 0;//Looking sides

Then we get the state of the input devices:

On the PC we use the keyboard and the mouse to move the camera:

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

After that we fill the variables mentioned above. the keyboard ones are simple; the mouse ones are as follows:

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

First we assume that the mouse position before the user's input was the center of the window, then we rest the current position in both the x and y axises from the positions at the center of the screen to know how much the user moved the mouse. Then we multiply the result with a modulator.

To assume that the position of the mouse before the movement was the center of the screen we must actually place it there with the following call (after getting the movement information):

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


On the XBOX 360 we use the gamepad

GamePadState gamepad = GamePad.GetState(PlayerIndex.One);

The thumb sticks movement is captured in the -1 to 1 range, so we can just assign them

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

To go up and down we use buttons

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

The speed depends on how much we move the sticks

speed = new Vector3(df, ds, dy).Length()*30;

After we obtain the user's input, we must update the camera:

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


The first three arguments are the components of the direction vector, the next two denote how much the direction where the user is looking changed, the sixth argument is the speed of the camera and the last one is the time that passed since the last time the update method was called.

Finally we apply the updated view matrix of the camera:

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

 

3.3.9. The Draw method
The only difference with previous draw methods is that we draw a mesh that's a member of the terrain:

floor.terrainMesh.Draw(gameTime);

 

3.3.10. Conclusion
It's very important that you understand the concepts of this tutorial because it is the base of the next one. The important concepts you learned are terrain loading and camera movement.

Click here to go to the forum discussion of this tutorial

 

Advertisement
 

3.3.11. 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.Terrain;
using BetaCell.Environment.Light;
using BetaCell.Environment.Mesh.Visitors;
using BetaCell.Environment.Mesh;
using BetaCell.Environment;
using BetaCell.Effects.Composer;
using BetaCell.Environment.Texture.Feeder;
using BetaCell.Environment.Texture;
using BetaCell.Effects;
using BetaCell.Common.Vertex;

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
        BCTerrain floor;

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

            BCMaterialBase material = new BCMaterialBase();
            material.specRange = 0.8f;
            material.diff = Color.Gray;
            material.spec = Color.White;
            material.amb = Color.DarkGray;
            BCMaterialVisitor materialVisitor = new BCMaterialVisitor(material);

            //Color set effect composition part
            BCColorSet colorSet = new BCColorSet(Color.Black);
            BCFunction baseColor = colorSet.getFunction("Shader.ColorSet");
            BCDynamicEffectVisitor colorSetter = new BCDynamicEffectVisitor(baseColor, colorSet, 0);

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

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

            //Creating a sampler for the texture
            BCSampler sampler = new BCSampler(
                "LINEAR", "LINEAR", "LINEAR", "4",
                "WRAP", "WRAP", "0xffffffff"
            );

            //Creating a texture feeder for the texture
            //Adding a sampler per expected texture of the channel blend effect part
            BCTextureFeeder texFeeder = new BCTextureFeeder();
            texFeeder.addSampler("gBlendMap", sampler);
            texFeeder.addSampler("gTex0", sampler);
            texFeeder.addSampler("gTex1", sampler);
            texFeeder.addSampler("gTex2", sampler);

            //Getting the texture effect part function from the texture feeder
            BCFunction channelBlendPart = texFeeder.getFunction("Shader.Texture.ChannelBlend");

            //Creating the texture effect setter visitor
            BCDynamicEffectVisitor textureEffectSetter =
                new BCDynamicEffectVisitor(channelBlendPart, texFeeder, BCDynamicEffect.MAX_FUNCTIONS);

            //Setting texture
            Texture2D reference = content.Load<Texture2D>("Content/gameAssets/channelsR");
            BCMemoryTextureStrategy referenceStrategy = new BCMemoryTextureStrategy(reference);
            BCTextureVisitor blendMapTex = new BCTextureVisitor("gBlendMap", referenceStrategy);

            //Alternate way to initiate the texture in a single definition
            BCTextureVisitor grassTex = new BCTextureVisitor(
                "gTex0",
                new BCMemoryTextureStrategy(content.Load<Texture2D>("Content/gameAssets/grass"))
            );
            grassTex.strategy.setScale(new Vector2(30, 30));

            BCTextureVisitor roadTex = new BCTextureVisitor(
                "gTex1",
                new BCMemoryTextureStrategy(content.Load<Texture2D>("Content/gameAssets/stone"))
            );
            roadTex.strategy.setScale(new Vector2(30, 30));

            BCTextureVisitor stoneTex = new BCTextureVisitor(
                "gTex2",
                new BCMemoryTextureStrategy(content.Load<Texture2D>("Content/gameAssets/road"))
            );
            stoneTex.strategy.setScale(new Vector2(30, 30));

            floor = new BCTerrain(device,
                "Content/gameAssets/hm_128", 1.0f, 1.0f, 128, 128,
                0.8f, 0, 1, 2, null, new BCVertexPosNorTexContainer(), content);
            BCMeshGraphicUtil.init(floor.terrainMesh, device);

            floor.terrainMesh.visit(blendMapTex);
            floor.terrainMesh.visit(grassTex);
            floor.terrainMesh.visit(roadTex);
            floor.terrainMesh.visit(stoneTex);

            floor.terrainMesh.visit(materialVisitor);
            floor.terrainMesh.visit(colorSetter);
            floor.terrainMesh.visit(lightSetter);
            floor.terrainMesh.visit(textureEffectSetter);

            base.Initialize();
        }

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

        void buildViewMatrix()
        {
            Vector3 pos = new Vector3(-20, 100, -20);
            Vector3 look = new Vector3(0, 0, 1);
            look.Normalize();

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

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

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

        protected override void Update(GameTime 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() * 30;
#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 = 10f;
            if (keys.IsKeyDown(Keys.LeftControl))
            {
                speed = 20f;
            }

            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)
        {
            graphics.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);

            floor.terrainMesh.Draw(gameTime);

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