Understanding Shaders in WebGL1: Vertex and Fragment Shaders, GLSL Syntax, and Cube Rendering
This article provides a comprehensive guide to WebGL1 shaders, explaining the roles of vertex and fragment shaders, GLSL syntax, storage qualifiers, precision, control structures, and demonstrates rendering a 3D cube with indexed drawing and reusable utility functions.
This article, originally published on the Rare Earth Juejin community, introduces WebGL1 shaders and explains that WebGL is a subset of OpenGL, using GLSL version 1.00 for shader programming.
GLSL (OpenGL Shading Language) is a C‑like high‑level language that gives developers fine‑grained control over the rendering pipeline without needing assembly or hardware‑specific languages.
WebGL programs run in two stages: a CPU stage where JavaScript code executes, and a GPU stage where shaders written in GLSL control rendering.
The GPU processing flow is illustrated (blue parts are controllable), and WebGL can only render points, lines, and triangles, which compose complex 3D models.
Vertex Shader
A vertex shader processes each vertex, computing its position. For a triangle with three vertices, the vertex shader runs three times.
Example of the simplest vertex shader:
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}WebGL automatically calls the main function. The attribute qualifier fetches per‑vertex data, and the built‑in gl_Position variable receives the transformed vertex position.
Pseudo‑code showing vertex shader execution:
const points = [0, 0.5, 0.5, 0, -0.5, -0.5];
const vertex = points.map(p => vertexShader(p));In JavaScript we obtain the attribute location, create a buffer, upload vertex data, and describe how to read it:
const positionLocation = gl.getAttribLocation(program, 'a_position');
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 0.5, 0, -0.5, -0.5]), gl.STATIC_DRAW);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);Fragment Shader
A fragment (pixel) shader runs for each interpolated fragment after the vertex shader, outputting color via the built‑in gl_FragColor variable.
Example fragment shader code:
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}The uniform qualifier provides global constant data to both vertex and fragment shaders; it does not require a buffer.
JavaScript sets the uniform value:
const colorLocation = gl.getUniformLocation(program, 'u_color');
gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1);GLSL Syntax
GLSL is a strongly typed language; every statement ends with a semicolon. Its syntax resembles TypeScript, and comments use the same // and /* */ forms as JavaScript.
Variable names cannot start with gl_ , webgl_ , or _webgl_ . Operators such as ++, --, +=, &&, ||, and the ternary operator are supported.
Basic data types are float, int, and bool. Floats must contain a decimal point (e.g., 1.0 ), and type conversion is done with constructors like float(1) .
float f = float(1);Vectors and matrices are built‑in types:
vec2, vec3, vec4
ivec2, ivec3, ivec4
bvec2, bvec3, bvec4
mat2, mat3, mat4Vectors are created with constructors, e.g., vec3 pos = vec3(1., 2., 3.); . Swizzling allows component access and replication ( a.x , a.rgb , a.xx , etc.).
mat4 m4 = mat4(1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16.);Control flow mirrors JavaScript:
if (true) {} else if (true) {} else {}
for (int i = 0; i < 3; i++) { continue; }Functions are declared with a return type; void indicates no return value. GLSL does not allow recursion. Parameter qualifiers include in , const in , out , and inout .
float add(float a, float b) { return a + b; }
void add(float a, float b, out float c) { c = a + b; }Precision qualifiers ( highp , mediump , lowp ) control numeric accuracy and performance. In vertex shaders, int and float default to highp ; in fragment shaders, float must have an explicit precision declaration.
precision mediump float;
mediump float size;
highp int len;
lowp vec4 v;Cube Rendering Example
The article then demonstrates rendering a 3D cube using indexed drawing to reuse the eight cube vertices.
const gl = createGl();
const program = createProgramFromSource(gl, `
attribute vec4 aPos;
attribute vec4 aColor;
varying vec4 vColor;
void main() { gl_Position = aPos; vColor = aColor; }
`, `
precision mediump float;
varying vec4 vColor;
void main() { gl_FragColor = vColor; }
`);
const points = new Float32Array([
-0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5, -0.5,
0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5
]); // 8 vertices
const colors = new Float32Array([
1,0,0, 0,1,0, 0,0,1, 0,0,0,
... // per‑vertex colors
]);
const indices = new Uint8Array([
0,1,2, 0,2,3, // front
1,4,2, 4,7,2, // right
4,5,6, 4,6,7, // back
5,3,6, 5,0,3, // left
0,5,4, 0,4,1, // top
7,6,3, 7,3,2 // bottom
]);
const [posLoc, posBuffer] = createAttrBuffer(gl, program, 'aPos', points);
const [colorLoc, colorBuffer] = createAttrBuffer(gl, program, 'aColor', colors);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(posLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(colorLoc);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0,0,0,0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);The example also defines helper functions createShader , createProgramFromSource , createAttrBuffer , and createGl to simplify WebGL setup.
glDrawArray is faster but duplicates vertex data; glDrawElements is slightly slower but saves memory by reusing vertices via indices.
Depth testing is enabled with gl.enable(gl.DEPTH_TEST) and the buffers are cleared before drawing to correctly render 3D geometry.
Repeated bindBuffer calls are intentional because WebGL is a state machine; explicit binding avoids bugs when many buffers are in use.
Varying Qualifier
The varying qualifier passes data from the vertex shader to the fragment shader, where it is interpolated across fragments. In the example, aColor is assigned to vColor in the vertex shader and used as gl_FragColor in the fragment shader.
The article concludes that shaders give fine control over vertex positions and pixel colors, require data to be sent from JavaScript via attribute (using Float32Array ) or uniform (global constants), and that varying variables interpolate between the two stages.
Future articles will cover matrix transformations, animation, and more advanced techniques.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.