Bingo, Computer Graphics & Game Developer
本文建立在CodingLabs以及LearnOpenGL中介绍的基本PBR概念上。
PBR自身概念不陌生,使用满足基于物理的BRDF,满足能量守恒即可。IBL的效果能大大提高观感。 这里选用Cook-Torrance BRDF
漫反射部分可选用Lambertian或Oren Nyar BRDF
Cook-Torrance的高光部分如下,公式具体含义可见
Normal Distribution Function, Geometry, Fresnel三部分各自选用Trowbridge-Reitz GGX,Schlick-GGX, Fresnel-Schlick approximation(离线也常用Schlick的近似菲涅尔来代替繁重计算)
Cook-Torrance的DFG部分有很多实现,GGX拖尾效果较好,其他部分可参考
其中法线分布中的表示表面粗糙度。 几何项中的k根据direct lighting与IBL选用不同粗糙度映射方式。 最后使用光照与视线方向的G相乘
菲涅尔反射计算中的可以根据下表查找所得
Material | F0(Linear) | F0(sRGB) |
---|---|---|
Water | (0.02, 0.02, 0.02) | (0.15, 0.15, 0.15) |
Plastic / Glass (Low) | (0.03, 0.03, 0.03) | (0.21, 0.21, 0.21) |
Plastic High | (0.05, 0.05, 0.05) | (0.24, 0.24, 0.24) |
Glass (high) / Ruby | (0.08, 0.08, 0.08) | (0.31, 0.31, 0.31) |
Diamond | (0.17, 0.17, 0.17) | (0.45, 0.45, 0.45) |
Iron | (0.56, 0.57, 0.58) | (0.77, 0.78, 0.78) |
Copper | (0.95, 0.64, 0.54) | (0.98, 0.82, 0.76) |
Gold | (1.00, 0.71, 0.29) | (1.00, 0.86, 0.57) |
Aluminium | (0.91, 0.92, 0.92) | (0.96, 0.96, 0.97) |
Silver | (0.95, 0.93, 0.88) | (0.98, 0.97, 0.95) |
最后渲染方程(由菲涅尔给出)
贴图选用HDR图像文件,免费HDR文件可从sIBL archive下载
将上述渲染方程左右拆开各做预计算,其中左边为漫反射部分
转立体角为球坐标系下
这里使用Monte Carlo Estimator将二重积分转为离散采样(),这里采用Uniform Sampling(可更换为MIS方法更快收敛)
此处LearnOpenGL对于黎曼和的推导有误,可参考CodingLabs中最后的推导过程. LearnOpengl使用GPU进行预计算,这里我采用CPU端保存为HDR文件以方便Debug,实现部分基本知识公式的翻译(可查看Sophia中的precomputedMap.h查看)
for(int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
float phiT = 2PI * i / width;
float thetaT = PI * j / height;
// avoid situations when up = normal
vec3 up(sin(thetaT) * cos(phiT), cos(thetaT), sin(thetaT) * sin(phiT));
vec3 right = up.crosse(normal);
vec3 normal = right.crosse(up);
vec3 irradiance = 0;
// diffuse irradiance compute hemisphere
for (float phi = 0; phi < 2PI; phi += 2PI / samples) {
for (float theta = 0; theta < PI_DIV2; theta += PI_DIV2 / samples) {
local = vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
world = local.x * right + local.y * up + local.z * N;
x = SphericalPhi(world) / 2PI * width,
y = SphericalTheta(world) / PI * height;
irradiance += GetColor(x, y) * cos(theta) * sin(theta);
}
}
SetColor(i, j, PI * irradiance / (samples * samples));
}
}
右边高光部分也可以拆分为两部分,其中BRDF与IBL在某一分布下做预计算
由于微表面的法线分布选用GGX,因此Importance Sampling的策略与PBRT中建议的一致,都是选择在法线分布上进行重要性采样。 积分拆为数值积分
具体公式详解可以参考UE4的PBR报告
很多地方都没有提到这一点,GGX的法线分布pdf在PBRT上有过推导
因此代入IS下的pdf后,整体就变得更为简洁
首先是左半边的Pre-filtering HDR environment map部分。 这里也就是UE4中所说的近似带来误差最大的部分(由于预计算,无法得知R具体为多少),也就是默认V=R=N,即垂直望去反射光线,法线,视线共线。 根据UE4中的展示,其误差效果仍然能够接受
代码实现部分基本上都是公式的直接翻译
for (int i = 0; i < width; i++){
for (int j = 0; j < height; j++){
float phi = 2PI * i / width;
float theta = PI * j / height;
vec3 N(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
vec3 V = N = R;
float sumWeight = 0;
vec3 prefilteredColor = 0;
for (int s = 0; s < sampleCount; s++) {
random = hammersley(s, sampleCount);
H = importanceSampleGGX(random, N, roughness);
L = (2.0 * V.dot(H) * H - V).getNormalized();
cosL = max(N.dot(L), 0.0f);
if (cosL > 0.0) {
x = SphericalPhi(L) / 2PI * width;
y = s3SphericalTheta(L) / PI * height;
// The cos here is because UE4 said
// "As shown in the code below, we have found weighting by coslk achieves better results"
prefilteredColor += GetColor(x, y) * cosL;
sumWeight += cosL;
}
}
SetColor(i, j, prefilteredColor / sumWeight);
}
}
Write(fileName);
这里使用CPU计算会有一个高亮点的问题,如若采样数不够(Sophia采用OpenMP进行多核计算,即便如此,20000的采样数也需要花费几小时才能完成),如图是一个高光与周围亮度值差距较大的一张HDR纹理,下图即便是采用了30000的采样数也没能解决,可见一味的提升采样数并不能完全的解决问题。
这里LearnOpenGL在计算prefilteredColor时,不直接从原图上采样,而是选用mipmap进行处理,由于我使用了CPU端计算,且自行实现mipmap间插值较为繁琐,因此选用了采样数较大的没有出现bright bots效果的作为替代。 具体实现可参考LearnOpenGL
最后将不同Roughness的结果存到不同mimap level上即可,下图为几张不同粗糙度时的结果。
剩下的右半边BRDF部分,做一定预处理
vec3 integrateBRDF(float NoV, float roughness, int samples) {
// V on XoY plane
vec3 V(sqrt(1.0f - NoV * NoV), NoV, 0.0f);
vec3 N = 0;
float A = 0.0f, B = 0.0f;
for (int i = 0; i < samples; ++i) {
vec2 random = hammersley(i, samples);
vec3 H = importanceSampleGGX(random, N, roughness);
vec3 L = (2.0 * V.dot(H) * H - V).getNormalized();
float NoL = max(L.y, 0.0f);
float NoH = max(H.y, 0.0f);
float VoH = max(V.dot(H), 0.0f);
if (NoL > 0.0f) {
float G = geometrySmith(N, V, L, roughness);
float GVisibility = (G * VoH) / (NoV * NoH);
float Fc = pow(1.0f - VoH, 5.0f);
A += (1.0f - Fc) * GVisibility;
B += Fc * GVisibility;
}
}
return vec3(A, B, 0.0f) / samples;
}
BRDF预计算结果如下图,横纵坐标分别为与Roughness
最后结果也较为可观,尽管没有indirectLighting部分,但总体效果已经很不错了。