Procedural Materials with Shaders in Three.js
July 24, 2024
July 24, 2024
With shaders, we can easily create visual effects (VFX) for our previously created environment or assign procedural materials.
We can easily create materials using GLSL shaders.
The simplest way to create procedural materials is by mixing and modifying different Perlin noises.
Understanding the Perlin noise algorithm is essential for creating procedural materials. The following GLSL code snippet shows a simple implementation of Perlin noise:
GLSLfloat 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 );}
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:
GLSLfloat 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;}
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:
The following GLSL code snippet shows a simple implementation of Worley noise:
GLSLfloat 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:
The following GLSL code snippet shows a simple implementation of Turbulence FBM:
GLSLfloat 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;}
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:
GLSLfloat 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:
GLSLuniform 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.
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
Start your day right with the daily newsletter that entertains and informs. Subscribe now for free!