Vertex Shaders & Transformations
Under non-uniform scaling, transforming normals with the model matrix skews them.
13.1 The MVP transform
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aUV;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
uniform mat3 uNormalMatrix; // transpose(inverse(mat3(uModel))), computed on the CPU
out vec3 vWorldPos;
out vec3 vNormal;
out vec2 vUV;
void main() {
vec4 world = uModel * vec4(aPos, 1.0);
vWorldPos = world.xyz;
vNormal = normalize(uNormalMatrix * aNormal); // correct for non-uniform scaling
vUV = aUV;
gl_Position = uProjection * uView * world; // clip space
}13.2 Building matrices (normally done on the CPU with GLM, but useful to know)
mat4 translate(vec3 t) {
return mat4(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
t.x, t.y, t.z, 1); // translation in the 4th COLUMN
}
mat4 scale(vec3 s) { return mat4(vec4(s.x, 0, 0, 0), vec4(0, s.y, 0, 0), vec4(0, 0, s.z, 0), vec4(0, 0, 0, 1)); }
mat4 rotateY(float a) {
float c = cos(a), s = sin(a);
return mat4(c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1);
}
mat4 perspective(float fovy, float aspect, float n, float f) {
float t = 1.0 / tan(fovy * 0.5);
return mat4(t / aspect, 0, 0, 0,
0, t, 0, 0,
0, 0, (f + n) / (n - f), -1,
0, 0, 2.0 * f * n / (n - f), 0); // OpenGL convention: NDC z in [-1, 1]
}
mat4 lookAt(vec3 eye, vec3 center, vec3 up) {
vec3 f = normalize(center - eye);
vec3 s = normalize(cross(f, up));
vec3 u = cross(s, f);
return mat4(s.x, u.x, -f.x, 0,
s.y, u.y, -f.y, 0,
s.z, u.z, -f.z, 0,
-dot(s, eye), -dot(u, eye), dot(f, eye), 1);
}13.3 Why the normal matrix?
Under non-uniform scaling, transforming normals with the model matrix skews them. The correct transform is the inverse-transpose of the upper-left 3×3. Compute it on the CPU (or transpose(inverse(mat3(uModel))) in the shader — expensive per vertex).
13.4 Vertex animation
Other uses: wind on grass/foliage, cloth approximations, morph targets (mix(posA, posB, w)), billboards (camera-facing quads), displacement from height maps (textureLod in the vertex shader).
uniform float uTime;
void main() {
vec3 p = aPos;
p.y += sin(p.x * 4.0 + uTime) * 0.1; // wave
p += aNormal * 0.05 * sin(uTime * 3.0); // pulsing along normals
gl_Position = uProjection * uView * uModel * vec4(p, 1.0);
}13.5 Billboards
// Expand a point (center) into a camera-facing quad: corner in [-1,1]²
vec3 camRight = vec3(uView[0][0], uView[1][0], uView[2][0]);
vec3 camUp = vec3(uView[0][1], uView[1][1], uView[2][1]);
vec3 world = center + (camRight * corner.x + camUp * corner.y) * size;
gl_Position = uProjection * uView * vec4(world, 1.0);