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 (JavaScript object) { color, roughness, metalness, map, normalMap, ... } ↓ getProgram( material, scene, object ) [WebGLRenderer.js L2155] ├─ Build a cache key: │ materialType + lightConfig + fog? + shadows? + skinning? + ... ├─ Cache hit? → reuse compiled program └─ Cache miss: ├─ Assemble shader source from #include chunks ├─ Call material.onBeforeCompile( parameters ) ← user hook ├─ gl.createShader() × 2 (vertex + fragment) ├─ gl.compileShader() × 2 ├─ gl.createProgram() ├─ gl.attachShader() × 2 └─ gl.linkProgram() ↓ WebGL Program (lives on GPU) Uniform locations extracted with gl.getUniformLocation() Attribute locations extracted with gl.getAttribLocation() ↓ Per-draw: setUniforms() + bindVertexArray() + gl.drawElements()
Material Base Class Material.js

All materials extend Material. It defines the GPU rendering state: blending, depth, stencil, culling.

Appearance

PropertyLineDescription
opacityL109Alpha value 0.0–1.0. Only effective when transparent = true
transparentL122If true, alpha blending is enabled and object is sorted back-to-front
sideL86FrontSide (default) / BackSide / DoubleSide — which triangle faces to render
wireframeRender triangles as line outlines (uses gl.LINES instead of gl.TRIANGLES)
alphaTestDiscard fragments with alpha below this threshold (avoids sort cost for foliage)

Blending

PropertyLineDescription
blendingL78NormalBlending (default), AdditiveBlending, MultiplyBlending, SubtractiveBlending, CustomBlending
blendSrc / blendDstL141GL blend factors used when blending = CustomBlending
blendEquationL157AddEquation (default), SubtractEquation, ReverseSubtractEquation, MinEquation, MaxEquation

Depth Buffer

PropertyLineDescription
depthTestL218If true, fragment is discarded if it fails the Z test (default: true)
depthWriteL229If false, fragments don't update the Z-buffer (used for transparent objects)
depthFuncLessEqualDepth (default), LessDepth, GreaterDepth, etc.

Hooks

PropertyDescription
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
MeshBasicMaterialFlat color or texture, no lighting. Cheapest option. Good for unlit UI elements or debug.
MeshLambertMaterialDiffuse-only lighting (Lambertian reflectance). Per-vertex lighting calculation — fast but not physically accurate.
MeshPhongMaterialDiffuse + specular highlights (Blinn-Phong). Per-fragment — accurate highlights, still not PBR.
MeshStandardMaterialPhysically-based rendering (PBR). Roughness + metalness workflow. IBL support via scene.environment.
MeshPhysicalMaterialExtends Standard with transmission, clearcoat, iridescence, sheenColor — for glass, car paint, fabric.
MeshDepthMaterialEncodes depth to RGBA. Used internally for shadow maps.
ShaderMaterialFull custom GLSL vertex + fragment shaders. You write everything.
RawShaderMaterialLike ShaderMaterial but three.js doesn't inject any built-in uniforms or defines — absolute control.
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;
}
When using 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:

Cache key components: material.type (e.g. "MeshStandardMaterial") material.customProgramCacheKey() numDirectionalLights (adds defines: NUM_DIR_LIGHTS=2) numPointLights numSpotLights numHemiLights numDirLightShadows (adds: USE_SHADOWMAP, shadowmap code) scene.fog type (adds: USE_FOG / USE_FOGEXP2) object.isSkinnedMesh (adds: USE_SKINNING) geometry morphAttributes (adds: USE_MORPHTARGETS) material.instancingCount (adds: USE_INSTANCING) material.map? (adds: USE_MAP) material.normalMap? (adds: USE_NORMALMAP) ... ~50 more flags

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
During projectObject() traversal: Light objects → currentRenderState.addLight( light ) currentRenderState.setupLights() └─ Accumulates: ambientLightColor += ambient lights directional[] += {direction, color} per DirectionalLight point[] += {position, color, distance, decay} per PointLight spot[] += {position, direction, angle, ...} per SpotLight hemi[] += {skyColor, groundColor, direction} per HemisphereLight renderObject() → setProgram() → uniforms.upload() └─ For each light type: gl.uniform3f( ambientLightColorLoc, r, g, b ) gl.uniform3fv( directionalLightsLoc, flattenedDirectionals ) gl.uniformMatrix4fv( shadowMatrixLoc, shadowMatrices ) gl.bindTexture( GL_TEXTURE_2D, shadowMapTexture ) ← sampler2D
External References