OpenGL ES SDK for Android
ARM Developer Center
|
This sample will show you how to efficiently implement geometry clipmaps using OpenGL ES 3.0. The sample makes use of 2D texture arrays as well as instancing to efficiently render an infinitely large terrain. The terrain is asynchronously uploaded to the GPU using pixel buffer objects.
The source for this sample can be found in the folder of the SDK.
The terrain representation is based on the paper of Losasso and Hoppe [1].
The basic building block of the terrain is a tesselated square with N-by-N (in this implementation, 64-by-64) vertices. A tesselated square can be represented efficiently with a single triangle strip.
Generate vertex data for a block:
Generate index buffer for a block:
This block lies on the horizontal plane (XZ-plane) and is stitched together in a grid to draw an arbitrarely vast terrain as seen above.
To avoid aliasing and excessive detail in drawing, terrain which is drawn farther away requires lower resolution. This is achieved by scaling up the basic N-by-N block in powers of two.
When stitching together the clipmap, there are small holes which must be filled. This is done by drawing smaller "fixup" and/or "trim" regions as seen above. Strips of degenerate triangles must also be drawn on the edges where the clipmap level changes.
The degenerate triangles which connect level N and N + 1 are drawn using vertices from level N. Trim regions which connect level N and N + 1 are drawn using vertices from level N + 1.
The exact layout of blocks must be carefully planned to ensure a seamless terrain. An important thing to note is that the distance between two adjacent N-by-N blocks is N - 1. Most offsets seen in the sample code use this distance with the occasional 2 texel offset to account for the width of the horizontal and vertical fixup regions.
The vertex buffer along with the index buffer is uploaded once to the GPU at startup.
Positions of blocks are moved along with the camera in discrete steps. Moving in discrete steps is important to avoid a vertex "swimming" effect. As the camera moves, lower clipmap levels can change position while higher levels don't and therefore the trim region used to connect two clipmap levels might have to change to be able to fill the entire terrain.
On every frame, clipmap level offsets are computed as such:
Offsets of blocks within a clipmap level are relative to the clipmap level offset.
OpenGL ES 3.0 added guaranteed support for sampling textures in the vertex shader. This allows the application to dynamically update vertex data in ways which would have been costly with older methods. The vertex buffer is fixed, and never has to be updated. (see advanced_samples/Terrain/jni/shaders.h)
While the vertex buffer represents the fixed grid structure in the horizontal plane, the vertical Y component is dynamic and is sampled from a heightmap texture.
Each clipmap level is backed by its own 255x255 texture. As the size for each level is the same, it is convenient and efficient to use a 2D texture array, introduced in OpenGL ES 3.0. Using a texture array avoids having to bind different textures for drawing different clipmap levels which reduces the number of draw calls required.
Using the clipmap method, only parts of the terrain will be visible in a LOD at a time. As the camera moves around, new area has to be updated in the heightmap textures to give the illusion of a seamless, never-ending terrain.
The heightmap can be updated either by uploading new data from pre-computed heightmaps or using frame buffer objects with shaders to update the heightmap procedurally.
This sample implements heightmap update by copying samples from a pre-computed 1024x1024 buffer which is generated by band-pass filtering white noise. The copy is done asynchronously using pixel buffer objects.
The clipmap rendering code uses the GL_REPEAT texture wrapping feature to ensure that only a small part of the texture has to be updated every time the camera moves.
The pre-computed heightmap is repeated to make the terrain infinite.
The resolution of the clipmap abruptly changes resolution between levels. This discontinuity in detail results in artifacts. To avoid this, two heightmap levels (current and next) are sampled and blended together in the vertex shader.
To avoid filtering the heightmap value from the next clipmap level, the filtered version of the heightmap is precomputed and included along with the height of the current level.
When drawing the terrain, most of the terrain will not be visible. To improve performance it is important to avoid drawing blocks which will never be shown.
This terrain sample implements simple frustum culling based on axis-aligned bounding boxes.
The idea of this frustum culling implementation is to represent all planes of the camera frustum as plane equations. When an axis-aligned box is tested for visibility, we check every corner of the bounding box against the frustum planes, one plane at a time.
If every corner of the bounding box is on the "wrong" side of a plane (negative distance), we can prove that the mesh contained inside the box will never be drawn. Thus, the mesh can be culled if we can prove invisibility for at least one of the frustum planes.
To obtain the plane equations for the frustum in world space, an inverse transform from clip space is done.
OpenGL ES 3.0 introduced a new way of passing uniform data to shaders. Instead of making many calls to glUniform* when uniforms change, it is possible to let the uniform data be backed by a regular OpenGL buffer object.
A very simple example of this (GLSL) is:
It is possible to use multiple uniform blocks inside a shader, which makes it necessary to access uniform blocks by index.
After linking the shader program, we can query it with:
Now, we want to specify where the uniform buffer will pull data from:
The uniform buffer now sources data from binding 0. To bind a buffer for the shader to use, we need to use the new indiced versions of glBindBuffer.
The clipmap consists of simple building blocks which are drawn many times. To drastically reduce the number of draw calls required, the blocks can be drawn using instancing, which was introduced in OpenGL ES 3.0.
Uniform buffer objects are used to access per-instance data in the vertex shader. When drawing instanced, a built-in variable gl_InstanceID is available, which can be used to access an array of per-instance data in a buffer object.
[1] http://research.microsoft.com/en-us/um/people/hoppe/geomclipmap.pdf