left-icon

Direct2D Succinctly®
by Chris Rose

Previous
Chapter

of
A
A
A

CHAPTER 18

Rendering a Height Map

Rendering a Height Map


Now we will examine one very common way to render 3-D data known as a height map. Height maps are an important visualization tool for 3-D data sets (data with at least three parametric variables). They are often used in games for terrain generation, as they can be made to look like mountainous terrain. They can be thought of as the 3-D equivalent to the line chart; instead of a single line being rendered to represent the data, a mountainous surface is rendered.

The height map we will render will be a collection of points connected with solid triangles. The x and z values will form a grid or surface, and the y values will be rendered as the heights of elements in the grid. This type of height map could be used if a researcher wants to visualize the effect of two variables (x and z) on a third variable (y).

To create a height map, we can alter the vertices of the cube in the standard Direct3D template. Create a new Direct3D application and open the CubeRenderer.cpp file. Find the line with VertexPositionColor cubeVertices[] = and replace the definition of the vertices with the following. I have commented out the lines to replace in the following code listing to show where the new code goes.

     /*VertexPositionColor cubeVertices[] =

          {

               {XMFLOAT3(-0.5f, -0.5f, -0.5f), XMFLOAT3(0.0f, 0.0f, 0.0f)},

               {XMFLOAT3(-0.5f, -0.5f,  0.5f), XMFLOAT3(0.0f, 0.0f, 1.0f)},

               {XMFLOAT3(-0.5f,  0.5f, -0.5f), XMFLOAT3(0.0f, 1.0f, 0.0f)},

               {XMFLOAT3(-0.5f,  0.5f,  0.5f), XMFLOAT3(0.0f, 1.0f, 1.0f)},

               {XMFLOAT3( 0.5f, -0.5f, -0.5f), XMFLOAT3(1.0f, 0.0f, 0.0f)},

               {XMFLOAT3( 0.5f, -0.5f,  0.5f), XMFLOAT3(1.0f, 0.0f, 1.0f)},

               {XMFLOAT3( 0.5f,  0.5f, -0.5f), XMFLOAT3(1.0f, 1.0f, 0.0f)},

               {XMFLOAT3( 0.5f,  0.5f,  0.5f), XMFLOAT3(1.0f, 1.0f, 1.0f)},

          };*/

     const int mapSize = 10;

     VertexPositionColor cubeVertices[mapSize*mapSize];

     float height = 0.0f;

     for(int z = 0; z < mapSize; z++)   {

          for(int x = 0; x < mapSize; x++)   {

               height = (float)(rand()%100) / 100.0f;

               cubeVertices[x+(z * mapSize)].pos = XMFLOAT3(x, height, z);

               cubeVertices[x+(z * mapSize)].color = XMFLOAT3(0.0f, height,

               0.0f);

               }

          }

This code defines a 2-D grid running left to right and into the screen. The y values are the data points we will be plotting. In this example they are random values from 0.0f to 1.0f, but you would usually load these values from a data source. This is our vertex buffer in system RAM.

Now that we have stored the points to render for our height map in the vertex buffer, we need to construct a list of triangles that describes adjacent points. The vertex buffer does not describe any shapes, it is just a list of points. We wish to specify a collection of flat triangles connecting adjacent points, such that when the data is rendered, each of the y values from the nested for loops in the previous code becomes little mountains and crevices. The following code creates an index buffer for this purpose (again, I have commented out the original cubeIndices definition, which we are replacing):

     /*unsigned short cubeIndices[] =

          {

               0,2,1, // -x

               1,2,3,

               4,5,6, // +x

               5,7,6,

               0,1,5, // -y

               0,5,4,

               2,6,7, // +y

               2,7,3,

               0,4,6, // -z

               0,6,2,

               1,3,7, // +z

               1,7,5,

          };*/

// There's (mapSize-1)*(mapSize-1) squares and 2 triangles per square,

// so 6 points each.

unsigned short cubeIndices[(6*(mapSize-1))*(mapSize - 1)];

int idx = 0; // Index counter to keep track of which square we're defining

// Step through the points and define the indices that create adjacent squares

// from them.

for(int z = 0; z < (mapSize-1); z++){

     for(int x = 0; x < (mapSize-1); x++){

          unsigned short farLeftPoint = x + z * mapSize; // Far left point

          unsigned short farRightPoint = farLeftPoint+1; // Far right point

          unsigned short nearLeftPoint = x + (z+1)*mapSize;// Near left point

          unsigned short nearRightPoint = nearLeftPoint+1; // Near right point

                

          // Define 6 points that create 2 triangles that are the square

          cubeIndices[idx++] = farLeftPoint;

          cubeIndices[idx++] = nearRightPoint;

          cubeIndices[idx++] = nearLeftPoint;

                

          cubeIndices[idx++] = farLeftPoint;

          cubeIndices[idx++] = farRightPoint;

          cubeIndices[idx++] = nearRightPoint;

          }

     }

As mentioned previously, it is normal for an index buffer to reference the same vertices more than once to describe adjacent triangles. If two triangles are directly beside each other and they share a line, we need not store six vertices (one for each point of each of the two triangles). We can store four vertices and describe the two adjacent triangles with our index buffer, reusing two of the vertices:

Triangles Forming a Square

In Figure 46, there are two triangles formed by drawing lines from four points. Our vertex buffer holds the four points and our index buffer holds the indices that create the triangles: { 0, 1, 3, 1, 2, 3}. The first three integers in the index buffer describe the lines that create the yellow colored triangle { 0, 1, 3 }. The next three indices describe the lines that make the green colored triangle { 1, 2, 3 }. In this way, vertices 1 and 3 are reused, and memory use and processing time is improved on the GPU.

In this height map code, we are stepping through the points one at a time, defining triangles with each. Two triangles make a square, and we step through to mapSize-1 because the final points on the edges of the map are the right-hand sides of the squares. In other words, there is one less square than the number of points. When you execute the program, you should see a spinning, green height map. Higher values are rendered more green than lower ones.

In the Render method, there is a call to IASetPrimitiveTopology which asks the GPU to render the points as triangles. Another good way to render a height map is to use lines. You can change the parameter of this method from D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST to D3D11_PRIMITIVE_TOPOLOGY_LINELIST and your height map will be rendered as a collection of colored lines.

Tip: In the previous example, I used local arrays to store the indices and vertices in system memory; with even a moderately sized height map this will very quickly lead to a stack overflow. Parameters local to functions are stored on the stack such that memory allocated for them is automatically cleared when the function returns. If you require a larger height map, you should consider using the heap with the “new” operator.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.