Generating 3D Worley-Perlin Noise Clouds in GLSL
This article details the implementation of the hybrid Perlin-Worley
noise algorithm used to generate procedural 3D cloud textures inside a
3D storage image using OpenGL Shading Language (GLSL) compute shaders.
It breaks down the mathematical foundations of combining gradient and
cellular noises, the use of Fractional Brownian Motion (fBm) for
multi-frequency detail, and the process of writing the resulting cloud
densities directly into an image3D texture.
The Hybrid Perlin-Worley Algorithm
The standard algorithm for procedural volumetric cloud generation—popularized by Guerrilla Games—combines 3D Perlin noise and 3D Worley (cellular) noise. Perlin noise provides a connected, organic, gradient-based base structure, while Worley noise introduces cellular, billowy patterns resembling cumulus puffs.
The generation process consists of three main stages:
- Generating Base Perlin Noise: Creating a coherent low-frequency density field.
- Generating Worley Noise Octaves: Computing inverted distance-to-point cellular patterns across multiple octaves using Fractional Brownian Motion (fBm).
- Erosion and Remapping: Using high-frequency Worley noise to erode and carve the boundaries of the base Perlin noise structure.
1. Cellular (Worley) Noise Function
Worley noise divides 3D space into a grid where each cell contains a randomized feature point. For any coordinate, it returns the minimum Euclidean distance to the nearest point. Inverting this distance creates concentrated centers that mimic water vapor clumps:
vec3 hash33(vec3 p) {
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
p += dot(p, p.yxz + 33.33);
return fract((p.xxy + p.yxx) * p.zyx);
}
float worley3D(vec3 p, float freq) {
vec3 id = floor(p * freq);
vec3 fd = fract(p * freq);
float minDist = 1.0;
for (int z = -1; z <= 1; ++z) {
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
vec3 offset = vec3(x, y, z);
vec3 point = hash33(mod(id + offset, freq));
vec3 diff = offset + point - fd;
minDist = min(minDist, length(diff));
}
}
}
return 1.0 - clamp(minDist, 0.0, 1.0);
}2. Multi-Octave fBm Worley
Layering octaves of Worley noise creates fine surface irregularities:
float worleyFbm(vec3 p, float baseFreq) {
return worley3D(p, baseFreq) * 0.625 +
worley3D(p, baseFreq * 2.0) * 0.25 +
worley3D(p, baseFreq * 4.0) * 0.125;
}3. Noise Blending via Remapping
To erode the Perlin noise with Worley noise, a linear remapping function contracts the Perlin values wherever the inverted Worley noise is weak:
float remap(float value, float inMin, float inMax, float outMin, float outMax) {
return outMin + ((value - inMin) / (inMax - inMin)) * (outMax - outMin);
}
float perlinWorleyBlend(float perlin, float worley) {
return remap(perlin, 0.0, 1.0, worley, 1.0);
}Writing to a 3D Storage Image in Compute Shaders
The computation runs in parallel using a GLSL compute shader dispatch. Each invocation maps directly to a voxel index within the 3D texture.
#version 450 core
layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in;
layout(rgba8, binding = 0) uniform writeonly image3D u_CloudNoiseTexture;
uniform float u_Frequency = 4.0;
void main() {
ivec3 voxelCoord = ivec3(gl_GlobalInvocationID.xyz);
ivec3 textureSize = imageSize(u_CloudNoiseTexture);
if (any(greaterThanEqual(voxelCoord, textureSize))) {
return;
}
vec3 uvw = vec3(voxelCoord) / vec3(textureSize);
// Compute Base Perlin Noise (assumes a standard 3D perlin implementation)
float basePerlin = perlin3D(uvw * u_Frequency);
// Compute Worley channels at different frequencies
float worleyLow = worleyFbm(uvw, u_Frequency);
float worleyMid = worleyFbm(uvw, u_Frequency * 2.0);
float worleyHigh = worleyFbm(uvw, u_Frequency * 4.0);
// Create the primary Perlin-Worley cloud base density
float perlinWorley = perlinWorleyBlend(basePerlin, worleyLow);
// Pack into RGBA channels:
// R: Base Perlin-Worley density
// G, B, A: High-frequency Worley octaves used for raymarched boundary erosion
vec4 result = vec4(perlinWorley, worleyLow, worleyMid, worleyHigh);
imageStore(u_CloudNoiseTexture, voxelCoord, result);
}Execution Considerations
- Tiling: Ensure the pseudo-random hashing function
wraps properly with
mod(id + offset, freq)so that textures tile seamlessly during real-time volumetric raymarching. - Storage Format: While
rgba8is standard and memory-efficient for packing four octaves, high-precision applications can substitutergba16fto eliminate visible stepping artifacts within density gradients.