镜面 IBL 预过滤贴图,本质是在提前计算不同粗糙度下,镜面反射方向附近能看到的环境光平均值。预过滤贴图计算的核心流程:对 cubemap 的每一个方向 R,对每一个 roughness,做一次 GGX 重要性采样。
计算中用到的函数:
cpp
// 把一个 32 位整数的二进制位完全反转
uint ReverseBits32(uint bits){
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return bits;
}
// 将反转后的32位整数映射到[0, 1]
// 2.3283064365386963e-10 ≈ 2^(-32)
float radicalInverse(uint bits){
return float(ReverseBits32(bits)) * 2.3283064365386963e-10;
}
// 输入遍历序号 i 和 遍历总数 sampleCount
// 输出 i/sampleCount 和 反转后被映射到[0, 1]的 i
float2 hammersley(int i, int N){
return float2(float(i) / float(N), radicalInverse(uint(i)));
}
void pixarONB(float3 n, out float3 b1, out float3 b2)
{
float sign_ = n.z >= 0.0 ? 1.0 : -1.0;
float a = -1.0 / (sign_ + n.z);
float b = n.x * n.y * a;
b1 = float3(1.0 + sign_ * n.x * n.x * a, sign_ * b, -sign_ * n.x);
b2 = float3(b, sign_ + n.y * n.y * a, -n.y);
}
float3 rotateToNormal(float3 L, float3 N)
{
float3 tangent, bitangent;
pixarONB(N, tangent, bitangent);
tangent = normalize(tangent);
bitangent = normalize(bitangent);
return normalize(tangent * L.x + bitangent * L.y + N * L.z);
}
float3 importanceSampleGGX(float2 Xi, float3 N, float roughness)
{
float a = roughness * roughness;
float cosTheta = sqrt((1.0 - Xi.x) / (1.0 + (a * a - 1.0) * Xi.x));
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
float phi = Xi.y * TWO_PI;
float3 L = normalize(float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta));
return rotateToNormal(L, N);
}
float distributionIBL(float NdotH, float roughness)
{
float a2 = roughness * roughness;
float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * denom * denom);
}
float3 getEnvironmentB1(float3 rayDir, float level)
{
float2 tc = float2((atan2(rayDir.z, rayDir.x) / TWO_PI) + 0.5,
acos(rayDir.y) / PI);
return SAMPLE_TEXTURE2D_LOD(_Channel1, sampler_Channel1, tc, level).rgb;
}
镜面 IBL 预过滤贴图计算:
cpp
// 通过法线方向 N 和粗糙度 roughness, 通过重要性采样 GGX 分布得到"预过滤环境光颜色"
// 通常用于 PBR IBL 的镜面反射环境光
float3 getPreFilteredColour(float3 N, float roughness, int sampleCount)
{
// R:反射向量、N:法线向量、V:视角向量(被初始化为法线向量)
// 假设观察方向正好等于反射方向,也等价于假设视角方向和表面法线对齐。
float3 R = N;
float3 V = R;
// 权重累计,最后做归一化,防止颜色过亮或过暗
float totalWeight = 0.0;
// 用于累加采样到的环境颜色
float3 prefilteredColor = float3(0,0,0);
// #define ZERO 0
for (int i = ZERO; i < sampleCount; i++)
{
// 生成一个伪随机、平均分布的二维采样点 Xi.x、Xi.y ∈ [0, 1]
// Hammersley 序列是一种低差异,比纯随机采样更均匀
float2 Xi = hammersley(i, sampleCount);
// 根据 Xi 和 roughness 生成一个在法线 N 的附近的半程向量 H
// 其中 roughness 越大,生成的半程向量离法线 N 越远
float3 H = importanceSampleGGX(Xi, N, roughness);
// H = normalize(L + V), 所以根据 V 和 H 可以计算出 L
float3 L = normalize(reflect(-V, H));
// dot_c = max(dot(a, b), 0)
float NdotL = dot_c(N, L);
// 只保留表面上半球方向的采样,下半的反射看不到
if (NdotL > 0.0)
{
// 初始化环境贴图的 mip level, level 越大,采样越模糊的 mip
float level = 0.0;
// 条件编译,如果定义了就启用这部分代码,计算合适的 mip level
#if ENV_FILTERING == 1
// 计算两个点积
// 法线和半程向量的夹角余弦
float NdotH = dot_c(N, H);
// 观察方向和半程向量的夹角余弦
float VdotH = dot_c(V, H);
// 计算采样概率密度 pdf
float pdf = distributionIBL(NdotH, roughness * roughness) * NdotH / (4.0 * VdotH);
// 当前采样大约覆盖的立体角
float omegaS = 1.0 / (float(sampleCount) * pdf);
// 表示环境 cubemap 中一个像素大约覆盖的立体角
float omegaP = 4.0 * PI / (6.0 * 512.0 * 512.0);
level = max(0.5 * log2(omegaS / omegaP) + 1.0, 0.0);
#endif
// 沿着方向 L,在指定 mip level 上采样环境颜色
// NdotL 这是 Lambert 余弦权重,方向越接近法线,贡献越大;越贴近边缘,贡献越小
prefilteredColor += getEnvironmentB1(L, level) * NdotL;
// 累加权重
totalWeight += NdotL;
}
}
// 返回归一化后的颜色
return prefilteredColor / totalWeight;
}