Skip to content

Instantly share code, notes, and snippets.

@samlecuyer
Last active September 20, 2016 10:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samlecuyer/62ee98c6e4e5730681cbfd688fc1b908 to your computer and use it in GitHub Desktop.
Save samlecuyer/62ee98c6e4e5730681cbfd688fc1b908 to your computer and use it in GitHub Desktop.
perlin noise post 1
<style type="text/css">
canvas.perlin {
display: block;
margin: auto;
}
canvas.plasma {
height: 200px;
}
</style>
_Note: the diagrams here require a browser that supports canvases_
_If you're on a mobile browser, they may not show up._
I've been reading about Perlin noise and I think it's an amazing method of generating gradients of colors and terrains.
I originally came across it because my friend Bill sent me a link to a [procedural map generator](https://twitter.com/williamberryiii/status/765420845971808256). Without driving you away from my own post (except it's way cooler), I'll explain the general map technique briefly.
An area is divided into a number of polygons using a [Voronoi diagram](https://en.wikipedia.org/wiki/Voronoi_diagram) and a heightmap is generated using Perlin noise to figure out the elevations of various areas on the map. Although I'm very familiar with Voronoi diagrams and how they're created, I had no idea how heightmaps were created.
### What is it?
Perlin noise is an algorithm developed by Ken Perlin in the 1980s that generates what's called _gradient noise_. Basically, it interpolates points in a pre-set lattice. It can work for any number of dimensions, though we'll use 3 today.
It does this by calculating an n-dimensional grid of points that have somewhat random vectors off of each point, then calculates how each of those vectors "average out" to the input point. It's a little more complicated than that but it's a simple enough explanation for now. The algorithm itself, though, is simple and can be implemented in a couple dozen lines of code.
A key takeaway from this is that the Perlin noise function is not random. The vectors in the grid can be random, but once they're generated the function is completely referentially transparent -- that is, the same inputs will always produce the same outputs.
### What does it look like?
We'll take a look at the Perlin noise function in two dimensions, since it's far simpler than three to discuss.
The noise function has a signature of `perlin.noise(x, y, z)`, which takes 3 floats of any size and returns a float between -1 and 1.
The input values are the coordinates of a point in three dimensional space: `x` is horizontal, `y` is vertical, `z` is depth.
If we graph the function along the x plane at a fixed y and z, then we'll see how it goes up and down.
It looks like the following:
<canvas id="first" class="perlin"></canvas>
_the perlin noise along the x axis_
That might not seem very useful, so let's talk about it. Despite seeming a bit random, the output is a wave. It is subject to the same frequency and amplitude transforms that other waves can be subjected to.
The output above has a very high frequency and a very low amplitude.
We can reduce the frequency by multiplying the inputs by a new frequency. Because the frequencies will be quite small, it can sometimes be simpler to divide the inputs by a new period (the inverse of frequency). We can also increase the output by simply multiplying the result by an amplitude.
We can make a much more reasonable looking graph by using
perlin.noise(x/12, y/12, z/12) * 25:
<canvas id="second" class="perlin"></canvas>
_lower frequency, higher amplitude_
If we're thinking of this in terms of terrain, that might look like a jagged ridge.
If we decrease the frequency by even more, we can create a landscape with smoother rolling hills:
perlin.noise(x/50, y/50, z/50) * 50:
<canvas id="third" class="perlin"></canvas>
_even lower frequency, even higher amplitude_
It's important to keep in mind that the graphs we're looking at are at a particular depth in 3-dimensional spaces. We can animate the wave as though it were coming toward us (out of the page) and continue to see how the same slice transforms along the z-axis.
<canvas id="fourth" class="perlin"></canvas>
_the perlin noise on the x and z axes_
### Changing dimensions
I write this section with the following disclaimer: I kind of hate this.
By _this_, I mean the following diagram. Displaying the graph in two dimensions is hard. It's hard to show depth.
Until now, we've been graphing the output of the function as a vertical value along a single axis in the noise. We're going to start showing it in two dimensions.
The typical way to do this is to move along both the x and y axes and use lightness and darkness to indicate the variations in the output.
It's almost like we're trying to photograph a slice of an infinite smoke cloud.
<canvas id="fifth" class="perlin"></canvas>
_such a tired, overused graph_
Again, this is a slice of the box at a specific z value. Every pixel is black with its opacity adjusted to match the output of the noise. Remember, the output is somewhere between -1 and 1. We can just add 1 to get a range of 0 to 1 then divide by 2, and we have a percentage of opacity for each pixel.
What's much more fun is to move through space with colors.
In addition to the RGBA color scheme you're familiar with, we can also use the [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) (hue, saturation, value) scheme to create a light and dark scheme of shades within a particular color using the output value as the lightness value.
By decreasing the frequency significantly and using an HSV color, we can create a pretty cool plasma effect:
<canvas id="sixth" class="perlin plasma"></canvas>
There's a lot more that can be said about the Perlin nois function, but it'll have to wait for other posts. We can talk about octaves and heightmaps then.
The code for this post is available as a [github gist](https://gist.github.com/samlecuyer/62ee98c6e4e5730681cbfd688fc1b908)
<script src="https://static.tumblr.com/me6da6z/VIZocbre9/improvednoise.js"></script><script type="text/javascript">
var first = document.getElementById('first');
drawPoints(first, 1, 10)
var second = document.getElementById('second');
drawPoints(second, 12, 25)
var third = document.getElementById('third');
drawPoints(third, 50, 50)
var fourth = document.getElementById('fourth');
var ctx = fourth.getContext('2d');
var fourthZ = 0.5;
function animFourth(argument) {
requestAnimationFrame(animFourth)
fourthZ += 0.01
ctx.clearRect(0,0, fourth.width, fourth.height)
drawPointsZ(ctx, 100, 75, fourthZ, '#595959');
drawPointsZ(ctx, 100, 75, fourthZ + 0.01, '#4d4d4d');
drawPointsZ(ctx, 100, 75, fourthZ + 0.02, '#404040');
drawPointsZ(ctx, 100, 75, fourthZ + 0.03, '#333333');
}
requestAnimationFrame(animFourth);
var fifth = document.getElementById('fifth');
var z = 0;
var delta = 0.01;
drawGrid(fifth, 25, 128);
var sixth = document.getElementById('sixth');
var z = 0;
var delta = 0.01;
function animPlasma() {
requestAnimationFrame(animPlasma)
z += delta;
drawPlasma(sixth, 100, 255, z)
}
requestAnimationFrame(animPlasma);
// the rest of these are graphing functions
function drawPoints(canvas, freq, amp) {
var ctx = canvas.getContext('2d');
var perlin = new ImprovedNoise();
var points = new Array(canvas.width).fill().map(function (_, x) { return perlin.noise(x/freq, 0, 0.5)});
ctx.beginPath();
points.forEach(function (val, i) {
ctx.lineTo(i, val * amp + (canvas.height/2));
});
ctx.stroke();
}
function drawPointsZ(ctx, freq, amp, z, color) {
var perlin = new ImprovedNoise();
var canvas = ctx.canvas;
var points = new Array(canvas.width).fill().map(function (_, x) { return perlin.noise(x/freq, 0, z)});
ctx.beginPath();
points.forEach(function (val, i) {
ctx.lineTo(i, val * amp + (canvas.height/2));
});
ctx.strokeStyle = color;
ctx.stroke();
}
function drawPlasma(canvas, freq, amp, z) {
var ctx = canvas.getContext('2d');
var image = ctx.createImageData(canvas.width, canvas.height);
var perlin = new ImprovedNoise();
for (var y = 0; y < image.height; y ++) {
for (var x = 0; x < image.width; x ++) {
var noise = perlin.noise(x/freq, y/freq, z) + 1;
var base = ((y * image.width) + x) * 4;
var color = HSVtoRGB(195/360, 1, noise / 2)
image.data[base] = color.r
image.data[base + 1] = color.g
image.data[base + 2] = color.b
image.data[base + 3] = noise * amp
}
}
ctx.putImageData(image, 0, 0);
}
function drawGrid(canvas, freq, amp) {
var ctx = canvas.getContext('2d');
var image = ctx.createImageData(canvas.width, canvas.height);
var perlin = new ImprovedNoise();
for (var y = 0; y < image.height; y ++) {
for (var x = 0; x < image.width; x ++) {
var noise = perlin.noise(x/freq, y/freq, 0.5) + 1;
var base = ((y * image.width) + x) * 4;
image.data[base + 3] = noise * amp
}
}
ctx.putImageData(image, 0, 0);
}
// I copied this straight off of StackOverflow
// http://stackoverflow.com/a/17243070
function HSVtoRGB(h, s, v) {
var r, g, b, i, f, p, q, t;
if (arguments.length === 1) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
</script>
@samlecuyer
Copy link
Author

This is the source code for the post at http://cateches.is/post/149353123260/perlin-noise

@gfixler
Copy link

gfixler commented Sep 20, 2016

"We can just add 1 to get a range of 0 to 1"

Did you mean a range of 0 to 2?

Also, you have a "nois" in there (should be "noise").

Fun post!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment