Pass Uniforms and Varyings to Three.js GLSL Shaders
Using custom shaders in Three.js requires an efficient and secure way
to send data from CPU-side JavaScript to GPU-side GLSL. This article
explains how to securely pass custom uniforms (global variables) and
varying variables (which interpolate data from vertex to fragment
shaders) to a raw GLSL shader using Three.js’s
RawShaderMaterial. By utilizing structured type-checking,
input sanitization, and explicit attribute declarations, you can build
high-performance WebGL pipelines while avoiding common driver bugs and
security vulnerabilities.
Understanding RawShaderMaterial vs ShaderMaterial
In Three.js, ShaderMaterial automatically prepends
built-in attributes and uniforms (like projectionMatrix,
modelViewMatrix, and position) to your GLSL
code. RawShaderMaterial, however, prepends nothing.
Using RawShaderMaterial is the most secure and
predictable approach for raw GLSL integration because it forces you to
explicitly declare all attributes, uniforms, and precision qualifiers.
This prevents namespace collisions, unauthorized data exposure, and
compiler errors across different GPU drivers.
Step 1: Defining and Sanitizing Uniforms in JavaScript
Uniforms are read-only variables passed from JavaScript to both the
vertex and fragment shaders. To pass them securely, you must validate
and sanitize the data on the CPU before sending it to the GPU.
Unsanitized inputs (such as infinite or NaN float values)
can crash the user’s graphics driver or lead to WebGL context loss.
import * as THREE from 'three';
// 1. Sanitize CPU-side inputs
const rawScale = 2.5;
const secureScale = isNaN(rawScale) || !isFinite(rawScale) ? 1.0 : rawScale;
// 2. Define the uniforms object
const customUniforms = {
uTime: { value: 0.0 },
uScale: { value: secureScale },
uColor: { value: new THREE.Color(0x3498db) }
};Step 2: Passing Custom Attributes and Varyings via BufferGeometry
Attributes are per-vertex variables. To pass custom attribute data
securely to the vertex shader, use THREE.BufferAttribute
with typed arrays. This ensures memory-safe boundaries on the GPU.
Varyings are used to pass data from the vertex shader to the fragment shader. They are declared in the vertex shader, written to, and then declared with the exact same name and type in the fragment shader for interpolation.
const geometry = new THREE.BufferGeometry();
// Set up standard positions (attributes)
const vertices = new Float32Array([
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// Pass a custom attribute (e.g., custom displacement factors per vertex)
const displacements = new Float32Array([0.1, 0.5, 0.9]);
geometry.setAttribute('aDisplacement', new THREE.BufferAttribute(displacements, 1));Step 3: Writing the Raw GLSL Shaders
When writing raw shaders, you must manually define the GLSL version, float precision, and all built-in matrices.
Vertex Shader
The vertex shader receives the attributes and uniforms, processes the
geometry, and passes the interpolated data to the fragment shader using
a varying variable.
#version 150 // Specify GLSL version
precision mediump float;
// Built-in Three.js uniforms (must be declared manually in RawShaderMaterial)
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
// Custom Uniforms passed from JS
uniform float uTime;
uniform float uScale;
// Attributes passed from BufferGeometry
in vec3 position;
in float aDisplacement;
// Varying variable to pass to the Fragment Shader
out vec3 vColorInterpolation;
void main() {
// Safely calculate a displacement effect using the custom attribute and uniform
vec3 updatedPosition = position;
updatedPosition.z += sin(uTime + aDisplacement) * uScale;
// Pass color data based on displacement to the fragment shader
vColorInterpolation = vec3(aDisplacement, 0.5, 1.0 - aDisplacement);
gl_Position = projectionMatrix * modelViewMatrix * vec4(updatedPosition, 1.0);
}Fragment Shader
The fragment shader receives the uniform values and the interpolated
varying values from the vertex shader to calculate the
final pixel color.
#version 150
precision mediump float;
// Custom Uniforms
uniform vec3 uColor;
// Interpolated varying from the vertex shader
in vec3 vColorInterpolation;
// Final output color
out vec4 fragColor;
void main() {
// Securely combine the uniform color with the interpolated attribute data
vec3 finalColor = uColor * vColorInterpolation;
fragColor = vec4(finalColor, 1.0);
}Step 4: Creating the RawShaderMaterial
Now, link the shaders, uniforms, and geometry together using
THREE.RawShaderMaterial.
const material = new THREE.RawShaderMaterial({
uniforms: customUniforms,
vertexShader: vertexShaderSource,
fragmentShader: fragmentShaderSource,
glslVersion: THREE.GLSL3 // Ensures modern WebGL 2 / GLSL 3.00 es syntax
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);To update the uniforms securely during runtime (for example, in an
animation loop), update the value property of your uniform
reference directly. Three.js handles the secure GPU upload in the
background:
function animate(timestamp) {
requestAnimationFrame(animate);
// Safely update the uniform using elapsed seconds
customUniforms.uTime.value = timestamp * 0.001;
renderer.render(scene, camera);
}
animate(0);