Terrain based on Heightmaps

Step 1:
File > Add Project > XNA > Content Pipeline Extension Library

Step 2:
[ContentProcessor(DisplayName = "VikPro")]
public class ContentProcessor1 : ContentProcessor<TInput, TOutput>
TInput will be TextureContent and TOutput will be ModelContent

Step 3:
The Algorithm -
Create an instance of MeshBuilder by calling StartMesh.
MeshBuilder baseBuilder = MeshBuilder.StartMesh("Terrain");

Convert input type for ease of use. We can use it just like it is if we wanted to.
PixelBitmapContent<float> heightFields = (PixelBitmapContent<float>)input.Mipmaps[0];
Now GetPixel will return float values.

Add each pixel as point in the Mesh builder
for (int y = 0; y < heightFields.Height; y++)
    for (int x = 0; x < heightFields.Width; x++)
         Vector3 tmp;
         tmp.X = xScale * (x - heightFields.Width / 2f);
         tmp.Z = yScale * (y- heightFields.Height / 2f);
         tmp.Y = heightScale * (heightFields.GetPixel(x, y) - 1);

Create Material content and inform XNA that the vertices will have texture coordinates.
BasicMaterialContent material = new BasicMaterialContent();
material.SpecularColor = new Vector3(.4f, .4f, .4f);

int texCoordId = baseBuilder.CreateVertexChannel<Vector2>(VertexChannelNames.TextureCoordinate(0));
Create Triangle based on the points we created.
for (int y = 0; y < heightFields.Height-1; y++)
    for (int x = 0; x < heightFields.Width-1; x++)
         AddVertex(baseBuilder, texCoordId, heightFields.Width, x, y);
         AddVertex(baseBuilder, texCoordId, heightFields.Width, x+1, y);
         AddVertex(baseBuilder, texCoordId, heightFields.Width, x+1, y+1);

         AddVertex(baseBuilder, texCoordId, heightFields.Width, x, y);
         AddVertex(baseBuilder, texCoordId, heightFields.Width, x+1, y+1);
         AddVertex(baseBuilder, texCoordId, heightFields.Width, x, y+1);

static void AddVertex(MeshBuilder builder, int texCoordId, int w, int x, int y)
    builder.SetVertexChannelData(texCoordId, new Vector2(x, y));
    builder.AddTriangleVertex(x + y * w);

Call FinishMesh to obtain MeshContent. Use context to convert to ModelContent(output)
MeshContent terrain = baseBuilder.FinishMesh();

Whats the fuss with ContentWriter and Reader?
Model.Tag can have any generic data (object). In case we want some custom information in to be stored for runtime use, add this to Model.Tag. If this data is custom class, then create Reader and Writer to store this information (like a custom Serializer/Deserializer)

Example: Really stupid useless one – Saving information like author, version, name, etc
Suppose I want to save this ModelTag class. I need a writer that will save this info in the XNB File.
public class ModelTag
    public string name;
    public string author;
    public string version;

    public ModelTag(string n, string a, string v)
       name = n;
       author = a;
       version = v;
The writer will be something like this-
public class TagWriter : ContentTypeWriter< ModelTag >
   protected override void Write(ContentWriter output, TWrite value)

The reader will be like-
protected override TRead Read(ContentReader input, TRead existingInstance)
    string n= input.ReadString();
    string a= input.ReadString();
    string v=input.ReadString();

    ModelTag tmp = new ModelTag(n, a, v);
    return tmp;

In the Process function I’ll have -
model.Tag = new ModelTag("Terrain", "Vikram", "");

A very nice and comprehensive sample can be found here at Creators Club.
In this example, find the Terrain Processor. They generate the normals of each tri and save this as Tag info. This can be pretty useful.

NFS: When assigning height values from the image, the Y co-or will get the height value and not the Z component. Otherwise, the terrain will be vertical and you'll end up messing it up using CreateRotation till you finally realize it. Stupid you.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.