Saturday, 13 July 2013

The basics of terrain rendering

screenIn the previous post called The heightmap: from concept to template  you saw how to create a C++ class template for a heightmap.  This post is the next step: you’ll gain an understanding of how to use OpenGL to render a terrain that uses that heightmap.  The screenshot on the right shows what you’ll have if you follow this post carefully.  The screenshot is produced from the heightmap concept and the BMP file introduced in the previous post.  

Broadly speaking, the terrain is rendered in two steps: 1) create the geometry 2) send the geometry to OpenGL.   The first step in technology agnostic and and could be of use even if you do not like OpenGL.   

Let’s start with step one. Terrain geometry needs vertices, so you need to use each element in the heightmap matrix to create a three-dimensional vertex.  This is not difficult because a matrix element can be represented as the triple: {c,r,h} where c is the column, r is the row and h is the value of the element.  Imagine h is zero for all matrix elements, then the terrain is a flat rectangular grid, composed of equality sized squares. If there are m columns and n rows in the heightmap there are m * n vertices, and consequently you’ll have (m-1) * (n-1) squares in your terrain.  You can easily see how it works if the heights are not zero imagining that the corners of the squares are ‘pulled up’ based on the value of h.  Clearly your imagination with not be sufficient in itself: you need a function that maps each triple {c,r,h} to a vertex {x,y,z}. I call this function the transformer

Before you can define the transformer, you need to commit to a meaning for the components of the vertex {x,y,z}.  When talking about terrains, I find it useful to think in terms of the cardinal directions.   Orientate your terrain so that the top row of squares lies north and the left column of squares is on the west side.  OpenGL does not know the meaning of the cardinal directions, so we need to decide on a Cartesian coordinate system that maps nicely to these directions.  Let x increase from east to west, y increase from south to north and z increase upwards.  Take a moment to consider what this means. You should also decide on an origin for the vertex on the north-west corner: let that be {0,0,h’} where h’ is a mapping from the h value of the matrix element at {0,0}.

The transformer needs to know the size of your terrain.  OpenGL does not define units, but it is a good idea for you to tie meaning to the floating point numbers sent to the rendering engine.  For convenience I always use the rule: one meter = 1.0f.  Let square_length be the length of the side of a square.  So, if your terrain stretches 10km west to east and you have 20 squares in a row, then your square_length is 500.0f.   Using this value, the transformer can calculate two components: x = r * square_length and y = c * square_length * (-1).  Notice that the sign for y is changed because r increases towards the south while the coordinate system’s y increases towards the north.

The mapping from h to z should also be uncomplicated.  If you use a byte for h the value of h ranges from 0 to 255.  One way to use this range is to decide on the maximum and minimum height you want for your terrain. Let’s call those max_h and min_h. Keep in mind that min_h could be negative and that a negative value for h could mean below water level.  From these bounds you calculate height_scale = (max_h – min_h) / 255, and you get the function z = h * height_scale which calculates the final component of the vertex.     

Listing 1 shows the functor template called terrain::Transformer.  This class implements the component calculations in C++.  The subclass called terrain::TransformerByte can be used for a heightmap with byte elements. It simply provides a convenient constructor that takes the bounds of h as arguments.  The basic Byte, Scalar and Vector types used in the listing are defined in GameEx.

Listing 1:

That wraps up step one: you have geometry.  Now we decide how render the geometry.  Let’s create a triangle strip for each column of squares.  Consider the squares in the west-most column. The vertices on left (west) side of those squares have c = 0, and those on the right hand side c = 1.  For this column you create the triangle strip by walking the following  {c,r} sequence: {1,0}, {0,0}, {1,1}, {0,1}, {1,2}, {0.2}, {1,3} …. {1,m},{0.m}.  This traversal through the elements in a heightmap, can be implemented on the terrain::Heightmap class template.  Listing 2 shows the new traverse_triangles method.  This method takes a function as argument.  It calls this function for each triple {c,r,h} it visits while walking in the desired sequence column by column.  It emits a sentinel triple {-1,-1,0} to indicate the end of a column has been reached.

Listing 2:

Listing 3 combines the geometry and rendering approach in a class template called TerrainObject.  Take a close look at the draw method: it first draws solid triangles and then red lines ‘over’ those triangles.  Notice how traverse_triangles is called in line 19: it uses a C++ lambda function.  

Listing 3:

Listing 4 shows how these concepts are brought together in the GameEx framework.  If you use another game library this part would obviously be vastly different.  The lines in the listing is all that is needed to show the terrain and have a crude camera to explore your creation a bit.

Listing 4:

This concludes the post. Using the BMP file as as input,  you created a three-dimensional image that has 4900 triangles.  And maybe you leant a bit more of the new C++ language features.

After thoughts:

The post called Faster Terrain Render show you how to use buffers to draw this terrain.

No comments:

Post a Comment