Normal Mapping & Environment Mapping

Normal maps store tangent-space normals (usually BC5 two-channel; reconstruct Z).

20.1 Normal mapping with a TBN basis

Normal maps store tangent-space normals (usually BC5 two-channel; reconstruct Z).

Convention alert: DirectX-style normal maps have green = "down" (Y-), OpenGL-style green = "up" (Y+). If lighting looks inverted on bumps, flip nTS.y.

Texture2D<float2> gNormalMap : register(t1);    // BC5: XY in [0,1]

float3 UnpackNormalBC5(float2 xy)
{
    float2 n = xy * 2.0 - 1.0;
    return float3(n, sqrt(saturate(1.0 - dot(n, n))));
}

float3 PerturbNormal(VSOut i)
{
    float3 N = normalize(i.normalW);
    float3 T = normalize(i.tangentW.xyz - N * dot(N, i.tangentW.xyz));  // Gram–Schmidt
    float3 B = cross(N, T) * i.tangentW.w;                              // handedness
    float3x3 TBN = float3x3(T, B, N);                                   // rows = T, B, N
    float3 nTS = UnpackNormalBC5(gNormalMap.Sample(gAniso, i.uv));
    return normalize(mul(nTS, TBN));       // row vector × TBN = T*x + B*y + N*z
}

20.2 Normal mapping without tangents (derivative-based)

float3 PerturbNormalNoTangent(float3 N, float3 P, float2 uv, float3 nTS)
{
    float3 dp1 = ddx(P), dp2 = ddy(P);
    float2 duv1 = ddx(uv), duv2 = ddy(uv);
    float3 dp2perp = cross(dp2, N), dp1perp = cross(N, dp1);
    float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
    float invmax = rsqrt(max(dot(T, T), dot(B, B)));
    float3x3 TBN = float3x3(T * invmax, B * invmax, N);
    return normalize(mul(nTS, TBN));
}

20.3 Parallax occlusion mapping (sketch)

float2 ParallaxOcclusion(Texture2D<float> heightMap, SamplerState s, float2 uv, float3 viewTS, float scale)
{
    const int steps = 32;
    float layer = 1.0 / steps, depth = 0;
    float2 delta = viewTS.xy / viewTS.z * scale / steps;
    float2 dx = ddx(uv), dy = ddy(uv);                 // gradients computed before the loop
    float  h = 1.0 - heightMap.SampleGrad(s, uv, dx, dy);
    [loop]
    for (int k = 0; k < steps && depth < h; ++k)
    {
        uv -= delta; depth += layer;
        h = 1.0 - heightMap.SampleGrad(s, uv, dx, dy);
    }
    float2 prev = uv + delta;
    float after = h - depth;
    float before = (1.0 - heightMap.SampleGrad(s, prev, dx, dy)) - depth + layer;
    return lerp(uv, prev, after / (after - before));
}

20.4 Environment reflections & refraction

TextureCube gEnv : register(t5);

float3 EnvReflect(float3 N, float3 V, float roughness)
{
    float3 R = reflect(-V, N);
    uint w, h, mips; gEnv.GetDimensions(0, w, h, mips);
    return gEnv.SampleLevel(gLinear, R, roughness * (mips - 1)).rgb;  // blurrier mips for rough surfaces
}

float3 EnvRefract(float3 N, float3 V, float eta /* 1/1.33 for water */)
{
    return gEnv.Sample(gLinear, refract(-V, N, eta)).rgb;
}

20.5 Skybox

With reversed-Z, write depth 0 instead (o.pos.z = 0) and use GREATER_EQUAL.

cbuffer SkyCB : register(b0) { float4x4 gViewRotProj; };  // view without translation × proj

struct SkyOut { float4 pos : SV_Position; float3 dir : TEXCOORD0; };

SkyOut SkyVS(float3 pos : POSITION)
{
    SkyOut o;
    o.dir = pos;
    o.pos = mul(gViewRotProj, float4(pos, 1)).xyww;  // z = w → depth 1.0 (use LESS_EQUAL)
    return o;
}

float4 SkyPS(SkyOut i) : SV_Target { return gEnv.Sample(gLinear, i.dir); }