Normal, Parallax & Environment Mapping
Normal maps store tangent-space normals (RGB = XYZ remapped from [-1, 1] to [0, 1]).
18.1 Normal mapping with a TBN matrix
Normal maps store tangent-space normals (RGB = XYZ remapped from [-1, 1] to [0, 1]).
Two-channel (BC5) normal maps: reconstruct z = sqrt(1 - x² - y²). Without precomputed tangents, derive a TBN from screen-space derivatives (dFdx(vWorldPos), dFdx(vUV)).
// Vertex shader
layout (location = 3) in vec4 aTangent; // xyz = tangent, w = handedness (±1)
out mat3 vTBN;
void main() {
vec3 N = normalize(uNormalMatrix * aNormal);
vec3 T = normalize(uNormalMatrix * aTangent.xyz);
T = normalize(T - dot(T, N) * N); // Gram–Schmidt re-orthogonalization
vec3 B = cross(N, T) * aTangent.w;
vTBN = mat3(T, B, N); // columns: tangent, bitangent, normal
...
}
// Fragment shader
uniform sampler2D uNormalMap; // load with a LINEAR (non-sRGB) format!
void main() {
vec3 tn = texture(uNormalMap, vUV).xyz * 2.0 - 1.0; // [0,1] → [-1,1]
// tn.y = -tn.y; // flip green if the map uses DirectX convention
vec3 N = normalize(vTBN * tn); // tangent → world space
...
}18.2 Parallax & parallax occlusion mapping
Offset texture coordinates according to a height map and the view direction (in tangent space) to fake depth:
vec2 parallaxOcclusion(vec2 uv, vec3 viewTS) { // viewTS: view direction in tangent space
const float minLayers = 8.0, maxLayers = 32.0;
float numLayers = mix(maxLayers, minLayers, abs(viewTS.z));
float layerDepth = 1.0 / numLayers;
vec2 deltaUV = viewTS.xy / viewTS.z * uHeightScale / numLayers;
float currentDepth = 0.0;
float mapDepth = 1.0 - texture(uHeightMap, uv).r;
while (currentDepth < mapDepth) {
uv -= deltaUV;
mapDepth = 1.0 - textureLod(uHeightMap, uv, 0.0).r;
currentDepth += layerDepth;
}
vec2 prevUV = uv + deltaUV; // interpolate between the last two layers
float after = mapDepth - currentDepth;
float before = (1.0 - textureLod(uHeightMap, prevUV, 0.0).r) - currentDepth + layerDepth;
float w = after / (after - before);
return mix(uv, prevUV, w);
}18.3 Environment mapping
uniform samplerCube uEnv;
vec3 I = normalize(vWorldPos - uCameraPos);
vec3 R = reflect(I, N);
vec3 reflection = texture(uEnv, R).rgb; // mirror reflection
vec3 T = refract(I, N, 1.0 / 1.52); // glass (air → glass IOR)
vec3 refraction = texture(uEnv, T).rgb;
float F = 0.04 + 0.96 * pow(1.0 - max(dot(-I, N), 0.0), 5.0);
vec3 glass = mix(refraction, reflection, F);
// Chromatic dispersion: refract R, G, B with slightly different IORs18.4 Skybox shader
// Vertex
out vec3 vDir;
void main() {
vDir = aPos; // cube vertex = direction
mat4 viewNoTranslation = mat4(mat3(uView));
vec4 p = uProjection * viewNoTranslation * vec4(aPos, 1.0);
gl_Position = p.xyww; // depth = 1.0 (far plane); use GL_LEQUAL depth func
}
// Fragment
uniform samplerCube uSkybox;
in vec3 vDir;
out vec4 outColor;
void main() { outColor = texture(uSkybox, vDir); }