• What Is Normal Mapping?

    • Technique to add surface detail without adding geometry
    • A texture stores per-pixel surface normals (in tangent space)
    • The shading normal is perturbed based on the texture
    • Result: the surface appears to have bumps and grooves at no geometry cost

  • Tangent Space

    • A local coordinate system defined per surface point
    • Axes: T (tangent), B (bitangent), N (normal)
    • N — geometric surface normal
    • T — tangent direction, aligned with UV u axis
    • B — bitangent, aligned with UV v axis, B = cross(N, T)
    • Normal map stores normals in this local space
      • Flat surface: (0, 0, 1) in tangent space → encoded as (0.5, 0.5, 1.0) in texture
      • Bump pointing right: (1, 0, 0) → encoded as (1.0, 0.5, 0.5)

  • TBN Matrix

    • Transforms from tangent space to world space
    • TBN = mat3(T, B, N) — columns are the tangent space axes in world space
    • world_normal = normalize(TBN * tangent_space_normal)
    • Computing T and B from mesh data
      • Stored per-vertex in the mesh (precomputed from UVs)
      • Or computed in shader from UV derivatives: dFdx(uv), dFdy(uv)
    • Mikktspace (Mikkelsen 2008)
      • Standard algorithm for computing tangents
      • Used by Blender, Godot, Unreal, Unity
      • Ensures consistent tangents across different tools

  • Normal Map Encoding

    • Tangent-space normal maps: blue-ish (most normals point up = (0,0,1))
    • Decode: N = normalize(texture(normalMap, uv).rgb * 2.0 - 1.0)
    • BC5 compression: stores only RG channels (Z reconstructed as sqrt(1 - R² - G²))
    • OpenGL vs DirectX convention: Y axis is flipped
      • OpenGL: +Y = up in tangent space
      • DirectX: -Y = up in tangent space
      • Fix: N.y = -N.y when loading DirectX normal maps in OpenGL

  • In a Path Tracer

    • Use the perturbed normal for BRDF evaluation and sampling
    • Important: use geometric normal for ray offset (avoid self-intersection)
    • Use shading normal for BRDF evaluation

  • Bump Mapping vs Normal Mapping vs Displacement

    • Bump mapping: height texture → compute normal from height gradient
      • N = normalize(N + dh/du * T + dh/dv * B)
    • Normal mapping: directly store normals in texture (more control)
    • Displacement mapping: actually move vertices based on height texture
      • Requires tessellation or ray marching
      • True geometric detail — correct silhouettes and self-shadowing

  • Geometric vs Shading Normal Consistency

    • Problem: shading normal can point away from the ray direction
      • Happens at silhouette edges where normal interpolation creates inconsistencies
      • dot(ray_dir, shading_normal) > 0 — ray hits “back” of shading normal
    • Fix: flip shading normal if inconsistent with geometric normal
    • Or: use geometric normal for ray offset, shading normal for BRDF only