Loading

Volumetric Cloud in Deferred Rendering

Technical

This post walks through the cloud rendering system I built for a custom DirectX 12 deferred rendering engine. The engine implements an Iris-compatible shader pipeline (the modding framework behind Minecraft Java Edition shader packs), and the cloud system is part of the EnigmaDefault ShaderBundle, which targets a Complementary Reimagined visual style.

The system features two distinct cloud rendering approaches: a vanilla geometry cloud pipeline driven by CPU-side mesh generation (ported from Sodium’s architecture), and a volumetric cloud pipeline using screen-space ray marching in the deferred lighting pass. The volumetric approach replaces the geometry clouds entirely in the EnigmaDefault ShaderBundle, producing clouds with self-shadowing, height-based shading gradients, and forward scattering.

Rendering Pipeline Architecture

The cloud system integrates into the engine’s multi-pass deferred rendering pipeline. Vanilla geometry clouds are rendered during the GBuffer pass (gbuffers_clouds), while volumetric clouds are computed entirely in screen space during the first deferred lighting pass (deferred1). The volumetric cloud depth output feeds into the composite pass for volumetric light (god ray) modulation.

flowchart TD
    subgraph GBuffer["GBuffer Pass"]
        GT["gbuffers_terrain"]
        GC["gbuffers_clouds\nVanilla geometry clouds"]
        GW["gbuffers_water"]
    end

    subgraph Deferred["Deferred Pass 1"]
        DL["Deferred Lighting"]
        AF["Atmospheric Fog"]
        VC["Volumetric Clouds\nRay March in Screen Space"]
        VCO["colortex5.a\nCloud Linear Depth"]
    end

    subgraph Composite["Composite Passes"]
        C1["Composite 1\nVolumetric Light + God Rays"]
        C5["Composite 5\nTonemapping + Color Grading"]
    end

    GBuffer --> Deferred
    DL --> AF --> VC --> VCO
    Deferred --> Composite
    VCO -.->|"vlFactor modulation"| C1
    C1 --> C5

Vanilla Cloud Rendering

The vanilla cloud pipeline is a faithful port of Sodium’s CloudRenderer (Minecraft Java Edition performance mod). Clouds are generated as CPU-side geometry, uploaded to a vertex buffer, and rasterized through the standard GBuffer pass.

Geometry Generation

The cloud texture (clouds.png) is a 256x256 bitmap where each pixel represents a 12x12x4 block cell. The CPU reads this texture at load time and builds geometry using a spiral traversal algorithm that expands outward from the player’s position. This ensures clouds closest to the camera are generated first.

The traversal works in three phases:

  1. Center cell at the player’s grid position
  2. Diamond expansion from layer 1 to the configured radius, visiting cells in a diamond pattern
  3. Corner fill for layers beyond the base radius, completing the square coverage area

Each visible cell generates either a single horizontal quad (Fast mode) or a full 12x12x4 box with up to 6 exterior faces plus interior backfaces (Fancy mode). A face culling system eliminates faces that cannot be seen from the current camera orientation, reducing vertex count significantly.

The coordinate system maps from Minecraft conventions (Y-up) to the engine’s Z-up system:

MinecraftEngineDescription
+X (East)+Y (Left)Horizontal axis
+Y (Up)+Z (Up)Vertical axis
+Z (South)+X (Forward)Depth axis

Each face receives a directional brightness multiplier to simulate basic ambient occlusion: top faces at full brightness (1.0), bottom faces darkened (0.7), and side faces at intermediate values (0.8 and 0.9).

Vanilla Clouds in Action

Vanilla style clouds during daytime, showing the CPU-generated geometry with directional face brightness
Vanilla geometry clouds during daytime, rendered in Fancy mode with per-face brightness
Vanilla style clouds at night with moonlight illumination
Vanilla clouds at night, showing the moonlit color tinting applied by the CPU
Vanilla style clouds at midnight with minimal ambient light
Vanilla clouds at midnight, demonstrating the darkened ambient with subtle blue tint

Volumetric Cloud Ray Marching

The EnigmaDefault ShaderBundle replaces vanilla geometry clouds entirely with screen-space ray marched volumetric clouds. The vanilla gbuffers_clouds pixel shader simply calls discard on every fragment, and the volumetric system takes over in deferred1.ps.hlsl.

This approach is a faithful port of Complementary Reimagined’s cloud system (reimaginedClouds.glsl), adapted from GLSL to HLSL with engine-specific coordinate system adjustments.

Ray March Algorithm

The core idea is straightforward: for each screen pixel, cast a ray from the camera through the pixel into the scene. If the ray intersects the cloud layer (a horizontal slab defined by CLOUD_ALT1 ± CLOUD_STRETCH), march along the ray within that slab, sampling cloud density at each step.

flowchart LR
    A[Screen Pixel] --> B[Cast Ray from Camera]
    B --> C{Intersects Cloud Slab?}
    C -->|No| D[Return transparent]
    C -->|Yes| E[Compute near/far distances]
    E --> F[March: sample density at each step]
    F --> G{Cloud Hit?}
    G -->|No| F
    G -->|Yes| H[Compute Shading]
    H --> I[Self-Shadow Sampling]
    I --> J[Height Gradient + Scattering]
    J --> K[Output color + opacity]

The intersection test computes where the view ray enters and exits the cloud slab:

float cloudUpper = float(cloudAltitude) + cloudStretch;
float cloudLower = float(cloudAltitude) - cloudStretch;

float distToUpper = (cloudUpper - camPos.z) / safeZ;
float distToLower = (cloudLower - camPos.z) / safeZ;

float nearDist = max(min(distToUpper, distToLower), 0.0);
float farDist  = min(max(distToUpper, distToLower), renderDistance);

The sample count scales with quality level: 16 samples at low, 32 at medium, and 48 at high. A dither offset (interleaved gradient noise) staggers samples across neighboring pixels to reduce banding artifacts, which temporal anti-aliasing then smooths out.

Texture-Driven Cloud Shape

Unlike many volumetric cloud implementations that rely on 3D Perlin or Worley noise, this system uses a 2D texture lookup for cloud density. The cloud-water.png texture (256x256) stores cloud density in its blue channel. This is a key design choice from Complementary Reimagined that trades some volumetric complexity for significantly lower sampling cost.

The cloud-water.png texture used for cloud density sampling, blue channel contains the cloud shape data
The cloud-water.png texture, where the blue channel encodes cloud density patterns

The sampling pipeline works as follows:

  1. Wind animation: The world position is offset by a time-based wind vector using worldTime (synchronized to the game world clock, not real time). This means clouds accelerate when the game uses time scaling.

  2. Coordinate mapping: The wind-animated position is scaled by CLOUD_NARROWNESS (0.07) and converted to a 256x256 texture coordinate using GetRoundedCloudCoord, which applies smoothstep rounding for soft cloud edges.

  3. Height masking: The raw texture density is multiplied by a height-based falloff that peaks at the cloud altitude center and drops to zero at the slab boundaries.

  4. Sharp threshold: The masked density is raised to the 8th power (Pow2(Pow2(Pow2(noise)))), producing the characteristic sharp-edged Reimagined cloud look. Values below 0.001 are discarded.

bool GetCloudNoise(float3 tracePos, inout float3 tracePosM, int cloudAltitude)
{
    tracePosM = ModifyTracePos(tracePos);

    float2    coord         = GetRoundedCloudCoord(tracePosM.xy, CLOUD_ROUNDNESS_SAMPLE);
    Texture2D cloudWaterTex = customImage3;
    float     noise         = cloudWaterTex.Sample(sampler0, coord).b;

    float heightFactor = abs(tracePos.z - float(cloudAltitude));
    float heightMask   = saturate(1.0 - heightFactor * CLOUD_NARROWNESS);
    noise *= heightMask;

    // 8th-power threshold: x^8 via three nested squares
    float threshold = Pow2(Pow2(Pow2(noise)));
    return threshold > 0.001;
}
Close-up view showing the texture sampling points visible as noise patterns on the cloud surface
Close-up of volumetric clouds showing the texture-driven density sampling pattern

Self-Shadow Computation

At quality level 2 and above, the system computes self-shadowing by sampling cloud density along the light direction from each cloud hit point. Two additional texture samples are taken at increasing distances along the shadow light vector, and each sample attenuates the light value proportionally.

#if CLOUD_QUALITY >= 2
{
    float shadowStep = CLOUD_SHADOW_STEP; // 1.0 world units

    [unroll]
    for (int s = 1; s <= 2; s++)
    {
        float3 shadowWorldPos = tracePos + lightDir * (shadowStep * float(s));
        float3 shadowPosM     = ModifyTracePos(shadowWorldPos);
        float2 shadowCoord    = GetRoundedCloudCoord(shadowPosM.xy, CLOUD_ROUNDNESS_SHADOW);
        float  shadowNoise    = cloudWaterTex.Sample(sampler0, shadowCoord).b;
        light -= shadowNoise * cloudShadingM * CLOUD_SHADOW_STRENGTH;
    }
    light = max(light, CLOUD_SHADOW_MIN); // Prevent full black
}
#endif

The shadow sampling uses a blurrier roundness value (0.35 vs 0.125 for shape) to produce softer shadow boundaries. The cloudShadingM weight is height-dependent (1 - heightGrad^2), making shadows stronger at the cloud bottom and weaker at the top, which matches how real clouds scatter light.

Cloud Coloring and Lighting

The cloud color model blends ambient and direct illumination based on several factors:

Height gradient shading maps each sample’s vertical position within the cloud slab to a 0.0 (bottom) to 1.0 (top) range, then applies a power curve (CLOUD_SHADING_POWER = 2.5). This creates naturally darker cloud bottoms and brighter tops.

Forward scattering uses a half-Lambert transform of the view-to-sun dot product (VdotS * 0.5 + 0.5), brightening clouds when looking toward the sun.

Time-of-day color interpolates between warm sunlight tones during the day and cool blue moonlight at night, driven by sunVisibility^2 for a smooth transition through sunrise and sunset.

The final color formula combines these terms:

float3 cloudColor = cloudAmbient * 0.95 * (1.0 - 0.35 * cloudShading)
                  + cloudLight * (0.1 + cloudShading);

A distance fog factor is applied to the cloud itself, blending toward the sky color at the render distance boundary to avoid hard cutoffs.

Volumetric Clouds in Action

View from inside the volumetric cloud layer showing the density field from within
View from inside the cloud layer, showing the ray marched density field from within
Volumetric clouds seen from above, showing the cloud top illumination and height gradient
Volumetric clouds seen from above, with bright tops and self-shadowed undersides
Volumetric clouds at night seen from above, with moonlight illumination
Nighttime volumetric clouds from above, lit by moonlight with cool blue ambient tones

Cloud Configuration

All cloud parameters are exposed in settings.hlsl as compile-time defines with slider ranges, following the Iris/OptiFine shader options convention. This allows artists to tune the cloud appearance without modifying shader logic.

ParameterDefaultRangePurpose
CLOUD_QUALITY31, 2, 3Ray march sample count (16/32/48)
CLOUD_ALT1192-96 to 800Primary cloud layer altitude
CLOUD_STRETCH4.2fixedCloud slab vertical thickness
CLOUD_SPEED_MULT1000 to 900Wind animation speed multiplier
CLOUD_NARROWNESS0.07fixedHeight density falloff rate
CLOUD_SHADING_POWER2.51.0 to 3.5Height gradient curve exponent
CLOUD_SHADOW_STRENGTH0.350.1 to 0.7Self-shadow attenuation per sample
CLOUD_SHADOW_MIN0.30.1 to 0.5Minimum light (prevents full black)
CLOUD_R/G/B10025 to 300RGB color tint (percentage)
DOUBLE_REIM_CLOUDS00, 1Enable dual cloud layers

The night lighting uses separate ambient and moonlight colors with their own multipliers, giving fine control over the cloud appearance across the full day-night cycle:

#define CLOUD_NIGHT_AMBIENT float3(0.09, 0.12, 0.17)
#define CLOUD_NIGHT_AMBIENT_MULT 1.4
#define CLOUD_NIGHT_LIGHT float3(0.11, 0.14, 0.20)
#define CLOUD_NIGHT_LIGHT_MULT 0.9

Volumetric Light Integration

The volumetric cloud system outputs a cloud linear depth value to colortex5.a, which the composite pass reads to modulate volumetric light (god ray) intensity. When a god ray sample falls behind a cloud, the vlFactor attenuates the light contribution, preventing light shafts from incorrectly shining through cloud bodies.

// In deferred1: output cloud depth for VL modulation
cloudLinearDepth = sqrt(lTracePos / renderDistance);

This creates a natural interaction between the two atmospheric effects without requiring the volumetric light pass to re-sample cloud density.

Final Results

Volumetric clouds with Reimagined stylized rendering during daytime, showing self-shadowing and height gradient shading
Bird’s eye view of the volumetric cloud layer, demonstrating the texture-driven cloud shape and distance fog falloff

Design Philosophy

Across the cloud rendering system, several principles guided the architecture and implementation decisions:

Dual-Layer Compatibility — The engine maintains both vanilla geometry clouds and volumetric ray marched clouds as separate, swappable systems. The vanilla pipeline serves as a reliable fallback, while the EnigmaDefault ShaderBundle cleanly disables it with a single discard and activates the volumetric path. No shared mutable state exists between the two approaches.

Texture Over Procedural — Cloud shape uses a 2D texture lookup (cloud-water.png blue channel) rather than expensive 3D Perlin or Worley noise. Combined with the 8th-power threshold for sharp edges, this produces visually rich clouds at a fraction of the per-sample cost. The tradeoff is less volumetric depth variation, which the height gradient and self-shadow systems compensate for.

Configuration as First-Class — Every artistic parameter lives in settings.hlsl with explicit slider ranges, following the Iris shader options convention. Artists can reshape clouds (altitude, thickness, speed, color tint, shadow intensity) without touching shader logic. The compile-time define approach means unused quality paths are eliminated by the compiler, keeping the GPU cost predictable.

Pipeline Integration Over Isolation — Rather than treating clouds as a standalone effect, the system feeds cloud depth into the volumetric light pass and respects terrain occlusion per sample. This creates natural interactions (god rays attenuated by clouds, clouds hidden behind mountains) without duplicating sampling work across passes.