Sampling 3D Smoke Density with sampler3D in GLSL

Sampling 3D volumetric smoke density in GLSL involves using a sampler3D uniform representing a voxel grid to retrieve continuous scalar density values along a ray. By mapping world-space coordinates to normalized 3D texture coordinates (vec3 in the range [0.0, 1.0]) and using hardware linear filtering, shaders can smoothly reconstruct volumetric fields inside a raymarching loop. This guide details how to set up the uniform, map coordinate spaces, query densities using the built-in texture() function, and handle edge cases for realistic smoke rendering.

Defining the sampler3D Uniform

In GLSL, a 3D texture is declared via the sampler3D data type. Smoke density values are typically stored as single-channel floating-point data (e.g., GL_R16F or GL_R8 in OpenGL), meaning the density value is extracted from the red channel (.r):

#version 330 core

uniform sampler3D u_SmokeDensityTexture;
uniform vec3 u_VolumeMin; // World-space minimum corner of the bounding box
uniform vec3 u_VolumeMax; // World-space maximum corner of the bounding box

Coordinate Normalization (World to UVW Space)

The texture() lookup function requires texture coordinates in normalized UVW space, ranging from 0.0 to 1.0 across each axis. When raymarching through a bounding volume in world space, you must convert the current sampling point pos to texture space:

vec3 worldToTexCoord(vec3 worldPos) {
    return (worldPos - u_VolumeMin) / (u_VolumeMax - u_VolumeMin);
}

If the sample position falls outside the [0.0, 1.0] range, hardware sampling modes dictate the behavior. To prevent smoke from repeating or bleeding at the boundaries, configure the OpenGL texture wrapping to GL_CLAMP_TO_BORDER with a border color of zero density (0.0).

The GLSL Lookup Call

Inside the fragment shader, density lookups are executed with the overloaded texture() function, which takes the sampler3D and a vec3 coordinate:

vec3 uvw = worldToTexCoord(currentRayPosition);
float density = texture(u_SmokeDensityTexture, uvw).r;

Hardware Trilinear Interpolation

Because smoke requires smooth transitions between discrete voxel boundaries, the texture sampler should be configured on the CPU side with GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER set to GL_LINEAR. When queried via GLSL, the GPU automatically performs trilinear filtering—interpolating across all eight adjacent voxels around the sample coordinate—producing continuous gradients without visible voxel stepping.

Raymarching Implementation

To evaluate smoke density across a volume, lookups are integrated into a standard raymarching loop:

out vec4 FragColor;

void main() {
    vec3 rayOrigin = ...; // Camera or ray start point
    vec3 rayDir = ...;    // Normalized ray direction
    
    // Determine entry and exit points along the ray with the volume AABB
    float tNear, tFar;
    intersectBoundingBox(rayOrigin, rayDir, u_VolumeMin, u_VolumeMax, tNear, tFar);

    float stepSize = 0.05;
    float accumulatedDensity = 0.0;
    float transmittance = 1.0;
    float absorption = 0.5;

    for (float t = tNear; t < tFar; t += stepSize) {
        vec3 samplePos = rayOrigin + rayDir * t;
        vec3 uvw = worldToTexCoord(samplePos);

        // Fetch density from the 3D texture
        float density = texture(u_SmokeDensityTexture, uvw).r;

        if (density > 0.0) {
            // Beer-Lambert law for light attenuation
            transmittance *= exp(-density * stepSize * absorption);
            accumulatedDensity += density * stepSize;

            // Early exit if opaque
            if (transmittance < 0.01) {
                break;
            }
        }
    }

    FragColor = vec4(vec3(accumulatedDensity), 1.0 - transmittance);
}

Optimization Techniques

  1. Empty Space Skipping: Check low-resolution mipmaps or bounding spheres to jump past void areas before initiating dense sample loops.
  2. Jittering: Offset the starting ray position along rayDir using a dither texture or screen-space pseudo-random noise to turn regular sampling banding artifacts into less perceptible noise.