How Three.js Handles WebGL2 and WebGL1 Fallback
This article explores how Three.js manages graphics rendering contexts behind the scenes, focusing on its automatic initialization of WebGL2 and its internal fallback mechanism to WebGL1. You will learn how the engine detects browser capabilities, negotiates context creation with the HTML5 canvas, and maintains feature and shader compatibility across different hardware and browser generations.
The Context Creation Lifecycle
When you instantiate a WebGLRenderer in Three.js, the
engine immediately begins the process of acquiring a rendering context
from the provided or newly created <canvas>
element.
Under the hood, Three.js prioritizes WebGL2. The initialization sequence follows a strict hierarchy during context acquisition:
- WebGL2 Attempt: The renderer first calls
canvas.getContext( 'webgl2', attributes ). If the browser and hardware support WebGL2, this returns aWebGL2RenderingContext, and the initialization succeeds. - WebGL1 Fallback: If the WebGL2 call returns
null, Three.js catches the failure and falls back to WebGL1 by callingcanvas.getContext( 'webgl', attributes ). - Legacy Fallback: If standard WebGL1 is unavailable,
it attempts to query
canvas.getContext( 'experimental-webgl', attributes )to support older browsers. - Failure Handling: If all attempts return
null, Three.js throws an error, and the renderer fails to initialize. Developers can detect this using utility classes likeWebGL.isWebGLAvailable().
Feature Detection and WebGLCapabilities
Once a context is successfully acquired, Three.js queries the context
to determine its version and properties. It creates an internal
WebGLCapabilities instance, which acts as a central
repository of GPU limits and supported features.
Within WebGLCapabilities, Three.js sets a boolean flag:
isWebGL2. This flag is crucial because the engine queries
it constantly during the rendering loop. If isWebGL2 is
true, Three.js unlocks native WebGL2 features such as:
- Vertex Array Objects (VAOs): Handled natively in
WebGL2, whereas WebGL1 requires the
OES_vertex_array_objectextension. - Multisampled Renderbuffers: Used for native Anti-Aliasing (MSAA) in render targets.
- 3D Textures and 2D Texture Arrays: Native in WebGL2, unavailable or limited in WebGL1.
- Instanced Rendering: Handled natively via
drawArraysInstancedanddrawElementsInstanced, rather than the WebGL1 extensionANGLE_instanced_arrays.
If isWebGL2 is false, Three.js gracefully
downgrades. It attempts to load the equivalent WebGL1 extensions. If the
extensions are missing, the associated features are disabled or
simulated via software workarounds.
Shader Compilation and GLSL Translation
One of the most complex aspects of the WebGL2 to WebGL1 fallback is shader compatibility. WebGL2 uses GLSL ES 3.00, while WebGL1 is restricted to GLSL ES 1.00.
To prevent developers from having to write two versions of every
shader, Three.js handles shader translation automatically inside the
WebGLProgram class:
- GLSL Version Headers: When compiling a shader,
Three.js prepends
#version 300 esto the shader source if a WebGL2 context is active. For WebGL1 contexts, this header is omitted, defaulting the compiler to GLSL ES 1.00. - Keyword Translation: Three.js utilizes custom
preprocessors to rewrite shader code on the fly. For instance, in
WebGL1, shaders use
attributeandvaryingkeywords, whereas WebGL2 usesinandout. Three.js internally maps these qualifiers based on the active WebGL version. - Extension Polyfills: If a shader written for WebGL1
uses extensions like
GL_OES_standard_derivativesorGL_EXT_draw_buffers, the Three.js shader compiler automatically enables these extensions in the WebGL1 prepended header. In WebGL2, these extensions are native, so Three.js bypasses the extension pragmas entirely.
Texture and Parameter Mapping
WebGL2 introduces stricter internal texture formats and coordinate wrapping rules. When falling back to WebGL1, Three.js adjusts its internal state machine to prevent WebGL errors:
- Non-Power-of-Two (NPOT) Textures: WebGL1 has strict
limitations on NPOT textures; they cannot use mipmapping or repeat
wrapping modes (
gl.REPEAT). If WebGL1 is active and an NPOT texture is detected, Three.js automatically downgrades the minification filter togl.LINEARorgl.NEARESTand forces clamping to the edge. WebGL2 supports NPOT textures natively without these restrictions. - Internal Formats: WebGL2 supports advanced texture
formats (like float textures and depth textures) natively. In WebGL1,
Three.js queries extensions like
OES_texture_floatandWEBGL_depth_textureto provide matching functionality.