środa, 5 listopada 2008

GPU terrain in OpenGL tutorial - part 2

This post contains the actual tutorial. For an introduction and some theoretical background on it, see the previous post.

First off, if you just want to see an exemplary working implementation of the method, check out the Destination engine source code. The code related to terrain resides in the class terrain, which is declared in v_local.h, and its implementation spans across files v_assets_world.cpp (procedural creation of terrain patch data and uploading it to buffer objects) and v_scene_world.cpp (actual drawing).

For the purposes of this tutorial I will assume that the reader has intermediate knowledge of C/C++ and OpenGL. Also, the entire terrain mesh should be square to avoid singularities, which of course can be resolved, but they're out of this tutorial's scope.

Let's start with creating some data for our to-be terrain renderer to show. L3DT is a very nice piece of software for designing large terrains. It runs in Windows and also in Linux via Wine (flawlessly, I should say). Go along with the built-in tutorial and create your terrain maps.

When all your maps are calculated, export the heightfield as a 16-bit unsigned raw image: select RAW from the drop-down list, then open the Options dialog and change the top-most option to "16-bit unsigned (metres)".

Export your texture map, too. Save it to a single image, the bigger in size the better. Choose the format to your preference, i.e. the easiest for you to load into your application.

Let's get rolling with the code! First, let's declare a couple of constants:

#define TERRAIN_PATCH_SIZE 17
#define TERRAIN_NUM_VERTS (TERRAIN_PATCH_SIZE * TERRAIN_PATCH_SIZE + TERRAIN_PATCH_SIZE * 4 - 4) // plus skirt verts at each edge minus duplicated corners
#define TERRAIN_NUM_BODY_INDICES ((2 * TERRAIN_PATCH_SIZE + 2) * (TERRAIN_PATCH_SIZE - 1))
#define TERRAIN_NUM_SKIRT_INDICES (2 * (TERRAIN_PATCH_SIZE * 2 + (TERRAIN_PATCH_SIZE - 2) * 2) + 2)
#define TERRAIN_NUM_INDICES (TERRAIN_NUM_BODY_INDICES + TERRAIN_NUM_SKIRT_INDICES)

These will be used to avoid redundant typing and calculations.

Then, load your heightmap and textures into texture units 0 and 1:

glActiveTexture(GL_TEXTURE_0);
glBindTexture(heightmapID);
glTexImage2D(GL_TEXTURE_2D,
0,
GL_LUMINANCE8_ALPHA8,
heightMapWidth, heightMapHeight,
0,
GL_LUMINANCE_ALPHA,
GL_UNSIGNED_SHORT,
heightMapData);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

glActiveTexture(GL_TEXTURE_1);
glBindTexture(textureID);
glTexImage2D(GL_TEXTURE_2D,
0,
GL_RGBA8, // set according to your texture's format
texWidth, texHeight,
0,
GL_RGBA,
GL_UNSIGNED_BYTE, // set according to your texture's format
texData);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

Note that for the heightmap, we're setting the texture parametres to clamp texture coordinates (this is to avoid ugly skirt seam artifacts) andthe nearest filtering mode (increases hardware compatibility). Also, the internal format parametre for the heightmap is GL_LUMINANCE8_ALPHA8. This usually means a grayscale image with an alpha (transparency) channel, but here it's a hack to enable us to use full 16-bit precision of the heightmap. Of course, there's also the GL_LUMINANCE16 format available. You might have heard, though, that it's not supported by all hardware. This is true. Thus the hack. It will be explained in detail once we get to the shaders.

Next, let's generate our buffers:

GLuint bufferIDs[2];

glGenBuffers(2, bufferIDs);

// vertices
glBindBuffer(GL_ARRAY_BUFFER, bufferIDs[0]);
float vertices[TERRAIN_NUM_VERTS * 3];
fillVerts(vertices);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferIDs[1]);
unsigned short indices[TERRAIN_NUM_INDICES];
fillIndices(indices);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

Then, we fill the buffers with data:

void terrain::fillVerts(float *verts) {
int i, j;
const float invScaleX = 1.f / (TERRAIN_PATCH_SIZE - 1.f);
const float invScaleZ = 1.f / (TERRAIN_PATCH_SIZE - 1.f);
float s, t;

// fill patch body vertices
float *v = verts;
for (i = 0; i < t =" i" j =" 0;" s =" j" j =" 0;" s =" j" i =" 1;" t =" i" p =" indices;" i =" 0;" j =" 0;" i =" 0;" i =" 1;" i =" TERRAIN_PATCH_SIZE">= 0; i--) {
*(p++) = i + TERRAIN_PATCH_SIZE * (TERRAIN_PATCH_SIZE - 1);
*(p++) = i * 2 + TERRAIN_PATCH_SIZE * TERRAIN_PATCH_SIZE + 1;
}
// -X edge
for (i = TERRAIN_PATCH_SIZE - 2; i >= 1; i--) {
*(p++) = i * TERRAIN_PATCH_SIZE;
*(p++) = i * 2 + TERRAIN_PATCH_SIZE * (TERRAIN_PATCH_SIZE + 2);
}
*(p++) = 0;
*(p++) = TERRAIN_PATCH_SIZE * TERRAIN_PATCH_SIZE;
assert(p - indices == TERRAIN_NUM_INDICES);
}

Some parts of it may seem enigmatic. Basically, we're creating a so-called triangle strip to cut on memory usage. Hopefully these 2 diagrams showing vertex indices will explain things. The first diagram also shows the l and d segments, which were described in the previous post and we will use later on.



The second diagram shows the skirt. Starting at the last patch body vertex #288 (marked with the red dot), the arrows show the skirt indexing order, in the repeating sequence red-green-blue.

The terrain's dimensions in its basic form will span from (0, 0) to (1, 1), with its centre at (0.5, 0.5). This is not what we want. Let's move it so that its centre is at (0, 0) and scale it up to make 1 heightmap texel equal 1 space unit. To achieve this, we need a special model view matrix:

float terMVMat[16];

void terrainSetMatrix(void) {
// create an OpenGL matrix to centre the terrain at (0, 0, 0) and scale it
// up to its full dimensions
// we can put all of this stuff together in 1 matrix because we're not
// applying any other transformations
terMVMat[0] = heightMapWidth;
terMVMat[1] = 0.f;
terMVMat[2] = 0.f;
terMVMat[3] = 0.f;
terMVMat[4] = 0.f;
terMVMat[5] = 1.f;
terMVMat[6] = 0.f;
terMVMat[7] = 0.f;
terMVMat[8] = 0.f;
terMVMat[9] = 0.f;
terMVMat[10] = heightMapWidth;
terMVMat[11] = 0.f;
terMVMat[12] = -(heightMapWidth / 2);
terMVMat[13] = 0.f;
terMVMat[14] = -(heightMapWidth / 2);
terMVMat[15] = 1.f;
}

We need to set one more parametre (the starting level for quadtree recursion) before we can proceed:

void terrainCalcMaxLevels(void) {
int pow2 = (m_heightMapWidth - 1) / (TERRAIN_PATCH_SIZE - 1);
terrainMaxLevels = 0;
for (int i = 1; i <>

After the buffers have been created, we can move on to setting up the shaders. Write the code for creating and compiling the vertex and fragment shaders and linking the program (a nice OpenGL shaders tutorial can be found here). Then, request uniform parametres:

GLint biasID, scaleID;
if ((biasID = glGetUniformLocation(programID, "bias")) == -1) {
// handle error here
}
if ((scaleID = glGetUniformLocation(programID, "scale")) == -1) {
// handle error here
}

Also, post texture unit indices (0 and 1) to variables heightMap and tex, respectively.

Time for the shaders! Here's the vertex processing code:

uniform sampler2D heightMap;
uniform vec2 bias;
uniform float scale;

vec2 scaleBiasPos(vec2 pos) {
return vec2(pos.x * scale + bias.x, pos.y * scale + bias.y);
}

void main() {
vec4 pos = gl_Vertex;
pos.xz = scaleBiasPos(pos.xz);
vec4 c = texture2D(tex0, pos.xz);
pos.y *= c.x * 255.0 + c.a * 65280.0;
gl_Position = gl_ModelViewProjectionMatrix * pos;
gl_TexCoord[0].xy = pos.xz;
}

Note the part in bold. This is the actual hack for using full 16-bit precision of the heightmap. By uploading it as GL_LUMINANCE8_ALPHA8, we fooled OpenGL into thinking this is a grayscale image with transparency. Thus, for each pixel, it read the first byte as the grayscale component, and the second as the alpha channel. Now, OpenGL converts all colour values to normalized floating point variables (i.e. in the range 0..1). This line of code does approximately the same work as the following (except on floating point numbers, as opposed to integers):

unsigned short in = 12345; // some random number
unsigned short out;
unsigned char *p = (unsigned char *)∈
// getting the result from unsigned chars
out = p[0] + (p[1] << face="courier new">// getting same result from normalized floats
// note that (x << face="courier new">// 2^8 = 256
// (x << face="courier new">float x = p[0] / 255.f;
float a = p[1] / 255.f;
out = x * 255.f + (a * 255.f) * 256.f;
// 255 * 256 = 65280
out = x * 255.f + a * 65280.f;

Of course, there is some precision loss due to conversions, but the value of out after the last line should be close enough to 12345 for our purposes.

And the fragment shader:

uniform sampler2D tex; // colour map

void main() {
gl_FragColor = texture2D(tex, gl_TexCoord[0].xy);
}

Now, on to the actual rendering part. Start the recursion like this:

// use our shaders
glUseProgram(programID);
// apply our model view matrix
glPushMatrix();
glLoadMatrixf(terMVMat);
// set up geometry buffers for rendering
glBindBuffer(GL_ARRAY_BUFFER, bufferIDs[0]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferIDs[1]);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, 0);

// recurse - call for the entire terrain mesh first
terrainRender(0.f, 0.f, 1.f, 1.f, terMaxLevels, 1.f);

// restore previous state when we're done
glPopMatrix();
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDisableClientState(GL_VERTEX_ARRAY);

These 2 functions make up the terrain renderer's core:

void terrainRender(float minU, float minV, float maxU, float maxV, int level, float scale) {
float halfU = (minU + maxU) * 0.5;
float halfV = (minV + maxV) * 0.5;

if (!noCull) { // global variable to control frustum culling
float bounds[2][3] = {
{(minU - 0.5) * heightMapWidth, 0.f, (minV - 0.5) * heightMapWidth},
{(maxU - 0.5) * heightMapWidth, 65536.f, (maxV - 0.5) * heightMapWidth}
};
if (frustumCullBBox(bounds)) // patch out of view frustum?
return;
}

float d2 = (maxU - minU) * heightMapWidth / (TERRAIN_PATCH_SIZE_F - 1.f);
d2 *= d2;

float v[3] = {(halfU - 0.5) * heightMapWidth, terMVMat[5] * 0.5, (halfV - 0.5) * heightMapWidth};
VectorSubtract(v, camera, v); // vector operation: v = v - camera

// use distances squared to avoid expensive square roots
float f2 = VectorLength(v) / d2;

if (f2 > C * C || level <>

For a graphical explanation of l (actually, VectorLength(v)) and d, check out the second diagram in the picture above.

And that's it! Consolidate all of this in your project and you should get a nice, fast-rendering terrain. Enjoy!

And as I said - for a working implementation, check out the Destination engine.

0 komentarze: