Open In App

How to Implement Shadows in WebGL?

Last Updated : 28 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

Shadows in WebGL are techniques used to simulate the effect of light blocking and casting shadows on surfaces within an environment. Implementing shadows in WebGL involves rendering scenes from multiple perspectives, such as using shadow mapping or screen-space techniques, to create realistic visual effects.

The below approaches implement Shadows in WebGL:

Basic Shadow with Shadow Mapping

In this approach, we are implementing a basic shadow using shadow mapping in WebGL. We first set up a WebGL context and define vertex and fragment shaders to render a simple triangle. The gl-matrix library is used to handle matrix transformations for the orthographic projection. The triangle is drawn with its vertex colors, creating a basic shadow effect by projecting the geometry onto the canvas.

Example: The below example performs Basic Shadow with Shadow Mapping.

HTML
<!DOCTYPE html>
<html>

<head>
    <title>Shadows in WebGL</title>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            font-family: Arial, sans-serif;
        }


        h3 {
            margin: 0;
        }

        canvas {
            width: 400px;
            height: 300px;
            border: 1px solid black;
        }
    </style>
</head>

<body>
    <h3>Basic Shadow with Shadow Mapping</h3>
    <canvas id="webglCanvas"></canvas>

    <script src=
"https://cdn.jsdelivr.net/npm/[email protected]/dist/gl-matrix.js"></script>
    <script>
        const canvas = document.getElementById('webglCanvas');
        const gl = canvas.getContext('webgl');

        if (!gl) {
            alert('WebGL not supported');
            throw new Error('WebGL not supported');
        }

        const vertexShaderSource = `
            attribute vec4 a_position;
            attribute vec4 a_color;
            uniform mat4 u_modelViewProjectionMatrix;
            varying vec4 v_color;

            void main() {
                gl_Position = u_modelViewProjectionMatrix * a_position;
                v_color = a_color;
            }
        `;

        const fragmentShaderSource = `
            precision mediump float;
            varying vec4 v_color;

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

        function createShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);

            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('Shader compilation failed:', 
                              gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }

        function createProgram(gl, vertexShaderSource, 
                                   fragmentShaderSource) {
            const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
            const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

            const program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);

            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                console.error('Program linking failed:', gl.getProgramInfoLog(program));
                return null;
            }
            return program;
        }

        const program = createProgram(gl, vertexShaderSource,
                                          fragmentShaderSource);
        gl.useProgram(program);

        const vertices = new Float32Array([
            0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0,
            -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0,
            0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0
        ]);

        const positionLocation = gl.getAttribLocation(program, 'a_position');
        const colorLocation = gl.getAttribLocation(program, 'a_color');

        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 28, 0);

        gl.enableVertexAttribArray(colorLocation);
        gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, 28, 8);

        const modelViewProjectionMatrix = mat4.create();
        mat4.ortho(modelViewProjectionMatrix, -1, 1, -1, 1, -1, 1);

        const uModelViewProjectionMatrix = gl.getUniformLocation(program, 
                                           'u_modelViewProjectionMatrix');
        gl.uniformMatrix4fv(uModelViewProjectionMatrix, false,
                            modelViewProjectionMatrix);

        function drawScene() {
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.drawArrays(gl.TRIANGLES, 0, 3);
        }

        drawScene();
    </script>
</body>

</html>

Output:

shadows
Output

Shadow Effect Using 2D Canvas Context

In this approach, we are implementing a shadow effect using the 2D canvas context. We create interactive sliders and a color picker to dynamically adjust the shadow properties of a rectangle drawn on the canvas. Users can modify the shadow's blur, offset, and color in real-time, with changes reflected immediately on the canvas.

Example: The below example performs Shadow Effect Using 2D Canvas Context.

HTML
<!DOCTYPE html>
<html>

<head>
    <title>Shadows in WebGL</title>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            font-family: Arial, sans-serif;
        }
        h3 {
            margin: 0;
        }

        canvas {
            width: 400px;
            height: 300px;
            border: 1px solid black;
        }

        .controls {
            margin: 20px;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 20px;
        }

        .control {
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        label {
            display: block;
            margin-bottom: 5px;
        }

        input[type="range"] {
            width: 150px;
        }

        input[type="color"] {
            margin-top: 5px;
            padding: 0;
            border: none;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <h3>Shadow Effect Using 2D Canvas Context</h3>
    <canvas id="canvas2D"></canvas>

    <div class="controls">
        <div class="control">
            <label for="shadowBlur">Shadow Blur:</label>
            <input type="range" id="shadowBlur"
                   min="0" max="50" value="10">
        </div>
        <div class="control">
            <label for="shadowOffsetX">Shadow Offset X:</label>
            <input type="range" id="shadowOffsetX"
                   min="-50" max="50" value="5">
        </div>
        <div class="control">
            <label for="shadowOffsetY">Shadow Offset Y:</label>
            <input type="range" id="shadowOffsetY"
                   min="-50" max="50" value="5">
        </div>
        <div class="control">
            <label for="shadowColor">Shadow Color:</label>
            <input type="color" id="shadowColor" 
                   value="#000000">
        </div>
    </div>

    <script>
        const canvas = document.getElementById('canvas2D');
        const ctx = canvas.getContext('2d');

        canvas.width = 400;
        canvas.height = 300;

        const shadowBlurInput = document.getElementById('shadowBlur');
        const shadowOffsetXInput = document.getElementById('shadowOffsetX');
        const shadowOffsetYInput = document.getElementById('shadowOffsetY');
        const shadowColorInput = document.getElementById('shadowColor');

        function drawRectangle() {
            const shadowBlur = parseInt(shadowBlurInput.value);
            const shadowOffsetX = parseInt(shadowOffsetXInput.value);
            const shadowOffsetY = parseInt(shadowOffsetYInput.value);
            const shadowColor = shadowColorInput.value;

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            ctx.fillStyle = '#ff5722';
            ctx.shadowColor = shadowColor;
            ctx.shadowBlur = shadowBlur;
            ctx.shadowOffsetX = shadowOffsetX;
            ctx.shadowOffsetY = shadowOffsetY;

            ctx.fillRect(100, 100, 200, 100);
        }

        drawRectangle();

        shadowBlurInput.addEventListener('input', drawRectangle);
        shadowOffsetXInput.addEventListener('input', drawRectangle);
        shadowOffsetYInput.addEventListener('input', drawRectangle);
        shadowColorInput.addEventListener('input', drawRectangle);
    </script>
</body>

</html>

Output:


Explore