LWJGL 3D Terrain Update
by
, October 3rd, 2012 at 03:06 PM (11818 Views)
Updated post here. I'm starting to modernize my code by moving to a shader-based model.
Made some changes to my code and pretty much everything is significantly better. My previous post: LWJGL 3D Terrain
The aesthetics were greatly improved by switching from bilinear interpolation to a sine-based interpolation function. For more details on how I generated the height map see this page.
For fun I switched the colors up to give brownish hue to make the terrain look more like rock. I also upped the height map resolution from 300x300 up to 1024x1024, which is made possible by changes made to the render loop.
I'm now caching the terrain into an OpenGL Display List which holds the draw instructions into a pre-compiled instruction set on the server (a.k.a. graphics card). This takes a huge load off of the memory bus and CPU. In fact, I'm now getting about 440 million quads per second throughput (compared to 12.6 million quads per second before), which is pretty close to the specs of my graphics card. The next step is to implement some LOD code to further increase performance without affecting quality too much.
Here's a brief overview on how I'm rendering the terrain (with some code, too).
After generating the height map, I create a display list which stores the vertex, color, and normals for the terrain. Display lists cannot be changed once they are created, so they're best to use for items that are expected to not change.
Here's how I'm generating the terrain display list:
public void generate_cache() { // create a cache of the quads, normals, and colors float vertices[] = new float[12]; float color[] = new float[6]; float normal[] = new float[6]; float vec1[] = new float[3]; float vec2[] = new float[3]; display_list = GL11.glGenLists(1); GL11.glNewList(display_list, GL11.GL_COMPILE); for (int row = 0; row < elevation.getHeight() - 1; ++row) { GL11.glBegin(GL11.GL_QUAD_STRIP); for (int col = 0; col < elevation.getWidth() - 1; ++col) { // for each quad // vertex data vertices[0] = col * scale_width; vertices[1] = row * scale_height; vertices[2] = elevation.getData(row, col) * scale_elev; vertices[3] = col * scale_width; vertices[4] = (row + 1) * scale_height; vertices[5] = elevation.getData(row + 1, col) * scale_elev; vertices[6] = (col + 1) * scale_width; vertices[7] = row * scale_height; vertices[8] = elevation.getData(row, col + 1) * scale_elev; vertices[9] = (col + 1) * scale_width; vertices[10] = (row + 1) * scale_height; vertices[11] = elevation.getData(row + 1, col + 1) * scale_elev; // color data color[0] = 0.6f - elevation.getData(row, col); color[1] = 0.55f - elevation.getData(row, col); color[2] = 0.5f - elevation.getData(row, col); color[3] = 0.6f - elevation.getData(row + 1, col); color[4] = 0.55f - elevation.getData(row, col); color[5] = 0.5f - elevation.getData(row + 1, col); // normal data // vector 1 is v0 to v1 vec1[0] = vertices[1 * 3] - vertices[0]; vec1[1] = vertices[1 * 3 + 1] - vertices[1]; vec1[2] = vertices[1 * 3 + 2] - vertices[2]; // vector 2 is v0 to v2 vec2[0] = vertices[2 * 3] - vertices[0]; vec2[1] = vertices[2 * 3 + 1] - vertices[1]; vec2[2] = vertices[2 * 3 + 2] - vertices[2]; calc_normal(vec1, vec2, normal, 0); // vector 1 is v1 to v3 vec1[0] = vertices[3 * 3] - vertices[1 * 3]; vec1[1] = vertices[3 * 3 + 1] - vertices[1 * 3 + 1]; vec1[2] = vertices[3 * 3 + 2] - vertices[1 * 3 + 2]; // vector 2 is v1 to v0 vec2[0] = vertices[0 * 3] - vertices[1 * 3]; vec2[1] = vertices[0 * 3 + 1] - vertices[1 * 3 + 1]; vec2[2] = vertices[0 * 3 + 2] - vertices[1 * 3 + 2]; calc_normal(vec1, vec2, normal, 3); GL11.glNormal3f(normal[0], normal[1], normal[2]); GL11.glColor3f(color[0], color[1], color[2]); GL11.glVertex3f(vertices[0], vertices[1], vertices[2]); GL11.glNormal3f(normal[3], normal[4], normal[5]); GL11.glColor3f(color[3], color[4], color[5]); GL11.glVertex3f(vertices[3], vertices[4], vertices[5]); } GL11.glEnd(); } GL11.glEndList(); }
The first step to creating a display list is to generate one using the glGenLists method. You can generate numerous contiguous display lists at once, too by passing a different parameter other than 1. The method will return an integer which you will use to address the display list.
After you generate the display lists you need to start giving commands to be compiled to the list. This is done using the glNewList method. It takes two parameters, the address of the list and what it's initial behavior is. You have two choices: Compile only, or compile and execute.
At this point you can begin giving commands to add to the display list. My data can be conveniently mapped to a bunch of quad strips so that's exactly what I do.
In order for lighting to work properly vertices need to have their normals defined. I'm doing this by taking the cross product of two edges connected to that vertex. It's important to note that the cross product is not associative and care must taken to ensure all the normals are facing the right way. Another important note is that normals are expected to be "normalized" (i.e. have a magnitude of 1). I could manually normalize all of my normals, but instead I decided to enable GL_NORMALIZE.
Finally, all I need to do is execute the display list in my render loop to draw the terrain.
public void drawTerrain() { GL11.glCallList(display_list); // I saved this value from above as an int field }