GPU-accelerated heightmap generation in WebGL

Published: 10 Nov 2011

I needed to generate a heightmap to feed to the terrain mesh in my procedural generation project. I started with a modified version of libnoise.js, a javascript port of libnoise, using just the Perlin noise module* generate my heightmap on the CPU. This was not a fast solution, taking approximately 2 seconds to generate a 512x512 heightmap directly in a 1D javascript array. Of course this is to be expected; javascript is single threaded, and is a rather slow (relatively speaking). The heightmap generation procedure is embarrassingly parallelizable, so I initially focused on replacing the single-threaded solution.

Enter Web Workers, a new javascript feature that allows concurrent execution of multiple javascript threads in the background. I changed the generation procedure to use multiple threads by splitting the workload into background threads. Each thread worked on its portion of the problem in isolation, and serialized/posted the result back to the main execution thread which joined results to create the final heightmap. Running this solution with three threads and a different algorithm reduced the generation time to under 400ms (excluding workup startup/cleanup time). Much better than the original, but I decided it would be useful to have the option to generate a new texture every frame and the new solution was still too slow for that (60fps ~ 15ms per frame).

The final answer lay right back where I started. My goal was to push the heightmap to the GPU for the terrain mesh to use, so why not use the GPU to generate the heightmap? I found Ashima Arts’ set of fast GLSL noise generators. I plugged his simplex noise GLSL code into a fragment shader, applied it to a fractal sum and put the resulting float into a vec4 for gl_FragColor to output, which gave a heightmap similar to that generated by the original javascript-CPU method. This was all done in a framebuffer object (conceptually a virtual screen), which was used as a texture for the terrain mesh. A 1x1 plane is setup to fill the entire framebuffer, and its rendered fragment coordinates are fed to the noise generator. This gives a texture with dimensions equal to that of the framebuffer object view port, in which every point has been passed through a noise generator to generate a black-and-white heightmap. The entire heightmap generation is done in parallel on the GPU very quickly. How fast? 0.75 milliseconds* for the same 512x512 heightmap that took 2000 milliseconds on the CPU in javascript.

Not Bad

Here’s the fragment shader. Note that I used the three dimensional version of the simplex noise generator, passing in the octave as the third dimension gave a nicer noise texture than using the two-dimensional version.

// *** Ashima Art's 3d simplex noise code would go here, omitted for brevity ***
// *** the 2D version with just position as parameters doesn't look as nice ***

void main(void) {
    vec2 npos = gl_FragCoord.xy / scale;
    const float persist = 0.5;
    float persistence = 1.0;    
    float h = 0.0;
    
    // This loop doesn't run for me in Chrome without --use-gl=desktop
    // With that flag, it takes around 5 refreshes before it actually renders properly
    // Also changing 11 to any number between 1 and 10 stops the entire loop from running again
    // My real code has this loop manually unrolled at 9 iterations because that's too many WTFs for me.
    for (int i = 0; i < 11; i++) {
        h += snoise(vec3(npos,512.0*float(i + 1))) * persistence;
        persistence *= persist;
        npos *= 2.0;
    }
    
    h = clamp((1.1 + h) / 2.2, 0.0, 1.0); // adjust h to be in the 0.0 to 1.0 range instead of -1.0 to 1.0
    gl_FragColor = vec4(h, h, h, h);
}

And a snippet of the Three.js code (not functional alone)

var terrain_size = 512;

cameraFB = new THREE.OrthographicCamera(-1 * terrain_size / 2, 
                                         terrain_size / 2, 
                                         terrain_size / 2, 
                                         -1 * terrain_size / 2, 
                                         -10000, 10000);
cameraFB.position.z = 100;
sceneFB = new THREE.Scene();

FBTexture = new THREE.WebGLRenderTarget(terrain_size, terrain_size, {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.NearestFilter,
    format: THREE.RGBFormat
});

var quad = new THREE.Mesh(new THREE.PlaneGeometry(terrain_size, terrain_size, 1, 1), 
                    new THREE.ShaderMaterial({
                        uniforms: {
                            scale: {
                            type: "f",
                            value: 256.0
                        }
                    },
                    vertexShader: shaders['terrainGenerator'].vertex, // the next shader
                    fragmentShader: shaders['terrainGenerator'].fragment // the above shader
                })
);
quad.position.z = -100;
sceneFB.add(quad);
renderer.render(sceneFB, cameraFB, FBTexture, true);

// FBTexture can now be provided as a texture uniform to another shader

simple vertex shader for the above

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

* If you read Perlin’s presentation, the module isn’t actually Perlin noise. Perlin’s noise algorithm is called gradientCoherentNoise3D in the library, while the “Perlin” module is a fractal sum of Perlin’s noise function. * Estimate based on the time it took to generate 10000 different heightmaps.