Hello! This has gotten a lot of attention recently. The github repo is /alan-luo/planetprocedural/. The actual program is found at https://alan-luo.github.io/planetprocedural/. There's a more updated version of these docs here, as well. Cheers!

- Introduction
- Concepts
- Mountains and Hills
- Rivers
- Sky
- Clouds
- Celestial Objects
- Trees
- Conclusion and Future Work

This is a handbook meant to explain the concepts and execution behind the "Little Planet Procedural" project, my directed study midterm project.

As some background, the project is meant as a case study of how code can create diverse and infinite art through basic techniques of procedural generation. It is inspired by the likes of No Man's Sky.

Each time the page is refreshed, a new planet is generated. There are night scenes and day scenes.

The project is written entirely with raw HTML5 Canvas.

There is, for the most part, only one concept used throughout the entire project. To see this, let's first consider the following problem. Say we want to generate some hills. How should we go about doing so? We want something that looks kind of like this:

We want there to be some randomness, but we also want an organic nature to the terrain. In other words, we want local similarity, but global randomness. Generating random points is no good, since it gives us too much local variation.

We could mediate this by stretching it all the way out. We call this interpolation.

Technically, with a more complex interpolation that smoothes out the ridges, this method could achieve feasible terrain. In fact, there are an infinite number of ways to generate realistic terrain. However, we will only look at one, called Perlin Noise. The details of Perlin Noise are complex and will not be explored. However, the result is seen in the first image, which is generated using Perlin Noise.

It turns out we can extend this reasoning to higher dimensions. What if we want to generate three-dimensional terrain? We'd want to achieve the same local similarity and global variance on a two-dimensional input. Rather than representing this as a three-dimensional terrain, we can treat map the third dimension onto the continuum from black to white. This gives us cloud-like patterns.

This might look familiar. It's used in a lot of movies and games to create special effects. In fact, Ken Perlin, the creator of Perlin Noise, won an academy award for his work!

We refer to the "dimension" of Perlin noise as the dimension of the input. So, 2D terrain is 1D noise, generated by a dimension of input and a dimension of output. 3D terrain is 2D noise.

The cool thing is that the third dimension does not have to be physical. We could lift a plane of 2D noise through 3D space to give the impression that the terrain is changing, but we are really just using 3D noise with time as a variable.

With this intuition, we can formally define noise.

- N-dimensional noise is a function from
**R**^{n}to the continuous interval [0, 1]

To manipulate noise, we treat it like we treat sine funcitons. A noise function *f* of a vector **x** can take the form a*f*(b**x**)+**c**. This will make the amplitude vary from 0 to a, stretch by a factor of b, and offset by c.

In this case, the image on the right is the same noise on the left, but stretched out.

Now that these basic concepts are down, let's get rolling!

The hills and mountains are both generated by one-dimensional Perlin Noise. There are two things to notice. Firstly, the mountains have much more chaotic noise than the hills. Secondly, both have "layers" of color.

The variation is generated through what we call octaves. Like any function, Perlin Noise follows the concept of constructive interference. Suppose we have a sine wave. See how smooth it is?

Now suppose we want to introduce some variation to it. One thing we could do is superimpose a smaller sine wave on top - one that has both a smaller wavelength and amplitude.

We can do this again and again to get more and more "noisy" (haha) functions. (If you add them infinitely you get something somewhat bizarre - but that's a different story.)

We call these "octaves" because the first harmonic of a signal, or twice the fundamental frequency, or the wave `sin(2x)`

corresponding to the wave `sin(x)`

, corresponds to the musical sound one octave up. For example, the note A4 on a piano is 440Hz, while A5 is 880Hz, A3 is 220Hz, and so on.

We can similarly create octaves for Perlin Noise. Implementation-wise, I write one function for drawing and filling in noise:

```
function make1DNoise(axis, amplitude, scale, params) {
var newNoise = [];
for(var i=0; i<CANVAS_WIDTH; i++) {
newNoise.push({x: i, y:axis+amplitude*
params.noiseFunction(scale*i, params.zaxis)});
}
newNoise.push(LOWER_LEFT); newNoise.push(LOWER_RIGHT);
ctx.fillStyle = params.fillColor;
fillPath(newNoise);
}
```

(`fillPath`

is not a default canvas function - it's a function I wrote to make and fill a path from an array of points)

There are a few things to notice here. First of all, I pass in axis, amplitude, and scale as arguments, corresponding very similarly to the constants that manipulate a sine function. Secondly, I pass in a params variable that holds a number of useful things. For instance, I can step through the other dimension of the noise even if I am only using 1D noise, a feature that will become useful soon. More importantly, I reference this thing called `noiseFunction`

. What's that about?

It turns out that in scripting language, Javascript included, everything is pretty much the same. Objects are functions and vice versa. So, I can pass in a function as an argument of a function, and save myself some time. I essentially wrote two noise functions: one for mountains, and one for hills.

```
function mountainNoise(x, z) {
return simplex.noise2D(z, x)+0.5*simplex.noise2D(0, 2*x
+0.25*simplex.noise2D(0, 4*x)+0.125*simplex.noise2D(0, 8*x);
}
function hillNoise(x, z) {
return simplex.noise2D(z, x);
}
```

(Simplex Noise is just a variant of Perlin Noise)

I pass these functions in as parameters of my previous function, and thus can use the same function for drawing mountains and hills. Notice that the mountains have four octaves and the hills one, corresponding to their very different noise shapes.

You might also notice that there are bands of color on the hills and on the mountains.

To see how these work, let's first make the following observation. Let's say we have a plane intersecting a field of 2D noise. In otherworse, let's take a slice out of the 2D noise.

If we do this, we're essentially fixing one variable of the input. In other words, we only have one remaining variable. So we've essentially reduced the dimension to 1. If we then project this sliced noise, we will see that it is 1D.

Since 2D noise follows our rule of local similarity, by taking a series of adjacent slices out of 2D noise, we can thus construct a series of similar but not identical noise functions. So, the bands of color on the mountains and hills are generated by setting slightly different fixed y-values as inputs for a 2D noise function.

```
for(var i=0; i<3; i++) {
make1DNoise(430, 200, 0.005, {noiseFunction:mountainNoise,
fillColor:toHslString(mountainshade), zaxis:0.07*i});
mountainshade.s-=0.05; mountainshade.l-=0.04;
}
```

The z-axis parameter of the draw function changes how much the 1D noise is stepped through, or the position of the slicing plane. In this case, we move the plane by 0.07 each step. The last line just slighly varies the color of each new layer drawn - more on this later.

The rivers are pretty much done the same way as the mountains. They are generated starting from the minimum point of the noise. Two sloped lines are drawn from this point. One-octave Perlin Noise extrudes on both sides. This is repeated four times to create four bands of color.

With the sky, we need to consider color schemes. Recall that we have two types of scenes: night and day.

First, we need to introduce some basic ideas of color theory. We can arrange the primary colors around a circle equally spaced from each other, then add all the in between colors. We call this the color wheel.

One paradigm for choosing aesthetically pleasing color schemes is to choose colors that are as evenly spaced around the wheel as possible in order to maintain a constant relative coloring. For instnace, green, orange, and blue would be a bad choice because blue is farther from orange than it is from green.

We can obtain a number of color schemes by choosing colors this way. **Analagous** schemes choose one color and other similar colors.

**Complementary** schemes choose one color and other similar colors.

**Triad** schemes choose three evenly spaced colors.

There are many other schemes, but we won't delve into them here. In our case, we use analagous schemes for night scenes, and complementary schemes in day scenes.

Most colors in the program are processed as hsb values rather than as rgb. Most computer colors are in the form of three bytes: one value each for red, green, and blue, coorresponding to the range from 0 to 255.

However, I process colors as a hue, a saturation, and a brightness. This is a much easier way to describe what a color "looks like" instead of tinkering with rgb values. Hue describes the angle position on the color wheel, with 0 = 360 degrees referring to red. Saturation describes how much color there is, with 0 being grayscale, and 1 being entirely colored. Brightness describes how much white there is, with 0 being totally black, and 1 being totally white.

I wrote a function that converts an hsb (sometimes known as hsl) color input to a usable string for canvas.

```
function toHslString(color) {
return "hsl("+(color.h%1.0)*360+",
"+(color.s%1.0)*100+"%, "+(color.l%1.0)*100+"%)";
}
```

Notice that taking the hue mod 1.0 makes the wheel into a circle.

The details of the colors are mostly tinkered with through experimentation. There are two different schemes - one for day, one for night. I've left the full code here.

```
if(data.time=="night") {
baseColor = randomColor({brightness:"dark"});
mountainColor = {h:baseColor.h+Math.random()*0.2-0.1,
s:baseColor.s+Math.random()*0.2-0.1,
l:baseColor.l+Math.random()*0.1+0.15};
hillColor = {h:mountainColor.h+Math.random()*0.2-0.1,
s:mountainColor.s+Math.random()*0.2-0.1,
l:mountainColor.l+Math.random()*0.1+0.15};
riverColor = {h:baseColor.h,
s:baseColor.s-Math.random()*0.1-0.05,
l:baseColor.l+Math.random()*0.1-0.05};
skyColor2 = {
h:baseColor.h,
s:baseColor.s,
l:baseColor.l-0.2
};
cloudColor={h:baseColor.h, s:0.4, l:0.2};
treeColor={h:baseColor.h+0.5, s:0.4, l:0.2};
leafColor={h:treeColor.h+0.5, s:0.8, l:0.6};
planetColor={h:hillColor.h, s:0.4, l:0.4};
} else if(data.time=="day") {
baseColor = randomColor({brightness:"medium"});
mountainColor = {h:baseColor.h+0.4+Math.random()*0.1,
s:0.2+Math.random()*0.2,
l:baseColor.l+Math.random()*0.1-0.05};
hillColor = {h:mountainColor.h+Math.random()*0.2-0.1,
s:0.4+Math.random()*0.2,
l:baseColor.l+Math.random()*0.1};
riverColor = {h:baseColor.h,
s:baseColor.s-Math.random()*0.1-0.05,
l:baseColor.l+Math.random()*0.1+0.2
};
skyColor2 = {
h:baseColor.h,
s:baseColor.s,
l:baseColor.l-0.2
};
cloudColor={h:baseColor.h, s:0.3, l:0.9};
treeColor={h:baseColor.h-0.25, s:0.6, l:0.4};
leafColor={h:treeColor.h+0.5, s:0.8, l:0.6};
planetColor={h:treeColor.h+0.5, s:0.8, l:0.6};
}
```

You'll notice the sky is partitioned into triangular cells. This is done with an algorithm called Delaunay Triangulation. A Delaunay triangulation, informally, is a triangulation of a set of points that makes every triangle as close to equilateral as possible. It's a way of triangulating points to make them look "nice and even."

(I took this image from Wikipedia.)

For the sky, I generate a gradient. Then, I generate a bunch of random points, and also add some points on the edges. I throw all of this in to a Delaunay triangulation algorithm to get a set of triangles. For each triangle, I fill the entire triangle with the color at the pixel at the center of the triangle.

```
var grd=ctx.createLinearGradient(0,CANVAS_HEIGHT,0,0);
grd.addColorStop(0,colors.skyColor);
grd.addColorStop(1,colors.skyColor2);
ctx.fillStyle=grd;
ctx.fill();
var trianglePoints = [];
trianglePoints.push(
[0, CANVAS_HEIGHT], [0, 0],
[CANVAS_WIDTH, CANVAS_HEIGHT], [CANVAS_WIDTH, 0]);
for(var i=0; i<15; i++) { //add some stuff on top and bototm
trianglePoints.push([Math.random()*CANVAS_WIDTH, 0]);
trianglePoints.push([Math.random()*CANVAS_WIDTH, CANVAS_HEIGHT]);
}
for(var i=0; i<10; i++) { //add some stuff on the sides
trianglePoints.push([0, Math.random()*CANVAS_HEIGHT]);
trianglePoints.push([CANVAS_WIDTH, Math.random()*CANVAS_HEIGHT]);
}
for(var i=0; i<50; i++) { //add some stuff in the middle
trianglePoints.push([Math.random()*CANVAS_WIDTH,
Math.random()*CANVAS_HEIGHT]);
}
for( /* do this for each triangle */) {
var center;
center.x = (newtriangle[0].x+
newtriangle[1].x+
newtriangle[2].x)/3;
center.y = (newtriangle[0].y+
newtriangle[1].y+
newtriangle[2].y)/3;
var centercolor = ctx.getImageData(center.x, center.y, 1, 1).data;
var fillcolor = "rgb("+centercolor[0]+",
"+centercolor[1]+", "+centercolor[2]+")";
ctx.fillStyle = fillcolor;
fillPath(newtriangle);
}
```

The clouds are also generated using Perlin Noise. We essentially use a threshold function to chop off the noise past a certain point. To see this, imagine we intersect the field of noise with a plane, like before, but this time on the outputs.

If we take this and view it from the top, we see our cloud patterns.

You'll also notice that the noise in this image seems to be much cleaner than the real clouds. That's because we also introduce an element of variation on the threshold. Also, in order to simulate actual clouds, we stretch out the noise horizontally by 10x. Also, we draw the clouds at 40% opacity.

```
function makeClouds(threshold, offset, variance) {
ctx.globalAlpha = 0.4;
ctx.beginPath();
for(var i=0; i<CANVAS_WIDTH; i++) {
for(var j=0; j<CANVAS_HEIGHT; j++) {
var noiseValue = simplex.noise2D(i*0.001+offset,
j*0.01+offset);
if(noiseValue>params.threshold
+Math.random()*params.variance) {
drawPixel({x:i, y:j}, colors.cloudColor);
}
}
}
ctx.globalAlpha = 1.0;
}
```

There are a number of celestial objects in the scenes. At day, a sun is generated. At night, a planet and stars are generated.

We generate a bunch of stars at random points in the sky. Most stars are just little cirlces, but each star has a random chance of being a "big star." The big stars are meant to simulate twinkling, and are modelled as an intersection of two thin rectangles, with a gradient fill.

```
//5% chance to make a big star
if(Math.random()<0.05){
ctx.beginPath();
var starwidth = Math.random()*7+3;
ctx.rect(starx-1, stary-starwidth, 2, 2*starwidth);
ctx.rect(starx-starwidth, stary-1, 2*starwidth, 2);
var grd=ctx.createRadialGradient(starx, stary, 3,
starx, stary, starwidth+5);
grd.addColorStop(0,"white");
grd.addColorStop(1,"rgba(1, 1, 1, 0.0)");
ctx.fillStyle=grd;
ctx.fill();
}
```

The planet is pretty much hard-coded in. In future work I may add some variance. For now, I just generate a circle and a bunch of ellipses at an angle to simulate light. I use a method very similar to that I used for the clouds to introduce some texture, as well.

```
function makePlanet(position, radius, params) {
ctx.beginPath();
ctx.arc(position.x, position.y, radius, 0, 2*Math.PI);
ctx.fillStyle = colors.planetColor;
ctx.fill();
ctx.save();
ctx.clip();
//in a square around the planet
var xposmax = position.x+radius;
var yposmax = position.y+radius;
ctx.globalCompositeOperation = 'overlay';
for(var xpos=position.x-radius; xpos<xposmax; xpos++) {
for(var ypos=position.y-radius; ypos<yposmax; ypos++) {
if(simplex.noise2D(xpos, ypos)>0.1+Math.random()*0.2) {
drawPixel({x:xpos, y:ypos}, 'rgba(0, 0, 0, 0.05)');
}
if(simplex.noise2D(xpos*0.03, ypos*0.03)>
0.1+Math.random()*0.2) {
drawPixel({x:xpos, y:ypos}, 'rgba(0, 0, 0, 0.05)');
}
}
}
ctx.fillStyle="rgba(255, 255, 255, 0.1)";
ctx.beginPath();
ctx.ellipse(position.x+60, position.y-60, 60,
40, 45 * Math.PI/180, 0, 2 * Math.PI);
ctx.fill(); ctx.beginPath();
ctx.ellipse(position.x+40, position.y-40, 80,
60, 45 * Math.PI/180, 0, 2 * Math.PI);
ctx.fill(); ctx.beginPath();
ctx.ellipse(position.x+20, position.y-20, 100,
80, 45 * Math.PI/180, 0, 2 * Math.PI);
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
ctx.restore();
}
```

The sun is just a circle with another circle around it.

```
function makeSun(position, params) {
ctx.beginPath();
ctx.arc(position.x, position.y, params.innerradius, 0, 2*Math.PI);
ctx.globalAlpha = 0.95;
ctx.fillStyle=toHslString({h:baseColor.h, s:0.3, l:0.8});
ctx.fill();
ctx.arc(position.x, position.y, params.outerradius, 0, 2*Math.PI);
ctx.globalAlpha = 0.5;
ctx.fillStyle=toHslString({h:baseColor.h, s:0.3, l:0.9});
ctx.fill();
ctx.globalAlpha = 1.0;
}
```

The trees have two components. The program has two canvas components. The first shows the scene. Whenever a tree is clicked on, the second shows a fractal tree.

We should first consider the goal in making trees. We model the trunk as a rectangle, and the leaves as a blob. So, what exactly is a blob? How can we create one? Essentially, we'd like the blob to look random if you see it from a distance, but still maintain its local circular structure. Sound familiar?

Turns out, all we do is take a loop of Perlin noise. In the past, we've been inputting lines to the noise function. However, nothing stops us from putting in a circle.

Essentially, we make a circle out of a bunch of vertices. Then, for every vertex on the circle, we input its coordinate into a noise function and extrude it by that much.

```
var pointcount = 30, radius = 10, blobpoints;
for(var j=0; j<pointcount; j++) {//map points onto a circle
var prepos = {
x:leafcenter.x+radius*Math.cos(j*(2*Math.PI)/pointcount),
y:leafcenter.y+radius*Math.sin(j*(2*Math.PI)/pointcount)};
newradius = radius + 5*simplex.noise2D(prepos.x, prepos.y);
var newpos = {
x:leafcenter.x+newradius*Math.cos(j*(2*Math.PI)/pointcount),
y:leafcenter.y+newradius*Math.sin(j*(2*Math.PI)/pointcount) };
blobpoints.push(newpos);
}
fillPath(blobpoints);
```

Clicking on the trees is supposed to simulate "zooming in" on the tree. So, clicking on the same tree twice gives the same "zoomed" tree. This is done through an L-system. We're not going to go into L-systems in depth. However, we can describe one as a type of recursive drawing function. In this case, we write a function where a line segment is drawn (the "trunk" of the tree), followed by two additional segments that sprout off towards either side. However, each call of this function sets off two new calls of the function, resulting in a constant branching.

In order to achieve variation while making the same tree translate to the same L-system, we introduce an angle variation that is seeded by the position of the tree.

```
function makeBigTree(newseed) {
resetCanvas();
setRandomSeet(newseed);
branch(50);
}
function branch(len){
var theta = random()*(Math.PI/3);
drawLine({x:0, y:0}, {x:0, y:len}, ctx2);
ctx2.translate(0, len);
len *= 0.66;
if (len > 2) {
ctx2.save();
ctx2.rotate(theta);
branch(len);
ctx2.restore();
ctx2.save();
ctx2.rotate(-theta);
branch(len);
ctx2.restore();
}
}
canvas.addEventListener('mousedown', doMouseDown, false);
function doMouseDown(evt) {
var mousePos = getMousePos(canvas, evt);
for(var i=0; i<clickboxes.length; i++) {
if(mousePos.x>clickboxes[i].left
&& mousePos.x<clickboxes[i].right
&& mousePos.y>clickboxes[i].top
&& mousePos.y<clickboxes[i].bottom) {
clickboxes[i].action(i);
}
}
}
```

We use the same property of Javascript functions as objects as before here. `clickboxes`

is an array that stores rectangular bounding boxes of each tree. Each bounding box has a corresponding action, which is a function property. The action is created by the L-system drawing function seeded by the clickbox index.

Overall, this has been a fun project. It's showcased the ability for a relatively small set of techniques to generate some really cool looking stuff. There are still many, many techniques that I learned about that did not make it into the final project, including fractals and cellular automata.

If I do additional work on this, I'd like to:

- Implement scene types: e.g. presets for deserts, grasslands...
- Use a wider range of techniques
- Make the world bigger, adding star systems and such