Tutorial · Intermediate · ~50 min
Three.js · GLSL · ShaderMaterial · Uniforms

Custom ShaderMaterial in Three.js

ShaderMaterial gives full control over GLSL vertex and fragment shaders while keeping Three.js's scene graph, matrices, and geometry handling. This tutorial covers the complete workflow: uniforms, varyings, texture sampling, procedural patterns, and animated effects.

1Minimal ShaderMaterial skeleton

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; const mat = new THREE.ShaderMaterial({ vertexShader: /* glsl */ ` void main() { // modelViewMatrix, projectionMatrix, position are injected by Three.js gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: /* glsl */ ` void main() { gl_FragColor = vec4(0.13, 0.77, 0.37, 1.0); // solid green } `, }); const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 32), mat); scene.add(mesh);
Three.js automatically prepends projectionMatrix, modelViewMatrix, normalMatrix, position, normal, uv, and uv2 declarations to ShaderMaterial shaders. RawShaderMaterial does not (you declare everything yourself).

2Uniforms and updating them

const mat = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0 }, u_color: { value: new THREE.Color(0x22c55e) }, u_scale: { value: 2.5 }, }, vertexShader: /* glsl */ ` uniform float u_time; uniform float u_scale; void main() { vec3 pos = position; pos.y += sin(pos.x * u_scale + u_time) * 0.2; // wave deformation gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } `, fragmentShader: /* glsl */ ` uniform vec3 u_color; void main() { gl_FragColor = vec4(u_color, 1.0); } `, }); // Update in the render loop — direct property write, no method needed renderer.setAnimationLoop(t => { mat.uniforms.u_time.value = t * 0.001; renderer.render(scene, camera); });

3Varyings: pass data vertex → fragment

const mat = new THREE.ShaderMaterial({ vertexShader: /* glsl */ ` varying vec2 vUv; varying vec3 vNormal; varying vec3 vWorldPos; void main() { vUv = uv; vNormal = normalize(normalMatrix * normal); vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: /* glsl */ ` varying vec2 vUv; varying vec3 vNormal; varying vec3 vWorldPos; void main() { // Simple Lambertian lighting vec3 lightDir = normalize(vec3(1.0, 2.0, 1.5)); float diff = max(dot(vNormal, lightDir), 0.0); // UV-based colour with lighting vec3 col = vec3(vUv, 0.5); // R=u, G=v, B=0.5 gl_FragColor = vec4(col * (0.2 + 0.8 * diff), 1.0); } `, });

4Texture sampling

const loader = new THREE.TextureLoader(); const tex = loader.load('/shared/textures/noise.png'); const mat = new THREE.ShaderMaterial({ uniforms: { u_map: { value: tex }, u_time: { value: 0 }, }, vertexShader: /* glsl */ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: /* glsl */ ` uniform sampler2D u_map; uniform float u_time; varying vec2 vUv; void main() { // Scroll the UV over time vec2 scrolledUv = vUv + vec2(u_time * 0.1, 0.0); vec4 texel = texture2D(u_map, scrolledUv); gl_FragColor = texel; } `, });

5Procedural noise pattern

// Include a GLSL hash + value noise helper; no external texture needed const noiseFrag = /* glsl */ ` precision highp float; varying vec2 vUv; uniform float u_time; float hash(vec2 p) { p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); } float noise(vec2 p) { vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); return mix(mix(hash(i + vec2(0,0)), hash(i + vec2(1,0)), u.x), mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); } float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5; i++) { v += noise(p) * a; p *= 2.1; a *= 0.5; } return v; } void main() { float n = fbm(vUv * 4.0 + u_time * 0.3); vec3 col = mix(vec3(0.05, 0.1, 0.2), vec3(0.1, 0.8, 0.5), n); gl_FragColor = vec4(col, 1.0); } `;

6ShaderMaterial vs RawShaderMaterial

// ShaderMaterial automatically injects: // - Built-in uniforms: modelMatrix, viewMatrix, projectionMatrix, // modelViewMatrix, normalMatrix, cameraPosition // - Built-in attributes: position, normal, uv, tangent, color // - #define directives for lights if lights:true const mat = new THREE.ShaderMaterial({ lights: true, // inject THREE_MAX_LIGHTS defines and light uniforms fog: true, // inject fog uniforms u_fogColor, u_fogNear, u_fogFar depthWrite: true, transparent: false, // ShaderMaterial can mix Three.js chunks via #include <common> etc. }); // RawShaderMaterial: NO automatic injections // You must declare precision, all uniforms, and all attributes yourself. // Use when you want the smallest possible shader with no overhead. const rawMat = new THREE.RawShaderMaterial({ vertexShader: ` precision highp float; attribute vec3 position; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` precision highp float; void main() { gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); } `, });
When debugging a shader, add console.log(mat.vertexShader) — Three.js shows the final shader source including all injected code, which makes it easier to understand what's being compiled.

Frequently Asked Questions

What will I learn in this tutorial?

Write a fully custom GLSL vertex and fragment shader in Three.js using ShaderMaterial — uniforms, varyings, texture sampling, and noise-driven effects.

What topics are covered in this tutorial?

This tutorial covers: Minimal ShaderMaterial skeleton, Uniforms and updating them, Varyings: pass data vertex → fragment, Texture sampling, Procedural noise pattern, Extending ShaderMaterial vs RawShaderMaterial.

What tools and technologies does this tutorial use?

This tutorial uses Three.js, GLSL, ShaderMaterial, Uniforms.

How long does this tutorial take?

This tutorial takes approximately 50 minutes to complete.

What prerequisites do I need before starting?

This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.