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-debutantfolder on your desktop.
Initialize the project
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
<!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
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
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
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:
getShaderInfoLogsaves hours (GLSL syntax is strict, no warnings). - Use 3x3 matrices for 2D: More efficient than 4x4; avoid complex translations at first.
- Precision qualifiers:
mediumpdefault 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
enableVertexAttribArrayignores vertices, black screen. - Uniform not found: Typos like
U_matrixinstead ofu_matrixcrash linking.
Next Steps
- Add textures:
sampler2Din fragment,texture2Dfor images. - Upgrade to WebGL2:
#version 300 es,in/outinstead of attribute/varying. - Libraries: three.js to abstract, but return to raw WebGL for performance.
- Resources: GLSL ES 1.00 Spec, WebGL Fundamentals.