Fix Color Banding in Three.js Post-Processing
Color banding is a common visual artifact in Three.js web
applications, characterized by visible steps or “stripes” in smooth
color gradients, especially when utilizing post-processing pipelines.
This article explains how to eliminate color banding by configuring the
precision, format, and color space properties of a
WebGLRenderTarget. You will learn the optimal settings for
texture types and color spaces to ensure smooth, high-fidelity gradients
in your 3D scenes.
Why Color Banding Occurs in Post-Processing
By default, a WebGLRenderTarget in Three.js uses an
8-bit unsigned byte texture format
(THREE.UnsignedByteType). This allocates only 256 distinct
values per color channel (Red, Green, Blue). When post-processing
shaders (such as vignette, depth of field, or color grading) manipulate
these colors, the mathematical operations quickly exhaust the limited
8-bit range. This lack of precision causes rounding errors, resulting in
visible stair-step patterns instead of smooth gradients.
1. Increase Bit Depth using
type
The most effective way to eliminate color banding is to increase the bit depth of the render target. Instead of 8-bit integers, use 16-bit float values (Half-Float precision), which provide significantly more color steps and high-dynamic-range (HDR) capabilities.
Configure the type property of your render target to
THREE.HalfFloatType:
const renderTarget = new THREE.WebGLRenderTarget(width, height, {
type: THREE.HalfFloatType
});Using THREE.HalfFloatType is the industry standard for
post-processing. It offers a massive precision upgrade over
UnsignedByteType while maintaining excellent performance
and compatibility across desktop and mobile devices. Avoid
THREE.FloatType (32-bit) unless absolutely necessary, as it
doubles memory usage and bandwidth with minimal visual improvement over
half-float.
2. Set the Correct Color Format
To ensure maximum compatibility and accuracy during post-processing
blend operations, define the format property explicitly.
Use THREE.RGBAFormat to store red, green, blue, and alpha
channels:
const renderTarget = new THREE.WebGLRenderTarget(width, height, {
format: THREE.RGBAFormat,
type: THREE.HalfFloatType
});Using RGBAFormat ensures proper alpha channel alignment,
which is critical for blending multiple post-processing passes.
3. Configure the Color Space
Modern versions of Three.js (r152+) have deprecated the old
encoding property in favor of colorSpace.
Managing where color conversions occur in your post-processing pipeline
is essential for preventing color degradation.
- Intermediate Render Targets: If the render target
is used as an intermediate step within an
EffectComposerchain, keep its color space linear. Math operations like lighting and blending must be calculated in linear space. - Final Render Target / Output: The final pass must convert the linear color space to the output sRGB color space for display. If you are manually managing your render target and rendering to the screen, set the output color space:
// For newer Three.js versions (r152+)
renderTarget.texture.colorSpace = THREE.SRGBColorSpace;
// For older Three.js versions
renderTarget.texture.encoding = THREE.sRGBEncoding;If you use EffectComposer, the composer’s final pass
(like OutputPass or ShaderPass with gamma
correction) generally handles this conversion automatically.
Complete WebGLRenderTarget Configuration Example
When manually instantiating a WebGLRenderTarget for a
custom post-processing setup, use the following optimized
configuration:
const width = window.innerWidth;
const height = window.innerHeight;
const targetOptions = {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.HalfFloatType, // Prevents banding by providing 16-bit precision
depthBuffer: true,
stencilBuffer: false
};
const myRenderTarget = new THREE.WebGLRenderTarget(width, height, targetOptions);Alternative Solution: Enable Dithering
If your target hardware does not support HalfFloatType,
or if you still notice minor banding in dark gradients, enable dithering
on your materials. Dithering introduces a tiny, imperceptible amount of
noise that breaks up the visible lines of color banding.
You can enable dithering globally on materials that cover large areas of your scene, such as skies or background walls:
const material = new THREE.MeshStandardMaterial({
color: 0x333333,
dithering: true // Smooths out gradients at 8-bit precision
});