
基于图像或
IBL (
基于图像的照明 )的照明是一类照明方法,它不基于分析光源(在
上一课中进行了讨论),而是将照明对象的整个环境视为一个连续光源。 在一般情况下,此类方法的技术基础在于处理环境的立方图(在现实世界中准备或在三维场景的基础上创建),以便存储在地图中的数据可直接用于照明计算:实际上,立方图的每个纹理像素都被视为光源。 通常,这使您可以捕获场景中全局照明的效果,这是传达当前场景的整体“色调”并帮助更好地“嵌入”照明对象的重要组件。
由于IBL算法考虑了某个“全局”环境中的照明,因此其结果被认为是对背景照明的更精确模拟,甚至是对全局照明的非常粗略的近似。 从结合到PBR模型的角度来看,这方面使IBL方法变得有趣,因为在照明模型中使用环境光可以使对象看起来在物理上更加正确。
为了将IBL的影响纳入已经描述的PBR系统中,我们返回熟悉的反射率方程:
Lo(p, omegao)= int\极限 Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
如前所述,主要目标是计算所有入射辐射方向的积分
wi 半球
\欧米茄 。 在
上一课中,积分
的计算并不繁琐,因为我们预先知道了光源的数量,因此,我们知道了与它们对应的所有几个光入射方向。 同时,积分不能迅速解决:
任何下降向量
wi 从环境中可以携带非零能量的亮度。 因此,对于该方法的实际适用性,需要满足以下要求:
- 您需要想出一种方法来获取任意方向向量的场景能量亮度 wi ;
- 积分的求解必须实时发生。
好吧,第一点是自己解决的。 一种解决方案的提示已经在这里溜走了:表示场景或环境辐射的一种方法是经过特殊处理的立方图。 这样的映射中的每个纹理像素可以被视为单独的发射源。 通过根据任意向量从此类映射中采样
wi 我们很容易获得该方向上场景的能量亮度。
因此,我们获得了任意矢量的场景能量亮度
wi :
vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;
然而,值得注意的是,求解积分需要我们从环境地图中提取样本,而不是从一个方向,而是从半球中的所有可能样本中提取样本。 如此-对于每个阴影片段。 显然,对于实时任务,这实际上是不可行的。 一种更有效的方法是即使在我们的应用程序之外,也要预先计算部分被积运算。 但是为此,您将不得不袖手旁观,并深入探究反射率表达的本质:
Lo(p, omegao)= int\极限 Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
可以看出,与扩散相关的部分
kd 和镜子
ks BRDF组件是独立的。 您可以将积分分为两部分:
Lo(p, omegao)= int\极限 Omega(kd fracc pi)Li(p, omegai)n cdot omegaid omegai+ int\极限 Omega(ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
这样划分的部分将使我们能够分别处理每个部分,在本课程中,我们将处理负责漫射照明的部分。
分析了扩散分量上的积分形式后,我们可以得出结论,朗伯扩散分量本质上是常数(颜色
s 折光率
kd 和
pi 在被积数条件下是常数),并且不依赖于其他变量。 鉴于这一事实,我们可以将常量放在积分的符号之外:
Lo(p, omegao)=kd fracc pi int\极限 OmegaLi(p, omegai)n cdot omegaid omegai
所以我们得到一个积分,仅取决于
wi (假设
p 对应于环境立方图的中心)。 根据此公式,您可以计算甚至更好地预先计算一个新的三次方图,该图存储了针对样本每个方向的漫射分量积分(或纹理图)的计算结果。
wo 使用卷积运算。
卷积是对数据集中的每个元素进行某种计算,同时考虑到数据集中所有其他元素的数据的操作。 在这种情况下,此类数据是场景或环境图的能量亮度。 因此,要计算三次方图中每个样本方向的值,我们必须考虑取自样本点周围半球中样本所有其他可能方向的值。
要对环境图进行卷积,您需要为每个样本得出的方向求解积分
wo 通过沿方向执行多个离散样本
wi 属于半球
\欧米茄 ,然后平均总能量亮度。 半球,在此基础上获取采样方向
wi 沿矢量取向
wo 表示要为其计算当前卷积的目标方向。 看一下图片以获得更好的理解:
这种预先计算的三次方图,用于存储样本每个方向的积分结果
wo 也可以视为存储将入射在沿方向定向的某个表面上的场景中所有间接漫射照明求和的结果求和
wo 。 换句话说,此类立方贴图称为辐照度贴图,因为卷积前的立方环境贴图使您可以直接从任意方向采样场景照射的幅度
wo ,无需进行其他计算。
确定能量亮度的表达式还取决于采样点的位置 p 我们正好躺在辐射图的中心。 在所有间接漫射照明的源也将是单个环境图的意义上,此假设施加了限制。 在灯光异质的场景中,这可能会破坏现实的幻觉(尤其是在室内场景中)。 现代渲染引擎通过在场景反射探测器中放置特殊的辅助对象来解决此问题。 每个这样的对象都从事一项任务:它针对其周围环境形成自己的辐照图。 使用这种技术,可以在任意点照射(和能量亮度) p 将由最近的反射样本之间的简单插值确定。 但是对于当前的任务,我们同意环境地图是从其中心采样的,我们将在进一步的课程中分析反射样本。
下面是环境的三次方图和从中得出的辐照图(基于
波引擎 )的示例,该图对每个输出方向的环境能量亮度进行平均
wo 。
因此,此卡将卷积结果存储在每个纹理像素中(对应于方向
wo ),从外观上看,这样的地图看起来就像存储环境地图的平均颜色。 从该图向任何方向的样本将返回从该方向发出的辐射值。
PBR和HDR
在
上一课中 ,已经简要地指出,对于PBR照明模型的正确操作,考虑到存在的光源的HDR亮度范围非常重要。 由于输入端的PBR模型基于非常具体的物理量和特性以一种或另一种方式接受参数,因此逻辑上要求光源的能量亮度与其实际原型相匹配。 无论如何证明每种光源的辐射通量的值都没有关系:进行粗略的工程估算或转换为
物理量 -在任何情况下,室内灯和太阳之间的特性差异都将是巨大的。 如果不使用
HDR范围,将无法准确确定各种光源的相对亮度。
因此,PBR和HDR永远是朋友,这是可以理解的,但是这个事实与基于图像的照明方法有什么关系? 在上一课中,我们证明了将PBR转换为HDR渲染范围很容易。 还有一个“但是”:由于来自环境的间接照明是基于环境的三次方图,因此需要一种在环境图中保留此背景照明的HDR特性的方法。
到目前为止,我们一直使用以LDR格式创建的环境地图(例如
skyboxes )。 我们使用它们中的颜色样本照原样进行渲染,这对于直接阴影对象是完全可以接受的。 当使用环境图作为物理可靠的测量源时,这是完全不合适的。
RGBE-HDR图像格式
熟悉RGBE图像文件格式。 扩展名为“
.hdr ”的文件用于存储动态范围很宽的图像,为三色一组的每个元素分配一个字节,为公共指数分配一个字节。 该格式还允许您存储颜色强度范围超出LDR范围[0.,1.]的立方环境贴图。 这意味着光源可以保持其真实强度,由这种环境图表示。
该网络有很多RGBE格式的免费环境地图,可以在各种实际条件下拍摄。 这是
sIBL存档站点中的示例:
您可能会对看到的结果感到惊讶:毕竟,此扭曲的图像根本看起来不像是一张具有明显分解成6个面的规则立方图。 解释很简单:环境图是从球体投影到平面上的-应用了
等距矩形扫描 。 这样做是为了能够以不支持立方卡的存储模式的格式原样存储。 当然,这种投影方法有其缺点:水平分辨率远高于垂直分辨率。 在大多数渲染应用中,这是可以接受的比率,因为通常有趣的环境和照明细节完全位于水平面,而不位于垂直平面。 好吧,加上所有内容,我们需要将转换代码返回到立方图。
在stb_image.h中支持RGBE格式
自行下载此图像格式需要了解
格式规范 ,这并不困难,但仍然很费力。 对
我们来说幸运的是,在单个头文件中实现的
stb_image.h图像加载库支持加载RGBE文件,返回浮点数数组-我们需要达到目的! 在您的项目中添加一个库,加载图像数据非常简单:
#include "stb_image.h" [...] stbi_set_flip_vertically_on_load(true); int width, height, nrComponents; float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0); unsigned int hdrTexture; if (data) { glGenTextures(1, &hdrTexture); glBindTexture(GL_TEXTURE_2D, hdrTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); 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); stbi_image_free(data); } else { std::cout << "Failed to load HDR image." << std::endl; }
该库自动将值从内部HDR格式转换为常规的真实32位数字,默认情况下具有三个颜色通道。 将原始HDR图像的数据保存为普通的2D浮点纹理就足够了。
将等角度扫描转换为立方图
可以使用相等的矩形扫描从环境图直接选择样本,但是,这将需要昂贵的数学运算,而从法线立方图进行获取实际上将没有性能。 正是基于这些考虑,在本课程中,我们将处理将相等的矩形图像转换为立方图的过程,稍后再使用。 但是,此处还将显示使用三维矢量从等角矩形图中直接采样的方法,以便您可以选择适合自己的工作方法。
要进行转换,您需要绘制一个单位大小的立方体,从内部对其进行观察,在其面上投影等距矩形图,然后从这些面中提取六张图像作为立方体图的面。 此阶段的顶点着色器非常简单:它仅按原样处理多维数据集的顶点,并将它们的未变形位置传递给片段着色器以用作三维样本矢量:
#version 330 core layout (location = 0) in vec3 aPos; out vec3 localPos; uniform mat4 projection; uniform mat4 view; void main() { localPos = aPos; gl_Position = projection * view * vec4(localPos, 1.0); }
在片段着色器中,我们对立方体的每个面都进行着色,就好像我们试图用一张具有相等矩形贴图的图纸轻轻地包裹立方体一样。 为此,采用转移到片段着色器的样本方向,并通过特殊的三角魔术方法进行处理,最后,从等矩形贴图进行选择,就好像它实际上是三次贴图一样。 选择结果将直接保存为立方体面片段的颜色:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform sampler2D equirectangularMap; const vec2 invAtan = vec2(0.1591, 0.3183); vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(vz, vx), asin(vy)); uv *= invAtan; uv += 0.5; return uv; } void main() { // localPos vec2 uv = SampleSphericalMap(normalize(localPos)); vec3 color = texture(equirectangularMap, uv).rgb; FragColor = vec4(color, 1.0); }
如果实际上使用此着色器和关联的HDR环境贴图绘制一个多维数据集,则会得到以下内容:
即 可以看出,实际上我们将矩形纹理投影到了立方体上。 太好了,但这如何帮助我们创建真实的立方图? 要结束此任务,有必要使用摄像机看着每个面将同一个立方体渲染6次,同时将输出写入单独的
帧缓冲区对象:
unsigned int captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
当然,我们不会忘记组织用于存储未来立方图的六个面中每个面的内存:
unsigned int envCubemap; glGenTextures(1, &envCubemap); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); for (unsigned int i = 0; i < 6; ++i) {
进行此准备后,仅需直接在立方贴图的边缘进行等矩形贴图的各个部分的转移。
我们不会赘述过多,尤其是因为代码重复了在
帧缓冲区和
全向阴影中的课程中看到的内容。 原则上,这一切都归结为准备六个单独的视图矩阵,将相机严格地定向到立方体的每个面,以及一个特殊的投影矩阵,其视角为90°以捕获立方体的整个面。 然后,仅执行六次渲染,并将结果保存在浮点帧缓冲区中:
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)) };
此处,附加了帧缓冲区的颜色,并交替更改三次贴图的连接面,这导致将渲染直接输出到环境贴图的一个面。 该代码只需执行一次,之后,我们将
获得完整的
envCubemap环境图,其中包含转换HDR环境图的原始等矩形版本的结果。
我们将通过绘制最简单的skybox着色器来测试生成的立方贴图:
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 projection; uniform mat4 view; out vec3 localPos; void main() { localPos = aPos; // mat4 rotView = mat4(mat3(view)); vec4 clipPos = projection * rotView * vec4(localPos, 1.0); gl_Position = clipPos.xyww; }
请注意
clipPos向量的组成部分的
技巧 :在记录顶点的变换坐标时,我们使用
xyww四元组来确保天空盒的所有片段的最大深度为1.0(该方法已在
相应的课程中使用 )。 不要忘记将比较函数更改为
GL_LEQUAL :
glDepthFunc(GL_LEQUAL);
片段着色器仅从三次贴图选择:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; void main() { vec3 envColor = texture(environmentMap, localPos).rgb; envColor = envColor / (envColor + vec3(1.0)); envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0); }
从地图上进行的选择基于立方体顶点的内插局部坐标,在这种情况下,这是选择的正确方向(同样,在天空盒的课程中对此进行了讨论,
大约为Per。 )。 由于忽略了视图矩阵中的传输分量,因此天空盒的渲染将不依赖于观察者的位置,从而产生了无限远的背景的幻觉。 由于这里我们直接将数据从HDR卡输出到默认的帧缓冲区(即LDR接收器),因此有必要调出音调压缩。 最后,几乎所有HDR卡都存储在线性空间中,这意味着必须将
伽玛校正用作最终处理和弦。
因此,当输出获得的天空盒以及已经熟悉的球体数组时,将获得类似的结果:
好吧,我们花了很多精力,但是最后我们成功地习惯了读取HDR环境图,将其从等边线转换为立方图,然后将HDR立方图输出为场景中的天空盒。 此外,用于通过渲染立方图的六个面来转换为立方图的代码对于我们在
环境图卷积的任务中进一步有用。 整个转换过程的代码在
此处 。
三次卡的卷积
正如本课开始时所说的,我们的主要目标是要解决间接漫射照明所有可能方向的积分问题,同时要考虑到给定的环境辐射(以立方环境图的形式)。 众所周知,我们可以获得场景能量亮度的值
L(p,wi) 对于任意方向
wi 通过从HDR采样该方向的环境立方图。 为了解决积分问题,有必要从半球所有可能的方向采样场景的能量亮度
\欧米茄 每个审查的片段。
显然,从半球的所有可能方向采样来自环境的照明的任务
\欧米茄 在计算上是不可行的-这样的方向是无限的。 但是,有可能通过采取有限数量的随机或均匀分布在半球内部的方向来应用近似值。
这将使我们能够获得与真实辐照相当好的近似值,本质上以有限和的形式解决了我们感兴趣的积分。但是对于实时任务,甚至仍然难以置信地采用这种方法,因为每个片段都需要采样,并且采样数必须足够高才能获得可接受的结果。因此,最好在渲染过程之外为该步骤提前准备数据。由于半球的方向决定了我们从哪个空间区域捕获辐射,因此可以根据所有可能的出射方向预先计算半球每个可能方向的辐射W的Ø :
大号Ó(p ,ω ö)= ķ d Çπ&Integral;Ω大号我(p,ω我)ñ&CenterDot;&ω我ðω我
结果,对于给定的任意向量 瓦特我,我们可以进行采样predrasschitannoy辐照卡的,以便获得对这个方向上的漫辐照度的值。为了确定当前碎片点处的间接漫射辐射的大小,我们从沿碎片表面法线方向定向的半球获取总辐射。换句话说,获得场景的辐照归结为一个简单的选择: vec3 irradiance = texture(irradianceMap, N);
此外,为了创建辐照图,有必要对环境图进行卷积,将其转换为立方图。我们知道,对于每个碎片,其半球都被认为沿表面法线定向ñ 。
在这种情况下,三次方图的卷积被减少以计算所有方向的平均能量亮度 w ^ 我半球内Ω沿着法线方向ñ :
幸运的是,我们在课程开始时所做的耗时的初步工作现在可以非常轻松地在特殊的片段着色器中将环境图转换为立方图,其输出将用于形成新的立方图。为此,用于将等矩形环境图转换为立方图的那段代码非常有用。仍然仅需采用另一个处理着色器: #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main() { // vec3 normal = normalize(localPos); vec3 irradiance = vec3(0.0); [...] // FragColor = vec4(irradiance, 1.0); }
此处,environmentMap采样器是先前从等边关系派生的环境的HDR立方图。卷积环境图的方法有很多,在这种情况下,对于立方图的每个纹理元素,我们将生成几个半球样本矢量Ω沿样品方向定向,并对结果求平均值。样本向量的数量将是固定的,向量本身将在半球内均匀分布。我注意到被积数是一个连续函数,对此函数的离散估计仅是一个近似值。而且,我们采用的采样向量越多,我们将越接近积分的解析解。反射率表达式的积分取决于立体角d w-使用起来不太方便的值。而不是整合立体角d w改变表达式,导致球坐标上的积分θ 和
ϕ :
角度Phi将代表半球底部平面中的方位角,范围从0到 2个π 。
角度角度 θ代表仰角,从0到1个2个π 。
反射率的修改表达式如下:大号Ó(p ,φ ö,θ ö)= ķ d Çπ ∫ 2 π φ = 0 ∫ 12 πθ=0大号我(p,φ我,θ我)COS(θ)罪(θ)dφdθ
这种积分的解决方案将需要在半球中抽取有限数量的样本 Ω和平均的结果。知道样本数n 1 和
对于每个球坐标 n 2,我们可以将积分转换为黎曼和:大号Ó(p ,φ ö,θ ö)= ķ d Çπ 1Ñ 1 ñ 2 Ñ 1 &Sigma; φ=0 Ñ 2 &Sigma; θ=0大号我(p,φ我,θ我)COS(θ)罪(θ)dφdθ
由于两个球面坐标都离散地变化,因此,如上图所示,在每个时刻,都在半球中以一定的平均面积进行采样。由于球面的性质,离散采样区域的大小不可避免地会随着仰角的增加而减小θ并接近天顶。为了补偿这种减小面积的效果,我们在表达式中增加了权重系数š 我Ñ θ 。
结果,基于每个片段的球面坐标以代码形式在半球中执行离散采样的方法如下: vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, 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));
变量sampleDelta决定沿半球表面的离散台阶的大小。通过更改此值,可以增加或减少结果的准确性。在两个周期内,规则的3维样本矢量由球坐标形成,从切线转移到世界空间,然后用于从HDR采样立方环境图。样品的结果存储在辐照度变量中,在处理结束时将其除以所获得的样品数,以获得辐照度的平均值。请注意,从纹理采样的结果由两个量调制:cos(theta) -考虑到大角度光的衰减,以及sin(theta)-补偿接近天顶时样品面积的减少。剩下的只是处理渲染和捕获envCubemap环境图卷积结果的代码。首先,创建一个立方体贴图来存储辐射(在进入主渲染周期之前,您需要做一次): 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);
由于辐照图是通过对环境图的能量亮度的均匀分布样本求平均而获得的,因此它实际上不包含高频部分和元素-相当小的分辨率纹理(此处为32x32)和启用的线性滤波就足以存储它。接下来,将捕获帧缓冲区设置为此分辨率: glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
捕获卷积结果的代码类似于将环境图从等边转移到三次的代码,仅使用了卷积着色器: irradianceShader.use(); irradianceShader.setInt("environmentMap", 0); irradianceShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
完成此阶段后,我们将在手上获得预先计算的辐照图,可直接用于计算间接漫射照明。要检查卷积如何进行,我们将尝试使用辐射图替换环境图中的天空盒纹理:结果,如果您看到的东西看起来像一张非常模糊的环境图,那么很可能卷积就成功了。PBR和间接照明
所得的辐照图在反射率的除法表达式的扩散部分中使用,代表间接照明所有可能方向的累加贡献。由于在这种情况下,光线不是来自特定光源,而是来自整个环境,因此我们将漫反射和镜面间接照明视为背景(环境),代替了以前使用的恒定值。首先,请不要忘记添加带有辐射图的新采样器: uniform samplerCube irradianceMap;
拥有一个辐照图来存储有关场景和垂直于表面的间接漫射辐射的所有信息,获取有关特定片段辐照的数据就像从纹理中制作一个样本一样简单: // vec3 ambient = vec3(0.03); vec3 ambient = texture(irradianceMap, N).rgb;
但是,由于间接辐射同时包含漫反射和反射镜分量的数据(如我们在反射率表达式的分量版本中所见),因此我们需要以特殊方式调制漫射分量。与上一课一样,我们使用菲涅耳表达式确定给定表面的光反射程度,从而获得光的折射程度或扩散系数: vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
由于背景照明从半球的所有方向落在表面法线的基础上 N,无法确定唯一的中位数(中途)向量来计算菲涅耳系数。为了在这种条件下模拟菲涅耳效应,必须基于法线和观测矢量之间的角度来计算系数。但是,较早之前,作为计算菲涅耳系数的参数,我们使用了基于微表面模型并取决于表面粗糙度而获得的中值矢量。由于在这种情况下,粗糙度不包括在计算参数中,因此,总是会高估表面对光的反射程度。整体而言,间接照明应与直接照明具有相同的行为,即从粗糙的表面,我们期望边缘处的反射率较低。但是由于不考虑粗糙度,然后根据菲涅耳进行间接照明的镜面反射程度在粗糙的非金属表面上看起来是不现实的(在下图中,为了更清楚起见,夸大了所描述的效果):您可以通过在Fremlin-Schlick表达式中引入粗糙度来避免这种麻烦,该表达式由SébastienLagarde描述: vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); }
给定计算菲涅尔集时的表面粗糙度,用于计算背景分量的代码采用以下形式: vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
事实证明,基于图像的照明的使用本质上可以归结为立方图中的一个样本。所有困难主要与环境图的初步准备和转移到辐射图有关。从一堂关于分析光源的熟悉场景中学习,该光源包含一系列具有不同金属度和粗糙度的球体,并添加来自环境的漫射背景光,您将获得以下内容:看起来仍然很奇怪,因为具有高金属度的材料仍然需要反射才能真正看起来像金属(毕竟金属不会反射漫射照明)。并且在这种情况下,唯一的反射是从点分析光源获得的。但是,我们已经可以说球体看起来更加沉浸在环境中(在切换环境贴图时尤其明显),因为这些表面现在可以正确地响应场景环境的背景照明。本课程的完整源代码在这里。。在下一课中,我们将最终处理反射率表示的后半部分,该部分用于间接镜面照明。完成此步骤后,您将真正感受到PBR方法在照明中的强大功能。附加材料
- 编码实验室:基于物理的渲染:PBR模型的介绍以及辐照度图的构造方式及其原因的解释。
- 阴影的数学:ScratchAPixel对本课中使用的一些数学技术的简要概述,尤其是关于极坐标和积分。
PS:我们有一个电报会议,以协调转账。如果您有强烈的帮助翻译的愿望,欢迎您!