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

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

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

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!

Tags: Procedural Generation, Javascript

View All Posts