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)
| Range | Constant | Linear | Quadratic |
|---|---|---|---|
| 7 | 1.0 | 0.7 | 1.8 |
| 13 | 1.0 | 0.35 | 0.44 |
| 20 | 1.0 | 0.22 | 0.20 |
| 32 | 1.0 | 0.14 | 0.07 |
| 50 | 1.0 | 0.09 | 0.032 |
| 100 | 1.0 | 0.045 | 0.0075 |
| 200 | 1.0 | 0.022 | 0.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
| Technique | Description |
|---|---|
| Depth bias | Subtract small value from shadow depth |
| Normal offset | Offset sample along surface normal |
| Front-face culling | Render shadow map with front faces culled |
glPolygonOffset | glPolygonOffset(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)
| Attachment | Format | Content |
|---|---|---|
| Color 0 | GL_RGBA16F | World position (xyz), can store depth in w |
| Color 1 | GL_RGBA16F | Normal (xyz), roughness in w |
| Color 2 | GL_RGBA8 | Albedo (rgb), metallic in a |
| Color 3 | GL_RGBA8 | Emissive (rgb), AO in a |
| Depth | GL_DEPTH24_STENCIL8 | Depth + 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);