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);