Open In App

How to Implement Lighting in WebGL?

Last Updated : 23 Jul, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

Lighting in WebGL is important for adding depth, dimension, and mood to 3D scenes. This article will guide you through setting up basic lighting, making it approachable for beginners while covering essential concepts. Refer to the article Implement Lighting in WebGL for more detailed lighting techniques Implement Lighting in WebGL.

Approach

  • Set up the basic structure of the webpage, including necessary WebGL scripts and a <canvas> element to render the 3D scene.
  • Initialize the WebGL context and create the shaders that will handle lighting calculations.
  • Write GLSL (OpenGL Shading Language) code to define how light affects the colors of objects using ambient, directional, or point lights.
  • Use JavaScript to render the objects in the scene with the lighting applied, continuously updating the scene.

Example

The code sets up a WebGL context, which allows the rendering of 3D graphics in the browser. The program uses projection and model-view matrices to transform 3D coordinates into screen space, and the normal matrix ensures correct lighting transformations.

Step 1:Create the Basic HTML Structure

Start by creating an HTML file that includes a <canvas> element where WebGL will render the scene. Also, include references to any required JavaScript libraries (if needed).

HTML
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL Lighting Example</title>
    <style>
        body {
            margin: 0;
        }

        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>

<body>
    <canvas id="webglCanvas"></canvas>
    <!-- Include glMatrix library -->
    <script src=
"https://cdn.jsdelivr.net/npm/[email protected]/dist/gl-matrix-min.js">
  </script>
    <script src="script.js"></script>
</body>

</html>

Step 2:Initialize WebGL Context in JavaScript

Within the referenced JavaScript file, initialize the WebGL context. This is necessary to access WebGL's drawing capabilities.

JavaScript
// Initialize WebGL context
const canvas = document.getElementById('webglCanvas');
let gl = canvas.getContext('webgl');

if (!gl) {
    console.error('WebGL not supported, falling back on experimental-webgl');
    gl = canvas.getContext('experimental-webgl');
}

if (!gl) {
    alert('Your browser does not support WebGL');
}

Step 3:Write the Vertex and Fragment Shaders

Shaders are small programs that run on the GPU. The vertex shader is responsible for handling the position and lighting of each vertex, while the fragment shader determines the color of each pixel.

JavaScript
// Vertex shader program
const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec4 aVertexNormal;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    uniform mat4 uNormalMatrix;

    uniform vec3 uLightDirection;

    varying lowp vec4 vColor;

    void main(void) {
        gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;

        // Apply lighting
        vec3 ambientLight = vec3(0.3, 0.3, 0.3);
        vec3 directionalLightColor = vec3(1, 1, 1);
        vec3 transformedNormal = mat3(uNormalMatrix) * aVertexNormal.xyz;
        float directional = max(dot(transformedNormal, uLightDirection), 0.0);

        vColor = vec4(ambientLight + (directionalLightColor * directional), 1.0);
    }
`;

// Fragment shader program
const fsSource = `
    varying lowp vec4 vColor;

    void main(void) {
        gl_FragColor = vColor;
    }
`;

Step 4: Initialize the Shader Program

Compile the vertex and fragment shaders, then link them into a WebGL shader program. This program will be used later to render the scene.

JavaScript
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 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;
}

const shaderProgram =
    initShaderProgram(gl, vsSource, fsSource);

Step 5: Set Up Buffers for Cube Geometry

Define the vertex positions and normals for a cube. These are stored in buffers, which will be sent to the GPU for rendering.

JavaScript
function initBuffers(gl) {
    // Create a buffer for the cube's vertex positions.
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

    const positions = [
        -1.0, -1.0, 1.0,
        1.0, -1.0, 1.0,
        1.0, 1.0, 1.0,
        -1.0, 1.0, 1.0,
        -1.0, -1.0, -1.0,
        1.0, -1.0, -1.0,
        1.0, 1.0, -1.0,
        -1.0, 1.0, -1.0,
    ];

    gl.bufferData(gl.ARRAY_BUFFER,
        new Float32Array(positions), gl.STATIC_DRAW);

    // Create a buffer for the cube's vertex normals.
    const normalBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);

    const vertexNormals = [
        // Front
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,

        // Back
        0.0, 0.0, -1.0,
        0.0, 0.0, -1.0,
        0.0, 0.0, -1.0,
        0.0, 0.0, -1.0,
    ];

    gl.bufferData(gl.ARRAY_BUFFER,
        new Float32Array(vertexNormals), gl.STATIC_DRAW);

    return {
        position: positionBuffer,
        normal: normalBuffer,
    };
}

const buffers = initBuffers(gl);

Step 6: Define the Render Loop

Create a render loop that continuously updates the scene. The cube will rotate, and the scene will be redrawn in each frame.

JavaScript
let cubeRotation = 0.0;

function render(now) {
    now *= 0.001;  // Convert to seconds
    cubeRotation = now;

    drawScene(gl, shaderProgram, buffers, cubeRotation);

    requestAnimationFrame(render);
}
requestAnimationFrame(render);

Step 7: Draw the Scene

Finally, define how to draw the scene. This involves setting up the matrices for projection and model-view transformations, applying the lighting, and rendering the cube.

JavaScript
function drawScene(gl, programInfo, buffers, rotation) {
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    const fieldOfView = 45 * Math.PI / 180;
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const zNear = 0.1;
    const zFar = 100.0;
    const projectionMatrix = mat4.create();

    mat4.perspective(projectionMatrix,
        fieldOfView, aspect, zNear, zFar);

    const modelViewMatrix = mat4.create();
    mat4.translate(modelViewMatrix,
        modelViewMatrix, [-0.0, 0.0, -6.0]);
    mat4.rotate(modelViewMatrix,
        modelViewMatrix, rotation, [0, 0, 1]);
    mat4.rotate(modelViewMatrix,
        modelViewMatrix, rotation * .7, [0, 1, 0]);

    const normalMatrix = mat4.create();
    mat4.invert(normalMatrix,
        modelViewMatrix);
    mat4.transpose(normalMatrix,
        normalMatrix);

    {
        const numComponents = 3;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;
        gl.bindBuffer(gl.ARRAY_BUFFER,
            buffers.position);
        gl.vertexAttribPointer(
            programInfo.attribLocations.vertexPosition,
            numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(programInfo
            .attribLocations.vertexPosition);
    }

    {
        const numComponents = 3;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
        gl.vertexAttribPointer(
            programInfo
                .attribLocations.vertexNormal,
            numComponents, type,
            normalize, stride, offset);
        gl.enableVertexAttribArray(programInfo
            .attribLocations.vertexNormal);
    }

    gl.useProgram(programInfo.program);

    gl.uniformMatrix4fv(programInfo
        .uniformLocations.projectionMatrix,
        false, projectionMatrix);
    gl.uniformMatrix4fv(programInfo
        .uniformLocations.modelViewMatrix,
        false, modelViewMatrix);
    gl.uniformMatrix4fv(programInfo
        .uniformLocations.normalMatrix,
        false, normalMatrix);


    gl.uniform3fv(programInfo
        .uniformLocations
        .lightDirection, [-0.3, -0.7, -1.0]);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 8);


Compiling complete Javascript code together:

JavaScript
// Initialize WebGL context
const canvas = document
    .getElementById('webglCanvas');
const gl = canvas
    .getContext('webgl');

if (!gl) {
    console.error('WebGL not supported, falling back on experimental-webgl');
    gl = canvas.getContext('experimental-webgl');
}

if (!gl) {
    alert('Your browser does not support WebGL');
}

// Vertex shader program
const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec4 aVertexNormal;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    uniform mat4 uNormalMatrix;

    uniform vec3 uLightDirection;

    varying lowp vec4 vColor;

    void main(void) {
        gl_Position =
         uProjectionMatrix * uModelViewMatrix * aVertexPosition;

        // Apply lighting
        vec3 ambientLight = vec3(0.3, 0.3, 0.3);
        vec3 directionalLightColor = vec3(1, 1, 1);
        vec3 transformedNormal = 
        mat3(uNormalMatrix) * aVertexNormal.xyz;
        float directional = 
        max(dot(transformedNormal, uLightDirection), 0.0);

        vColor =
         vec4(ambientLight + (directionalLightColor * directional), 1.0);
    }
`;

// Fragment shader program
const fsSource = `
    varying lowp vec4 vColor;

    void main(void) {
        gl_FragColor = vColor;
    }
`;

// Initialize shader program
const shaderProgram =
    initShaderProgram(gl, vsSource, fsSource);

// Collect all the info needed to use the shader program.
const programInfo = {
    program: shaderProgram,
    attribLocations: {
        vertexPosition: gl
            .getAttribLocation(shaderProgram,
                'aVertexPosition'),
        vertexNormal: gl
            .getAttribLocation(shaderProgram,
                'aVertexNormal'),
    },
    uniformLocations: {
        projectionMatrix: gl
            .getUniformLocation(shaderProgram,
                'uProjectionMatrix'),
        modelViewMatrix: gl
            .getUniformLocation(shaderProgram,
                'uModelViewMatrix'),
        normalMatrix: gl
            .getUniformLocation(shaderProgram,
                'uNormalMatrix'),
        lightDirection: gl
            .getUniformLocation(shaderProgram,
                'uLightDirection'),
    },
};

// Define the positions and normals for the cube
const buffers = initBuffers(gl);

let cubeRotation = 0.0;

function render(now) {
    now *= 0.001;  // convert to seconds
    cubeRotation = now;

    drawScene(gl, programInfo,
        buffers, cubeRotation);

    requestAnimationFrame(render);
}
requestAnimationFrame(render);

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 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 initBuffers(gl) {
    // Create a buffer for the cube's vertex positions.
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl
        .ARRAY_BUFFER, positionBuffer);

    // Define the positions for the cube
    const positions = [
        -1.0, -1.0, 1.0,
        1.0, -1.0, 1.0,
        1.0, 1.0, 1.0,
        -1.0, 1.0, 1.0,
        -1.0, -1.0, -1.0,
        1.0, -1.0, -1.0,
        1.0, 1.0, -1.0,
        -1.0, 1.0, -1.0,
    ];

    gl.bufferData(gl
        .ARRAY_BUFFER,
        new Float32Array(positions),
        gl.STATIC_DRAW);

    // Create a buffer for the cube's vertex normals.
    const normalBuffer = gl.createBuffer();
    gl.bindBuffer(gl
        .ARRAY_BUFFER, normalBuffer);

    // Define the normals for the cube
    const vertexNormals = [
        // Front
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,
        0.0, 0.0, 1.0,

        // Back
        0.0, 0.0, -1.0,
        0.0, 0.0, -1.0,
        0.0, 0.0, -1.0,
        0.0, 0.0, -1.0,
    ];

    gl.bufferData(gl
        .ARRAY_BUFFER,
        new Float32Array(vertexNormals),
        gl.STATIC_DRAW);

    return {
        position: positionBuffer,
        normal: normalBuffer,
    };
}

function drawScene(gl,
    programInfo, buffers, rotation) {
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);
    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    const fieldOfView = 45 * Math.PI / 180;
    const aspect = gl.canvas
        .clientWidth / gl.canvas.clientHeight;
    const zNear = 0.1;
    const zFar = 100.0;
    const projectionMatrix = mat4.create();

    mat4
        .perspective(projectionMatrix,
            fieldOfView, aspect, zNear, zFar);

    const modelViewMatrix = mat4.create();
    mat4.translate(modelViewMatrix,
        modelViewMatrix, [-0.0, 0.0, -6.0]);
    mat4.rotate(modelViewMatrix,
        modelViewMatrix, rotation, [0, 0, 1]);
    mat4.rotate(modelViewMatrix,
        modelViewMatrix, rotation * .7, [0, 1, 0]);

    const normalMatrix = mat4.create();
    mat4.invert(normalMatrix,
        modelViewMatrix);
    mat4.transpose(normalMatrix,
        normalMatrix);

    {
        const numComponents = 3;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;
        gl.bindBuffer(gl.ARRAY_BUFFER,
            buffers.position);
        gl.vertexAttribPointer(programInfo
            .attribLocations.vertexPosition,
            numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(programInfo
            .attribLocations.vertexPosition);
    }

    {
        const numComponents = 3;
        const type = gl.FLOAT;
        const normalize = false;
        const stride = 0;
        const offset = 0;
        gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
        gl.vertexAttribPointer(programInfo
            .attribLocations.vertexNormal,
            numComponents, type, normalize, stride, offset);
        gl.enableVertexAttribArray(programInfo
            .attribLocations.vertexNormal);
    }

    gl.useProgram(programInfo.program);

    gl.uniformMatrix4fv(programInfo
        .uniformLocations
        .projectionMatrix,
        false, projectionMatrix);
    gl.uniformMatrix4fv(programInfo
        .uniformLocations.modelViewMatrix,
        false, modelViewMatrix);
    gl.uniformMatrix4fv(programInfo
        .uniformLocations.normalMatrix,
        false, normalMatrix);

    gl.uniform3fv(programInfo
        .uniformLocations
        .lightDirection, [-0.3, -0.7, -1.0]);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 8);
}


Output:

Recording-2024-08-19-014729
Implement Lighting in WebGL

Article Tags :

Explore