Running on Empty

The few things I know, I like to share.

XNA Framework GameEngine Development. (Part 18, Brute Force Terrain with Physics)

Introduction

Welcome to Part18 of the XNA Framework GameEngine Development series.  In this article I will introduce terrain rendering concepts.  All good outdoor games need terrain as a primary building block, so it is essential to render terrain as realistically and as performant as possible.  There are dozens of algorithmic approaches to terrain rendering, I could literally spend an entire series discussing terrain.  Here I will discuss some of my favorite terrain rendering methods and maybe a few of your own if you leave suggestions.

part18.jpg

Release

Here is the sourcecode.

Content Pipeline

The XNA content pipeline allows us to create custom processors for data.  I use the content pipeline to convert a 2D heightmap into a 3D model.  This is a well known technique for creating simple terrain, but I will describe it here briefly.

  • Create a greyscale image with black being low laying areas and white being high altitude areas.
  • Assume the world is a square lattice with vertices at precise intervals in the lattice.
  • The height at each point in the lattice is set as the pixel in the greyscale image.
  • Connect the vertices as a triangle strip.

Create a mesh from any greyscale image that is a power of 2 + 1 and voila you have a simple brute force heightmap.

BruteForceTerrainProcessor

Create a new Content Pipeline project.  I will be expanding on this project later, add the following content processor code.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

using TInput = Microsoft.Xna.Framework.Content.Pipeline.Graphics.Texture2DContent;
using TOutput = Microsoft.Xna.Framework.Content.Pipeline.Processors.ModelContent;

namespace RoeEngine2ContentPipeline
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// TODO: change the ContentProcessor attribute to specify the correct
    /// display name for this processor.
    /// </summary>
    [ContentProcessor(DisplayName = "BruteForceTerrainProcessor")]
    public class BruteForceTerrainProcessor : ContentProcessor<TInput, TOutput>
    {
        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            MeshBuilder builder = MeshBuilder.StartMesh("Terrain");

            input.ConvertBitmapType(typeof(PixelBitmapContent<float>));

            PixelBitmapContent<float> heightfield;
            heightfield = (PixelBitmapContent<float>)input.Mipmaps[0];

            for (int y = 0; y < heightfield.Height; y++)
            {
                for (int x = 0; x < heightfield.Width; x++)
                {
                    Vector3 position;

                    position.X = (x - heightfield.Width / 2);
                    position.Z = (y - heightfield.Height / 2);
                    position.Y = (heightfield.GetPixel(x, y) - 1);

                    builder.CreatePosition(position);
                }
            }

            int texCoordId = builder.CreateVertexChannel<Vector2>(
                            VertexChannelNames.TextureCoordinate(0));

            for (int y = 0; y < heightfield.Height - 1; y++)
            {
                for (int x = 0; x < heightfield.Width - 1; x++)
                {
                    AddVertex(builder, texCoordId, heightfield.Width, x, y);
                    AddVertex(builder, texCoordId, heightfield.Width, x + 1, y);
                    AddVertex(builder, texCoordId, heightfield.Width, x + 1, y + 1);

                    AddVertex(builder, texCoordId, heightfield.Width, x, y);
                    AddVertex(builder, texCoordId, heightfield.Width, x + 1, y + 1);
                    AddVertex(builder, texCoordId, heightfield.Width, x, y + 1);
                }
            }

            MeshContent terrain = builder.FinishMesh();

            ModelContent model = context.Convert<MeshContent, ModelContent>(terrain, "ModelProcessor");

            model.Tag = new HeightMapContent(heightfield);

            return model;
        }

        /// <summary>
        /// Helper for adding a new triangle vertex to a MeshBuilder,
        /// along with an associated texture coordinate value.
        /// </summary>
        static void AddVertex(MeshBuilder builder, int texCoordId, int w, int x, int y)
        {
            builder.SetVertexChannelData(texCoordId, new Vector2(x, y) / w);

            builder.AddTriangleVertex(x + y * w);
        }
    }
}

Now we need a way to write and read data that we want to store in the model.Tag field.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

namespace RoeEngine2ContentPipeline
{
    public class HeightMapContent
    {
        private float[,] _height;

        public float[,] Height
        {
            get { return _height; }
            set { _height = value; }
        }

        public HeightMapContent(PixelBitmapContent<float> bitmap)
        {
            _height = new float[bitmap.Width, bitmap.Height];
            for (int y = 0; y < bitmap.Height; y++)
            {
                for (int x = 0; x < bitmap.Width; x++)
                {
                    // the pixels will vary from 0 (black) to 1 (white).
                    // by subtracting 1, our heights vary from -1 to 0, which we then
                    // multiply by the "bumpiness" to get our final height.
                    _height[x, y] = (bitmap.GetPixel(x, y) - 1);
                }
            }
        } 
    }

    /// <summary>
    /// A TypeWriter for HeightMapInfo, which tells the content pipeline how to save the
    /// data in HeightMapInfo. This class should match HeightMapInfoReader: whatever the
    /// writer writes, the reader should read.
    /// </summary>
    [ContentTypeWriter]
    public class HeightMapInfoWriter : ContentTypeWriter<HeightMapContent>
    {
        protected override void Write(ContentWriter output, HeightMapContent value)
        {
            output.Write(value.Height.GetLength(0));
            output.Write(value.Height.GetLength(1));
            foreach (float height in value.Height)
            {
                output.Write(height);
            }
        }

        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(HeightMapReader).AssemblyQualifiedName;
        }

        public override string GetRuntimeType(TargetPlatform targetPlatform)
        {
            return typeof(HeightMap).AssemblyQualifiedName;
        }
    }

    public class HeightMap
    {
        public float[,] Heights;

        public HeightMap(float[,] heights)
        {
            Heights = heights;
        }
    }

    public class HeightMapReader : ContentTypeReader<HeightMap>
    {
        protected override HeightMap Read(ContentReader input, HeightMap existingInstance)
        {
            int width = input.ReadInt32();
            int height = input.ReadInt32();
            float[,] heights = new float[width, height];

            for (int x = 0; x < width; x++)
            {
                for (int z = 0; z < height; z++)
                {
                    heights[x, z] = input.ReadSingle();
                }
            }
            return new HeightMap(heights);
        }
    }
}

I chose to create the reader and writer in the same namespace, this makes getting the runtime reader and type much easier.

Creating Terrain SceneObjects

Now that we have a content processor, we can now import the model and render it in our game.  Include a reference to the content pipeline in both the content reference and in the reference section of the main engine project.

Create a BruteForceTerrain SceneObject

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.SceneObject.BaseObjects;
using RoeEngine2.Interfaces;
using JigLibX.Physics;
using JigLibX.Collision;
using Microsoft.Xna.Framework;
using RoeEngine2.Models;
using RoeEngine2.Managers;
using JigLibX.Utils;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2ContentPipeline;

namespace RoeEngine2.SceneObject.StandardObjects
{
    public class BruteForceTerrain: OccluderSceneObject, IRoePhysics
    {
        private Body _body;
        public Body Body
        {
            get { return _body; }
        }

        private CollisionSkin _collisionSkin;
        public CollisionSkin CollisionSkin
        {
            get { return _collisionSkin; }
        } 

        public BruteForceTerrain()
        {
            _body = new Body();
            _collisionSkin = new CollisionSkin(null);

            RoeModel terrainModel = new RoeModel("Content/Models/TerrainRoE");
            ModelManager.AddModel(terrainModel, "terrainModel");

            this.ModelName = "terrainModel";
            this.OcclusionModelName = "terrainModel";
        }

        public BruteForceTerrain(Vector3 newPosition)
        {
            Position = newPosition;

            _body = new Body();
            _collisionSkin = new CollisionSkin(null);

            RoeModel boxmodel = new RoeModel("Content/Models/Box");

            ModelManager.AddModel(boxmodel, "boxmodel");

            this.ModelName = "boxmodel";
            this.OcclusionModelName = "boxmodel";
        }

        public Vector3 SetMass(float mass)
        {
            return Vector3.Zero;
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                HeightMap heightMap = model.BaseModel.Tag as HeightMap;

                Array2D field = new Array2D(heightMap.Heights.GetUpperBound(0), heightMap.Heights.GetUpperBound(1));

                for (int x = 0; x < heightMap.Heights.GetUpperBound(0); x++)
                {
                    for (int z = 0; z < heightMap.Heights.GetUpperBound(1); z++)
                    {
                        field.SetAt(x, z, heightMap.Heights[x, z] * Scale.Y + Position.Y );
                    }
                }

                _body.MoveTo(Position, Matrix.Identity);

                _collisionSkin.AddPrimitive(new JigLibX.Geometry.Heightmap(field, Position.X, Position.Z, Scale.X, Scale.Z),
                                            (int)MaterialTable.MaterialID.UserDefined,
                                            new MaterialProperties(0.7f, 0.7f, 0.6f));

                PhysicsSystem.CurrentPhysicsSystem.CollisionSystem.AddCollisionSkin(_collisionSkin);

                ReadyToRender = true;
            }
        }

        public override void UnloadContent()
        {
            PhysicsManager.RemoveObject(this);
        }

        public override string ToString()
        {
            return "Brute Force Terrain";
        }
    }
}

Simple, now we can use this new class to render the terrain object.

            BruteForceTerrain terrain = new BruteForceTerrain();
            terrain.Position = new Vector3(0f, -15f, 0f);
            terrain.Scale = new Vector3(10, 10, 10);
            SceneGraphManager.AddObject(terrain);

Conclusion

In this article I introduced the content pipeline and simple brute force terrain.  This article will be the kickoff for a terrain rendering sub series.  I look forward to your comments and suggestions.

About these ads

March 12, 2008 - Posted by | C#, XBOX360, XNA

9 Comments »

  1. hi,
    like the tutorials they are very informative. btw have you changed your mind about BulletX or are you planning on updating the recent source codes to BulletX. I ask b’cuz I have no idea as to which physics library to pick up, BulletX or JigLibX.
    anyways, thanks for the series.

    Comment by Derth | March 13, 2008 | Reply

  2. Thank you for this tutorial. I think I will use your engine for a new 3D project. He is cleaner than mine. I’ll have to write a framework to use with a 3D screen, the latest 42″ Philips one, based on Wowvx technology. I’ll share my work if you want.

    I’m looking forward to reading your shadowing articles.

    Comment by Dracul86 | March 14, 2008 | Reply

  3. Derth, yes I have moved to JigLibX. BulletX just didn’t have as clean of an interface as I would like. However, I am not completely happy about everything I have seen in the JigLibX either. Suppose I cannot be too picky though, they are both open source.

    Glad you like the series.

    Comment by roecode | March 14, 2008 | Reply

  4. Something I’d like to see added to this series is the ability to do animation, ragdolls etc (maybe importing models using the Collada format if necessary as X and fbx doesn’t seem to have this implemented)

    Comment by Zenox | March 15, 2008 | Reply

  5. Zenox, yes I do plan to do animation. I have done some ragdolls, just have not discussed them yet. I do also plan to discuss file formats such as collada and custom file formats. Just keep reading, I will start a section on this after I complete the shadowing section.

    Comment by roecode | March 15, 2008 | Reply

  6. Hello, the description of your engine on sourceforge says it is a managed DirectX engine ;)

    Comment by andris | September 29, 2008 | Reply

  7. Why heightmap and not terrain model maked with some 3d tool ?

    Comment by alancastrocl | May 27, 2009 | Reply

    • Excellent question, heightmaps are commonly used for terrain for a few reasons. 1) They are easy to implement since the possibility of overhangs does not exist. 2) They are relatively small files sizes. 3) They fit into the evenly spaced latice structure easily. 4) Easier to do LOD calculations

      Modelled terrain is nice for one small piece of land, but for an entire world landscape… you would need an army of artists. I simply do not have that. So what you see here is a heightmap that was created using readily available algorithmic methods.

      Comment by roecode | May 28, 2009 | Reply

  8. Hi,
    i can not download the codes,please email me the rar file.:)

    and if anything about PhysX by NVIDIA.

    thank u.

    Comment by Omid | November 20, 2009 | Reply


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: