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

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

    [ContentTypeWriter]
    public class HeightMapInfoWriter : ContentTypeWriter
    {
        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
    {
        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);         }     } }[/sourcecode] 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";         }     } }[/sourcecode] Simple, now we can use this new class to render the terrain object. [sourcecode language='csharp']            BruteForceTerrain terrain = new BruteForceTerrain();             terrain.Position = new Vector3(0f, -15f, 0f);             terrain.Scale = new Vector3(10, 10, 10);             SceneGraphManager.AddObject(terrain);[/sourcecode] 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.

March 12, 2008 Posted by | C#, XBOX360, XNA | 9 Comments