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:

  1. Generating Base Perlin Noise: Creating a coherent low-frequency density field.
  2. Generating Worley Noise Octaves: Computing inverted distance-to-point cellular patterns across multiple octaves using Fractional Brownian Motion (fBm).
  3. 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 rgba8 is standard and memory-efficient for packing four octaves, high-precision applications can substitute rgba16f to eliminate visible stepping artifacts within density gradients.