Custom Shaders in WebGL

WebGL is a powerful graphics API that enables developers to create interactive 3D graphics in the browser. At the heart of WebGL’s rendering pipeline are shaders, small programs that run on the GPU to determine how objects appear on the screen. Custom shaders allow for stunning visual effects, from realistic lighting to artistic distortions. In this article, we’ll explore what shaders are, how they work, and how you can write custom shaders in WebGL.

What Are Shaders?

Shaders are written in GLSL (OpenGL Shading Language) and are executed on the GPU in parallel, making them highly efficient for rendering complex graphics. There are two main types of shaders in WebGL:

  1. Vertex Shaders – These process individual vertices of a 3D model, handling transformations, projection, and vertex attributes like position, color, and texture coordinates.
  2. Fragment Shaders – These determine the color of individual pixels (fragments) on the screen, handling lighting, texturing, and other pixel effects.

When combined, these shaders define how objects are rendered in WebGL.

Writing a Custom Shader in WebGL

Let’s break down a basic example of a custom WebGL shader program. Our goal is to render a simple 2D triangle with a gradient color effect using shaders.

Step 1: Setting Up WebGL

First, we need to create a WebGL context and initialize a shader program:

js

Copy

Edit
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
const gl = canvas.getContext('webgl');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);

Step 2: Writing the Vertex Shader

This vertex shader simply passes the vertex position and color to the fragment shader:

glsl

Copy

Edit
const vertexShaderSource = `
    attribute vec2 a_position;
    attribute vec3 a_color;
    varying vec3 v_color;
    
    void main() {
        gl_Position = vec4(a_position, 0.0, 1.0);
        v_color = a_color;
    }
`;
  • a_position represents the vertex position in 2D space.
  • a_color is the color passed to the fragment shader.
  • gl_Position is a built-in GLSL variable that determines where the vertex is drawn.
  • v_color is passed to the fragment shader.

Step 3: Writing the Fragment Shader

The fragment shader uses the color passed from the vertex shader to determine the final color of the pixels:

glsl

Copy

Edit
const fragmentShaderSource = `
    precision mediump float;
    varying vec3 v_color;
    
    void main() {
        gl_FragColor = vec4(v_color, 1.0);
    }
`;
  • precision mediump float; ensures proper color calculations.
  • v_color is received from the vertex shader.
  • gl_FragColor is the final color output of the pixel.

Step 4: Compiling and Linking Shaders

Now, we need to compile and link our shaders into a WebGL program:

js

Copy

Edit
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 compile error:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

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 link error:', gl.getProgramInfoLog(program));
}
gl.useProgram(program);

Step 5: Creating and Binding Buffers

We define a triangle with position and color attributes and pass it to the GPU:

js

Copy

Edit
const vertices = new Float32Array([
    // Position   // Color (RGB)
    -0.5, -0.5,   1.0, 0.0, 0.0,  // Bottom left (red)
     0.5, -0.5,   0.0, 1.0, 0.0,  // Bottom right (green)
     0.0,  0.5,   0.0, 0.0, 1.0   // Top (blue)
]);

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

const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 5 * 4, 0);

const colorLocation = gl.getAttribLocation(program, 'a_color');
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(colorLocation, 3, gl.FLOAT, false, 5 * 4, 2 * 4);

Step 6: Rendering the Triangle

Finally, we draw the triangle:

js

Copy

Edit
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);

Final Result

This will render a colorful triangle with a gradient effect, defined by the colors assigned to each vertex.

Expanding Custom Shaders

Now that you understand the basics, you can expand upon this by:

  • Adding textures to your shaders.
  • Implementing lighting effects using normal vectors.
  • Creating animated shaders using time-based calculations.

If you’re using Three.js, you can also write custom shaders with the ShaderMaterial or RawShaderMaterial to achieve similar effects with an easier setup.

Leave a comment

Your email address will not be published. Required fields are marked *