Shaders are small programs that run on the GPU for every vertex and every pixel. Write your first vertex and fragment shaders in GLSL and learn how data flows in through attributes, uniforms, and varyings.
A shader is a tiny program the GPU runs massively in parallel — the vertex shader once per vertex, the fragment shader once per fragment (pixel). Because thousands of invocations run at once, shaders are how you achieve real-time graphics. You write two: the vertex shader positions geometry, and the fragment shader decides each pixel’s color.
Two programmable stages, run in parallel on the GPU:
VERTEX SHADER runs ONCE PER VERTEX
job: output the vertex's clip-space position
(transform by the MVP matrix), pass data to the fragment stage
FRAGMENT SHADER runs ONCE PER FRAGMENT (pixel)
job: output that pixel's final COLOR
(lighting, texturing, effects happen here)
Massively parallel: a 1080p frame is ~2 million fragments, all shaded at once.
That parallelism is what makes real-time rendering possible.The vertex shader’s core duty is to transform each incoming vertex position into clip space by multiplying it by the model-view-projection matrix — the coordinate-space journey from the Game Math course. It also forwards per-vertex data (like texture coordinates and normals) to the fragment shader as "varyings," which the rasterizer interpolates across the triangle.
#version 330 core
// per-vertex inputs (attributes) from the mesh
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aUV;
// same for every vertex this draw call (uniform)
uniform mat4 uMVP; // Model * View * Projection
// data passed to the fragment shader (interpolated across the triangle)
out vec2 vUV;
void main() {
vUV = aUV; // forward the texture coordinate
gl_Position = uMVP * vec4(aPos, 1.0); // transform to clip space (required)
}The fragment shader computes the final color of each pixel and writes it out. It receives the interpolated varyings from the vertex stage and any uniforms (time, colors, textures), then does the real work of shading — here just a flat color and a gradient. Nearly everything visually interesting in a game lives in fragment shaders.
#version 330 core
in vec2 vUV; // interpolated from the vertex shader
uniform float uTime; // a uniform: same for all fragments this frame
out vec4 fragColor; // the pixel's final color (RGBA)
void main() {
// a simple animated gradient using the interpolated UV + time
float pulse = 0.5 + 0.5 * sin(uTime);
vec3 color = vec3(vUV.x, vUV.y, pulse);
fragColor = vec4(color, 1.0);
}
// the fragment shader is where lighting, texturing, and effects happen (next).Three kinds of input feed a shader. Attributes are per-vertex data from the mesh (position, uv, normal). Uniforms are values constant for a whole draw call, set from the CPU each frame (matrices, time, light positions, textures). Varyings are outputs from the vertex shader that the rasterizer interpolates into the fragment shader. Knowing which is which is essential to wiring up any shader.
The three shader inputs:
ATTRIBUTES per-VERTEX data from the mesh buffer
(position, uv, normal, color) — vertex shader only
UNIFORMS constant for the whole DRAW CALL, set from the CPU each frame
(MVP matrix, time, light position, textures) — any stage
VARYINGS vertex shader OUTPUTS, interpolated by the rasterizer, read
as fragment shader INPUTS ('out' in vertex -> 'in' in fragment)
CPU sets uniforms + mesh buffers -> vertex shader (attributes+uniforms) ->
varyings interpolated -> fragment shader (varyings+uniforms) -> pixel.