W3docs

JavaScript WebGL API

Learn the JavaScript WebGL API: set up a rendering context, write and compile shaders, upload vertex buffers, and draw 3D graphics.

Introduction to WebGL

WebGL (Web Graphics Library, specifically version 1.0) leverages the power of OpenGL ES 2.0 in web environments, enabling developers to render detailed 3D graphics within any compatible web browser without the need for plugins. All examples in this chapter use the WebGL 1.0 API. This capability is essential for creating immersive games, interactive 3D applications, and complex visualizations directly in the browser. For modern projects, consider WebGL 2.0, which builds on OpenGL ES 3.0 and offers improved performance and features.

This chapter covers what you need to start drawing with WebGL: requesting a rendering context, writing and compiling shaders, uploading vertex data into buffers, and issuing draw calls. By the end you will understand the full pipeline behind a single rendered triangle and where to go next for lighting, texturing, and animation.

WebGL vs. the 2D Canvas

WebGL renders through the same <canvas> element used by the 2D Canvas API, but the two are very different. The 2D context (getContext('2d')) gives you a high-level drawing surface — fillRect, arc, drawImage. WebGL gives you a low-level, GPU-accelerated pipeline: you describe geometry as arrays of numbers and write small programs (shaders) that run on the GPU to decide where each vertex lands and what color each pixel gets. That extra effort buys hardware acceleration, true 3D, and the throughput needed for thousands of objects per frame.

You request the two contexts the same way, so it is good practice to feature-detect:

const canvas = document.querySelector('#webglCanvas');
// 'webgl2' is preferred where available; fall back to 'webgl' (1.0).
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');

if (!gl) {
  console.error('WebGL is not supported by this browser.');
}

Setting Up Your First WebGL Context

To begin with WebGL, setting up a rendering context attached to a canvas element in your HTML is crucial. For modern projects, you can also request a WebGL 2 context using canvas.getContext('webgl2') for improved performance and features:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Simple WebGL Example</title>
    <style>
        canvas {
            width: 400px;
            height: 400px;
            border: 1px solid black; /* Adds a border around the canvas */
        }
    </style>
</head>
<body>
    <canvas id="webglCanvas"></canvas>
    <script>
        // This script will run once the DOM content is fully loaded.
        document.addEventListener("DOMContentLoaded", function() {
            // Get the canvas element.
            const canvas = document.getElementById('webglCanvas');
            // Initialize the WebGL 1.0 context.
            const gl = canvas.getContext('webgl');

            // Check if WebGL is available.
            if (!gl) {
                console.error('WebGL is not supported by your browser.');
                return;
            }

            // Set the clear color to blue with full opacity.
            gl.clearColor(0.0, 0.0, 1.0, 1.0); // RGBA: Blue color

            // Clear the color buffer with the specified clear color.
            gl.clear(gl.COLOR_BUFFER_BIT);
        });
    </script>
</body>
</html>

Breakdown of the Code

  1. HTML Setup: The HTML portion sets up a canvas element where WebGL will render its output. A border is added to visually identify the canvas area on the webpage.
  2. CSS Styling: A simple style is applied to ensure the canvas has a specific size and a border for visibility.
  3. JavaScript for WebGL:
    • Event Listener: The JavaScript code is wrapped in an event listener that waits for the DOM content to be fully loaded before executing.
    • WebGL Context Initialization: It obtains the WebGL 1.0 context from the canvas. If WebGL isn't supported, the context will be null.
    • WebGL Availability Check: If the context is null, an error is logged to the console indicating lack of support.
    • Clear Color Setting: gl.clearColor(0.0, 0.0, 1.0, 1.0) sets the color (blue, fully opaque) that will fill the canvas when the color buffer is cleared. Note that this only stores the color — nothing is drawn yet.
    • Clearing the Color Buffer: gl.clear(gl.COLOR_BUFFER_BIT) actually paints the canvas with the previously set clear color, producing a solid blue square.

This example is fundamental but provides a good starting point for understanding how WebGL setups work. You can enhance this by adding more WebGL functionalities like shaders, buffers, and drawing commands to create graphical outputs.

Rendering a Simple Triangle

One of the first steps in learning WebGL is to render simple shapes. WebGL uses a normalized device coordinate (NDC) system: the visible area ranges from -1 to 1 on both the X and Y axes, with (0, 0) at the center of the canvas. Every shape you draw must be described in these coordinates (or transformed into them by a shader).

Drawing even a single triangle requires the full WebGL pipeline:

  1. Write a vertex shader that positions each corner.
  2. Write a fragment shader that colors each pixel.
  3. Compile and link them into a shader program.
  4. Upload the corner coordinates into a buffer.
  5. Connect the buffer to the shader's attribute and issue a draw call.

The example below walks through all five steps:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebGL Triangle Example</title>
    <style>
        canvas {
            width: 400px;
            height: 400px;
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <canvas id="webglCanvas"></canvas>
    <script>
        // Function to create a shader, upload GLSL source code, and compile the shader
        function loadShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);

            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }

            return shader;
        }

        // Function to initialize the shader program
        function initShaderProgram(gl, vsSource, fsSource) {
            const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
            const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
            const shaderProgram = gl.createProgram();
            gl.attachShader(shaderProgram, vertexShader);
            gl.attachShader(shaderProgram, fragmentShader);
            gl.linkProgram(shaderProgram);

            if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
                console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
                return null;
            }

            return shaderProgram;
        }

        // Function to initialize WebGL
        function initWebGL() {
            const canvas = document.getElementById('webglCanvas');
            // Note: Use 'webgl2' for modern projects
            const gl = canvas.getContext('webgl');

            if (!gl) {
                console.error('WebGL is not supported by your browser.');
                return;
            }

            // Set internal canvas resolution to match CSS dimensions
            canvas.width = 400;
            canvas.height = 400;

            // Vertex shader program
            const vsSource = `
                attribute vec4 aVertexPosition;
                void main(void) {
                    gl_Position = aVertexPosition;
                }
            `;

            // Fragment shader program
            const fsSource = `
                void main(void) {
                    gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange color
                }
            `;

            const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
            const programInfo = {
                program: shaderProgram,
                attribLocations: {
                    vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition')
                }
            };

            // Validate attribute location to prevent silent shader failures
            if (programInfo.attribLocations.vertexPosition === -1) {
                console.error('Failed to get the location of aVertexPosition');
                return;
            }

            // Create a buffer for the triangle's positions.
            const positionBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

            // Set the positions for the triangle.
            const positions = [
                0.0,  1.0,  // Vertex 1
                -1.0, -1.0, // Vertex 2
                1.0, -1.0  // Vertex 3
            ];
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

            // Draw the scene
            function drawScene() {
                // Note: High-DPI scaling is omitted for simplicity.
                gl.viewport(0, 0, canvas.width, canvas.height); 
                gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
                gl.clear(gl.COLOR_BUFFER_BIT);

                // Tell WebGL to use our program when drawing
                gl.useProgram(programInfo.program);

                // Attach the position buffer.
                gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
                gl.vertexAttribPointer(
                    programInfo.attribLocations.vertexPosition,
                    2,   // Number of components per vertex attribute
                    gl.FLOAT, false, 0, 0);
                gl.enableVertexAttribArray(
                    programInfo.attribLocations.vertexPosition);

                // Execute WebGL program
                gl.drawArrays(gl.TRIANGLES, 0, 3);

                requestAnimationFrame(drawScene);
            }

            drawScene();
        }

        // Call the initWebGL function after the document has loaded to ensure the canvas is ready.
        document.addEventListener("DOMContentLoaded", initWebGL);
    </script>
</body>
</html>

Explanation of the Code

  • Vertex Shader (vsSource): Reads the aVertexPosition attribute and assigns it to the built-in gl_Position, which determines where each vertex lands on screen.
  • Fragment Shader (fsSource): Sets gl_FragColor so every pixel inside the triangle is rendered orange (vec4(1.0, 0.5, 0.0, 1.0) is RGBA orange).
  • Shader Compilation (loadShader): Compiles a single shader from GLSL source and reports compile errors via getShaderInfoLog.
  • Shader Program Initialization (initShaderProgram): Links the compiled vertex and fragment shaders into one executable program that runs on the GPU.
  • Animation Loop: drawScene() issues the draw call, then requestAnimationFrame(drawScene) schedules the next frame, keeping rendering in sync with the display's refresh rate. For deeper coverage of frame scheduling, see JavaScript animations.

Why a Draw Call Needs So Much Setup

If you come from the 2D Canvas API, all this scaffolding can feel heavy for one triangle. The reason is that WebGL is stateful and explicit: nothing is drawn until you have (1) a linked program, (2) data in a buffer, (3) that buffer wired to a shader attribute with vertexAttribPointer, and (4) enableVertexAttribArray turned on. Forget any step and you get a blank canvas with no error — which is the single most common WebGL frustration. The attribLocations.vertexPosition === -1 check in the example guards against silently mistyped attribute names.

Working with Uniforms

Attributes vary per vertex; uniforms stay constant for an entire draw call and are the standard way to pass changing values — time, color, transformation matrices — from JavaScript into a shader. This is how you animate or recolor a scene without re-uploading geometry every frame.

A minimal recolor example: declare a uniform in the fragment shader, look up its location once, then update it each frame.

// In the fragment shader source:
//   precision mediump float;
//   uniform vec4 uColor;
//   void main(void) { gl_FragColor = uColor; }

const colorLocation = gl.getUniformLocation(shaderProgram, 'uColor');

function render(timeMs) {
  const t = timeMs * 0.001;            // seconds
  const r = (Math.sin(t) + 1) / 2;     // oscillate 0..1
  gl.useProgram(shaderProgram);
  gl.uniform4f(colorLocation, r, 0.5, 1.0 - r, 1.0); // RGBA
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

Because the geometry never changes, only the uColor uniform is updated per frame — far cheaper than rebuilding buffers.

Advanced Techniques in WebGL

As you progress, WebGL offers extensive functionality such as lighting, texturing, and geometry management:

  • Textures let you map images onto surfaces with gl.texImage2D and a sampler2D uniform in the fragment shader.
  • Index buffers (gl.ELEMENT_ARRAY_BUFFER + gl.drawElements) reuse shared vertices, cutting memory and draw cost for complex meshes.
  • Transformation matrices (model/view/projection) move from flat 2D coordinates into true 3D perspective; libraries like glMatrix handle the math.
  • Depth testing (gl.enable(gl.DEPTH_TEST)) ensures nearer objects correctly occlude farther ones.

For concrete implementations, refer to the official Khronos WebGL samples or an established 3D library like Three.js, which wraps the raw API in a far friendlier abstraction.

Best Practices for WebGL Development

  • Validate context creation: Always check that getContext returned a non-null value and provide a graceful fallback for browsers without GPU support.
  • Performance Optimization: Minimize state changes (useProgram, bindBuffer), batch draw calls, and use indexed drawing for shared vertices.
  • Manage GPU resources: Delete buffers, textures, and programs you no longer need with gl.deleteBuffer, gl.deleteTexture, and gl.deleteProgram to avoid leaks.
  • Cross-Browser Testing: Ensure your WebGL applications perform consistently across different browsers and devices, and handle the rare webglcontextlost event.
  • User Interaction: Drive uniforms from input events to make scenes dynamic — see JavaScript events for handling user input.

Conclusion

WebGL is a powerful tool for web developers looking to integrate real-time 3D graphics into their applications. With careful planning and creative implementation, you can build stunning visual experiences that run seamlessly in web browsers. By mastering WebGL through comprehensive tutorials and consistent practice, you will unlock a new realm of possibilities for web development.

Practice

Practice
What capabilities does WebGL offer for web developers?
What capabilities does WebGL offer for web developers?
Was this page helpful?