How to Use Three.js DataTexture for Procedural Textures

This article explains how to generate procedural textures dynamically in JavaScript using the DataTexture class in Three.js. You will learn how to initialize raw pixel buffers, populate them with mathematical patterns, and apply the resulting procedural texture to a 3D mesh material without needing to load external image files.

Understanding Three.js DataTexture

A DataTexture in Three.js allows you to create a texture directly from a raw array of pixel values. This is ideal for generating procedural patterns like noise, gradients, grids, or cellular automata at runtime.

Unlike a standard Texture that loads an image file (PNG/JPG), DataTexture consumes a typed array, usually a Uint8Array (for 8-bit color channels) or a Float32Array (for high-dynamic-range data).

Step-by-Step Implementation

Step 1: Define the Texture Dimensions

First, specify the width and height of your texture. For optimal compatibility and performance with mipmapping, use powers of two (e.g., 128, 256, 512).

const width = 256;
const height = 256;
const size = width * height;

Step 2: Create and Populate the Typed Array

Create a Uint8Array to hold the RGBA color data. Because each pixel has four channels (Red, Green, Blue, Alpha), the size of the array must be width * height * 4.

Next, run a loop to calculate the color of each pixel procedurally. In this example, we generate a smooth color gradient:

const data = new Uint8Array(4 * size);

for (let i = 0; i < size; i++) {
    const x = i % width;
    const y = Math.floor(i / width);

    // Calculate normalized coordinates (0 to 1)
    const u = x / width;
    const v = y / height;

    const stride = i * 4;

    // Procedural color formulas
    data[stride]     = Math.floor(u * 255); // Red
    data[stride + 1] = Math.floor(v * 255); // Green
    data[stride + 2] = 128;                 // Blue (constant)
    data[stride + 3] = 255;                 // Alpha (fully opaque)
}

Step 3: Instantiate the DataTexture

Pass the data buffer, width, height, and color format to the THREE.DataTexture constructor.

import * as THREE from 'three';

const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat);

Step 4: Flag the Texture for Update

When you modify or create raw data for a texture, you must notify Three.js that the texture data is ready to be uploaded to the GPU.

texture.needsUpdate = true;

Step 5: Apply the Texture to a Material

Now, you can use the procedural texture just like any standard texture by assigning it to a material’s map property.

const material = new THREE.MeshBasicMaterial({ map: texture });
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);

Updating Procedural Textures Dynamically

If you need to animate or change the procedural texture over time, you can modify the existing data array in your animation loop and set texture.needsUpdate = true on each frame.

function animate() {
    requestAnimationFrame(animate);

    // Example: shift colors over time
    const time = performance.now() * 0.001;
    for (let i = 0; i < size; i++) {
        const stride = i * 4;
        data[stride] = Math.floor((Math.sin(time + i * 0.01) + 1) * 127);
    }

    texture.needsUpdate = true;
    renderer.render(scene, camera);
}