Material & Shader System
How materials describe appearance, how three.js compiles GLSL shaders from chunks, and how uniforms (light data, textures, matrices) reach the GPU program.
The Big Picture: Material → GPU Program
Material Base Class Material.js
All materials extend Material. It defines the GPU rendering state: blending, depth, stencil, culling.
Appearance
| Property | Line | Description |
|---|---|---|
| opacity | L109 | Alpha value 0.0–1.0. Only effective when transparent = true |
| transparent | L122 | If true, alpha blending is enabled and object is sorted back-to-front |
| side | L86 | FrontSide (default) / BackSide / DoubleSide — which triangle faces to render |
| wireframe | — | Render triangles as line outlines (uses gl.LINES instead of gl.TRIANGLES) |
| alphaTest | — | Discard fragments with alpha below this threshold (avoids sort cost for foliage) |
Blending
| Property | Line | Description |
|---|---|---|
| blending | L78 | NormalBlending (default), AdditiveBlending, MultiplyBlending, SubtractiveBlending, CustomBlending |
| blendSrc / blendDst | L141 | GL blend factors used when blending = CustomBlending |
| blendEquation | L157 | AddEquation (default), SubtractEquation, ReverseSubtractEquation, MinEquation, MaxEquation |
Depth Buffer
| Property | Line | Description |
|---|---|---|
| depthTest | L218 | If true, fragment is discarded if it fails the Z test (default: true) |
| depthWrite | L229 | If false, fragments don't update the Z-buffer (used for transparent objects) |
| depthFunc | — | LessEqualDepth (default), LessDepth, GreaterDepth, etc. |
Hooks
| Property | Description |
|---|---|
| onBeforeCompile(parameters, renderer) | Called before GLSL compilation. Modify parameters.vertexShader / parameters.fragmentShader to inject custom code. |
| onBeforeRender(renderer, scene, camera, geometry, object, group) | Called before each draw — use to update uniforms dynamically. |
| onAfterRender(...) | Called after each draw — cleanup or state restore. |
| customProgramCacheKey() | Return a string to add to the cache key — required when your onBeforeCompile changes shader source based on state. |
Built-in Material Types
scene.environment.Shader Chunk System — How Built-in Shaders are Assembled
Three.js doesn't write monolithic GLSL files. Instead it composes shaders from small named chunks using #include <chunk_name> directives.
// Simplified excerpt: MeshStandardMaterial vertex shader
#include <common>
#include <uv_pars_vertex>
#include <normal_pars_vertex>
#include <shadowmap_pars_vertex>
void main() {
#include <uv_vertex>
#include <beginnormal_vertex>
#include <defaultnormal_vertex>
#include <begin_vertex>
#include <project_vertex> // ← clips position to screen
#include <shadowmap_vertex>
}
Each chunk is a string stored in ShaderChunk.js. The renderer replaces #include directives with the chunk source before compilation.
Why This Design?
Chunks let onBeforeCompile hooks surgically inject code:
material.onBeforeCompile = ( shader ) => {
// Add a custom uniform
shader.uniforms.myTime = { value: 0 };
// Inject code into a specific chunk's slot
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
// Vertex displacement using wave
transformed.y += sin( position.x * 5.0 + myTime ) * 0.1;
`
);
};
// Update the uniform each frame
material.userData.shader = null;
material.onBeforeCompile = ( shader ) => {
material.userData.shader = shader;
};
// In animate():
if ( material.userData.shader ) {
material.userData.shader.uniforms.myTime.value = performance.now() / 1000;
}
onBeforeCompile with state-dependent logic, implement customProgramCacheKey() to return a unique string per variant — otherwise the renderer reuses a cached program compiled with different code.Program Cache & Variants getProgram L2155
Three.js never compiles duplicate programs. The cache key encodes every factor that changes shader code:
This means adding a single point light causes all materials to recompile (one-time cost, then cached). The cache survives across frames but is per-scene and per-camera-depth-level.
Uniforms — How Data Reaches the Shader
Uniforms are variables that are constant for a single draw call but can change between draws. Three.js uploads them via the WebGLUniforms system.
// For MeshStandardMaterial, three.js sends ~40+ uniforms automatically:
// Matrices:
// modelMatrix, viewMatrix, projectionMatrix, normalMatrix
// Material appearance:
// diffuse (color), roughness, metalness, opacity
// Textures (each bound to a texture unit):
// map, roughnessMap, metalnessMap, normalMap, aoMap, ...
// Lighting (from RenderState):
// ambientLightColor
// directionalLights[N] { direction, color }
// directionalLightShadows[N] { shadowBias, shadowMapSize, ... }
// directionalShadowMap[N] (sampler2D)
// directionalShadowMatrix[N] (mat4)
// pointLights[N], spotLights[N], ...
// Environment:
// envMap (samplerCube), envMapIntensity
// Fog:
// fogColor, fogNear, fogFar (or fogDensity for FogExp2)
Custom Uniforms in ShaderMaterial
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0.0 },
color: { value: new THREE.Color( 0xff0000 ) },
tex: { value: new THREE.TextureLoader().load('img.png') },
},
vertexShader: `
uniform float time;
void main() {
vec3 pos = position;
pos.y += sin( pos.x + time ) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}
`,
fragmentShader: `
uniform vec3 color;
void main() {
gl_FragColor = vec4( color, 1.0 );
}
`,
});
// Animate:
function animate( t ) {
material.uniforms.time.value = t / 1000;
renderer.render( scene, camera );
}
Lighting Data Flow: Lights → Shader Uniforms
External References
- Khronos — GLSL Shader Objects — compile/link lifecycle
- WebGL Fundamentals — Shaders and GLSL
- LearnOpenGL — Blending
- Three.js Docs — MeshStandardMaterial
- src/renderers/shaders/ShaderLib.js — built-in shader entry points
- src/renderers/shaders/ShaderChunk.js — all chunk names & sources