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); }