Procedural Materials with Shaders in Three.js

July 24, 2024

Shaders

With shaders, we can easily create visual effects (VFX) for our previously created environment or assign procedural materials.

Procedural Material

We can easily create materials using GLSL shaders.

The simplest way to create procedural materials is by mixing and modifying different Perlin noises.

Perlin Noise

A visual depiction of what is being written about

Understanding the Perlin noise algorithm is essential for creating procedural materials. The following GLSL code snippet shows a simple implementation of Perlin noise:

GLSL
float noise( in vec3 p )
{
vec3 i = floor( p );
vec3 f = fract( p );
vec3 u = f*f*(3.0-2.0*f);
return mix( mix( mix( dot( hash( i + vec3(0.0,0.0,0.0) ), f - vec3(0.0,0.0,0.0) ),
dot( hash( i + vec3(1.0,0.0,0.0) ), f - vec3(1.0,0.0,0.0) ), u.x),
mix( dot( hash( i + vec3(0.0,1.0,0.0) ), f - vec3(0.0,1.0,0.0) ),
dot( hash( i + vec3(1.0,1.0,0.0) ), f - vec3(1.0,1.0,0.0) ), u.x), u.y),
mix( mix( dot( hash( i + vec3(0.0,0.0,1.0) ), f - vec3(0.0,0.0,1.0) ),
dot( hash( i + vec3(1.0,0.0,1.0) ), f - vec3(1.0,0.0,1.0) ), u.x),
mix( dot( hash( i + vec3(0.0,1.0,1.0) ), f - vec3(0.0,1.0,1.0) ),
dot( hash( i + vec3(1.0,1.0,1.0) ), f - vec3(1.0,1.0,1.0) ), u.x), u.y), u.z );
}

Fractal Noise

The noise code snippet provides a good noise texture, but if we want to depict real-world irregularities, it is too uniform and looks more like a blurred grid pattern. Real-world randomness is more dynamic. This can be achieved by summing several noise patterns of different sizes, a technique known as Fractal Brownian Motion (fBM).

By controlling the number of noise iterations (octaves), the intensity (amplitude), and the "tightness" (frequency) of each layer, we can achieve a much more dynamic noise pattern. The following GLSL code snippet shows an implementation of fractal noise (fBM):

In Blender 4.1, the noise texture can be set to FBM by default:

GLSL
float fbm(vec3 p, int octaves, float persistence, float lacunarity) {
float amplitude = 0.5;
float frequency = 1.0;
float total = 0.0;
float normalization = 0.0;
for (int i = 0; i < octaves; ++i) {
float noiseValue = noise(p * frequency);
total += noiseValue * amplitude;
normalization += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
total /= normalization;
return total;
}

Other Known Noises

Worley (Cellular) Noise

Worley noise, also known as cellular noise, is an algorithm often used to create simulated cell structures or other natural patterns. The image below is an example:

A visual depiction of what is being written about

The following GLSL code snippet shows a simple implementation of Worley noise:

GLSL
float worley(vec3 coords) {
vec2 gridBasePosition = floor(coords.xy);
vec2 gridCoordOffset = fract(coords.xy);
float closest = 1.0;
for (float y = -2.0; y <= 2.0; y += 1.0) {
for (float x = -2.0; x <= 2.0; x += 1.0) {
vec2 neighbourCellPosition = vec2(x, y);
vec2 cellWorldPosition = gridBasePosition + neighbourCellPosition;
vec2 cellOffset = vec2(
noise(vec3(cellWorldPosition, coords.z) + vec3(243.432, 324.235, 0.0)),
noise(vec3(cellWorldPosition, coords.z))
);
float distToNeighbour = length(
neighbourCellPosition + cellOffset - gridCoordOffset);
closest = min(closest, distToNeighbour);
}
}
return closest;
}

Turbulence FBM

Turbulence FBM is another well-known noise type often used to simulate natural patterns and textures. The image below is an example:

A visual depiction of what is being written about

The following GLSL code snippet shows a simple implementation of Turbulence FBM:

GLSL
float turbulenceFBM(vec3 p, int octaves, float persistence, float lacunarity) {
float amplitude = 0.5;
float frequency = 1.0;
float total = 0.0;
float normalization = 0.0;
for (int i = 0; i < octaves; ++i) {
float noiseValue = noise(p * frequency);
noiseValue = abs(noiseValue);
total += noiseValue * amplitude;
normalization += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
total /= normalization;
return total;
}

Creating Materials

By combining the previously described materials, we can create various textures or modify the rendering in some way. For example, by using a stepped configuration, we can achieve a wood-like texture. The following GLSL code snippet shows an implementation:

GLSL
float stepped(float noiseSample) {
float steppedSample = floor(noiseSample * 10.0) / 10.0;
float remainder = fract(noiseSample * 10.0);
steppedSample = (steppedSample - remainder) * 0.5 + 0.5;
return steppedSample;
}

There are many good procedural materials available on ShaderToy that can be used with minimal modifications in three.js.

ShaderToy use on a simple cube

Add the following variables to the beginning:

GLSL
uniform float iTime;
uniform vec2 iResolution;
varying vec2 vUv;

Modify the void mainImage function to account for the variables:

Compute fragCoord as the product of vUv and iResolution, and use gl_FragColor instead of fragColor at the end. This allows dynamic application and customization of procedural materials in three-dimensional scenes.

Summary

Using procedural materials and noises with GLSL shaders is an extremely effective way to create realistic and detailed textures. These techniques are not only aesthetically pleasing but also optimize performance by using less memory than traditional textures. The examples and code snippets provided above make it easy to create and apply various procedural materials in projects, whether for game development, animation, or any other 3D application.

Share this article

The Newsletter for Next-Level Tech Learning

Start your day right with the daily newsletter that entertains and informs. Subscribe now for free!

Related Articles