Procedural Generation, Javascript
Procedurally Generated Planet Textures
For my Ludum Dare entry, I procedurally generate the textures for the planets. In the game, my math is a little off, and I didn’t have time to derive the correct formula – but now, with unlimited time, I can think a little more clearly 😀.
Perlin Noise
A good place to start is Perlin noise.
The basic Perlin noise algorithm works by taking an input image, doubling its size, and adding more random values to it. The trick is to scale the added random values so that they don’t overpower the previous random values.
Observe:
function zeroImage(){
// create an image, 1x1, with a value of 0
return { data: [0], width: 1, height: 1 };
}
function doublePerlin(img, scale){
// img is an object, with:
// data: list of values, of length width*height,
// ranging from -1 to 1
// width: width of the image
// height: height of the image
// scale is the amount to scale the random value
// (-scale to scale)
function getAt(x, y){
// get the raw data at (x, y), wrapping around the edges
return img.data[(x % img.width) +
(y % img.height) * img.width];
}
function lerp(a, b, p){
// p ranges from 0 to 1
// at p = 0, then return a
// at p = 1, then return b
// otherwise, interpolate between a and b
return a + (b - a) * p;
}
function getSmooth(x, y){
var xp = x - Math.floor(x); // get the fractional part
var yp = y - Math.floor(y); // get the fractional part
x = Math.floor(x); // round down
y = Math.floor(y); // round down
return lerp(
lerp(getAt(x, y ), getAt(x + 1, y ), xp),
lerp(getAt(x, y + 1), getAt(x + 1, y + 1), xp),
yp);
}
// create our output image
var out = {
data: [],
width: img.width * 2,
height: img.height * 2
};
for (var y = 0; y < out.height; y++){
for (var x = 0; x < out.width; x++){
// get the value from the input image, smoothed
var val = getSmooth(x / 2, y / 2);
// add a random amount
val += scale * (2 * Math.random() - 1);
// save to the output image
out.data.push(val);
}
}
return out;
}
The algorithm is simple at this point – it just performs bilinear smoothing, and adds a random
amount according to the scale
input variable.
You could imagine scaling up an image, from one pixel, to 128 pixels:
var img = zeroImage();
for (var i = 0; i < 7; i++)
img = doublePerlin(img, scale(i));
It’s that scale(i)
where the magic happens. Here are some results for different functions:
Perlin Noise
Feel free to try in your browser using this simple script.
Perlin to Depth
The next phase of generating planet textures is to interpret the Perlin noise output as a height map.
If the noise is normalized to be between 0 and 1, then regions can be identified by cut-off points. For example, everything under 0.25 could be considered deep water, 0.25 to 0.5 as shallow water, 0.5 to 0.75 as low elevation, and above 0.75 as high elevation.
Then it’s just a matter of coloring a new texture, according to the regions. Here are some samples:
Colored Regions
Like before, feel free to play with it yourself. It’s pretty fun.
Spherical Mapping
The last bit is where things get interesting. Just cutting out a circle in the middle of the colored Perlin noise still wouldn’t look like a planet. A planet is a sphere. We need to warp the texture to stretch it over a sphere, so that it looks rounded.
Polar Coordinates
The first key insight is to use polar coordinates. Our strategy will start by scanning over the input image, and calculating the angle and distance of the current point, relative to the center of the image:
Current pixel is green, Center is blue
You still remember trigonometry, right?
var dx = x - width / 2;
var dy = y - height / 2;
var dist = Math.sqrt(dx * dx + dy * dy);
var ang = Math.atan2(dy, dx);
If you don’t know about Math.atan2
, now is a good time to learn. It calculates the angle of a
triangle given the two sides. The angle returned is in radians, which is important.
Once we have polar coordinates, it’s easy to realize that all we really need to do is transform
the dist
variable somehow, then convert the polar coordinates back to cartesian.
Let’s do the easy part first – assuming we have our modified value in new_dist
– how would we
convert back to cartesian? Trigonometry to the rescue, again!
var sx = new_dist * Math.cos(ang) + width / 2;
var sy = new_dist * Math.sin(ang) + height / 2;
Transforming the Distance
Lastly, we need to calculate the formula for transforming the dist
variable.
Basically, we want to perform this transformation:
We have dist
, and we need to calculate new_dist
. The max_dist
value is always the same –
width / 2
.
What kind of insane trigonometry solves this problem?! A combination of Math.acos
and realizing
that arc length is proportionate to radians.
var max_dist = width / 2;
var quarter_turn = Math.PI / 2;
var A = Math.acos(dist / max_dist);
var B = quarter_turn - A;
var new_dist = max_dist * B / quarter_turn;
That will do it!
Final Results
All together now:
Feel free to play with the demo!