本文核心知识主要参照learnopengl-cn文章总结归并,并根据个人学习方向进行了筛选摘抄,如有错误或不完整之处,可参照原文阅读。
本文紧接【PBR系列六】基于物理的环境光照(上):漫反射辐照度(Diffuse irradiance),上一部分中我们预计算了辐照度图作为光照的间接漫反射部分,以将 PBR 与基于图像的照明相结合。在本教程中,我们将重点关注反射方程的镜面部分。

一、镜面反射 IBL(Specular IBL)

1.1 原理


上述的镜面反射部分(被ks相乘)不是恒定的,并且依赖于入射光方向和视线入射方向,尝试实时地计算所有入射光和所有入射视线的积分是几乎不可能的。Epic Games推荐折中地使用预卷积镜面反射部分的方法来解决实时渲染的性能问题,这就是分裂和近似法(split sum approximation)

分裂和近似法将镜面反射部分从反射方程分离出两个部分,这样可以单独地对它们卷积,后面在PBR的shader中为镜面的非直接IBL将它们结合起来。跟预卷积辐射度图类似,分裂和近似法需要HDR环境图作为输入。为了更好地理解分裂和近似法,下面着重关注反射方程的镜面部分:

出于跟辐射度图相同的性能问题的考虑,我们要预计算类似镜面IBL图的积分,并且用片元的法线采样这个图。辐射度图的预计算只依赖于ωi,并且我们可以将漫反射项移出积分。但这次从BRDF可以看出,不仅仅是依赖于ωi:

如上方程所示,还依赖ωo,并且我们不能用两个方向向量来采样预计算的cubemap。预计算所有ωi和ωo的组合在实时渲染环境中不实际的。

Epic Games的分裂和近似法将镜面反射部分从反射方程分离出两个部分,这样可以单独地对它们卷积,后面在PBR的shader中为镜面的非直接IBL将它们结合起来。分离后的方程如下:

第一部分∫Li(p,ωi)dωi预过滤环境图(pre-filtered environment map),类似于辐射度图的预计算环境卷积图,但会加入粗糙度。随着粗糙度等级的增加,环境图使用更多的散射采样向量来卷积,创建出更模糊的反射。

对每个卷积的粗糙度等级,循环地在预过滤环境图的mimap等级存储更加模糊的结果。下图是5个不同粗糙度等级的预过滤环境图:

生成采样向量和它们的散射强度,需要用到Cook-Torrance BRDF的法线分布图(NDF),而其带了两个输入:法线和视线向量。当卷积环境图时并不知道视线向量,Epic Games用了更近一步的模拟法:假设视线向量(亦即镜面反射向量)总是等于输出采样向量ωo。所以代码变成如下所示:

vec3 N = normalize(w_o);
vec3 R = N;
vec3 V = R;

这种方式预过滤环境图卷积不需要关心视线方向。这就意味着当从某个角度看向下面这张图的镜面表面反射时,无法获得很好的掠射镜面反射(grazing specular reflections)。然而通常这被认为是一个较好的妥协:

第二部分∫Ωfr(p,ωi,ωo)n⋅ωidωi是镜面积分。假设所有方向的入射辐射率是全白的(那样L(p,x)=1.0),那就可以用给定的粗糙度和一个法线n和光源方向ωi之间的角度或n⋅ωi来预计算BRDF的值。Epic Games存储了用变化的粗糙度来预计算每一个法线和光源方向组合的BRDF的值,该粗糙度存储于2D采样纹理(LUT)中,它被称为BRDF积分图(BRDF integration map)。

2D采样纹理输出一个缩放(红色)和一个偏移值(绿色)给表面的菲涅尔方程式(Fresnel response),以便提供第二部分的镜面积分:

上图水平表示BRDF的输入n⋅ωi,竖向表示输入的粗糙度。

有了预过滤环境图和BRDF积分图,可以在最终shader中将它们结合起来以完成对反射方程的实现。

1.2 实现步骤

1.2.1 预过滤环境图(pre-filtered environment map)

预滤波环境贴图的方法与我们对辐射度贴图求卷积的方法非常相似。对于卷积的每个粗糙度级别,我们将按顺序把模糊后的结果存储在预滤波贴图的 mipmap 中。 首先,我们需要生成一个新的立方体贴图来保存预过滤的环境贴图数据。

在上一节教程中,我们使用球面坐标生成均匀分布在半球 Ω 上的采样向量,以对环境贴图进行卷积。虽然这个方法非常适用于辐照度,但对于镜面反射效果较差。镜面反射依赖于表面的粗糙度,反射光线可能比较松散,也可能比较紧密,但是一定会围绕着反射向量r,除非表面极度粗糙:

所有可能出射的反射光构成的形状称为镜面波瓣。随着粗糙度的增加,镜面波瓣的大小增加;随着入射光方向不同,形状会发生变化。因此,镜面波瓣的形状高度依赖于材质。 在微表面模型里给定入射光方向,则镜面波瓣指向微平面的半向量的反射方向。考虑到大多数光线最终会反射到一个基于半向量的镜面波瓣内,采样时以类似的方式选取采样向量是有意义的,因为大部分其余的向量都被浪费掉了,这个过程称为重要性采样。

1.2.1.1 蒙特卡洛积分和重要性采样

为了充分理解重要性采样,我们首先要了解一种数学结构,称为蒙特卡洛积分。蒙特卡洛积分主要是统计和概率理论的组合。蒙特卡洛可以帮助我们离散地解决人口统计问题,而不必考虑所有人。

例如,假设您想要计算一个国家所有公民的平均身高。为了得到结果,你可以测量每个公民并对他们的身高求平均,这样会得到你需要的确切答案。但是,由于大多数国家人海茫茫,这个方法不现实:需要花费太多精力和时间。

另一种方法是选择一个小得多的完全随机(无偏)的人口子集,测量他们的身高并对结果求平均。可能只测量 100 人,虽然答案并非绝对精确,但会得到一个相对接近真相的答案,这个理论被称作大数定律。我们的想法是,如果从总人口中测量一组较小的真正随机样本的N,结果将相对接近真实答案,并随着样本数 N 的增加而愈加接近。

蒙特卡罗积分建立在大数定律的基础上,并采用相同的方法来求解积分。不为所有可能的(理论上是无限的)样本值 x 求解积分,而是简单地从总体中随机挑选样本 N 生成采样值并求平均。随着 N 的增加,我们的结果会越来越接近积分的精确结果:

为了求解这个积分,我们在 a 到 b 上采样 N 个随机样本,将它们加在一起并除以样本总数来取平均。pdf 代表概率密度函数 (probability density function),它的含义是特定样本在整个样本集上发生的概率。例如,人口身高的 pdf 看起来应该像这样:

从该图中我们可以看出,如果我们对人口任意随机采样,那么挑选身高为 1.70 的人口样本的可能性更高,而样本身高为 1.50 的概率较低。

当涉及蒙特卡洛积分时,某些样本可能比其他样本具有更高的生成概率。这就是为什么对于任何一般的蒙特卡洛估计,我们都会根据 pdf 将采样值除以或乘以采样概率。到目前为止,我们每次需要估算积分的时候,生成的样本都是均匀分布的,概率完全相等。到目前为止,我们的估计是无偏的,这意味着随着样本数量的不断增加,我们最终将收敛到积分的精确解。

但是,某些蒙特卡洛估算是有偏的,这意味着生成的样本并不是完全随机的,而是集中于特定的值或方向。这些有偏的蒙特卡洛估算具有更快的收敛速度,它们会以更快的速度收敛到精确解,但是由于其有偏性,可能永远不会收敛到精确解。通常来说,这是一个可以接受的折衷方案,尤其是在计算机图形学中。因为只要结果在视觉上可以接受,解决方案的精确性就不太重要。下文我们将会提到一种(有偏的)重要性采样,其生成的样本偏向特定的方向,在这种情况下,我们会将每个样本乘以或除以相应的 pdf 再求和。

蒙特卡洛积分在计算机图形学中非常普遍,因为它是一种以高效的离散方式对连续的积分求近似而且非常直观的方法:对任何面积/体积进行采样——例如半球 Ω ——在该面积/体积内生成数量 N 的随机采样,权衡每个样本对最终结果的贡献并求和。

蒙特卡洛积分是一个庞大的数学主题,在此不再赘述,但有一点需要提到:生成随机样本的方法也多种多样。默认情况下,每次采样都是我们熟悉的完全(伪)随机,不过利用半随机序列的某些属性,我们可以生成虽然是随机样本但具有一些有趣性质的样本向量。例如,我们可以对一种名为低差异序列的东西进行蒙特卡洛积分,该序列生成的仍然是随机样本,但样本分布更均匀:

当使用低差异序列生成蒙特卡洛样本向量时,该过程称为拟蒙特卡洛积分。拟蒙特卡洛方法具有更快的收敛速度,这使得它对于性能繁重的应用很有用。

鉴于我们新获得的有关蒙特卡洛(Monte Carlo)和拟蒙特卡洛(Quasi-Monte Carlo)积分的知识,我们可以使用一个有趣的属性来获得更快的收敛速度,这就是重要性采样。我们在前文已经提到过它,但是在镜面反射的情况下,反射的光向量被限制在镜面波瓣中,波瓣的大小取决于表面的粗糙度。既然镜面波瓣外的任何(拟)随机生成的样本与镜面积分无关,因此将样本集中在镜面波瓣内生成是有意义的,但代价是蒙特卡洛估算会产生偏差。

本质上来说,这就是重要性采样的核心:只在某些区域生成采样向量,该区域围绕微表面半向量,受粗糙度限制。通过将拟蒙特卡洛采样与低差异序列相结合,并使用重要性采样偏置样本向量的方法,我们可以获得很高的收敛速度。因为我们求解的速度更快,所以要达到足够的近似度,我们所需要的样本更少。因此,这套组合方法甚至可以允许图形应用程序实时求解镜面积分,虽然比预计算结果还是要慢得多。

1.2.1.2 低差异序列

在本教程中,我们将使用重要性采样来预计算间接反射方程的镜面反射部分,该采样基于拟蒙特卡洛方法给出了随机的低差异序列。我们将使用的序列被称为 Hammersley 序列,Holger Dammertz 曾仔细描述过它。Hammersley 序列是基于 Van Der Corput 序列,该序列是把十进制数字的二进制表示镜像翻转到小数点右边而得。(译注:原文为 Van Der Corpus 疑似笔误,下文各处同)

给出一些巧妙的技巧,我们可以在着色器程序中非常有效地生成 Van Der Corput 序列,我们将用它来获得 Hammersley 序列,设总样本数为 N,样本索引为 i:

vec2 hammersley2d(uint i, uint N)
{// Radical inverse based on http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.htmluint bits = (i << 16u) | (i >> 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);float rdi = float(bits) * 2.3283064365386963e-10;return vec2(float(i) /float(N), rdi);
}

1.2.1.3 GGX 重要性采样

有别于均匀或纯随机地(比如蒙特卡洛)在积分半球 Ω 产生采样向量,我们的采样会根据粗糙度,偏向微表面的半向量的宏观反射方向。采样过程将与我们之前看到的过程相似:开始一个大循环,生成一个随机(低差异)序列值,用该序列值在切线空间中生成样本向量,将样本向量变换到世界空间并对场景的辐射度采样。不同之处在于,我们现在使用低差异序列值作为输入来生成采样向量:

//numSamples通过推入常量来控制采样数
for(uint i = 0u; i < consts.numSamples; i++)
{vec2 Xi = hammersley2d(i, consts.numSamples);...
}

此外,要构建采样向量,我们需要一些方法定向和偏移采样向量,以使其朝向特定粗糙度的镜面波瓣方向。我们可以如理论教程中所述使用 NDF,并将 GGX NDF 结合到 Epic Games 所述的球形采样向量的处理中:

// Based on http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_slides.pdf
vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal)
{// 基于粗糙度将二维点映射到半球float alpha = roughness * roughness;float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1;float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y));float sinTheta = sqrt(1.0 - cosTheta * cosTheta);vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);// 切线空间vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);vec3 tangentX = normalize(cross(up, normal));vec3 tangentY = normalize(cross(normal, tangentX));// 转换到世界空间return normalize(tangentX * H.x + tangentY * H.y + normal * H.z);
}

基于特定的粗糙度输入和低差异序列值 Xi,我们获得了一个采样向量,该向量大体围绕着预估的微表面的半向量。注意,根据迪士尼对 PBR 的研究,Epic Games 使用了平方粗糙度以获得更好的视觉效果。

使用低差异 Hammersley 序列和上述定义的样本生成方法,我们可以最终完成预滤波器卷积着色器:

#version 450layout (location = 0) in vec3 inPos;
layout (location = 0) out vec4 outColor;layout (binding = 0) uniform samplerCube samplerEnv;layout(push_constant) uniform PushConsts {layout (offset = 64) float roughness;layout (offset = 68) uint numSamples;
} consts;const float PI = 3.1415926536;// Based omn http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/
float random(vec2 co)
{float a = 12.9898;float b = 78.233;float c = 43758.5453;float dt= dot(co.xy ,vec2(a,b));float sn= mod(dt,3.14);return fract(sin(sn) * c);
}vec2 hammersley2d(uint i, uint N)
{// Radical inverse based on http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.htmluint bits = (i << 16u) | (i >> 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);float rdi = float(bits) * 2.3283064365386963e-10;return vec2(float(i) /float(N), rdi);
}// Based on http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_slides.pdf
vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal)
{//  基于粗糙度将二维点映射到半球float alpha = roughness * roughness;float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1;float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y));float sinTheta = sqrt(1.0 - cosTheta * cosTheta);vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);// 切线空间vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);vec3 tangentX = normalize(cross(up, normal));vec3 tangentY = normalize(cross(normal, tangentX));// 转换到世界空间return normalize(tangentX * H.x + tangentY * H.y + normal * H.z);
}// Normal Distribution function
float D_GGX(float dotNH, float roughness)
{float alpha = roughness * roughness;float alpha2 = alpha * alpha;float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;return (alpha2)/(PI * denom*denom);
}vec3 prefilterEnvMap(vec3 R, float roughness)
{vec3 N = R;vec3 V = R;vec3 color = vec3(0.0);float totalWeight = 0.0;float envMapDim = float(textureSize(samplerEnv, 0).s);for(uint i = 0u; i < consts.numSamples; i++) {vec2 Xi = hammersley2d(i, consts.numSamples);vec3 H = importanceSample_GGX(Xi, roughness, N);vec3 L = 2.0 * dot(V, H) * H - V;float dotNL = clamp(dot(N, L), 0.0, 1.0);if(dotNL > 0.0) {// Filtering based on https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/float dotNH = clamp(dot(N, H), 0.0, 1.0);float dotVH = clamp(dot(V, H), 0.0, 1.0);// 概率分布函数float pdf = D_GGX(dotNH, roughness) * dotNH / (4.0 * dotVH) + 0.0001;float omegaS = 1.0 / (float(consts.numSamples) * pdf);//  所有立方体面之间的立体角为1像素float omegaP = 4.0 * PI / (6.0 * envMapDim * envMapDim);//通过+1来实现更好的效果float mipLevel = roughness == 0.0 ? 0.0 : max(0.5 * log2(omegaS / omegaP) + 1.0, 0.0f);color += textureLod(samplerEnv, L, mipLevel).rgb * dotNL;totalWeight += dotNL;}}return (color / totalWeight);
}void main()
{       vec3 N = normalize(inPos);outColor = vec4(prefilterEnvMap(N, consts.roughness), 1.0);
}

输入的粗糙度随着预过滤的立方体贴图的 mipmap 级别变化(从0.0到1.0),我们根据据粗糙度预过滤环境贴图,把结果存在 prefilteredColor 里。再用 prefilteredColor 除以采样权重总和,其中对最终结果影响较小(NdotL 较小)的采样最终权重也较小。

vec3 color = textureLod(prefilteredEnv, inUVW, 1.0).rgb;

我们得到的结果看起来确实像原始环境的模糊版本:

如果 HDR 环境贴图的预过滤看起来差不多没问题,尝试一下不同的 mipmap 级别,观察预过滤贴图随着 mip 级别增加,反射逐渐从锐利变模糊的过程。
Lod为0.5:

Lod为2:

Lod为5:

1.2.2 BRDF积分图(BRDF integration map)

预过滤的环境贴图已经可以设置并运行,我们可以集中精力于求和近似的第二部分:BRDF。让我们再次简要回顾一下镜面部分的分割求和近似法:

我们已经在预过滤贴图的各个粗糙度级别上预计算了分割求和近似的左半部分。右半部分要求我们在 n⋅ωo 、表面粗糙度、菲涅尔系数 F0 上计算 BRDF 方程的卷积。这等同于在纯白的环境光或者辐射度恒定为 Li=1.0 的设置下,对镜面 BRDF 求积分。对3个变量做卷积有点复杂,不过我们可以把 F0 移出镜面 BRDF 方程:

F 为菲涅耳方程。将菲涅耳分母移到 BRDF 下面可以得到如下等式:

用 Fresnel-Schlick 近似公式替换右边的 F 可以得到:

让我们用 α 替换 (1−ωo⋅h)5 以便更轻松地求解 F0:

然后我们将菲涅耳函数 F 分拆到两个积分里:

这样,F0在整个积分上是恒定的,我们可以从积分中提取出F0。接下来,我们将α替换回其原始形式,从而得到最终分割求和的 BRDF 方程:

公式中的两个积分分别表示 F0 的比例和偏差。注意,由于 f(p,ωi,ωo) 已经包含 F 项,它们被约分了,这里的 f 中不计算 F 项。

和之前卷积环境贴图类似,我们可以对 BRDF 方程求卷积,其输入是 n 和 ωo 的夹角,以及粗糙度,并将卷积的结果存储在纹理中。我们将卷积后的结果存储在 2D 查找纹理(Look Up Texture, LUT)中,这张纹理被称为 BRDF 积分贴图,稍后会将其用于 PBR 光照着色器中,以获得间接镜面反射的最终卷积结果。

BRDF 卷积着色器在 2D 平面上执行计算,直接使用其 2D 纹理坐标作为卷积输入(NdotV 和 roughness)。代码与预滤波器的卷积代码大体相似,不同之处在于,它现在根据 BRDF 的几何函数和 Fresnel-Schlick 近似来处理采样向量:

vec2 BRDF(float NoV, float roughness)
{// 在进行2D查找时,法线总是沿着z轴const vec3 N = vec3(0.0, 0.0, 1.0);vec3 V = vec3(sqrt(1.0 - NoV*NoV), 0.0, NoV);vec2 LUT = vec2(0.0);for(uint i = 0u; i < NUM_SAMPLES; i++) {vec2 Xi = hammersley2d(i, NUM_SAMPLES);vec3 H = importanceSample_GGX(Xi, roughness, N);vec3 L = 2.0 * dot(V, H) * H - V;float dotNL = max(dot(N, L), 0.0);float dotNV = max(dot(N, V), 0.0);float dotVH = max(dot(V, H), 0.0); float dotNH = max(dot(H, N), 0.0);if (dotNL > 0.0) {float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);float G_Vis = (G * dotVH) / (dotNH * dotNV);float Fc = pow(1.0 - dotVH, 5.0);LUT += vec2((1.0 - Fc) * G_Vis, Fc * G_Vis);}}return LUT / float(NUM_SAMPLES);
}void main()
{outColor = vec4(BRDF(inUV.s, 1.0-inUV.t), 0.0, 1.0);
}

如你所见,BRDF 卷积部分是从数学到代码的直接转换。我们将角度 θ 和粗糙度作为输入,以重要性采样产生采样向量,在整个几何体上结合 BRDF 的菲涅耳项对向量进行处理,然后输出每个样本上 F0 的系数和偏差,最后取平均值。

你可能回想起理论教程中的一个细节:与 IBL 一起使用时,BRDF 的几何项略有不同,因为 k 变量的含义稍有不同:

由于 BRDF 卷积是镜面 IBL 积分的一部分,因此我们要在 Schlick-GGX 几何函数中使用 kIBL:

float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{float k = (roughness * roughness) / 2.0;float GL = dotNL / (dotNL * (1.0 - k) + k);float GV = dotNV / (dotNV * (1.0 - k) + k);return GL * GV;
}

BRDF积分图片元着色器为:

#version 450layout (location = 0) in vec2 inUV;
layout (location = 0) out vec4 outColor;
layout (constant_id = 0) const uint NUM_SAMPLES = 1024u;const float PI = 3.1415926536;float random(vec2 co)
{float a = 12.9898;float b = 78.233;float c = 43758.5453;float dt= dot(co.xy ,vec2(a,b));float sn= mod(dt,3.14);return fract(sin(sn) * c);
}vec2 hammersley2d(uint i, uint N)
{uint bits = (i << 16u) | (i >> 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);float rdi = float(bits) * 2.3283064365386963e-10;return vec2(float(i) /float(N), rdi);
}vec3 importanceSample_GGX(vec2 Xi, float roughness, vec3 normal)
{float alpha = roughness * roughness;float phi = 2.0 * PI * Xi.x + random(normal.xz) * 0.1;float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (alpha*alpha - 1.0) * Xi.y));float sinTheta = sqrt(1.0 - cosTheta * cosTheta);vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);vec3 up = abs(normal.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);vec3 tangentX = normalize(cross(up, normal));vec3 tangentY = normalize(cross(normal, tangentX));return normalize(tangentX * H.x + tangentY * H.y + normal * H.z);
}float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{float k = (roughness * roughness) / 2.0;float GL = dotNL / (dotNL * (1.0 - k) + k);float GV = dotNV / (dotNV * (1.0 - k) + k);return GL * GV;
}vec2 BRDF(float NoV, float roughness)
{// 在进行2D查找时,法线总是沿着z轴const vec3 N = vec3(0.0, 0.0, 1.0);vec3 V = vec3(sqrt(1.0 - NoV*NoV), 0.0, NoV);vec2 LUT = vec2(0.0);for(uint i = 0u; i < NUM_SAMPLES; i++) {vec2 Xi = hammersley2d(i, NUM_SAMPLES);vec3 H = importanceSample_GGX(Xi, roughness, N);vec3 L = 2.0 * dot(V, H) * H - V;float dotNL = max(dot(N, L), 0.0);float dotNV = max(dot(N, V), 0.0);float dotVH = max(dot(V, H), 0.0); float dotNH = max(dot(H, N), 0.0);if (dotNL > 0.0) {float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);float G_Vis = (G * dotVH) / (dotNH * dotNV);float Fc = pow(1.0 - dotVH, 5.0);LUT += vec2((1.0 - Fc) * G_Vis, Fc * G_Vis);}}return LUT / float(NUM_SAMPLES);
}void main()
{outColor = vec4(BRDF(inUV.s, 1.0-inUV.t), 0.0, 1.0);
}

分割积分和的 BRDF 卷积部分应该得到以下结果:

1.3 IBL实现

直接上IBL最终合成着色器代码:

#version 450layout (location = 0) in vec3 inWorldPos;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inUV;layout (binding = 0) uniform UBO {mat4 projection;mat4 model;mat4 view;vec3 camPos;
} ubo;layout (binding = 1) uniform UBOParams {vec4 lights[4];float exposure;float gamma;
} uboParams;layout(push_constant) uniform PushConsts {layout(offset = 12) float roughness;layout(offset = 16) float metallic;layout(offset = 20) float specular;layout(offset = 24) float r;layout(offset = 28) float g;layout(offset = 32) float b;
} material;layout (binding = 2) uniform samplerCube samplerIrradiance;
layout (binding = 3) uniform sampler2D samplerBRDFLUT;
layout (binding = 4) uniform samplerCube prefilteredMap;layout (location = 0) out vec4 outColor;#define PI 3.1415926535897932384626433832795
#define ALBEDO vec3(material.r, material.g, material.b)float D_GGX(float dotNH, float roughness)
{float alpha = roughness * roughness;float alpha2 = alpha * alpha;float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;return (alpha2)/(PI * denom*denom);
}float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{float r = (roughness + 1.0);float k = (r*r) / 8.0;float GL = dotNL / (dotNL * (1.0 - k) + k);float GV = dotNV / (dotNV * (1.0 - k) + k);return GL * GV;
}vec3 F_SchlickR(float cosTheta, vec3 F0, float roughness)
{return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}//预滤波线性采样
vec3 prefilteredReflection(vec3 R, float roughness)
{const float MAX_REFLECTION_LOD = 9.0; float lod = roughness * MAX_REFLECTION_LOD;float lodf = floor(lod);float lodc = ceil(lod);vec3 a = textureLod(prefilteredMap, R, lodf).rgb;vec3 b = textureLod(prefilteredMap, R, lodc).rgb;return mix(a, b, lod - lodf);
}//光源BRDF
vec3 specularContribution(vec3 L, vec3 V, vec3 N, vec3 F0, float metallic, float roughness)
{vec3 H = normalize (V + L);float dotNH = clamp(dot(N, H), 0.0, 1.0);float dotNV = clamp(dot(N, V), 0.0, 1.0);float dotNL = clamp(dot(N, L), 0.0, 1.0);vec3 lightColor = vec3(1.0);vec3 color = vec3(0.0);if (dotNL > 0.0) {float D = D_GGX(dotNH, roughness); float G = G_SchlicksmithGGX(dotNL, dotNV, roughness);vec3 F = F_Schlick(dotNV, F0);     vec3 spec = D * F * G / (4.0 * dotNL * dotNV + 0.001);        vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);          color += (kD * ALBEDO / PI + spec) * dotNL;}return color;
}void main()
{       vec3 N = normalize(inNormal);vec3 V = normalize(ubo.camPos - inWorldPos);vec3 R = reflect(-V, N); float metallic = material.metallic;float roughness = material.roughness;vec3 F0 = vec3(0.04); F0 = mix(F0, ALBEDO, metallic);//光源vec3 Lo = vec3(0.0);for(int i = 0; i < uboParams.lights[i].length(); i++) {vec3 L = normalize(uboParams.lights[i].xyz - inWorldPos);Lo += specularContribution(L, V, N, F0, metallic, roughness);}   vec2 brdf = texture(samplerBRDFLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;vec3 reflection = prefilteredReflection(R, roughness).rgb;  vec3 irradiance = texture(samplerIrradiance, N).rgb;// 漫反射辐照度(上)vec3 diffuse = irradiance * ALBEDO;   vec3 F = F_SchlickR(max(dot(N, V), 0.0), F0, roughness);// 镜面反射(下)vec3 specular = reflection * (F * brdf.x + brdf.y);// 环境的一部分vec3 kD = 1.0 - F;kD *= 1.0 - metallic;    vec3 ambient = (kD * diffuse + specular);vec3 color = ambient + Lo;// 色调映射color = vec3(1.0) - exp(-color * uboParams.exposure);  // 伽马校正color = pow(color, vec3(1.0f / uboParams.gamma));outColor = vec4(color, 1.0);
}

我们可以看到,最终我们将上一章节的漫反射和本章节的镜面反射融合在一起,形成了我们最终的效果,如下图:

如果你想对比基于物理的环境光照效果,你也可以在着色器中加入:

roughness = max(roughness, step(fract(inWorldPos.y * 2.02), 0.3));

对比效果如下:

从上图中我们可以明显的看出PBR的仿真效果。

【PBR系列七】基于物理的环境光照(下):镜面反射 IBL(Specular IBL)相关推荐

  1. 【PBR系列六】基于物理的环境光照(上):漫反射辐照度(Diffuse irradiance)

    本文核心知识主要参照learnopengl-cn文章总结归并,并根据个人学习方向进行了筛选摘抄,如有错误或不完整之处,可参照原文阅读. 基于图像的光照(IBL)是对光源物体的技巧集合,与直接光照不同, ...

  2. PBR——概述、基于物理的材质

    PBR概述 PBR,即Physically Based Rendering,主要分为基于物理的材质.基于物理的光照和基于物理的相机三个部分,目前来说对大家最为所熟知的是基于物理的材质部分.本文围绕基于 ...

  3. 实验篇(7.2) 03. 部署物理实验环境(下)❀ 远程访问

    [简介]考虑到有很多人初次接触FortiGate防火墙硬件,因此在讲解部署物理实验环境的时候,防火墙的初次登录内容介绍的比较多,以致于需要将文章分下.下二篇.下篇我们重点介绍服务器的配置及部署.  防 ...

  4. 【PBR系列一】PBR知识体系

    本文核心知识主要参照知乎毛星云浅墨的游戏编程文章总结归并,并根据个人学习方向进行了筛选摘抄,规划整体学完之后对内容进行代码实现,如有错误或不完整之处,可参照原文阅读. PBR知识体系概览 本系列主要打 ...

  5. 基于物理的渲染学习心得——面向使用的PBR理论

    本文是笔者在学习PBR理论中的一些心得,并试图以结合Substance系列软件为例,从使用(实用)层面介绍PBR理论. PBR的定义: PBR全称是基于物理的渲染(Physically Based R ...

  6. 基于物理渲染的基础理论

    本篇作为理论的概括介绍,并不涉及公式的部分 基于物理渲染的优点 很容易就可以作出真实和照片级的效果. 同一配置可以适用于在不同HDR光照环境下. 接口简单而直观,都是基于世界真实的参数.(如粗糙度,金 ...

  7. 【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】

    ✅ 重点参考了 LearnOpenGL CN 的内容,但大部分知识内容,小编已作改写,以方便读者理解. 文章目录 零. 成果预览图 一. 光照原理与投光物的配置 1.1 光照原理 1.2 投光物 二. ...

  8. 【GAMES-202实时渲染】3、预计算环境光照(球谐函数(SH)、IBL、Split Sum、环境光阴影计算(PRT))

    Lec5~7 1 环境光贴图着色计算(不考虑阴影) 1.1 Image-Based Lighting(IBL) 1.2 The Split Sum 2 环境光贴图计算(考虑阴影) 2.1 球谐函数(S ...

  9. Keil MDK STM32系列(九) 基于HAL和FatFs的FAT格式SD卡TF卡读写

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

最新文章

  1. 清华南开出品最新视觉注意力机制Attention综述
  2. android查看控件的xml属性,006 Android XML 控件属性设置技巧汇总
  3. VS-OpenCV三种加载图片的方式
  4. 留学计算机Ps模板,留学ps怎么写?出国留学ps模板
  5. 逆误差函数:torch.erfinv
  6. DL之ShuffleNet:ShuffleNet算法的架构详解
  7. idea 下划线字段转驼峰_Java如何实现数据库中表字段的下划线和驼峰式命名的Model相互转换,很方便的...-Go语言中文社区...
  8. Jenkins - 持续集成环境搭建【转】
  9. 天正CAD启动时显示服务器名称为空,如何解决天正建筑2014启动时出现error
  10. 微信订阅号和公众号的区别
  11. 15张超详细的Python学习路线图,纯良心分享,零基础学习宝典
  12. 应用spss可靠性分析软件
  13. 计算机 无法进入pe,电脑无法进入pe系统_电脑无法进入pe界面
  14. Action Recognition(行为识别)
  15. py2neo的neo4j数据库增删改查节点node、关系relationship、属性property操作
  16. 【计算机网络】网络基础(二)
  17. 五一的旅游照如何消除路人,急,在线等
  18. Mysql安装-Centos7-阿里云虚拟主机
  19. 别上“绵羊墙”多丢脸啊!
  20. 从像素之间谈起:像素游戏的画面增强(下)

热门文章

  1. java 多个PDF合成一个
  2. 利用脑电和功能磁共振成像(fMRI)捕捉自我生成、任务启动的思维的时空动态
  3. BCD编码和ASCII码
  4. appium用list定位相册里的图片
  5. 力扣解法汇总969- 煎饼排序
  6. POJ 1637 混合图的欧拉回路判定
  7. 毕业设计 基于单片机的智能盲人头盔系统 - 导盲杖 stm32
  8. 重邮计算机专业取得奖项,重邮邹宇航:保研北大,囊括国内外40余个重量级奖项的科创达人...
  9. WINFORM时间控件(DATATIMEPICKER)的显示格式设置
  10. 炒菜机器人“精确”破题中餐标准化