OpenGL Cheatsheet

Lighting

Use this OpenGL reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Lighting Model Overview

All lighting in OpenGL is implemented in shaders — there is no built-in lighting in core profile. The standard model decomposes light into three terms:

Final = Ambient + Diffuse + Specular

All calculations should be done in a consistent space (usually world space or view/eye space).

Phong Lighting Model

// Fragment shader — Phong model in world space
in vec3 vFragPos;
in vec3 vNormal;
in vec2 vUV;

uniform vec3 uLightPos;
uniform vec3 uLightColor;
uniform vec3 uViewPos;
uniform sampler2D uDiffuseMap;

out vec4 FragColor;

void main() {
    vec3 albedo = texture(uDiffuseMap, vUV).rgb;

    // Ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * uLightColor;

    // Diffuse
    vec3 norm    = normalize(vNormal);
    vec3 lightDir = normalize(uLightPos - vFragPos);
    float diff   = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * uLightColor;

    // Specular (Phong)
    float specPower = 64.0;
    vec3 viewDir  = normalize(uViewPos - vFragPos);
    vec3 reflDir  = reflect(-lightDir, norm);
    float spec    = pow(max(dot(viewDir, reflDir), 0.0), specPower);
    vec3 specular = spec * uLightColor;

    FragColor = vec4((ambient + diffuse + specular) * albedo, 1.0);
}

Blinn-Phong (Preferred)

Uses the halfway vector instead of the reflection vector — cheaper and avoids cutoff artifacts.

vec3 halfDir = normalize(lightDir + viewDir);
float spec   = pow(max(dot(norm, halfDir), 0.0), specPower);
// specPower for Blinn-Phong ≈ 4× the Phong exponent for similar appearance

Light Types

Directional Light

No position, no attenuation — simulates distant light (sun).

struct DirLight {
    vec3 direction;  // towards the light (normalized)
    vec3 color;
};

vec3 calcDirLight(DirLight light, vec3 norm, vec3 viewDir) {
    vec3 lightDir = normalize(-light.direction);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 half  = normalize(lightDir + viewDir);
    float spec = pow(max(dot(norm, half), 0.0), 32.0);
    return (diff + spec * 0.5) * light.color;
}

Point Light with Attenuation

struct PointLight {
    vec3  position;
    vec3  color;
    float constant;   // 1.0
    float linear;     // 0.09
    float quadratic;  // 0.032
};

float calcAttenuation(PointLight light, float dist) {
    return 1.0 / (light.constant
                + light.linear    * dist
                + light.quadratic * dist * dist);
}

Common attenuation constants (range approximations)

RangeConstantLinearQuadratic
71.00.71.8
131.00.350.44
201.00.220.20
321.00.140.07
501.00.090.032
1001.00.0450.0075
2001.00.0220.0019

Spotlight

struct SpotLight {
    vec3  position;
    vec3  direction;  // normalized, pointing away from light
    vec3  color;
    float cutoff;      // cos(inner angle) e.g. cos(radians(12.5))
    float outerCutoff; // cos(outer angle) e.g. cos(radians(17.5))
};

vec3 calcSpotLight(SpotLight light, vec3 fragPos, vec3 norm, vec3 viewDir) {
    vec3  lightDir = normalize(light.position - fragPos);
    float theta    = dot(lightDir, normalize(-light.direction));
    float epsilon  = light.cutoff - light.outerCutoff;
    float intensity = clamp((theta - light.outerCutoff) / epsilon, 0.0, 1.0);

    float diff = max(dot(norm, lightDir), 0.0);
    vec3  half = normalize(lightDir + viewDir);
    float spec = pow(max(dot(norm, half), 0.0), 32.0);

    float dist = length(light.position - fragPos);
    float att  = 1.0 / (1.0 + 0.09 * dist + 0.032 * dist * dist);

    return intensity * att * (diff + spec * 0.5) * light.color;
}

Material Maps

struct Material {
    sampler2D diffuse;    // albedo
    sampler2D specular;   // per-texel specular intensity
    sampler2D normal;     // tangent-space normals
    sampler2D emission;   // self-illumination
    float     shininess;
};

uniform Material uMat;

vec3 albedo   = texture(uMat.diffuse,   vUV).rgb;
vec3 specMap  = texture(uMat.specular,  vUV).rgb;
vec3 emission = texture(uMat.emission,  vUV).rgb;

Normal Mapping

Normal maps store tangent-space normals (blue-ish). Transform them to world/view space using the TBN matrix.

// Vertex shader — compute TBN
in vec3 aTangent;
in vec3 aBitangent;
out mat3 vTBN;

void main() {
    vec3 T = normalize(mat3(uModel) * aTangent);
    vec3 B = normalize(mat3(uModel) * aBitangent);
    vec3 N = normalize(mat3(uModel) * aNormal);
    vTBN = mat3(T, B, N);  // columns
}
// Fragment shader — decode and transform normal
vec3 n = texture(uNormalMap, vUV).rgb;
n = n * 2.0 - 1.0;         // [0,1] → [-1,1]
n = normalize(vTBN * n);   // tangent → world space

Computing tangents (CPU, for upload)

glm::vec3 edge1 = v1.pos - v0.pos;
glm::vec3 edge2 = v2.pos - v0.pos;
glm::vec2 duv1  = v1.uv  - v0.uv;
glm::vec2 duv2  = v2.uv  - v0.uv;

float f = 1.0f / (duv1.x * duv2.y - duv2.x * duv1.y);
glm::vec3 tangent = f * (duv2.y * edge1 - duv1.y * edge2);

PBR (Physically Based Rendering)

Based on the Cook-Torrance BRDF. Requires: albedo, metallic, roughness, AO.

// GGX / Trowbridge-Reitz NDF
float D_GGX(vec3 N, vec3 H, float roughness) {
    float a  = roughness * roughness;
    float a2 = a * a;
    float NdotH = max(dot(N, H), 0.0);
    float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
    return a2 / (3.14159265 * d * d);
}

// Schlick-GGX geometry function
float G_SchlickGGX(float NdotV, float roughness) {
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    return NdotV / (NdotV * (1.0 - k) + k);
}

float G_Smith(vec3 N, vec3 V, vec3 L, float roughness) {
    return G_SchlickGGX(max(dot(N,V),0.0), roughness)
         * G_SchlickGGX(max(dot(N,L),0.0), roughness);
}

// Fresnel-Schlick
vec3 F_Schlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}

// Cook-Torrance specular BRDF
vec3 cookTorrance(vec3 N, vec3 V, vec3 L, vec3 albedo, float metallic, float roughness) {
    vec3 H   = normalize(V + L);
    vec3 F0  = mix(vec3(0.04), albedo, metallic); // 0.04 = non-metals
    vec3 F   = F_Schlick(max(dot(H, V), 0.0), F0);
    float D  = D_GGX(N, H, roughness);
    float G  = G_Smith(N, V, L, roughness);

    vec3  num   = D * G * F;
    float denom = 4.0 * max(dot(N,V),0.0) * max(dot(N,L),0.0) + 0.0001;
    vec3  spec  = num / denom;

    vec3 kD = (1.0 - F) * (1.0 - metallic); // metals have no diffuse
    return (kD * albedo / 3.14159265 + spec) * max(dot(N,L), 0.0);
}

Image Based Lighting (IBL)

// Diffuse IBL — irradiance cube map
uniform samplerCube uIrradianceMap;
vec3 irradiance = texture(uIrradianceMap, N).rgb;
vec3 diffuse    = irradiance * albedo;

// Specular IBL — prefiltered env map + BRDF LUT
uniform samplerCube uPrefilterMap;
uniform sampler2D   uBRDF_LUT;

vec3  prefilteredColor = textureLod(uPrefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
vec2  brdf             = texture(uBRDF_LUT, vec2(max(dot(N,V),0.0), roughness)).rg;
vec3  specularIBL      = prefilteredColor * (F * brdf.x + brdf.y);

vec3 ambient = (diffuse + specularIBL) * ao;

Shadow Mapping

// 1. Render scene from light's POV into depth FBO
glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
glViewport(0, 0, SHADOW_W, SHADOW_H);
glClear(GL_DEPTH_BUFFER_BIT);
// Draw with depth-only shader (no fragment output)

// 2. Render scene normally
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, screenW, screenH);
glBindTextureUnit(1, shadowMap);
// Fragment shader — PCF soft shadows
uniform sampler2DShadow uShadowMap;
uniform mat4 uLightSpaceMatrix;

float calcShadow(vec3 fragPos, vec3 norm, vec3 lightDir) {
    vec4 lsPos   = uLightSpaceMatrix * vec4(fragPos, 1.0);
    vec3 projPos = lsPos.xyz / lsPos.w;
    projPos      = projPos * 0.5 + 0.5;  // [-1,1] → [0,1]

    // Bias to prevent shadow acne
    float bias = max(0.005 * (1.0 - dot(norm, lightDir)), 0.0005);
    projPos.z -= bias;

    // PCF — 3×3 kernel
    float shadow = 0.0;
    vec2 texelSize = 1.0 / textureSize(uShadowMap, 0);
    for (int x = -1; x <= 1; x++) {
        for (int y = -1; y <= 1; y++) {
            shadow += texture(uShadowMap, projPos + vec3(vec2(x,y)*texelSize, 0.0));
        }
    }
    return shadow / 9.0;
}

Shadow acne mitigation

TechniqueDescription
Depth biasSubtract small value from shadow depth
Normal offsetOffset sample along surface normal
Front-face cullingRender shadow map with front faces culled
glPolygonOffsetglPolygonOffset(1.0, 4.0) during shadow pass

Deferred Shading

// G-buffer outputs (MRT)
layout(location = 0) out vec3 gPosition;
layout(location = 1) out vec3 gNormal;
layout(location = 2) out vec4 gAlbedoSpec;  // rgb=albedo, a=spec intensity
// Lighting pass — iterate over all lights as full-screen quads or volumes
// Bind G-buffer textures, read and compute lighting per pixel

G-buffer formats (typical)

AttachmentFormatContent
Color 0GL_RGBA16FWorld position (xyz), can store depth in w
Color 1GL_RGBA16FNormal (xyz), roughness in w
Color 2GL_RGBA8Albedo (rgb), metallic in a
Color 3GL_RGBA8Emissive (rgb), AO in a
DepthGL_DEPTH24_STENCIL8Depth + stencil

Fog

// Exponential squared fog
float fogDensity = 0.05;
float dist       = length(vFragPos - uViewPos);
float fogFactor  = exp(-pow(fogDensity * dist, 2.0));
fogFactor        = clamp(fogFactor, 0.0, 1.0);
vec3 fogColor    = vec3(0.5, 0.6, 0.7);
FragColor = vec4(mix(fogColor, color, fogFactor), 1.0);