WebGL with Perlin Noise — Part 1

Sushindhran Harikrishnan
Neosavvy Labs
Published in
5 min readSep 7, 2017

I have been intrigued by Perlin Noise ever since I learned of it at my Computer Graphics class at NYU from the man himself, Ken Perlin. He won an academy award for the original implementation and in 2002 he wrote a paper on his improved noise.

What is Perlin Noise?

Perlin Noise is an algorithm that is used in procedural content generation. Perlin Noise can be used for any sort of wave-like material or texture. It could be used for procedural terrain (side-scrolling terrains in games for example), clouds, snow, water and fire effects. These effects are mostly Perlin noise in the 2nd and 3rd dimensions, but it can also be extended into the 4th dimension.

There are several really great articles out there explaining perlin noise and the math behind it. So I’m not going to go into that, but we’re going to use the noise function to build on what we’ve done in the previous WebGL articles.

Here are a couple of articles for reference.

https://mzucker.github.io/html/perlin-noise-math-faq.html

https://thebookofshaders.com/11/

Noise in 2D

Here is the Perlin Noise function we’re going to use. We’re going to call noise() with a 2D vector as a parameter and get a float value.

// Noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec3 fade(vec3 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }
float noise(vec3 P) {
vec3 i0 = mod289(floor(P)), i1 = mod289(i0 + vec3(1.0));
vec3 f0 = fract(P), f1 = f0 - vec3(1.0), f = fade(f0);
vec4 ix = vec4(i0.x, i1.x, i0.x, i1.x), iy = vec4(i0.yy, i1.yy);
vec4 iz0 = i0.zzzz, iz1 = i1.zzzz;
vec4 ixy = permute(permute(ix) + iy), ixy0 = permute(ixy + iz0), ixy1 = permute(ixy + iz1);
vec4 gx0 = ixy0 * (1.0 / 7.0), gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
vec4 gx1 = ixy1 * (1.0 / 7.0), gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
gx0 = fract(gx0); gx1 = fract(gx1);
vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0), sz0 = step(gz0, vec4(0.0));
vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1), sz1 = step(gz1, vec4(0.0));
gx0 -= sz0 * (step(0.0, gx0) - 0.5); gy0 -= sz0 * (step(0.0, gy0) - 0.5);
gx1 -= sz1 * (step(0.0, gx1) - 0.5); gy1 -= sz1 * (step(0.0, gy1) - 0.5);
vec3 g0 = vec3(gx0.x,gy0.x,gz0.x), g1 = vec3(gx0.y,gy0.y,gz0.y),
g2 = vec3(gx0.z,gy0.z,gz0.z), g3 = vec3(gx0.w,gy0.w,gz0.w),
g4 = vec3(gx1.x,gy1.x,gz1.x), g5 = vec3(gx1.y,gy1.y,gz1.y),
g6 = vec3(gx1.z,gy1.z,gz1.z), g7 = vec3(gx1.w,gy1.w,gz1.w);
vec4 norm0 = taylorInvSqrt(vec4(dot(g0,g0), dot(g2,g2), dot(g1,g1), dot(g3,g3)));
vec4 norm1 = taylorInvSqrt(vec4(dot(g4,g4), dot(g6,g6), dot(g5,g5), dot(g7,g7)));
g0 *= norm0.x; g2 *= norm0.y; g1 *= norm0.z; g3 *= norm0.w;
g4 *= norm1.x; g6 *= norm1.y; g5 *= norm1.z; g7 *= norm1.w;
vec4 nz = mix(vec4(dot(g0, vec3(f0.x, f0.y, f0.z)), dot(g1, vec3(f1.x, f0.y, f0.z)),
dot(g2, vec3(f0.x, f1.y, f0.z)), dot(g3, vec3(f1.x, f1.y, f0.z))),
vec4(dot(g4, vec3(f0.x, f0.y, f1.z)), dot(g5, vec3(f1.x, f0.y, f1.z)),
dot(g6, vec3(f0.x, f1.y, f1.z)), dot(g7, vec3(f1.x, f1.y, f1.z))), f.z);
return 2.2 * mix(mix(nz.x,nz.z,f.y), mix(nz.y,nz.w,f.y), f.x);
}
float noise(vec2 P) { return noise(vec3(P, 0.0)); }

Now let’s call noise() in our main() function.

void main() {
vec3 color;
float x = vPosition.x;
float y = vPosition.y;
color = vec3(noise(vec2(x * 21., y * 15.)));
gl_FragColor = vec4(color, 1.);
}

This generates a texture that looks as follows. Click on the image to view the webGL canvas for this. You can also view the source here.

2D Noise

Turbulence

Now let’s do something more interesting. When you take the curl of Perlin Noise, you get a nice turbulence function that helps us fake fluid dynamics. The turbulence function looks like this

float turbulence(vec3 P) {
float f = 0., s = 1.;
for (int i = 0 ; i < 9 ; i++) {
f += abs(noise(s * P)) / s;
s *= 2.;
P = vec3(.866 * P.x + .5 * P.z, P.y + 100., -.5 * P.x + .866 * P.z);
}
return f;
}

Now let’s try and create a gaseous cloud like effect with the turbulence function. Note that we’re using uTime to animate the fluid-like texture.

vec3 clouds(float x, float y) {
float L = turbulence(vec3(x, y, uTime * .1));
return vec3(noise(vec3(.5, .5, L) * .7));
}

Our main function now looks like this. We’re passing in the x and y coordinates to the cloud function.

void main() {
vec3 color;
float x = vPosition.x;
float y = vPosition.y;
vec3 cloudEffect = clouds(vPosition.x, vPosition.y);
color = cloudEffect + vec3(.5, .8, 0.95);
gl_FragColor = vec4(color, 1.);
}

The cloud texture looks like this. Click on the image to see the animation in action. Click here to view the source for this.

Cloud Effect

Applying noise to shapes

Let’s apply the cloud like effect to a sphere that we created in our previous ray tracing posts. We will still have the same initialize() and checkIntersectSphere() functions from the previous post, but we’re making a small tweak to the way we calculate the color for a pixel when the ray intersects with the sphere. Note how we multiply the cloudEffect value to the sphere color.

if (t > 0.0) {
vec3 surfacePoint = cameraSource + (t * cameraDirection);
vec3 surfaceNormal = normalize(surfacePoint - sphereCenter);
sphereBaseColor = colorOfSphere * (ambience + ((1.0 - ambience) * max(0.0, dot(surfaceNormal, lightSource))));
color = vec3(cloudEffect * sphereBaseColor) + vec3(0.5, 0.2, 0.1);
}

Our main() function will remain the same.

void main() {
vec3 color;
float x = vPosition.x;
float y = vPosition.y;
initialize();
color = checkIntersectSphere(spheres[0], rays[0], light[0]);
gl_FragColor = vec4(color, 1.);
}

This is how our sphere looks now. Click on the image to view the animated texture. Click here to view the source for this.

Sphere with gaseous texture.

I will follow this up with more textures and more in-depth explanations using noise functions in a new article. As always, if there are any questions or if there are any problems or mistakes in the code here, feel free to reach out.

--

--