PBR(Physically Based Rendering)
什么是基于物理的渲染?简单地说,还记得我们之前学习的法线贴图的内容吗?我们希望不修改物体实际几何形状的前提下去修改表面的法线方向来实现不同的光照效果,实现这个内容的基础就是我们的光照效果是基于某个光照模型------比如布林冯模型来做的,但是这种渲染模式某种意义上不会让你觉得很繁琐吗?因为他太不真实了------物体的光照模型是模拟出来的,然后光照效果也是模拟出来的,我们能否换一种更真实的做法来渲染呢?基于物理的渲染就是这样的思路:

基于微平面(Microfacet)的表面模型
所有的PBR技术都基于微平面理论。这项理论认为,达到微观尺度之后任何平面都可以用被称为微平面(Microfacets)的细小镜面来进行描绘。根据平面粗糙程度的不同,这些细小镜面的取向排列可以相当不一致。

产生的效果就是:一个平面越是粗糙,这个平面上的微平面的排列就越混乱。这些微小镜面这样无序取向排列的影响就是,当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散(Scatter)开来,进而产生出分布范围更广泛的镜面反射。而与之相反的是,对于一个光滑的平面,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射。

在微观尺度下,没有任何平面是完全光滑的。然而由于这些微平面已经微小到无法逐像素地继续对其进行区分,因此我们假设一个粗糙度(Roughness)参数,然后用统计学的方法来估计微平面的粗糙程度。我们可以基于一个平面的粗糙度来计算出众多微平面中,朝向方向沿着某个向量ℎ方向的比例。这个向量ℎ便是位于光线向量𝑙和视线向量𝑣之间的半程向量(Halfway Vector)。


总结来说就是,取代之前用一个反射系数来概括一个物体表面的反射能力,我们PBR中的物体都是基于微平面假设的------微平面其实就是指一个平面表面由多个不规则排列的微小表面组成。我们会统计所有组成这个平面的微小平面的朝向方向与根据光线和视线得到的半程向量的一致程度的数目比例来得到这个平面整体的粗糙度。
能量守恒
能量守恒作为我们物理学中不可逾越的定律,我们渲染过程中的光线传播当然也不能忽视这个过程。一般来说当光线照射到表面上后,首先入射光线会变成反射光线和折射光线,其中反射光线就是没有被物体吸收的光线能量而折射光线就是被吸收的部分,显然根据能量守恒定律,这二者的能量就不能大于入射光线的能量。在基于物理的渲染之中我们进行了简化,假设对平面上的每一点所有的折射光都会被完全吸收而不会散开。而有一些被称为次表面散射(Subsurface Scattering)技术的着色器技术将这个问题考虑了进去,它们显著地提升了一些诸如皮肤,大理石或者蜡质这样材质的视觉效果,不过伴随而来的代价是性能的下降。
对于金属(Metallic)表面,当讨论到反射与折射的时候还有一个细节需要注意。金属表面对光的反应与非金属(也被称为介电质(Dielectrics))表面相比是不同的。它们遵从的反射与折射原理是相同的,但是所有的折射光都会被直接吸收而不会散开,只留下反射光或者说镜面反射光。亦即是说,金属表面只会显示镜面反射颜色,而不会显示出漫反射颜色。由于金属与电介质之间存在这样明显的区别,因此它们两者在PBR渲染管线中被区别处理,而我们将在文章的后面进一步详细探讨这个问题。
反射光与折射光之间的这个区别使我们得到了另一条关于能量守恒的经验结论:反射光与折射光它们二者之间是互斥的关系。无论何种光线,其被材质表面所反射的能量将无法再被材质吸收。因此,诸如折射光这样的余下的进入表面之中的能量正好就是我们计算完反射之后余下的能量。
于是我们得到二者的计算方式:

反射率方程
没想到在GAMES101里学习的内容有一天终究还是派上了用场:是的,传说中的渲染方程还是登场了!(不过其实我不知道这是PBR的内容,GAMES101里只说这个是最接近现实情况的渲染方法)

让我们复习一下这个方程中的内容:
首先方程本身的物理意义就是光入射到点之后如何反射 ,L0代表的就是p点0方向(立体角)的反射光。 其实更准确的描述是辐射亮度------即特定大小范围内的单位时间光照能量。
然后是fr包括括号内这一串的内容,这是一个------BRDF函数,或者叫双边反射分布函数,他描述了表面如何反射光,它接受入射(光)方向𝜔𝑖,出射(观察)方向𝜔𝑜,平面法线𝑛以及一个用来表示微平面粗糙程度的参数𝑎作为函数的输入参数。BRDF可以近似的求出每束光线对一个给定了材质属性的平面上最终反射出来的光线所作出的贡献程度。
Li就是指输入的光线(下标为i就是input,为o就是output吧,我猜的),我们利用光线和平面间的入射角的余弦值cos𝜃来计算能量,亦即从辐射率公式𝐿转化至反射率公式时的𝑛⋅𝜔𝑖,最后是这个积分,这是一个立体角:

其实本身的方程内容也没有多复杂,说白了就是对立体角积分然后把入射的辐射通量乘以一个角度和BRDF函数而已,不过这个BRDF函数------作为描述不同材质如何反射光线的方程其实比较重要。
目前最主流的BRDF函数是:



这里就不展开说了,展开说就是长篇大论了,我们先了解到基本的概念。
在实际的开发中,有这么几个属性是不可或缺的:

光说不练假把式,我们来具体实现PBR的光照。
要完成PBR光照模型,就得做出来渲染方程;要做出来渲染方程,就得做出来BRDF函数;要做出来BRDF函数,就得先实现菲捏耳方程,法线分布函数和几何函数。其中:
法线分布函数(Normal Distribution Function, NDF)
cpp
float DistributionGGX(vec3 N, vec3 H, float roughness)
{
float a = roughness*roughness;
float a2 = a*a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH*NdotH;
float nom = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / denom;
}
NDF用来描述表面微观几何的分布,控制镜面反射的分布范围以及修改粗糙度参数影响高光的扩散程度,一句话,我们用NDF来描述物体表面粗糙度。
几何函数(Geometry Function)
cpp
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
几何函数用于模拟微表面间的遮蔽(Shadowing) 和 阴影(Masking) 效应。
菲涅尔方程(Fresnel Equation)
cpp
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
菲涅尔方程用来计算光线在交界处的反射比例(与入射角相关),决定材质反射特性。
然后是我们的主渲染循环:
cpp
void main()
{
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
// 计算基础反射率
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
// 反射方程
vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i)
{
// 计算每个光源的辐射度
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L);
float distance = length(lightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
// Cook-Torrance BRDF
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001;
vec3 specular = numerator / denominator;
// 能量守恒
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
float NdotL = max(dot(N, L), 0.0);
// 添加到出射辐射度
Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}
// 环境光照
vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color = ambient + Lo;
// HDR色调映射
color = color / (color + vec3(1.0));
// gamma校正
color = pow(color, vec3(1.0/2.2));
FragColor = vec4(color, 1.0);
}
效果如图:

可以看到从左到右分别是不同材质的PBR光照效果,从下往上球体的金属性从0.0变到1.0, 从左到右球体的粗糙度从0.0变到1.0。
我们也可以去添加纹理来实现PBR光照效果,但是有个小小的问题是,纹理在被创造时往往就被美术工作者们给定好了诸如反射率等属性以符合真实的视觉效果,而这些属性一般是在sRGB空间里给定的,与渲染方程中需求的线性空间不同:

所以可能会涉及一个空间的转换。
效果如下:

IBL(Image based lighting)------漫反射辐照
基于图像的光照(Image based lighting, IBL)是一类光照技术的集合。其光源不是如前一节教程中描述的可分解的直接光源,而是将周围环境整体视为一个大光源。IBL 通常使用(取自现实世界或从3D场景生成的)环境立方体贴图 (Cubemap) ,我们可以将立方体贴图的每个像素视为光源,在渲染方程中直接使用它。这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。
由于基于图像的光照算法会捕捉部分甚至全部的环境光照,通常认为它是一种更精确的环境光照输入格式,甚至也可以说是一种全局光照的粗略近似。基于此特性,IBL 对 PBR 很有意义,因为当我们将环境光纳入计算之后,物体在物理方面看起来会更加准确。
说白了,我们会把所有的周围环境都视作光源(在计算机中其实周围环境就是由环境立方体贴图,也就是大家熟知的天空盒实现),通过渲染方程计算发射光,这就是我们的IBL的核心。接下来我来展示如何实现IBL光照效果:
辐照度图
首先是一个新的概念:辐照度图,辐照度图(Irradiance Map)是计算机图形学中用于高效模拟环境光照(尤其是间接漫反射)的预计算纹理,其核心原理是将环境光能积分转化为可快速采样的数据。

可以这样理解辐射度图的作用:因为IBL的环境光范围比较大,一个个立体角去调用渲染方程开销非常大,于是我们就用一个容器------也就是辐射度图去预计算各个方向的环境贴图(天空盒是六个环境贴图组成的立方体贴图)的辐射强度,也就是我们最后渲染方程的结果的一部分,然后把这些数字存储在贴图里,我们渲染时只需要去采样贴图就能获得需要的数值。
cpp
// 1. 创建辐照度图帧缓冲
unsigned int irradianceMap;
glGenTextures(1, &irradianceMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap);
for (unsigned int i = 0; i < 6; ++i)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 2. 创建帧缓冲对象
unsigned int captureFBO;
unsigned int captureRBO;
glGenFramebuffers(1, &captureFBO);
glGenRenderbuffers(1, &captureRBO);
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
辐照度卷积着色器
辐照度计算的过程具体来说是通过卷积计算得到结果的------这就像深度学习的卷积一样,我们针对图计算辐照度时采取卷积计算以得到更贴切的结果。
cpp
#version 330 core
out vec4 FragColor;
in vec3 WorldPos;
uniform samplerCube environmentMap;
const float PI = 3.14159265359;
void main()
{
vec3 N = normalize(WorldPos);
vec3 irradiance = vec3(0.0);
// 计算切线空间
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(up, N));
up = normalize(cross(N, right));
float sampleDelta = 0.025;
float nrSamples = 0.0;
// 球面采样
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
{
// 球面坐标转笛卡尔坐标
vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N;
irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
nrSamples++;
}
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));
FragColor = vec4(irradiance, 1.0);
}
HDR环境贴图
一般来说,我们的环境贴图会采用HDR的环境贴图:这是因为环境贴图中往往包含较亮或者较暗(超过显示器亮度阈值)的部分,我们需要用HDR环境贴图以实现更好的视觉效果。除此之外,HDR环境贴图中的亮度也是线性空间的而不是像纹理一样是sRGB空间的,我们可以直接丢入算式中使用。
以下是完整的实现流程:
cpp
// 1. 加载HDR环境贴图
unsigned int hdrTexture = loadHDRTexture("environment.hdr");
// 2. 创建环境立方体贴图
unsigned int envCubemap;
glGenTextures(1, &envCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
for (unsigned int i = 0; i < 6; ++i)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 3. 将HDR环境贴图转换为立方体贴图
glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
glm::mat4 captureViews[] = {
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f))
};
// 4. 生成辐照度图
irradianceShader.use();
irradianceShader.setInt("environmentMap", 0);
irradianceShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
glViewport(0, 0, 32, 32);
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
for (unsigned int i = 0; i < 6; ++i)
{
irradianceShader.setMat4("view", captureViews[i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderCube();
}
// 5. 生成预过滤环境贴图
unsigned int prefilterMap;
glGenTextures(1, &prefilterMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap);
for (unsigned int i = 0; i < 6; ++i)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 0, GL_RGB, GL_FLOAT, nullptr);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
// 6. 生成BRDF查找表
unsigned int brdfLUTTexture;
glGenTextures(1, &brdfLUTTexture);
glBindTexture(GL_TEXTURE_2D, brdfLUTTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
流程如上,最后生成的BRDF表用于高效计算Cook-Torrance BRDF模型中镜面反射项的菲涅尔与几何项组合值的核心组件。

效果如下:

IBL(Image based lighting)------镜面反射
我们完成漫反射部分之后就来到了镜面反射部分。

既然漫反射都涉及到这么多的不同,显然当光源不是一个个独立的部分而是一张张完整的贴图时我们的求解方法也完全不同,而解决这个差异的关键就是蒙特卡洛积分。

法线分布函数与几何函数本质上是没有太大改变的,因为这两个函数是用来描述物体表面的粗糙情况和遮蔽情况,那我们修改光源又不修改物体本身,所以不会有改变:最大的变化来自于菲涅尔方程的变化:

也就是说,菲涅尔方程从基础的PBR模型里的单一光源方向输入切换成了整个半球的积分,且我们在半球积分时还需要应付不同入射方向的不同视线方向,所以这演变成了一个高维积分问题,而目前最好的高维积分求解的方法就是蒙特卡洛积分。
为什么其他积分不行?

归根到底,是时间复杂度问题。
镜面反射的流程本质上和漫反射大差不差:我们的漫反射提前通过卷积计算光照结果后存储在纹理中供采样读取,而镜面反射则是提前进行蒙特卡洛积分来计算结果后也是存储在纹理中进行读取。
cpp
// prefilter.fs - 预过滤环境贴图的生成
#version 330 core
out vec4 FragColor;
in vec3 WorldPos;
uniform samplerCube environmentMap;
uniform float roughness;
const float PI = 3.14159265359;
// 1. 低差异序列生成
float RadicalInverse_VdC(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 float(bits) * 2.3283064365386963e-10;
}
vec2 Hammersley(uint i, uint N)
{
return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}
// 2. 重要性采样
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
float a = roughness*roughness;
// 将均匀分布的随机数转换为GGX分布
float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
// 球面坐标转笛卡尔坐标
vec3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// 从切线空间转换到世界空间
vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
vec3 tangent = normalize(cross(up, N));
vec3 bitangent = cross(N, tangent);
return normalize(tangent * H.x + bitangent * H.y + N * H.z);
}
void main()
{
vec3 N = normalize(WorldPos);
vec3 R = N;
vec3 V = R;
// 3. 蒙特卡洛积分
const uint SAMPLE_COUNT = 1024u;
vec3 prefilteredColor = vec3(0.0);
float totalWeight = 0.0;
for(uint i = 0u; i < SAMPLE_COUNT; ++i)
{
// 生成采样点
vec2 Xi = Hammersley(i, SAMPLE_COUNT);
vec3 H = ImportanceSampleGGX(Xi, N, roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float NdotL = max(dot(N, L), 0.0);
if(NdotL > 0.0)
{
// 计算权重并累加
prefilteredColor += texture(environmentMap, L).rgb * NdotL;
totalWeight += NdotL;
}
}
prefilteredColor = prefilteredColor / totalWeight;
FragColor = vec4(prefilteredColor, 1.0);
}
这里是蒙特卡洛积分的实现。
cpp
// brdf.fs - BRDF查找表的生成
#version 330 core
out vec2 FragColor;
in vec2 TexCoords;
const float PI = 3.14159265359;
// 1. 几何函数
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
// 2. BRDF积分
vec2 IntegrateBRDF(float NdotV, float roughness)
{
vec3 V;
V.x = sqrt(1.0 - NdotV*NdotV);
V.y = 0.0;
V.z = NdotV;
float A = 0.0;
float B = 0.0;
vec3 N = vec3(0.0, 0.0, 1.0);
const uint SAMPLE_COUNT = 1024u;
for(uint i = 0u; i < SAMPLE_COUNT; ++i)
{
vec2 Xi = Hammersley(i, SAMPLE_COUNT);
vec3 H = ImportanceSampleGGX(Xi, N, roughness);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float NdotL = max(L.z, 0.0);
float NdotH = max(H.z, 0.0);
float VdotH = max(dot(V, H), 0.0);
if(NdotL > 0.0)
{
float G = GeometrySmith(N, V, L, roughness);
float G_Vis = (G * VdotH) / (NdotH * NdotV);
float Fc = pow(1.0 - VdotH, 5.0);
A += (1.0 - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
A /= float(SAMPLE_COUNT);
B /= float(SAMPLE_COUNT);
return vec2(A, B);
}
void main()
{
vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);
FragColor = integratedBRDF;
}
积分后我们正常进行渲染方程求解。
cpp
// pbr.fs - 使用预计算的镜面反射IBL
void main()
{
// ... 其他PBR计算 ...
// 1. 计算菲涅尔项
vec3 F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
// 2. 分离漫反射和镜面反射
vec3 kS = F;
vec3 kD = 1.0 - kS;
kD *= 1.0 - metallic;
// 3. 镜面反射IBL
const float MAX_REFLECTION_LOD = 4.0;
vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb;
vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);
// 4. 最终环境光照
vec3 ambient = (kD * diffuse + specular) * ao;
}
最后在着色器中使用。
效果如下:
