Vertex shader
The program that runs once per vertex, places it on screen and hands values to the fragment shader.
How it works
A mesh is a list of vertices: positions, usually with a normal and a UV each, grouped into triangles. Before
anything can be drawn, each vertex has to be placed on the screen. The vertex shader does that. It runs once per
vertex, reads that vertex’s attributes, and must write Varying A value the vertex shader writes for each vertex (an out) and the fragment shader reads (an in). The rasterizer blends it across the triangle, so each pixel gets its own mix.gl_Position: where the vertex lands, in a coordinate system
called clip space. Anything else it wants the fragment shader to know, it writes as a
varyingA value the vertex shader writes for each vertex (an out) and the fragment shader reads (an in). The rasterizer blends it across the triangle, so each pixel gets its own mix.
From the model to the screen
A vertex position starts in the model’s own coordinates (object space): the numbers the 3D artist exported, with the model’s origin at (0, 0, 0). Getting it on screen is a chain of matrix products:
- the model matrix places the object in the world (world space);
- the view matrix moves the world so the camera sits at the origin, looking down −z (view space);
- the projection matrix squeezes the camera’s view volume into clip space, and for a perspective camera puts
the distance into
w.
#version 300 es
in vec3 aPosition;
in vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uViewProjection; // view and projection, multiplied once on the host
out vec3 vWorldNormal;
void main() {
vec4 world = uModel * vec4(aPosition, 1.0);
vWorldNormal = mat3(uModel) * aNormal; // fine while the model has no non-uniform scale
gl_Position = uViewProjection * world;
}
The vertex shader stops at clip space. Dividing by Rasterization The fixed GPU step that turns each triangle into fragments, one for every pixel center it covers, and interpolates the vertex shader's outputs for each of them.w, which makes far things smaller, happens next, in the fixed
step of rasterizationThe fixed GPU step that turns each triangle into fragments, one for every pixel center it covers, and interpolates the vertex shader's outputs for each of them.
Moving vertices
Because it owns the position, the vertex shader can also move it. Waves, flags in the wind, swaying grass and breathing creatures are all vertex displacement: compute an offset from the time and the position, add it before projecting.
// a ripple along the surface normal, before the matrices
float height = 0.05 * sin(aPosition.x * 8.0 + uTime * 2.0);
vec3 moved = aPosition + aNormal * height;
Three limits shape every effect built this way:
- It sees one vertex at a time. It can’t look at its neighbors, add vertices or remove them.
- Detail comes from the mesh. A quad has four vertices, so no displacement makes it wave; a flag needs a subdivided plane. The fragment shader can fake small detail, but the silhouette only changes where vertices are.
- Normals don’t follow. Moving vertices leaves the stored normals pointing the old way, so lighting still shows the flat surface. The shader must recompute the normal, usually from the derivative of the same function that moved the vertex.
Cheap, then expensive
A mesh usually has far fewer vertices than the pixels it covers, so work done per vertex and interpolated is cheaper than the same work per pixel. That is why lighting was once computed per vertex. The catch is that interpolation is linear: anything that curves faster than the triangles, like a sharp highlight, comes out wrong.
The vertex shader also draws full-screen passes. With no buffer at all, it can make one triangle that covers the
whole screen from gl_VertexID, the index of the vertex being processed:
#version 300 es
out vec2 vUv;
void main() {
// vertices 0, 1, 2 → (0, 0), (2, 0), (0, 2): a triangle twice the screen's size
vec2 corner = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
vUv = corner;
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
The parts outside the screen are clipped away, and the visible part has UVs from 0 to 1.
In Godot and on this site
In a Godot spatial shader, the vertex stage is the vertex() function. VERTEX and NORMAL arrive in model space
and Godot applies the model, view and projection matrices after the function returns. render_mode world_vertex_coords hands them over in world space instead, render_mode skip_vertex_transform leaves the model
and view transforms to you (Godot still applies the projection), and writing POSITION sets the clip-space position
directly.
On this site, a card that moves vertices names a vertex entry in its render.json: Inputs <name>(Inputs i), which
returns i with a new position and normal in world space. The frame projects the result and interpolates it into
effect().