学习OpenGL。 第6.4课-IBL。 镜面曝光

OGL3
上一课中,我们准备了与IBL方法配合使用的PBR模型-为此,我们需要事先准备一个辐射图,以描述间接照明的扩散部分。 在本课程中,我们将关注反射率表示的第二部分-反射镜:

大号Ó p ö é ö = Ñ \极Ó Ë 一个ķ d ˚F ř 一个Ç Ç p + ķ 小号 ˚F ř 一个ç d ˚F ģ 4 ø Ë 一个ö ç d ö ñ Ô Ë         gai cdotnLip omegain cdot omegaid omegai




您可能会注意到Cook-Torrens镜像组件(带有一个子表达式 ks )不是恒定的,取决于入射光的方向以及观察的方向。 对于所有可能的光入射方向以及实时观察的所有可能方向,求解该积分是根本不可行的。 因此,Epic Games的研究人员提出了一种称为分割和逼近的方法,该方法可让您在一定条件下提前部分准备镜像组件的数据。

在这种方法中,反射率表达式的反射镜分量分为两个部分,可以分别进行预卷积,然后在PBR着色器中组合以用作间接镜面辐射源。 与辐照图生成一样,卷积过程在其输入处接收HDR环境图。

要了解分和近似法,让我们再次查看反射率的表达式,只在其中保留镜面分量的子表达式(在上一课中单独考虑了漫射部分):

Lop omegao= int\极 Omegaks fracDFG4 omegao cdotn omegai cdotnLip omegain cdot omegaid omegai= int\极 Omegafrp omegai omegaoLip omegain cdot omegaid omegai


与准备辐照图一样,该积分无法实时求解。 因此,建议类似地计算反射率表达式的镜像分量的贴图,并在主渲染周期中基于表面的法线从该贴图中进行简单选择。 但是,并不是所有事情都那么简单:由于积分仅取决于以下事实,因此可以轻松获得辐照图  omegai ,并且朗伯扩散分量的恒定子表达式可以从积分的符号中取出。 在这种情况下,积分不仅取决于  omegai 从BR​​DF公式很容易理解:

frpwiwo= fracDFG4 omegao cdotn omegai cdotn


积分下的表达式还取决于  omegao -对于两个方向矢量,几乎不可能从先前准备的立方图中进行选择。 点位置 p 在这种情况下,您将无法考虑-为什么在上一课中对此进行了讨论。 对所有可能组合的积分的初步计算  omegai omegao 不可能完成实时任务。

Epic Games的分割数量方法通过将初步计算问题分为两个独立的部分来解决此问题,随后可以将其结果合并以获取最终的计算值。 分割和方法从镜像组件的原始表达式中提取两个积分:

Lop omegao= int\极 OmegaLip omegaid omegai int\极 Omegafrp omegai omegaon cdot omegaid omegai


第一部分的计算结果通常称为预过滤环境图 ,它是经过此表达式指定的卷积处理的环境图。 所有这些都与获得辐照图的过程相似,但是在这种情况下,考虑到粗糙度值进行卷积。 较高的粗糙度值导致在卷积过程中使用更多不同的采样向量,从而导致结果更加模糊。 每个下一个选定粗糙度级别的卷积结果存储在准备好的环境图的下一个mip级别中。 例如,一张针对五个不同粗糙度级别进行卷积的环境贴图包含五个mip级别,如下所示:


样本向量及其散布是根据Cook-Torrens BRDF模型的正态分布函数( NDF )确定的。 该功能接受法线向量和观察方向作为输入参数。 由于在初步计算时观察方向是事先未知的,因此Epic Games开发人员不得不再作一个假设:凝视的方向(以及镜面反射的方向)始终与样本的输出方向相同。  omegao 。 以代码形式:

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

在这种情况下,在环境图的卷积过程中不需要注视方向,这使得实时计算变得可行。 但是另一方面,当与反射面成锐角观察时,我们会失去镜面反射的特征失真,如下图所示(从冻伤到PBR )。 通常,这种折衷被认为是可以接受的。


分割和表达式的第二部分包含镜像组件原始表达式的BRDF。 假设入射能量的亮度在所有方向上均由白光光谱表示(即, Lpx=1.0 ),则可以使用以下输入参数预先计算BRDF的值:材料粗糙度和法线之间的角度 n 和光的方向  omegai (或 n cdot omegai ) Epic Games方法涉及以二维纹理的形式存储被称为BRDF积分图的二维纹理的形式,用于存储粗糙度和光的法线与方向之间的角度的每种组合的BRDF计算结果,此后将其用作查找表LUT ) 。 此参考纹理使用红色和绿色输出通道存储比例和偏移量,以计算表面的菲涅耳系数,最终使我们能够为单独的总和求解表达式的第二部分:


如下创建该辅助纹理:将水平纹理坐标(范围为[0.,1.])视为输入参数值 n cdot omegai BRDF功能; 垂直纹理坐标被视为输入粗糙度值。

结果,有了这样的集成图和预处理的环境图,您可以将它们的样本组合起来以获得镜像组件的积分表达式的最终值:

 float lod = getMipLevelFromRoughness(roughness); vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod); vec2 envBRDF = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy; vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

对Epic Games的“分割和”方法的这一回顾应有助于使您对近似反射镜部分负责反射镜分量的过程有一个印象。 现在,让我们尝试自己准备卡数据。

预过滤HDR环境图


对环境图进行预过滤类似于获取辐照图。 唯一的区别是,现在我们考虑了粗糙度并将每个粗糙度级别的结果保存在三次贴图的新mip级别中。

首先,您必须创建一个新的立方图,其中将包含预过滤的结果。 为了创建必要数量的mip级别,我们只需调用glGenerateMipmaps() -必要的内存将分配给当前纹理:

 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的选择将基于存在的mip级别,因此有必要将缩减过滤器模式设置为GL_LINEAR_MIPMAP_LINEAR以启用三线性过滤。 镜像的预处理图像以仅128x128像素的基本mip级别的分辨率存储在三次方图的不同面上。 对于大多数材料来说,这已经足够了,但是,如果场景中增加了许多光滑,有光泽的表面(例如,一辆崭新的汽车),则可能需要提高此分辨率。

在上一课中,我们通过创建在半球中均匀分布的样本矢量来对环境图进行卷积 \欧 使用球坐标。 为了获得照射,该方法非常有效,对于镜面反射的计算还不能说。 镜面反射高光的物理学告诉我们,镜面反射光的方向与反射矢量相邻 正常表面 n 即使粗糙度不为零:


可能的反射出射方向的一般形式称为镜面波瓣镜面波瓣 ;“镜面辐射图的花瓣”-也许太冗长, 大约每秒钟 )。 随着粗糙度的增加,花瓣生长并扩展。 另外,其形状根据光的入射方向而变化。 因此,花瓣的形状高度取决于材料的特性。

回到微表面模型,我们可以想象镜瓣的形状描述了相对于微表面的中值矢量的反射方向,并考虑了光的某些给定方向。 理解到大多数反射光都位于基于中值矢量定向的镜面花瓣内,因此创建以类似方式定向的样本矢量是有意义的。 否则,其中许多将毫无用处。 这种方法称为重要性抽样

蒙特卡洛积分和重要性抽样


为了充分理解样本在意义上的含义,您首先必须熟悉诸如蒙特卡洛积分方法的数学工具。 此方法基于统计和概率理论的组合,可帮助在数值上解决大型样本的统计问题,而无需考虑该样本的每个元素。

例如,您要计算一个国家的平均人口增长。 为了获得准确和可靠的结果,必须衡量每个公民的成长并将结果平均。 但是,由于大多数国家的人口很大,因此这种方法实际上是无法实现的,因为它需要太多资源来执行。
另一种方法是创建一个较小的子样本,其中填充了原始样本的真正随机(无偏)元素。 接下来,您还要测量增长并对该子样本的结果取平均值。 尽管不是绝对准确,但是至少可以容纳一百个人,并且可以获得结果,但是仍然非常接近实际情况。 该方法的解释在于考虑大数定律。 其本质是这样描述的:在较小的子样本中进行一些测量的结果 N 由原始集合的真正随机元素组成的,将接近对整个初始集合进行的测量的控制结果。 此外,随着增长,近似结果趋于真实 N
蒙特卡洛积分是大数定律在求解积分中的应用。 考虑到整个(可能是无限的)值集,而不是求解积分 x 我们用 N 随机采样点并取平均值。 随着成长 N 保证近似结果接近于积分的精确解。

O= int limitsbafxdx= frac1N sumN1i=0 fracfxpdfx


为了求解积分,我们得到积分的值 N 从[a,b]中的样本中随机点中,将结果汇总并除以取平均值的总点数。 项 pdf 描述 概率密度函数 ,它显示了每个选定值在原始样本中出现的概率。 例如,用于公民成长的功能如下所示:


可以看出,使用随机采样点时,与生长150cm的人相比,达到170cm的生长值的机会要高得多。

显然,在蒙特卡洛积分期间,某些采样点比其他采样点更有可能出现在序列中。 因此,在任何用于蒙特卡洛估计的表达式中,我们都使用概率密度函数将所选值除以其出现的概率。 目前,在评估积分时,我们创建了许多均匀分布的采样点:获得任何采样点的机会都是相同的。 因此,我们的估计是无偏的 ,这意味着随着采样点数量的增加,我们的估计将收敛到积分的精确解。

但是,有些评估功能存在偏差 ,即 这意味着不是以真正随机的方式创建采样点,而是以某种程度或方向占优势。 这样的评估功能使Monte Carlo估计可以更快地收敛到精确解。 另一方面,由于评估函数的偏差,解决方案可能永远不会收敛。 在一般情况下,这被认为是可以接受的折衷方案,尤其是在计算机图形学问题中,因为估计值非常接近分析结果,并且如果其效果在视觉上看起来相当可靠,则不需要此估计值。 我们将很快看到按重要性划分的样本(使用有偏估计函数)允许您创建偏向某个方向的样本点,方法是将每个选定值乘以或除以概率密度函数的相应值,从而加以考虑。

蒙特卡洛积分在计算机图形问题中非常普遍,因为它是一种非常直观的方法,可以通过数值方法来估计连续积分的值,这非常有效。 只需取样一些区域或体积即可(例如,我们的半球 \欧 ),创建 N 随机采样点位于内部,并对获得的值进行加权求和。

蒙特卡洛方法是一个非常广泛的讨论主题,在这里我们不再赘述,但有一个更重要的细节:绝对没有一种方法可以创建随机样本 。 默认情况下,每个采样点都是完全(psvedo)随机的-这正是我们所期望的。 但是,利用准随机序列的某些属性,可以创建尽管具有随机性但仍具有有趣属性的向量集。 例如,在为集成过程创建随机样本时,可以使用所谓的低差异序列 ,这些序列可确保所创建采样点的随机性,但在一般情况下,它们的分布更为均匀:


使用低失配序列创建用于集成过程的一组样本矢量是准蒙特卡洛积分方法 。 蒙特卡罗准方法的收敛速度比一般方法快得多,对于具有高性能要求的应用程序来说,这是一个非常有吸引力的属性。

因此,我们知道了一般方法和准蒙特卡洛方法,但是还有一个细节可以提供更高的收敛速度:按重要性进行抽样。
如本课程中已指出的,对于镜面反射,反射光的方向被封装在镜面波瓣中,其大小和形状取决于反射面的粗糙度。 理解镜瓣外的任何(准)随机样本矢量都不会影响镜分量的积分表达,即 没用 使用蒙特卡洛方法的偏倚估计函数将样本矢量的生成集中在镜瓣区域是有意义的。

这就是通过意义进行采样的本质:采样向量的创建被封装在沿着微表面的中值向量定向的特定区域中,其形状由材料的粗糙度决定。 结合使用蒙特卡罗准方法,由于重要采样而在创建样本矢量的过程中具有低失配和偏倚的序列,我们可以实现很高的收敛速度。 由于对解决方案的收敛速度足够快,因此我们可以使用较少数量的样本矢量来获得可接受的足够估计。 原则上,所描述的方法组合使图形应用程序甚至可以实时解决镜像组件的积分问题,尽管初步计算仍然是一种更具收益的方法。

低失配序列


在本课程中,我们仍将对间接辐射的反射率表达式的镜像分量进行初步计算。 我们将使用具有低失配的随机序列和蒙特卡罗拟方法的重要样本。 使用的序列称为Hammersley序列 ,其详细描述由Holger Dammertz给出。 该序列又基于van der Corput序列 ,该序列使用相对于小数点的十进制小数的特殊二进制转换。

使用棘手的按位算术技巧,您可以在着色器中直接直接有效地设置van der Corpute序列,并根据该序列从选择中创建Hammersley序列的第i个元素。 N 项目:

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

Hammersley()从多个大小的样本返回低失配序列的第i个元素 N
并非所有的OpenGL驱动程序都支持按位操作(例如,WebGL和OpenGL ES 2.0),因此对于某些环境,可能需要使用它们的替代实现:

 float VanDerCorpus(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), VanDerCorpus(i, 2u)); } 

我注意到由于旧硬件中对循环运算符的某些限制,此实现通过所有32位进行。 结果,此版本的效率不如第一个选项高-但它可以在任何硬件上运行,甚至在没有位操作的情况下也可以运行。

GGX模型中的重要性样本


代替了半球内生成的样本矢量的均匀或随机(Monte Carlo)分布 \欧 ,它出现在我们要求解的积分中,我们将尝试创建矢量,使它们趋向于光的主要反射方向,该矢量的特征是微表面的中值矢量,并取决于表面粗糙度。 采样过程本身将与之前考虑的过程类似:打开一个具有足够大量迭代的循环,创建一个低失配序列的元素,基于此我们在切线空间中创建一个采样矢量,将该矢量传递到世界坐标并使用场景的能量亮度进行采样。 原则上,这些更改仅与以下事实有关:现在,低失配序列的一个元素用于指定新的样本向量:

 const uint SAMPLE_COUNT = 4096u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); 

另外,为了完全形成样本矢量,必须以某种方式将其定向在与给定粗糙度水平相对应的镜瓣方向上。 您可以从理论课上学习 NDF(正态分布函数),并与GGX NDF结合使用,以在作者Epic Games领域中指定样本矢量的方法:

 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); //       vec3 H; Hx = cos(phi) * sinTheta; Hy = sin(phi) * sinTheta; Hz = cosTheta; //        vec3 up = abs(Nz) < 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 * Hx + bitangent * Hy + N * Hz; return normalize(sampleVec); } 

结果是对于给定的粗糙度和低失配Xi序列的元素,样本矢量近似沿着微表面的中值矢量定向。 请注意,Epic Games根据迪士尼在PBR方法上的原始工作,使用粗糙度值的平方来获得更好的视觉质量。

完成Hammersley序列和样本矢量生成代码的实现后,我们可以给出预过滤和卷积着色器代码:

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


我们基于一些给定的粗糙度对环境图进行初步过滤,该粗糙度的级别随所得立方图的每个mip级别(从0.0到1.0)变化,并且过滤结果存储在prefilteredColor变量中接下来,将变量除以整个样品的总重量,对最终结果贡献较小的样品(具有较低的NdotL)也使总重量的增加较少。

将预过滤数据保存为MIP级别


仍然需要编写直接指示OpenGL过滤各种粗糙度级别的环境图,然后将结果保存在目标立方图的一系列mip级别中的代码。在这里,从课程中已经准备好的有关辐照图计算的代码会派上用场

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

该过程类似于辐照图的卷积,但是这次您应该在每个步骤中指定帧缓冲区的大小,将其减小一半以匹配mip级别。另外,必须将当前将执行渲染的mip级别指定为glFramebufferTexture2D()函数的参数

执行此代码的结果应该是一个三次方贴图,其中包含每个后续Mip级别的反射越来越模糊的图像。您可以将立方图用作skybox的数据源,并从低于零的任何mip级别获取样本:

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

此操作的结果将如下图所示:


它看起来像一个非常模糊的源环境图。如果结果相似,则很可能正确执行了预过滤HDR环境图的过程。尝试使用不同的Mip级别的样本进行实验,并观察每个下一个级别的模糊程度逐渐增加。

预过滤器卷积伪像


对于大多数任务,所描述的方法效果很好,但是迟早您将不得不面对预过滤过程生成的各种工件。这是最常见的处理方法。

三次方地图接缝的表现


从初步筛选器处理的立方图中选择具有高粗糙度的表面的值会导致从更接近其链末端的mip级别读取数据。从立方图采样时,默认情况下,OpenGL不会在立方图的面之间进行线性插值。由于较高的Mip级别具有较低的分辨率,并且考虑到较大的镜瓣而对环境图进行了卷积,因此在面部之间不存在纹理过滤情况变得很明显:


幸运的是,OpenGL能够通过一个简单的标志来激活此过滤:

 glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); 

只需在应用程序初始化代码中的某个位置设置标志,就可以消除此工件。

出现亮点


由于一般情况下的镜面反射包含高频细节以及亮度差异很大的区域,因此其卷积需要使用大量采样点来正确考虑环境中HDR反射内部值的较大分散。在示例中,我们已经采集了足够多的样本,但是对于某些场景和高粗糙度的材料来说,这仍然是不够的,您将在明亮区域周围看到许多斑点的外观:


您可以进一步增加样本数量,但这不是通用的解决方案,在某些情况下,仍然会出现伪像。但是您可以使用Chetan Jags方法,该方法可以减少工件的表现。为此,在初步卷积阶段,不是直接从环境图中进行选择,而是根据从被积物的概率分布函数和粗糙度获得的值,从其mip级别之一进行选择:

 float D = DistributionGGX(NdotH, roughness); float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001; //        float resolution = 512.0; float saTexel = 4.0 * PI / (6.0 * resolution * resolution); float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001); float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 

只需记住为环境贴图启用三线性过滤即可成功地从MIP级别中进行选择:

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

另外,不要忘了使用OpenGL直接为纹理创建Mip级别,仅在完全形成主Mip级别之后:

 //  HDR      ... [...] //  - glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glGenerateMipmap(GL_TEXTURE_CUBE_MAP); 

这种方法出奇地好,即使在高粗糙度下也可以去除几乎所有(经常是所有)滤波后的图点。

BRDF的初步计算


因此,我们已经成功地使用过滤器处理了环境图,现在我们可以以代表BRDF的单独总和的形式专注于近似的第二部分。要刷新内存,请再次查看近似解决方案的完整记录:

大号Óp ω ö= &Integral; Ω大号p ω ð ω * &Integral; Ω ˚F - [R p ω ω öñ &CenterDot;&ω ð ω


我们初步计算了总和的左侧部分,并在单独的立方图中记录了各种粗糙度水平的结果。右侧将需要BDRF表达式的卷积以及以下参数:angleÑ &CenterDot;&ω ,表面粗糙度和菲涅耳系数F 0 该过程类似于为完全白色的环境或恒定的能量亮度集成镜像的BRDF L i = 1.0 将BRDF转换为三个变量不是一件容易的事,但是在这种情况下 F 0可以从描述镜像BRDF的表达式中得出:

&Integral; Ω ˚F- [R pωωöñ&CenterDot;&ωdω=&Integral; Ω ˚F- [R pωωö ˚F ω Òħ ˚F ω Òħ ñ&CenterDot;&ωðω


在这里 F是描述菲涅耳集的计算的函数。将除数移动到BRDF的表达式中,可以转到以下等效的表示法:

&Integral; Ω ˚F - [R p ω ω ö˚F ω Òħ ˚FωÒħñ&CenterDot;&ωðω


替换正确的条目 在菲涅尔-施里克(Fresnel-Schlick)近似上的 F得到:

&Integral; Ω ˚F - [R p ω ω ö˚F ω Òħ ˚F0+1-˚F01-ωöħ5ñωðω


表示表达式 1 - ω öħ 5 怎么 / a l p h a简化关于F 0

Ωfr(p,ωi,ωo)F(ωo,h)(F0+(1F0)α)nωidωi


Ωfr(p,ωi,ωo)F(ωo,h)(F0+1αF0α)nωidωi


Ωfr(p,ωi,ωo)˚F ω Òħ ˚F0*1-α+αñ&CenterDot;&ωðω


下一个功能 我们将 F分为两个整数:

&Integral; Ω ˚F - [R p ω ω ö˚F ω Òħ ˚F0*1-αñ&CenterDot;&ωðω+&Integral;Ω˚F- [R pωωö˚F ω Òħ αñ&CenterDot;&ωðω


这样 F 0在积分下将是常数,我们可以将其从积分的符号中取出。接下来,我们将揭示将α放入原始表达式中,并获得BRDF的最终条目作为单独的和:

F0Ωfr(p,ωi,ωo)(1(1ωoh)5)nωidωi+Ωfr(p,ωi,ωo)(1ωoh)5nωidωi


结果两个积分表示比例和值的偏移量 F 0分别。注意˚F p ω ω ö包含条目F,因为这些出现相互抵消并从表达式中消失。使用已经开发的方法,BRDF卷积可以与输入数据一起进行:矢量之间的粗糙度和角度

ñW的Ø将结果写在2D纹理- 卡络合BRDFBRDF整合地图),其将作为在最终着色器使用辅助表值,这便形成了间接镜面照明的最终结果。

BRDF卷积着色器直接在平面上工作,使用二维纹理坐标作为卷积过程的输入参数(NdotV粗糙度)。该代码明显类似于预过滤的卷积,但是在此示例向量是在考虑几何函数BRDF和菲涅尔-施里克近似表达式的情况下进行处理的:

 vec2 IntegrateBRDF(float NdotV, float roughness) { vec3 V; Vx = sqrt(1.0 - NdotV*NdotV); Vy = 0.0; Vz = 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(Lz, 0.0); float NdotH = max(Hz, 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的卷积实现为上述数学计算的几乎字面形式。输入粗糙度和角度的输入参数。θ,根据样本的重要性形成样本矢量,并使用几何函数和转换后的BRDF菲涅耳表达式进行处理。结果,对于每个样本,值的缩放幅度和位移F 0,最后将其平均并以vec2形式返回理论课中,提到在计算IBL的情况下,BRDF的几何成分略有不同,因为系数

k的指定方式不同:

k d i r e c t = α + 1 28


ķ 大号 = α 22


由于在计算IBL的情况下,卷积BRDF是积分解的一部分,因此我们将使用系数 k I B L用于在Schlick-GGX模型中计算几何函数:

 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; } // ---------------------------------------------------------------------------- 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粗糙度参数不是平方,这是在应用此参数的其他地方完成的。我不确定问题出在哪里:是在Epic Games的作品中还是在迪士尼的最初作品中,但是值得一提的是,正是粗糙度值对参数a的直接分配才使BRDF集成图完全相同,这在Epic Games出版物中有所介绍。此外,BRDF卷积结果将以大小为512x512的2D纹理形式保存:



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

根据Epic Games的建议,此处使用16位浮点纹理格式。确保将重复模式设置为GL_CLAMP_TO_EDGE,以避免从边缘采样伪像。

接下来,我们使用相同的帧缓冲区对象,并在全屏四边形的表面上执行着色器:

 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卷积的结果,我们可以恢复基于单独近似值的近似镜面反射照明的积分计算结果。恢复的值随后将用作间接或背景镜面辐射。

IBL模型中的最终反射率计算


因此,为了获得在反射率的一般表达式中描述间接镜面分量的值,有必要将计算出的近似分量“粘合”为一个单独的整体作为单独的和。首先,将适当的采样器添加到最终着色器中以获取预先计算的数据:

 uniform samplerCube prefilterMap; uniform sampler2D brdfLUT; 

首先,我们通过基于反射向量的预处理环境贴图中的采样来获取表面上的间接镜面反射的值。请注意,此处根据表面粗糙度选择采样等级。对于较粗糙的表面,反射将更加模糊

 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级别(从零到第四),常量MAX_REFLECTION_LOD用于限制从生成的mip级别中进行选择。

接下来,我们根据法线和视图方向之间的粗糙度和角度从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); 

从地图获得的值包含该值的缩放比例和位移因子 F 0(这里取值F-菲涅耳系数)。然后将转换后的值F与从预过滤图获得的值进行组合,以获得原始积分表达式-specular的近似解因此,我们获得了反射率表示部分的解决方案,该部分负责镜面反射。对于一个完整的解决方案模型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; 

我注意到,镜面反射未乘以kS,因为它已经包含菲涅耳系数。

让我们用一组熟悉的球体运行我们的测试应用程序,这些球体具有不断变化的金属性和粗糙度特征,并看看它们在PBR的全部方面的外观:


您甚至可以走得更远,下载与PBR模型相对应的一组纹理,并从真实材料中获取球体


甚至可以从Andrew Maximov下载精美的模型以及准备好的PBR纹理


我认为您不必说服任何人当前的照明模型看起来更具说服力。而且,无论环境如何,照明在物理上看起来都是正确的。下面使用了几个完全不同的HDR环境图,这些图完全改变了灯光的性质-尽管您不必调整模型中的任何参数,但所有图像在物理上都是可靠的! (原则上,这种简化材料工作是PBR管道的主要优点,并且可以说获得更好的效果是令人愉悦的结果。请注意。


h,我们进入PBR渲染器实质的过程非常艰巨。我们通过一系列步骤得出了结果,当然,第一种方法可能会出错。因此,对于任何问题,我建议您仔细了解单色纹理球体的示例代码(当然还有着色器代码!)。或在评论中寻求建议。

接下来是什么?


我希望通过阅读这些内容,您已经对PBR渲染模型的工作有所了解,并弄清并成功启动了测试应用程序。在这些课程中,我们在主渲染周期之前为应用程序中的PBR模型计算了所有必要的辅助纹理贴图。对于培训任务,此方法是合适的,但不适用于实际应用。首先,这种初步准备应该发生一次,而不是每个应用程序都启动。其次,如果您决定添加更多的环境图,则还必须在启动时对其进行处理。如果再添加几张卡怎么办?真正的雪球。

这就是为什么通常情况下先准备辐射图和预处理后的环境图,然后将其保存到磁盘上的原因(BRDF集成图不依赖于环境图,因此通常可以计算或下载一次)。因此,您将需要一种格式来存储HDR立方卡,包括其mip级别。好吧,或者您可以使用一种广泛使用的格式来存储和加载它们(因此.dds支持保存Mip级别)。

另一个重要点:为了在这些课程中深入了解PBR管道,我描述了准备PBR渲染的完整过程,包括对IBL辅助卡的初步计算。但是,在实践中,您也可以使用为您准备这些卡的出色实用程序之一:例如cmftStudioIBLBaker

我们也没有考虑准备立方图的过程中反射样品反射探头)以及立方图插值和视差校正的相关过程。简而言之,该技术可以描述如下:我们在场景中放置许多反射样本对象,这些对象以立方图的形式形成局部环境图像,然后在其基础上形成IBL模型所需的所有辅助图。通过根据距相机的距离对来自多个样本的数据进行插值,您可以基于图像获得非常详细的照明,其质量实际上仅受我们准备放置在场景中的样本数量的限制。例如,从明亮的街道移到某个房间的黄昏时,此方法可让您正确更改照明。将来我可能会写一堂关于反射测试的课程,但是,目前,我只能推荐下面的Chetan Jags文章进行审核。

(样品的落实,更可以在发动机的原始教程笔者发现在这里prim.per。

附加材料


  1. 虚幻引擎4中的真实阴影:对Epic Games通过分割总和近似化镜像组件的表达式的方法的说明。基于本文,编写了IBL PBR课程的代码。
  2. 基于物理的阴影和基于图像的照明:一篇出色的文章,描述了在PBR交互式管道应用程序中包括IBL镜像分量计算的过程。
  3. 基于图像的照明:有关镜面反射IBL和相关问题(包括光探针插值问题)的冗长而详细的文章。
  4. Moving Frostbite to PBR : , PBR «AAA».
  5. Physically Based Rendering – Part Three : , IBL PBR JMonkeyEngine.
  6. Implementation Notes: Runtime Environment Map Filtering for Image Based Lighting : HDR , .

Source: https://habr.com/ru/post/zh-CN429744/


All Articles