WebGLRenderer Pipeline

The complete journey from renderer.render(scene, camera) to gl.drawElements() — every subsystem that fires and in what order.

WebGLRenderer Overview WebGLRenderer.js

WebGLRenderer is the orchestrator of the entire pipeline — it owns the WebGL context, manages all GPU state, coordinates shader compilation, and drives the render loop.

At construction it initializes ~20 sub-systems:

const renderer = new THREE.WebGLRenderer({ antialias: true });

// Internally creates:
// WebGLBufferRenderer   — calls gl.drawArrays / gl.drawElements
// WebGLIndexedBufferRenderer
// WebGLAttributes       — uploads BufferAttributes to GPU
// WebGLBindingStates    — manages VAOs (vertex array objects)
// WebGLTextures         — uploads textures, manages texture units
// WebGLPrograms         — compiles and caches GLSL shader programs
// WebGLRenderLists      — opaque/transparent object sorting
// WebGLRenderStates     — per-render light & shadow state
// WebGLShadowMap        — renders shadow depth maps
// WebGLState            — thin wrapper around gl.enable/disable/blend/depth
// WebGLUniforms         — uploads uniform values to shader programs
// ...and more
The render() Method — Full Pipeline L1609
1 Validation L1611–1618
Guard clauses: is the camera actually a Camera? Is the WebGL context lost (device GPU reset)? If either check fails, return immediately. Context loss is recoverable — three.js fires a webglcontextlost event and stops rendering until restored.
2 Matrix Update L1635–1659
scene.updateMatrixWorld() walks the entire scene graph and recomputes every node's matrixWorld (see Scene Graph). Then camera.updateMatrixWorld() adds the view matrix. Finally the combined VP matrix is built and the frustum planes are extracted.
scene.updateMatrixWorld();
camera.updateMatrixWorld();

// VP matrix for frustum culling:
_projScreenMatrix.multiplyMatrices(
  camera.projectionMatrix,
  camera.matrixWorldInverse
);
_frustum.setFromProjectionMatrix( _projScreenMatrix );
3 Render State Init L1652–1667
A RenderState object (holding light data) and a RenderList (the sorted draw list) are retrieved from pool caches (keyed by scene + camera depth). Both are reset/initialized. A stack is used so nested render calls (e.g. rendering to a texture inside a render) work correctly.
4 Scene Traversal — projectObject() L1681 → L1831
Depth-first recursive walk of the scene tree. For each node:
function projectObject( object, camera, groupOrder, sortObjects ) {
  if ( !object.visible ) return; // skip invisible subtrees

  // Layer test (camera.layers bitmask)
  const visible = object.layers.test( camera.layers );

  if ( visible ) {
    if ( object.isLight ) {
      currentRenderState.addLight( object );  // collect for lighting
    } else if ( object.isMesh || object.isLine || object.isPoints ) {
      // Frustum cull via bounding sphere
      if ( !_frustum.intersectsObject( object ) ) return;

      // Compute clip-space Z for depth sorting
      _vector4.setFromMatrixPosition( object.matrixWorld );
      _vector4.applyMatrix4( _projScreenMatrix );
      const z = _vector4.z; // depth in clip space

      // Push to opaque / transmissive / transparent list
      currentRenderList.push(
        object, geometry, material, groupOrder, z, group
      );
    }
  }

  // Recurse regardless of layer (children may be on different layers)
  for ( let i = 0; i < object.children.length; i++ ) {
    projectObject( object.children[i], camera, groupOrder, sortObjects );
  }
}
5 Render List Sorting L1685–1689
if ( sortObjects ) {
  currentRenderList.sort( _opaqueSort, _transparentSort );
}

// _opaqueSort: groupOrder → renderOrder → z ASC → id
// _transparentSort: groupOrder → renderOrder → z DESC → id
Opaque objects sorted front-to-back: early Z-rejection saves fragment shader work on obscured pixels. Transparent objects sorted back-to-front: required for correct alpha blending (painter's algorithm).
6 Shadow Pass L1706
shadowMap.render(shadowsArray, scene, camera) — for each shadow-casting light:
1. Set the render target to the light's shadow map texture (depth buffer)
2. Render the scene using MeshDepthMaterial (overrideMaterial) from the light's point of view
3. The resulting depth texture is later sampled in the main pass to test whether each fragment is in shadow
This is done before the main pass so shadow maps are ready when lighting uniforms are computed.
7 Light Setup L1723
currentRenderState.setupLights() compiles the collected lights into flat arrays ready for uniform upload: ambient sum, directional light array, point light array, etc. This happens once per render call, not per object.
8 Main Scene Render — renderScene() L1757 → L1949
Iterates through the three lists in order:
renderObjects( currentRenderList.opaque,       scene, camera );
renderObjects( currentRenderList.transmissive,  scene, camera );
renderObjects( currentRenderList.transparent,   scene, camera );
Opaque first (Z-prepass benefit), then transmissive (objects with refraction), then transparent (alpha-blended).
9 renderObject() — Per-Object Setup L2124
function renderObject( object, scene, camera, geometry, material, group ) {
  object.onBeforeRender( renderer, scene, camera, geometry, material, group );

  // Compute model-view and normal matrices (CPU side, once per object)
  object.modelViewMatrix.multiplyMatrices(
    camera.matrixWorldInverse,
    object.matrixWorld
  );
  object.normalMatrix.getNormalMatrix( object.modelViewMatrix );

  // Get or compile shader program
  const program = setProgram( camera, scene, geometry, material, object );

  // Double-sided transparency needs two passes
  if ( material.transparent && material.side === DoubleSide ) {
    // Pass 1: BackSide
    material.side = BackSide;
    renderBufferDirect( camera, scene, geometry, material, object, group );
    // Pass 2: FrontSide
    material.side = FrontSide;
    renderBufferDirect( camera, scene, geometry, material, object, group );
    material.side = DoubleSide; // restore
  } else {
    renderBufferDirect( camera, scene, geometry, material, object, group );
  }

  object.onAfterRender( renderer, scene, camera, geometry, material, group );
}
10 renderBufferDirect() — GPU Draw Call L1184
// 1. Apply GL state (depth, blend, stencil, face culling)
state.setMaterial( material, frontFaceCW );

// 2. Bind geometry's VAO; upload any dirty attributes
bindingStates.setup( object, material, program, geometry, index );

// 3. Upload uniforms to the active program
// (matrices, material params, lights, textures, shadows)
program.getUniforms().upload( gl, uniformsList, camera );

// 4. Determine draw range
const drawStart = Math.max( drawRange.start, group?.start ?? 0 );
const drawCount = Math.min( drawRange.count, group?.count ?? Infinity );

// 5. Execute the draw call
if ( index !== null ) {
  // Indexed geometry: vertices can be shared between triangles
  renderer.renderInstances( drawStart, drawCount, instanceCount );
  // → gl.drawElementsInstanced( mode, count, type, offset, instanceCount )
  // → or gl.drawElements( mode, count, type, offset )
} else {
  // Non-indexed: sequential vertices
  renderer.render( drawStart, drawCount );
  // → gl.drawArrays( mode, first, count )
}
The primitive mode is: gl.TRIANGLES for Mesh, gl.LINES/gl.LINE_STRIP for Line, gl.POINTS for Points.
Animation Loop WebGLAnimation.js

renderer.setAnimationLoop(callback) wraps requestAnimationFrame. The callback receives the current timestamp in milliseconds.

// Internal WebGLAnimation.js
let animationLoop = null;
let requestId = null;

function onAnimationFrame( time, frame ) {
  animationLoop( time, frame );
  requestId = xr.isPresenting
    ? xr.getSession().requestAnimationFrame( onAnimationFrame )
    : requestAnimationFrame( onAnimationFrame );
}

// Public API:
renderer.setAnimationLoop( callback ) {
  animationLoop = callback;
  if ( callback === null ) {
    cancelAnimationFrame( requestId );
  } else {
    requestId = requestAnimationFrame( onAnimationFrame );
  }
}

In XR (VR/AR) mode, xr.getSession().requestAnimationFrame is used instead — it syncs with the headset's display refresh rate and provides an XRFrame with pose data.

The timestamp passed to your callback is the same DOMHighResTimeStamp that requestAnimationFrame provides — milliseconds since page load, with sub-millisecond precision. This is why the README example divides by 1000 or 2000 to get slow rotation speeds.
RenderList — How Objects Are Sorted WebGLRenderLists.js
WebGLRenderList { renderItems[] ← reusable pool (avoids GC pressure) opaque[] ← objects with transparent=false transmissive[] ← objects with transmission > 0 (glass) transparent[] ← objects with transparent=true (alpha blend) } Each render item: { id, // auto-increment object, // the Object3D geometry, // its BufferGeometry material, // the Material to render with groupOrder, // from parent Group's renderOrder renderOrder, // from object.renderOrder z, // clip-space depth (used for sorting) group // geometry group (for multi-material) } Opaque sort: groupOrder → renderOrder → z ASC → id Transparent sort: groupOrder → renderOrder → z DESC → id

The pool reuse (renderItems[]) avoids creating new objects each frame, which would stress the garbage collector and cause frame time spikes.

WebGL State Management WebGLState.js

WebGL has expensive state-change calls. WebGLState shadows all GL state in JavaScript and only calls the WebGL API when the value actually changes:

// WebGLState.js — example: setBlending
function setBlending( blending, blendEquation, blendSrc, blendDst, ... ) {
  if ( blending === NoBlending ) {
    if ( currentBlendingEnabled ) {
      gl.disable( gl.BLEND );      // only calls GL if state changed
      currentBlendingEnabled = false;
    }
    return;
  }

  if ( ! currentBlendingEnabled ) {
    gl.enable( gl.BLEND );
    currentBlendingEnabled = true;
  }

  if ( blending !== currentBlending || ... ) {
    // Only update if different from last draw
    gl.blendEquationSeparate( ... );
    gl.blendFuncSeparate( ... );
    currentBlending = blending;
  }
}

This pattern is applied to: blend mode, depth test/write, stencil, face culling, line width, scissor, viewport, and more. It's a critical optimization — redundant GL state changes are one of the top causes of WebGL performance issues.

Render Targets — Rendering Off-Screen

A WebGLRenderTarget redirects rendering into a texture instead of the canvas. This is the foundation for post-processing, shadow maps, reflections, and SSAO.

// Create a render target (texture)
const target = new THREE.WebGLRenderTarget( 512, 512, {
  minFilter: THREE.LinearFilter,
  magFilter: THREE.NearestFilter,
  format: THREE.RGBAFormat,
});

// Render scene into the texture
renderer.setRenderTarget( target );
renderer.render( scene, camera );

// Back to canvas
renderer.setRenderTarget( null );
renderer.render( scene, camera );

// Use the texture
const material = new THREE.MeshBasicMaterial({ map: target.texture });

Internally, three.js binds the render target's framebuffer object (FBO) before rendering. The FBO directs draw output to the attached texture instead of the default framebuffer (canvas).

Key Renderer Configuration
Option / MethodDescription
antialias: trueRequest MSAA from the browser. Smooths triangle edges but increases GPU memory.
setPixelRatio(ratio)Scale canvas for HiDPI screens. Use Math.min(window.devicePixelRatio, 2) — 3× and 4× are wasteful.
setSize(w, h)Resize the canvas and WebGL viewport.
shadowMap.enabled = trueEnable shadow rendering. Also set light.castShadow and mesh.castShadow / receiveShadow.
shadowMap.typePCFSoftShadowMap (default soft), VSMShadowMap (variance, blurrable), BasicShadowMap (hard edges, cheap).
outputColorSpaceSRGBColorSpace (default) — ensures textures and output match sRGB display expectations.
toneMappingACESFilmicToneMapping (cinematic), LinearToneMapping (none), ReinhardToneMapping — for HDR → LDR conversion.
sortObjectsDefault true. Set false only if you manage draw order entirely via renderOrder yourself.
External References