在上一章中,我们通过预先计算辐照度贴图作为照明的间接漫反射部分,将 PBR 与基于图像的照明相结合。 在本章中,我们将关注反射方程的specular part镜面反射部分:

您会注意到 Cook-Torrance 镜面反射部分(乘以 ks)在积分上不是恒定的,它取决于入射光方向,还取决于入射视图方向。尝试求解所有入射光方向(包括所有可能的视图方向)​​的积分是一种combinatorial overload组合过载,并且对于实时计算而言过于昂贵。 Epic Games 提出了一种解决方案,他们能够为pre-convolute the specular part for real time purposes实时目的对镜面反射部分进行预卷积,但需要做出一些妥协,称为split sum approximation拆分和近似

split sum approximation拆分和近似将反射方程的镜面反射部分拆分为两个独立的部分,我们可以分别对它们进行卷积,然后在 PBR 着色器中组合这些部分以用于基于镜面间接图像的照明。与我们对辐照度图进行预卷积的方式类似,拆分和近似需要一个 HDR 环境图作为其卷积输入。为了理解拆分和近似,我们将再次查看反射方程,但这次关注镜面反射部分:

出于与 irradiance convolution辐照度卷积相同(性能)的原因,我们无法实时解决积分的镜面反射部分并期望得到合理的性能。 所以最好我们预先计算这个积分以获得类似镜面反射 IBL 贴图的东西,用片段的法线对这张贴图进行采样,然后完成它。 但是,这有点棘手。 我们能够预先计算辐照度图,因为积分仅取决于 ωi,我们可以move the constant diffuse albedo terms out of the integral将恒定漫反射反照率项移出积分。 这一次,积分不仅仅取决于 ωi,从 BRDF 可以看出:

积分也取决于 wo,我们不能真正对具有两个方向向量的预先计算的立方体贴图进行采样。 如前一章所述,位置 p 在这里无关紧要。 为 ωi 和 ωo 的每个可能组合预先计算这个积分在实时设置中是不切实际的。

Epic Games 的拆分和近似通过将预计算拆分为 2 个单独的部分来解决该问题,我们稍后可以将它们组合起来以获得我们所追求的预计算结果。 拆分和近似将镜面反射积分拆分为两个单独的积分:(简单的平移变换)

1.pre-filtered environment map:

第一部分(卷积时)称为pre-filtered environment map预过滤环境图,它(类似于 irradiance map辐照度图)是预先计算的environment convolution map环境卷积图,但这次考虑了粗糙度。 为了增加粗糙度,环境贴图与更多分散的样本向量进行卷积,产生更模糊的反射。 对于我们卷积的每个粗糙度级别,我们将顺序模糊的结果存储在预过滤贴图的 mipmap 级别中。 例如,在其 5 个 mipmap 级别中存储 5 个不同roughness values粗糙度值的预卷积结果的预过滤环境贴图

我们使用 Cook-Torrance BRDF 的正态分布函数 (NDF,normal distribution function,DFG中的D) 生成样本向量及其散射量,该函数将法线和视图方向作为输入。 由于我们在对环境贴图进行卷积时事先不知道view观察者视图方向,因此 Epic Games 通过 assuming the view direction (and thus the specular reflection direction) to be equal to the output sample direction ωo,假设观察者view视图方向(以及镜面反射方向)等于输出样本方向 ωo 来进一步近似。 这会将其自身转换为以下代码:

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

这样,预过滤的环境卷积不需要知道视图方向。 这确实意味着当从下图所示的角度观察镜面反射时,我们不会得到很好的grazing specular reflections掠射镜面反射(courtesy of the Moving Frostbite to PBR article); 然而,这通常被认为是可接受的折衷方案:

2. BRDF integration map

拆分和方程的第二部分等于镜面积分的 BRDF 部分。 如果我们假设每个方向的入射辐射都是完全白色的(因此 L(p,x)=1.0),我们可以在给定输入粗糙度和法线 n 与光方向 ωi 或 n 之间的输入角度的情况下预先计算 BRDF 的响应 ⋅ωiEpic Games 将pre-computed BRDF's response to each normal and light direction combination将预先计算的BRDF 对每个法线和光方向组合的响应存储在称为  BRDF integration map 集成图2D lookup texture  (LUT) 中的不同粗糙度值上2D lookup texture scale(红色)bias value(绿色)输出到表面的菲涅耳响应,从而为我们提供分割镜面反射积分的第二部分:

(这个图,横坐标是,normal 和light direction的结合,而从坐标是粗糙度,查找pre-computed BRDF's response的信息)

我们通过将平面的水平纹理坐标(范围在 0.0 和 1.0 之间)作为 BRDF 的输入 n⋅ωi 并将其垂直纹理坐标作为输入粗糙度值来生成查找纹理。 有了这个 BRDF integration map and the pre-filtered environment map,我们可以将两者结合起来得到镜面积分的结果

//根据粗糙度,获取纵坐标
float lod             = getMipLevelFromRoughness(roughness);
//根据反射角度,和粗糙度,获取prefiltered图中的颜色
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
// 根据 normal 和light direction的结合 以及 粗糙度,获取Fresnel response的第二部分
vec2 envBRDF          = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy;
//套公式
vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

这应该让您大致了解 Epic Games 的拆分和近似如何大致接近反射率方程的间接镜面反射部分。 现在让我们尝试自己构建预卷积部分。

3.Pre-filtering an HDR environment map (预过滤 一张HDR 环境贴图)

Pre-filtering an environment map预过滤环境贴图与我们convoluted an irradiance map卷积辐照度贴图的方式非常相似。 不同之处在于我们现在考虑了粗糙度并在预过滤贴图的 mip 级别中顺序存储更粗糙的反射。

首先,我们需要生成一个新的立方体贴图来保存预过滤的环境贴图数据。 为了确保我们为其 mip 级别分配足够的内存,我们调用 glGenerateMipmap 作为分配所需内存量的简单方法:

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

请注意,因为我们计划对 prefilterMap 的 mipmap 进行采样,您需要确保将其缩小过滤器设置为 GL_LINEAR_MIPMAP_LINEAR 以启用trilinear filtering。 我们将预过滤的镜面反射存储在其基本 mip 级别的 128 x 128 的每面分辨率中。 这对于大多数反射来说可能已经足够了,但是如果您有大量光滑的材质(想想汽车反射),您可能需要提高分辨率。

在上一章中,我们通过使用球坐标生成均匀分布在半球 Ω 上的样本向量来对环境图进行卷积。 虽然这对于辐照度来说效果很好,但对于镜面反射来说效率较低。 When it comes to specular reflections, based on the roughness of a surface, the light reflects closely or roughly around a reflection vector r over a normal n, but (unless the surface is extremely rough) around the reflection vector nonetheless当涉及镜面反射时,基于表面的粗糙度,光线在法线 n 上的反射矢量 r 附近或粗略地反射,但(除非表面非常粗糙)仍然围绕反射矢量发散

可能的出射光反射的一般形状称为specular lobe镜面波瓣。 随着粗糙度的增加,specular lobe镜面波瓣镜面波瓣的尺寸增加; 并且镜面反射瓣的形状会随着入射光方向的变化而变化。 因此,镜面波瓣的形状高度依赖于材料。

当涉及到微表面模型时,我们可以将镜面反射瓣想象为在给定一些入射光方向的情况下围绕微平面中间向量的反射方向。 鉴于大多数光线最终在微平面中间向量周围反射的镜面波瓣中结束,因此以类似的方式生成样本向量是有意义的,否则大多数会被浪费。 这个过程(以specular lobe生成样本)称为importance sampling重要性抽样

3.1 Monte Carlo integration and importance sampling (蒙特卡洛积分和重要性抽样)

为了完全掌握importance sampling重要性抽样,我们首先深入研究称为Monte Carlo integration蒙特卡洛积分的数学结构。蒙特卡洛积分主要围绕统计和概率论的结合。蒙特卡罗帮助我们离散地解决计算人口的一些统计数据或价值的问题,而不必考虑所有人口。

例如,假设您要计算一个国家所有公民的平均身高。为了得到你的结果,你可以测量每个公民并平均他们的身高,这将为你提供你正在寻找的确切答案。然而,由于大多数国家都有相当多的人口,这不是一个现实的方法:这将花费太多的精力和时间。

另一种方法是从该人群中选择一个更小的完全随机(无偏)子集,测量他们的身高,然后平均结果。这个人口可能只有 100 人。虽然不如确切答案准确,但您会得到一个相对接近基本事实的答案。这被称为大数定律。这个想法是,如果你从总人口中测量一组较小的真正随机样本的 N 组,结果将相对接近真实答案,并随着样本数量 N 的增加而变得更接近。

蒙特卡洛积分建立在law of large numbers大数定律的基础上,并采用相同的方法求解积分。与其求解所有可能的(理论上无限的)样本值 x 的积分,不如简单地生成从总人口和平均值中随机挑选的 N 个样本值。随着 N 的增加,我们保证得到更接近积分的确切答案的结果:

(就是池塘抓鱼做记号放回去,再抓,用来估算池塘鱼数量的那个方法)

为了求解积分,我们在总体 a 到 b 上抽取 N 个随机样本,将它们加在一起,然后除以样本总数来平均它们。 pdf 代表probability density function概率密度函数,它告诉我们特定样本在整个样本集中出现的概率。 例如,人口高度的 pdf 看起来有点像这样:

(如果我们要估算人口数量,那么我专门找1.65米的人来进行池塘算法就行,速度最快,效率最高,1.65重要性最高)

从这张图中我们可以看到,如果我们从人口中随机抽取样本,那么选择身高 1.70 的人的样本的可能性更高,而样本身高 1.50 的概率较低。

例如,我们可以对称为 low-discrepancy sequences低差异序列的东西进行蒙特卡洛积分,它仍然会生成随机样本,但每个样本分布更均匀(图片由 James Heald 提供): 

pseudorandom:伪随机    low-discrepancy sequences低差异序列

当使用low-discrepancy sequences低差异序列生成蒙特卡洛样本向量时,该过程称为Quasi-Monte Carlo integration准蒙特卡洛积分。 Quasi-Monte Carlo 方法具有更快的rate of convergence收敛速度,这使得它们对性能要求高的应用很感兴趣。

鉴于我们新获得的关于蒙特卡洛和准蒙特卡洛积分的知识,我们可以使用一个有趣的特性来实现更快的收敛速度,称为 importance sampling重要性采样。我们在本章之前已经提到过,但是当涉及到光的镜面反射时,反射光矢量被限制在镜面叶中,其大小由表面的粗糙度决定。将任何(准)随机生成的样本视为镜面叶外的任何(准)随机生成的样本与镜面积分无关,因此将样本生成集中在镜面叶内(重要性更高)是有意义的,但代价是使蒙特卡洛估计器有偏差。

这实质上就是importance sampling重要性采样的意义所在:在某些区域中生成样本向量,该区域受围绕microfacet's halfway vector微平面的中途向量的粗糙度约束。通过将准蒙特卡罗采样与低差异序列相结合,并使用重要性采样对样本向量进行偏置,我们获得了很高的convergence收敛速度。因为我们以更快的速度获得解决方案,所以我们将需要更少的样本来达到足够的近似值。

(百度的重要性采样,就是蒙特卡洛函数,增加一个重要性函数,根据重要性,来进行采样)

3.2 A low-discrepancy sequence 低差异序列

在本章中,我们将pre-compute the specular portion of the indirect reflectance equation using importance sampling given a random low-discrepancy sequence based on the Quasi-Monte Carlo method使用基于准蒙特卡罗方法的随机低差异序列,使用重要性采样预先计算间接反射率方程的镜面部分。 我们将使用的序列被称为Hammersley Sequence序列,正如 Holger Dammertz.仔细描述的那样。 Hammersley 序列基于 Van Der Corput 序列,该序列在其小数点周围镜像十进制二进制表示。

给定一些巧妙的技巧,我们可以在着色器程序中非常有效地生成 Van Der Corput 序列,我们将使用该程序在 N 个总样本上获得 Hammersley 序列样本 i:

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; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}  

(根据 i和N生成一个low-discrepancy sequence 低差异化序列)

GLSL Hammersley 函数为我们提供了大小为 N 的总采样集,采样为 i。

没有位运算符支持的 Hammersley 序列:

//没有位运算符支持的 Hammersley 序列
//并非所有与 OpenGL 相关的驱动程序都支持位运算符(例如 WebGL 和 OpenGL ES 2.0),在这种情况下,您可能需要使用不依赖位运算符的 Van Der Corput 序列的替代版本:float VanDerCorput(uint n, uint base)
{float invBase = 1.0 / float(base);float denom   = 1.0;float result  = 0.0;for(uint i = 0u; i < 32u; ++i){if(n > 0u){denom   = mod(float(n), 2.0);result += denom * invBase;invBase = invBase / 2.0;n       = uint(float(n) / 2.0);}}return result;
}
// ----------------------------------------------------------------------------
vec2 HammersleyNoBitOps(uint i, uint N)
{return vec2(float(i)/float(N), VanDerCorput(i, 2u));
}//请注意,由于旧硬件中的 GLSL 循环限制,序列会在所有可能的 32 位上循环。 此版本的性能较低,但如果您发现自己没有位运算符,则可以在所有硬件上工作。

3.3 GGX Importance sampling GGX 重要性抽样

我们不会在积分半球 Ω 上均匀或随机(蒙特卡洛)生成样本向量,而是根据表面粗糙度生成偏向microsurface halfway vector微表面中间向量的一般反射方向的样本向量。 采样过程将和我们之前看到的类似:开始一个大循环,生成一个随机(低差异)序列值,取序列值在切线空间生成样本向量,变换到世界空间,然后采样 场景的光芒。 不同的是,我们现在use a low-discrepancy sequence value as input to generate a sample vector使用低差异序列值作为输入来生成样本向量

(根据粗糙度,就是重要性,来随机生成一个接近halfway的vector,越粗糙,这个vector离halfway越远,但是有得有规律的远,得符合蒙特卡洛采样,10000个蒙卡一下,会形成一个花瓣形状符合现实)

const uint SAMPLE_COUNT = 4096u;
for(uint i = 0u; i < SAMPLE_COUNT; ++i)
{// 生成随机(低差异)序列值vec2 Xi = Hammersley(i, SAMPLE_COUNT);   

此外,为了建立一个样本向量,我们需要一些方法来将样本向量定向和偏置到一些表面粗糙度的镜面波瓣。 我们可以采用理论章节中描述的 NDF,并在 Epic Games 描述的球形样本向量过程中结合 GGX NDF:

//重要性采样
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{float a = roughness*roughness;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);// from spherical coordinates to cartesian coordinates// 从球坐标到笛卡尔坐标vec3 H;H.x = cos(phi) * sinTheta;H.y = sin(phi) * sinTheta;H.z = cosTheta;// from tangent-space vector to world-space sample vector//从切线空间向量到世界空间样本向量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);vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;return normalize(sampleVec);
}  

这为我们提供了一个sample vector样本向量,该样本向量基于一些输入粗糙度和低差异序列值 Xi,somewhat oriented around the expected microsurface's halfway vector。 请注意,Epic Games 基于Disney's最初的 PBR 研究使用平方粗糙度来获得更好的视觉效果。

通过定义低差异 Hammersley 序列和样本生成,我们可以最终确定pre-filter convolution shader预过滤卷积着色器

#version 330 core
out vec4 FragColor;
in vec3 localPos;uniform samplerCube environmentMap;
uniform float roughness;const float PI = 3.14159265359;float RadicalInverse_VdC(uint bits);
vec2 Hammersley(uint i, uint N);
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);void main()
{       vec3 N = normalize(localPos);    vec3 R = N;vec3 V = R;const uint SAMPLE_COUNT = 1024u;float totalWeight = 0.0;   vec3 prefilteredColor = vec3(0.0);     for(uint i = 0u; i < SAMPLE_COUNT; ++i){//生成低差异化序列(随机序列)vec2 Xi = Hammersley(i, SAMPLE_COUNT);//生成重要性采样,根据粗糙度生成一个接近halfway的vectorvec3 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进行采样,这个图是镜面积分的第一张图,第一个部分prefilteredColor += texture(environmentMap, L).rgb * NdotL;totalWeight      += NdotL;}}prefilteredColor = prefilteredColor / totalWeight;FragColor = vec4(prefilteredColor, 1.0);
}  

我们基于在预过滤立方体贴图的每个 mipmap 级别(从 0.0 到 1.0)上变化的一些输入粗糙度对环境进行预过滤,并将结果存储在 prefilteredColor 中。 得到的 prefilteredColor 除以总样本权重,其中对最终结果影响较小的样本(对于较小的 NdotL)对最终权重的贡献较小。

3.4 Capturing pre-filter mipmap levels (捕获预过滤器 mipmap 级别)

剩下要做的就是让 OpenGL 在多个 mipmap 级别上使用不同的粗糙度值对环境贴图进行预过滤。 使用辐照度章节的原始设置实际上很容易做到这一点:

prefilterShader.use();
prefilterShader.setInt("environmentMap", 0);
prefilterShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
unsigned int maxMipLevels = 5;
for (unsigned int mip = 0; mip < maxMipLevels; ++mip)
{// reisze framebuffer according to mip-level size.unsigned int mipWidth  = 128 * std::pow(0.5, mip);unsigned int mipHeight = 128 * std::pow(0.5, mip);glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);glViewport(0, 0, mipWidth, mipHeight);float roughness = (float)mip / (float)(maxMipLevels - 1);prefilterShader.setFloat("roughness", roughness);for (unsigned int i = 0; i < 6; ++i){prefilterShader.setMat4("view", captureViews[i]);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);renderCube();}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);   

该过程类似于辐照度图卷积,但这次我们将帧缓冲区的尺寸缩放到适当的 mipmap 比例,每个 mip 级别将尺寸减少 2。此外,我们在 glFramebufferTexture2D 中指定要渲染的 mip 级别 最后一个参数并将我们预过滤的roughness 粗糙度传递给pre-filter shader预过滤着色器

应该为我们提供一个经过适当预过滤的环境贴图它返回的反射越模糊,我们访问它的 mip 级别越高(mip级别越高,图片越小)。 如果我们在天空盒着色器中使用预过滤的环境立方体贴图并在其第一个 mip 级别之上强制采样,如下所示:

vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb; 

如果它看起来有点相似,那么您已经成功地预过滤了 HDR 环境贴图。 玩转不同的 mipmap 级别,以查看预过滤器贴图随着 mip 级别的增加逐渐从锐利反射变为模糊反射(mip级别越高,图片变得越小)

4.Pre-filter convolution artifacts 预过滤卷积伪影

虽然当前的预过滤器贴图适用于大多数用途,但迟早您会遇到几个与预过滤器卷积直接相关的渲染伪影。 我将在这里列出最常见的,包括如何修复它们。

4.1 Cubemap seams at high roughness 高粗糙度的立方体贴图接缝

在具有粗糙表面的表面上对预过滤器贴图进行采样意味着在其某些较低的 mip 级别上对预过滤器贴图进行采样。 在对立方体贴图进行采样时,OpenGL 默认不会在立方体贴图面上进行线性插值。 因为较低的 mip 级别具有较低的分辨率,并且预过滤器映射与更大的样本波瓣卷积,所以the lack of between-cube-face filtering 立方体面与面间缺乏过渡,变得非常明显

幸运的是,OpenGL 通过启用 GL_TEXTURE_CUBE_MAP_SEAMLESS 为我们提供了正确过滤立方体贴图面的选项:

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);  

只需在应用程序开始的某个地方启用此属性,接缝就会消失。

4.2 Bright dots in the pre-filter convolution 预过滤卷积中的亮点

由于镜面反射中的高频细节和变化很大的光强度,对镜面反射进行卷积需要大量样本才能正确解释 HDR 环境反射变化很大的性质。 我们已经采集了大量样本,但在某些环境中,在某些较粗糙的 mip 级别可能仍然不够,在这种情况下,您将开始看到在明亮区域周围出现虚线图案:

 一种选择是进一步增加样本数,但这对所有环境来说都不够。 正如 Chetan Jags 所述,我们可以通过(在预过滤卷积期间)不直接采样环境图,而是根据积分的 PDF(probability density function概率密度函数)和粗糙度,来决定环境图的 mip 级别来减少此伪影:

float D   = DistributionGGX(NdotH, roughness);
float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001; float resolution = 512.0; // resolution of source cubemap (per face)
float saTexel  = 4.0 * PI / (6.0 * resolution * resolution);
float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);//根据采样结果,pdf重要性来配置mipLevel
float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 

不要忘记在要从中采样其 mip 级别的环境贴图上启用trilinear filtering:(三线性过滤以双线性过滤为基础。会对pixel大小与texel大小最接近的两层Mipmap level分别进行双线性过滤,然后再对两层得到的结果进生线性插值。)

glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 

并让 OpenGL 在设置立方体贴图的基础纹理后生成 mipmap:

// convert HDR equirectangular environment map to cubemap equivalent
[...]
// then generate mipmaps
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

这效果出奇的好,应该可以去除粗糙表面上预过滤贴图中的大部分(如果不是全部)点。

5. Pre-computing the BRDF (预计算 BRDF)

随着预过滤环境的启动和运行,我们可以专注于second part of the split-sum approximation拆分和近似的第二部分:BRDF。 让我们再次简要回顾一下specular split sum approximation镜面分割和近似

我们已经在不同的粗糙度级别上预先计算了预滤波器映射中分割和近似的左侧部分。 右侧要求我们在角度 n⋅ωo、表面粗糙度和菲涅耳 F0 上对 BRDF 方程进行卷积。 这类似于将镜面反射 BRDF 与纯白色环境或 1.0 的恒定辐射度 Li 相结合。 用 3 个变量对 BRDF 进行卷积有点多,但我们可以尝试将 F0 移出镜面 BRDF 方程

F是菲涅耳方程。 将菲涅耳分母移到 BRDF 可以得到以下等价方程:

Fresnel-Schlick 近似代替最右边的 F 可以得到:

让我们将 (1−ωo⋅h)5 替换为 α,以便更容易求解 F0:

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

这样,F0 在积分上是常数,我们可以从积分中取出 F0。 接下来,我们将 α 代回其原始形式,得到最终的拆分和 BRDF 方程:

得到的两个积分分别代表 F0 的a scale and a bias 尺度和偏差。 请注意,由于 fr(p,ωi,ωo) 已经包含 F函数的项,它们都抵消了,从 fr 中删除了 F函数

与早期的卷积环境贴图类似,我们可以对 BRDF 方程的输入进行卷积:n 和 ωo 之间的角度,以及粗糙度。 我们将convoluted result卷积结果存储在称为 BRDF integration map的 2D 查找纹理 (LUT) 中,稍后我们在 PBR 光照着色器中使用它来获得convoluted indirect specular result最终的卷积间接镜面反射结果。

BRDF 卷积着色器在 2D 平面上运行,使用其 2D 纹理坐标直接作为 BRDF 卷积(NdotV and roughness)的输入。 卷积代码在很大程度上类似于 pre-filter convolution预过滤卷积,只是它现在根据我们的 BRDF 几何函数和 Fresnel-Schlick 近似处理样本向量:

// 整合BRDF ,根据n 和 ωo 之间的角度,以及粗糙度。计算最终镜面反射结果
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){//低离散,假随机,围绕halfway来偏移vectorvec2 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;
}

如您所见,BRDF 卷积是从数学到代码的直接转换。 我们将角度 θ 和粗糙度都作为输入,生成具有重要性采样的样本向量,在the geometry and the derived Fresnel term of the BRDF几何和 BRDF 的派生菲涅耳项上对其进行处理,and output both a scale and a bias to F0 for each sample, averaging them in the end.并为每个样本输出一个尺度和一个到 F0 的偏差,平均它们 到底。

您可能从理论章节中回忆起,当与 IBL 一起使用时,BRDF 的几何术语略有不同,因为它的 k 变量的解释略有不同:

由于 BRDF 卷积是镜面反射 IBL (image base light)积分的一部分,我们将用于 Schlick-GGX 几何函数:

 计算G的部分:

//G的小步骤
float GeometrySchlickGGX(float NdotV, float roughness)
{float a = roughness;float k = (a * a) / 2.0;float nom   = NdotV;float denom = NdotV * (1.0 - k) + k;return nom / denom;
}
// ----------------------------------------------------------------------------
//G的大步骤
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;
}  

请注意,虽然 k 将 a 作为其参数,但我们并没有像我们最初对 a 的其他解释那样将粗糙度平方。 可能因为 a 已经在这里平方了。 我不确定这是否与 Epic Games 的部分或原始迪士尼论文不一致,但直接将粗糙度转换为 a 会给出与 Epic Games 版本相同的 BRDF 集成图。

最后,为了存储 BRDF convolution result卷积结果,我们将生成 512 x 512 分辨率的 2D 纹理:

unsigned int brdfLUTTexture;
glGenTextures(1, &brdfLUTTexture);// pre-allocate enough memory for the LUT texture.
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); 

请注意,我们使用 Epic Games 推荐的 16 位精度浮点格式。 请务必将环绕模式设置为 GL_CLAMP_TO_EDGE 以防止边缘采样伪影。

然后,我们重新使用相同的帧缓冲区对象并在 NDC 屏幕空间四边形上运行此着色器:

glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0);glViewport(0, 0, 512, 512);
brdfShader.use();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
RenderQuad();glBindFramebuffer(GL_FRAMEBUFFER, 0);  

拆分和积分的卷积 BRDF 部分应为您提供以下结果:

使用预过滤的环境贴图和 BRDF 2D LUT,we can re-construct the indirect specular integral according to the split sum approximation. The combined result then acts as the indirect or ambient specular light.我们可以根据拆分和近似重建间接镜面反射积分。 然后,组合结果充当6间接或环境镜面反射光。

6.Completing the IBL reflectance 完成 IBL 反射率

为了使反射方程的间接镜面反射部分启动并运行,我们需要将stitch both parts of the split sum approximation together拆分和近似的两个部分缝合在一起。 让我们首先将预先计算的光照数据添加到 PBR 着色器的顶部:

uniform samplerCube prefilterMap;
uniform sampler2D   brdfLUT;  

首先,我们通过使用反射向量对pre-filtered environment map预过滤的环境贴图进行采样来获得表面的间接镜面反射。 请注意,我们根据表面粗糙度对适当的 mip 级别进行采样,从而使更粗糙的表面具有更模糊的镜面反射:

void main()
{[...]vec3 R = reflect(-V, N);   const float MAX_REFLECTION_LOD = 4.0;vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;    [...]
}

在预过滤步骤中,我们仅将环境映射卷积到最多 5 个 mip 级别(0 到 4),我们在此将其表示为 MAX_REFLECTION_LOD,以确保我们不会在没有(相关)数据的情况下对 mip 级别进行采样。

然后我们从 BRDF lookup texture (BRDF 查找纹理)中采样指定材质的粗糙度以及法线和视图向量之间的角度

vec3 F        = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);

Given the scale and bias to F0 (here we're directly using the indirect Fresnel result F) from the BRDF lookup texture,我们将其与 IBL 反射方程的左预过滤部分结合起来,并将近似积分结果重新构造为 specular镜面反射

这给了我们反射方程的间接镜面反射部分。 现在,将它与上一章中反射方程的漫反射 IBL 部分结合起来,我们得到完整的 PBR IBL 结果:

vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);vec3 kS = F;
vec3 kD = 1.0 - kS;
kD *= 1.0 - metallic;    vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse    = irradiance * albedo;const float MAX_REFLECTION_LOD = 4.0;
//镜面反射第一部分
vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;
//镜面反射第二部分
vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
// 二者套公式结合
vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);//环境光
vec3 ambient = (kD * diffuse + specular) * ao; 

Note that we don't multiply specular by ks as we already have a Fresnel multiplication in there.

现在,在粗糙度和金属属性不同的一系列球体上运行这个精确的代码,我们终于可以在最终的 PBR 渲染器中看到它们的真实颜色:

We could even go wild, and use some cool textured PBR materials:

Or load this awesome free 3D PBR model by Andrew Maximov:

我相信我们都同意我们的照明现在看起来更有说服力了。 更好的是,无论我们使用哪种环境贴图,我们的光照看起来都是正确的。 下面您将看到几个不同的预计算 HDR 贴图,它们完全改变了光照动态,但在不改变单个光照变量的情况下仍然看起来物理上正确!

好吧,这次 PBR 冒险原来是一段漫长的旅程。 有很多步骤,因此可能会出错,因此如果您遇到困难,请仔细检查球体场景或纹理场景代码示例(包括所有着色器),或者在评论中查看并询问。

通常只需将环境贴图预先计算为辐照度和预过滤器贴图,然后将其存储在磁盘上(请注意,BRDF 集成贴图不依赖于环境贴图,因此您只需要计算或加载一次)。这确实意味着您需要提供一种自定义图像格式来存储 HDR 立方体贴图,包括它们的 mip 级别。或者,您可以将其存储(并加载)为一种可用格式(例如支持存储 mip 级别的 .dds)。

也可通过使用 cmftStudio 或 IBLBaker 等几个出色的工具为您生成这些预先计算的地图。

我们跳过的一点是预先计算的立方体贴图的reflection probes反射探针 cubemap interpolation and parallax correction立方体贴图插值和视差校正。这是在场景中放置多个反射探测器的过程,这些探测器在该特定位置拍摄场景的立方体贴图快照,然后我们可以将其卷积为该部分场景的 IBL 数据。通过基于相机附近的几个探针之间的插值,我们可以实现基于局部高细节图像的照明,这仅受我们愿意放置的反射探针数量的限制。

Learn OpenGL 笔记7.4 PBR-Specular IBL(Image based lighting-特殊的基于图像的照明)相关推荐

  1. Learn OpenGL 笔记7.1 PBR Theory(physically based rendering基于物理的渲染 理论)

    PBR,或更通常称为基于物理的渲染,是一组渲染技术,它们或多或少基于与物理世界更接近的相同基础理论.由于基于物理的渲染旨在以物理上合理的方式模拟光线,因此与我们的原始光照算法(如 Phong 和 Bl ...

  2. Learn OpenGL 笔记7.3 PBR-IBL-Diffuse irradiance(Image based lighting-漫反射辐照度)

    IBL,或image based lighting基于图像的照明,是一组照明对象的技术,不是像前一章那样通过直接分析光,而是将周围环境视为一个 big light source大光源.这通常通过操纵立 ...

  3. Learn OpenGL 笔记6.9 Deferred Shading(延迟着色)

    到目前为止,我们进行照明的方式称为forward rendering前向渲染或forward shading前向着色.我们渲染对象,根据场景中的所有光源对其进行照明.我们为场景中的每个对象分别为每个对 ...

  4. Learn OpenGL 笔记5.11 Anti Aliasing(抗锯齿)

    这种清晰地看到边缘组成的像素结构的效果称为锯齿. 有很多称为抗锯齿技术的技术可以通过产生更平滑的边缘来对抗这种锯齿行为.(小时候打开一个新游戏,第一件事情就是把抗锯齿给关了,开抗锯齿太卡了) 起初,我 ...

  5. learn opengl 笔记 1.2

    1.GLFW GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口.它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入. 2.GLAD 由于OpenGL驱 ...

  6. Learn OpenGL 笔记6.5 Normal Mapping(法线贴图)

    我们通过在这些平面三角形上包裹 2D 纹理来增强真实感,隐藏多边形只是很小的平面三角形的事实. 从照明技术的角度来看,确定对象形状的唯一方法是通过其垂直法向量. 这种使用每片段法线与每表面法线相比的技 ...

  7. Learn OpenGL 笔记6.8 Bloom(高动态范围)

    由于monitor监视器的强度范围有限,明亮的光源和明亮的区域通常难以传达给观看者. 区分显示器上明亮光源的一种方法是让它们发光: 然后光线在光源周围流淌. 这有效地为观看者提供了这些光源或明亮区域非 ...

  8. Learn OpenGL 笔记6.10 SSAO(Screen Space Ambient Occlusion屏幕空间环境光遮蔽)

    我们在基本照明一章中简要介绍了该主题:ambient lighting环境光. Ambient lighting环境光是一个固定的光常数,我们添加到场景的整体照明中以模拟光的scattering散射. ...

  9. Learn OpenGL 笔记6.7 HDR(高动态范围)

    默认情况下,亮度和颜色值在存储到帧缓冲区时被限制在 0.0 和 1.0 之间. 这个起初看似无害的声明让我们总是在这个范围内的某个地方指定光线和颜色值,试图让它们适应场景. 这工作正常并给出了不错的结 ...

最新文章

  1. android专题-蓝牙扫描、连接、读写
  2. 鸿蒙系统增加了什么功能,华为再发新版鸿蒙OS系统!新增超级终端功能:可媲美iOS系统...
  3. CSS 元素的display属性
  4. thinkphp模板常用的方法
  5. Java代码怎么取消订阅功能,RxJava2 中多种取消订阅 dispose 的方法梳理( 源码分析 )...
  6. 了解java虚拟机mdash;非堆相关参数设置(4)
  7. c语言作业答案 填空题,C语言练习题-填空题(带答案)
  8. linux台式机双屏幕怎么连接,台式机Linux/Unix多系统安装详细教程
  9. oracle12c不使用cdb模式,oracle 12c non-cdb升级成cdb模式
  10. 电视剧《春草》剧情介绍
  11. 2019趋势科技面经
  12. css鼠标经过字体抖动,jQuery+css3实现文字跟随鼠标的上下抖动
  13. 关于Java对接读卡器遇到的坑Process finished with exit code -1073740940 (0xC0000374)
  14. Windows 简介
  15. kubernet安装helm
  16. 【XXE漏洞01】XML漏洞原理及实验
  17. intel网卡驱动方法1的安装说明书
  18. ios平台微信的语音文件AUD格式其实就是AMR格式
  19. 六、java版商城之一件代发设置 Spring Cloud+SpringBoot+mybatis+uniapp b2b2c o2o 多商家入驻商城 直播带货商城 电子商务
  20. 常用颜色值及英文名称

热门文章

  1. 怎么用多张图片制作动态图?
  2. 汽车电子学习【车载网络CAN/LIN】
  3. HTML5七夕情人节表白网页(流星动画3D相册) HTML+CSS+JS 求婚 html生日快乐祝福代码网页 520情人节告白代码 程序员表白源码 3D旋转相册 js烟花代码 css爱心表白
  4. guido正式对外发布python版本的年份_Guido van Rossum正式对外发布Python版本的年份是:______。...
  5. datadog 全观测性初体验
  6. 怎么用软件测试睡眠质量差怎么办,睡眠监测 App 到底有没有用?我睡了 34 晚,做了一个实验...
  7. 《青山翠影》玖 独行的时代 | 去程归程
  8. 全国高级计算机高新技术考证合格证书能申请人才入户吗?
  9. 量子计算 3 量子门与测量
  10. UVA - 1645 - Count(思路)