Sampling 3D Textures with sampler3D in GLSL

This article explains how to sample 3D volumetric textures in the OpenGL Shading Language (GLSL) using the sampler3D uniform type. It covers the declaration of 3D samplers, the structure of three-dimensional texture coordinates, the execution of the standard texture() lookup function, hardware-level trilinear filtering, and practical shader code implementation for rendering volumetric data.

Declaring the 3D Sampler

To sample a 3D texture in GLSL, you must first declare a uniform variable of type sampler3D in your shader, typically inside the fragment shader:

#version 330 core
out vec4 FragColor;

uniform sampler3D u_VolumeTexture;

This uniform acts as a handle to the 3D texture unit bound on the CPU side via the host graphics API (such as glBindTexture(GL_TEXTURE_3D, textureID) in OpenGL).

3D Texture Coordinates

Unlike 2D textures that rely on two coordinates (u, v), volumetric textures require three-dimensional normalized coordinates (s, t, r) or (x, y, z) passed as a vec3.

Each component corresponds to an axis in the volume:

  • s (x): Horizontal axis across the volume width.
  • t (y): Vertical axis across the volume height.
  • r (z): Depth axis across the volume layers or slices.

These coordinates normally span the normalized range from 0.0 to 1.0. A coordinate of vec3(0.5, 0.5, 0.5) targets the exact center of the 3D voxel grid.

The Sampling Function

In modern GLSL (version 3.30 and later), 3D textures are sampled using the overloaded texture() function. The function takes the sampler3D as the first argument and a vec3 representing the coordinates as the second argument:

vec4 texel = texture(u_VolumeTexture, texCoords);

In legacy GLSL (version 1.20 or lower), the dedicated function texture3D() was used instead, but this has been deprecated in favor of the unified texture() function.

Basic GLSL Fragment Shader Example

Below is a complete fragment shader demonstrating how to sample a volumetric texture:

#version 330 core

in vec3 v_TexCoord; // Interpolated 3D coordinate from the vertex shader
out vec4 FragColor;

uniform sampler3D u_VolumeTexture;

void main()
{
    // Sample the 3D texture
    vec4 volumeColor = texture(u_VolumeTexture, v_TexCoord);

    // Output the resulting color
    FragColor = volumeColor;
}

Hardware Filtering and Trilinear Interpolation

When texture() is called on a sampler3D, the GPU handles coordinate mapping and filtering based on the sampler parameters configured in the host application:

  • Trilinear Filtering: If the minification and magnification filters are set to GL_LINEAR, the GPU performs hardware-accelerated trilinear interpolation. It calculates the weighted average of the eight nearest voxels surrounding the sampling coordinate across all three dimensions.
  • Coordinate Clamping: Wrapping modes such as GL_CLAMP_TO_EDGE or GL_REPEAT apply across all three axes (GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, and GL_TEXTURE_WRAP_R), preventing artifacts at the volume boundaries.

Fetching Exact Voxels with texelFetch

If you need to bypass normalized coordinates and filtering to read an exact voxel from the volume grid, GLSL provides the texelFetch() function. This function uses integer coordinates (ivec3) and an explicit mipmap level:

ivec3 voxelCoord = ivec3(gl_FragCoord.xy, 10); // Target slice 10
int lod = 0;
vec4 exactVoxel = texelFetch(u_VolumeTexture, voxelCoord, lod);

Using sampler3D alongside texture() or texelFetch() provides efficient, hardware-accelerated access to voxel grids, making it fundamental for medical imaging, volumetric cloud simulation, and 3D lookup tables (LUTs).