Skip to content
Learni
View all tutorials
WebGL

How to Create Your First GLSL Shaders with WebGL in 2026

Lire en français

Introduction

GLSL (OpenGL Shading Language) is the standard language for programming shaders in WebGL, the JavaScript API for 3D rendering in browsers. In 2026, despite WebGPU's rise, GLSL remains essential for 90% of existing WebGL projects and performance-critical apps like browser games or data visualizations. This beginner tutorial takes you from the basics to your first effect: a rotating multicolored triangle. Think of a rendering engine like an assembly line—the vertex shaders transform positions (like workers shaping parts), and fragment shaders color the pixels (like painters finishing the product). You'll end up with a standalone HTML file, servable locally, totaling 200 lines of code. By the end, you'll know how to compile, link, and animate shaders, ready for advanced effects like textures or basic ray tracing. Pure WebGL—no frameworks—to understand the foundations.

Prerequisites

  • VS Code with the Live Server extension (to serve files locally over HTTP).
  • Basic HTML and JavaScript knowledge (variables, functions, async/await).
  • Modern browser (Chrome 120+ or Firefox 130+ for WebGL 1.0).
  • Create a glsl-debutant folder on your desktop.

Initialize the project

terminal
mkdir glsl-debutant
cd glsl-debutant
touch index.html vertex.glsl fragment.glsl script.js
code .

These commands create the project folder and the 4 essential files. Open VS Code with code . to edit. Live Server requires HTTP for fetching shaders; without it, CORS blocks access. Always test on a local server, not file://.

Basic HTML structure

The HTML provides the canvas: a rendering area like a blank TV screen. It links the JS script for WebGL initialization. Right-click index.html > Open with Live Server to launch at http://127.0.0.1:5500.

index.html file

index.html
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Premier Shader GLSL</title>
  <style>
    body { margin: 0; background: #222; display: flex; justify-content: center; align-items: center; height: 100vh; }
    canvas { border: 2px solid #fff; }
  </style>
</head>
<body>
  <canvas id="glCanvas" width="600" height="400"></canvas>
  <script src="script.js"></script>
</body>
</html>

This HTML skeleton centers a 600x400px canvas on a black background. The CSS ensures full-screen responsive rendering. The script.js will load WebGL: without it, the canvas stays blank. Fixed dimensions avoid complex resizing for beginners.

The Vertex Shader: Transforming Positions

A vertex shader processes each vertex (triangle corner) like a worker bending metal. It applies a rotation matrix via uniform (global data from JS). attribute = per-vertex data (position, color), varying = passed to fragment.

Vertex shader GLSL

vertex.glsl
attribute vec2 a_position;
attribute vec3 a_color;
varying vec3 v_color;

uniform mat3 u_matrix;

void main() {
  // Multiply position by 3x3 matrix for rotation/scale
  gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
  v_color = a_color;
}

This shader takes 2D positions/colors and applies u_matrix for rotation (like spinning a pizza uniformly). gl_Position is required in -1..1 clip space. No precision needed as WebGL1 infers it; avoid unnecessary vec4 for performance.

The Fragment Shader: Coloring Pixels

The fragment shader runs per pixel (fragment), like a brush coloring each point. It receives v_color from the vertex and applies it. Simple for starters: no textures, just interpolation.

Fragment shader GLSL

fragment.glsl
precision mediump float;

varying vec3 v_color;

void main() {
  gl_FragColor = vec4(v_color, 1.0);
}

precision mediump float ensures mobile compatibility (lowp=low, highp=precise but slow). gl_FragColor is legacy WebGL1; for WebGL2, use out vec4 outColor. Auto-interpolation blends colors between vertices.

JavaScript: Compiling and Linking Shaders

JS orchestrates everything: fetch shaders, compile (check errors), link into a program, upload data (buffers), set uniforms, and draw. Analogy: assembling a car (shaders=parts, program=engine).

JS initialization script

script.js
async function initWebGL() {
  const canvas = document.getElementById('glCanvas');
  const gl = canvas.getContext('webgl');
  if (!gl) { alert('WebGL non supporté'); return; }

  // Fetch and compile shaders
  const vsSource = await (await fetch('vertex.glsl')).text();
  const fsSource = await (await fetch('fragment.glsl')).text();

  const vertexShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vertexShader, vsSource);
  gl.compileShader(vertexShader);
  if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
    console.error('Vertex error:', gl.getShaderInfoLog(vertexShader));
    return;
  }

  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fragmentShader, fsSource);
  gl.compileShader(fragmentShader);
  if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
    console.error('Fragment error:', gl.getShaderInfoLog(fragmentShader));
    return;
  }

  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('Link error:', gl.getProgramInfoLog(program));
    return;
  }

  gl.useProgram(program);

  // Position buffers (triangle)
  const positions = new Float32Array([
    0, -0.5,
    -0.5, 0.5,
    0.5, 0.5
  ]);
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

  const positionLoc = gl.getAttribLocation(program, 'a_position');
  gl.enableVertexAttribArray(positionLoc);
  gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);

  // Color buffers
  const colors = new Float32Array([
    1, 0, 0,   // Red
    0, 1, 0,   // Green
    0, 0, 1    // Blue
  ]);
  const colorBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);

  const colorLoc = gl.getAttribLocation(program, 'a_color');
  gl.enableVertexAttribArray(colorLoc);
  gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 0, 0);

  // Uniform matrix
  const matrixLoc = gl.getUniformLocation(program, 'u_matrix');

  // Render loop
  function render(time) {
    time *= 0.001;

    // 2D rotation matrix
    const cos = Math.cos(time);
    const sin = Math.sin(time);
    const matrix = [
      cos, -sin, 0,
      sin,  cos, 0,
      0,    0,   1
    ];
    gl.uniformMatrix3fv(matrixLoc, false, matrix);

    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}

initWebGL();

This async script fetches, compiles, and links shaders, uploads buffers (RGB triangle positions), and sets a rotating uniform matrix. requestAnimationFrame animates at 60fps. Logs errors to console: always check getShaderInfoLog for GLSL syntax debugging. STATIC_DRAW buffers are optimized since data is static.

Test and Render

Open Live Server: you'll see a rotating RGB triangle! Check the console for errors. Tweak colors in JS or pass time as a varying for time-based effects.

Best Practices

  • Always log errors: getShaderInfoLog saves hours (GLSL syntax is strict, no warnings).
  • Use 3x3 matrices for 2D: More efficient than 4x4; avoid complex translations at first.
  • Precision qualifiers: mediump default for mobile; test highp on desktop.
  • Separate buffers: Positions/colors for reusability (e.g., swap colors without rebuffering positions).
  • requestAnimationFrame: Syncs with display, avoids CPU waste.

Common Errors to Avoid

  • CORS fetch: Don't serve via file://; Live Server is required for fetch('vertex.glsl').
  • Missing gl_Position: Shader compiles but nothing renders; always set as vec4(-1..1).
  • Attribs not enabled: Forgetting enableVertexAttribArray ignores vertices, black screen.
  • Uniform not found: Typos like U_matrix instead of u_matrix crash linking.

Next Steps

  • Add textures: sampler2D in fragment, texture2D for images.
  • Upgrade to WebGL2: #version 300 es, in/out instead of attribute/varying.
  • Libraries: three.js to abstract, but return to raw WebGL for performance.
  • Resources: GLSL ES 1.00 Spec, WebGL Fundamentals.
Check out our Learni WebGL/GLSL training.