Noise is great for procedural generation. Rather than just turning a pixel on or off in a vacuum, we apply a random gradient function over the entire output space, and so each pixel is close to its neighbors. If we interpret the output of Perlin noise as being a brightness value for each pixel, for instance, we get something like this, depending on the scale of our function:
Like a freeze frame of the static you’d get on an old tube TV tuned to a channel that didn’t exist, kind of.
We can re-scale the noise function’s input so that the gradient bounces back and forth more or less quickly between values. Simplex noise at a very small scale looks like this:
Now, neither of the above are particularly believable terrain, at least not on their own. In the past, I’d tried generating terrain by taking relatively course noise, and summing it with some deterministic function, like the distance from the center of the output space, or the distance from the edges. That worked okay, but resulted in very circular or very square islands, with a high peak in the center.
A better approach, at least for rapid development, is to re-scale our noise function so that we have relatively course noise responsible for the general outline of the world, and finer and finer noise functions added to it to create some excitement around the edges.
1 2 3 4 5 6 7 8 9 10 |
// scale is the scale at which we're running our simplex noise - we run it over and over, // and our smaller and smaller simplex worm makes smaller and smaller changes to the elevation for (var scale = (Math.pow(3, iterations)); scale > 1; scale = scale / 3) { for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { var newElevation = tectonics(x, y, scale); elevations[x][y] = elevations[x][y] + newElevation; } } } |
We can supply whatever value we’d like for the number of iterations – I think 5 is about right, but if you want your terrain to be more chaotic and for your daring adventurer to have less walking between mountains or bodies of water, you can reduce it.
The ‘tectonics’ function is my name for actually grabbing the noise for this iteration and adding the value to a given pixel:
1 2 3 4 5 6 7 8 |
// each time we run the tectonics function, we determine how much elevation to add // or subtract from the pixel at x, y, for the scale in question var tectonics = function (x, y, scale) { var effectiveX = x / scale; var effectiveY = y / scale; // 255 is the maximum elevation, since it's the maximum intensity of a channel in RGB return Math.floor((255 * (scale / maxElevationBeforeRescaling)) * s.noise(effectiveX, effectiveY)); } |
That “maxElevationBeforeRescaling” variable is determined based on how many iterations we’re going to perform:
1 2 3 4 5 |
// determine how high the pixel would be if every iteration's noise value were 1 var maxElevationBeforeRescaling = 0; for (var i = 0; i < iterations; i++) { maxElevationBeforeRescaling += Math.pow(3, iterations - i); } |
Running the code above (with the height and width supplied, and something to paint in an html5 canvas) we can get some pretty clouds:
That’s much nicer than the original static. WIth colors, we can make it clear:
1 2 3 4 5 6 7 8 9 10 11 12 |
// finally, with the elevation of each pixel established, we color the canvas: green at elevations above 127 for land, // blue at elevations below 127 for water, and brighter color for higher elevation. for (x = 0; x < width; x++) { for (y = 0; y < height; y++) { if (elevations[x][y] > 127) { context.fillStyle = 'rgb(0,' + elevations[x][y] + ',' + Math.floor(elevations[x][y] / 4) + ')'; } else { context.fillStyle = 'rgb(0,' + Math.floor(elevations[x][y] / 4) + ',' + elevations[x][y] + ')'; } context.fillRect(x, y, 1, 1); } } |
And ultimately we get a pretty convincing distinction between land and water:
You can hit kaiserleib.github.io/islands and refresh to get different coastlines, or view the complete source at github.com/kaiserleib/islands.
The thing that’s most impressive to me about this approach is how compact it is, given how nice the output is. With 50 or so lines of code (assuming we take the Simplex noise function as a given) we get a reasonably believable world, complete with elevation data. The generation process doesn’t mesh at all with real geology, but it’s compatible with algorithms for rainfall and erosion and biome placement and you can get a realistic-looking world out of it.
Now, it’s not terribly runtime-efficient. You have to compute Simplex noise five times for each pixel, which adds up fast. But it’s a comprehensible approach.