IBL,或image based lighting基于图像的照明,是一组照明对象的技术,不是像前一章那样通过直接分析光,而是将周围环境视为一个 big light source大光源。这通常通过操纵立方体贴图环境贴图(取自现实世界或从 3D 场景生成)来完成,以便我们可以直接在我们的照明方程中使用它:将每个立方体贴图纹素视为一个light emitter光发射器。通过这种方式,我们可以有效地捕捉环境的全局光照和总体感觉,让对象在其环境中拥有更好的归属感。

由于基于图像的照明算法捕获某些(全局)环境的照明,因此其输入被认为是更精确的环境照明形式,甚至是全局照明的粗略近似。这使得 IBL 对于 PBR 很有趣,因为当我们考虑环境的光照时,物体看起来在物理上更加准确。

要开始将 IBL 引入我们的 PBR 系统,让我们再次快速浏览一下反射率方程:

如前所述,我们的主要目标是求解半球 Ω 上所有入射光方向 wi 的积分。求解前一章中的积分很容易,因为我们事先知道了对积分有贡献的确切的几个光方向 wi。然而,这一次,来自周围环境的每个入射光方向 wi 都可能具有一些辐射,这使得求解积分变得不那么简单。这给了我们解决积分的两个主要要求:

1.在给定任何方向向量 wi 的情况下,我们需要一些方法来检索场景的辐射度。
2.求解积分需要快速且实时。
现在,第一个要求相对容易。我们已经暗示过了,但是表示环境或场景辐照度的一种方式是采用(处理过的)环境立方体贴图的形式。给定这样一个立方体贴图,我们可以将立方体贴图的每个纹素可视化为一个单一的发射光源。通过使用任何方向向量 wi 对这个立方体贴图进行采样,我们从该方向检索场景的辐射度

给定任何方向向量 wi 的情况下获取场景的radiance辐射度非常简单:

//根据角度获取cubemap的颜色
vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;  

尽管如此,求解积分需要我们不仅从一个方向采样环境贴图,而且还需要从半球 Ω 上的所有可能方向 wi 采样,这对于每个片段着色器调用来说都太昂贵了。 为了以更有效的方式求解积分,我们需要对大部分计算进行预处理或pre-compute预计算。 为此,我们必须更深入地研究反射方程:

仔细看一下反射方程,我们发现 BRDF 的漫反射 kd 和镜面反射 ks 项是相互独立的,我们可以将积分一分为二:

通过将积分分成两部分,我们可以分别关注漫反射项和镜面反射项; 本章的重点是扩散积分。

仔细观察漫射积分,我们发现diffuse lambert term漫射朗伯项是一个常数项(颜色 c、折射率 kd 和 π 在积分上是常数)并且不依赖于任何积分变量。 鉴于此,我们可以将常数项移出扩散积分

这给了我们一个只依赖于 wi 的积分(假设 p 位于环境图的中心)。 有了这些知识,我们可以计算或预先计算一个新的立方体贴图,该立方体贴图存储在每个样本方向(或纹理像素)中,通过卷积获得漫反射积分的结果。

考虑到数据集中的所有其他条目,卷积正在对数据集中的每个条目应用一些计算; 数据集是场景的辐射度或环境图。 因此,对于立方体贴图中的每个采样方向,我们都会考虑半球 Ω 上的所有其他采样方向。

为了对环境图进行integral积分,我们通过在半球 Ω 上discretely离散采样大量方向 wi 并平均它们的辐射来解决每个输出 wo 采样方向的积分。 我们构建样本方向 wi 的半球面向我们正在卷积的输出样本方向。

(理解:公式中,kd,c,pi,都提出来变成常数了,则只有wi积分需要计算了,而wi积分可以直接用一张cubemap来表示)

这个预先计算的立方体贴图,对于每个采样方向 wo 存储积分结果,可以被认为是场景的所有indirect diffuse light间接漫射光的预先计算的总和,这些光照射到沿方向 wo 对齐的某个表面上。 这样的立方体贴图被称为irradiance map辐照度贴图,因为convoluted卷积的立方体贴图有效地允许我们从任何方向 wo 直接采样场景的(预先计算的)辐照度。

辐射方程还取决于位置 p,我们假设它位于辐射图的中心。 这确实意味着所有漫反射间接光都必须来自单个环境贴图,这可能会打破现实的错觉(尤其是在室内)。 渲染引擎通过在整个场景中放置反射探针来解决这个问题,每个反射探针计算自己的周围环境的辐照度图。 这样,位置 p 处的辐照度(和辐射度)是其最近反射探头之间的插值辐照度。 现在,我们假设我们总是从环境贴图的中心对其进行采样。

下面是一个立方体贴图环境贴图及其生成的辐照度贴图(由wave engine波引擎提供)的示例,平均了每个方向 wo 的场景radiance辐照度

通过将convoluted卷积结果存储在每个立方体贴图纹理像素中(在 wo 的方向上),辐照度贴图的显示有点像环境的平均颜色或照明显示。 从此环境贴图中的任何方向采样都将为我们提供场景在该特定方向上的辐照度

1.PBR and HDR (Physic Based Render   and  High Dynamic Range)

我们在前一章中简要介绍了它:在 PBR 管道中考虑场景照明的高动态范围非常重要。由于 PBR 的大部分输入基于真实的物理属性和测量,因此将入射光值与其物理等效值紧密匹配是有意义的。无论我们是对每盏灯的辐射通量进行有根据的猜测,还是使用它们的直接物理等效物,简单的灯泡或太阳之间的差异都是显着的。如果不在 HDR 渲染环境中工作,就不可能正确指定每个灯光的相对强度。

因此,PBR 和 HDR 齐头并进,但它们与基于图像的照明有何关系?我们在上一章中已经看到,让 PBR 在 HDR 中工作相对容易。但是,对于基于图像的照明,我们将环境的间接光强度基于环境立方体贴图的颜色值,我们需要某种方法将照明的高动态范围存储到环境贴图中

到目前为止,我们一直在使用的环境贴图作为立方体贴图(例如用作天空盒)处于低动态范围 (LDR) 中。我们直接使用它们从个人面部图像中获取的颜色值,范围在 0.0 到 1.0 之间,并按原样处理它们。虽然这对于视觉输出可能很好,但当将它们作为物理输入参数时,它就不起作用了。

1.1 The radiance HDR file format (辐射HDR 文件格式)

输入The radiance HDR file format辐射文件格式。 radiance 文件格式(扩展名为 .hdr)存储一个完整的立方体贴图,其中包含所有 6 个面作为浮点数据。 这允许我们指定 0.0 到 1.0 范围之外的颜色值,以赋予灯光正确的颜色强度。 文件格式还使用了一个巧妙的技巧来存储每个浮点值,不是每个通道的 32 位值,而是每个通道 8 位,使用颜色的 alpha 通道作为指数(这确实会损失精度)。 这工作得很好,但需要解析程序将每种颜色重新转换为其等效的浮点数。

有很多辐射 HDR 环境贴图可以从sIBL archive等来源免费获得,您可以在下面看到一个示例:

这可能不是您所期望的,因为图像看起来是扭曲的,并且没有显示我们之前看到的环境贴图的 6 个单独的立方体贴图面中的任何一个。 此环境贴图从球体投影到平面上,这样我们就可以更轻松地将环境存储到单个图像中,称为 equirectangular 贴图。 这确实有一个小警告,因为大部分视觉分辨率存储在水平视图方向,而在底部和顶部方向保留的较少。 在大多数情况下,这是一个不错的折衷方案,因为几乎所有渲染器都会在水平观察方向上发现大部分有趣的照明和环境。

1.2 HDR and stb_image.h

直接加载 radiance HDR 图像需要一些文件格式知识,这并不太难,但也很麻烦。 幸运的是,流行的头库 stb_image.h 支持将辐射 HDR 图像直接加载为an array of floating point values浮点值数组,完全符合我们的需求。 将 stb_image 添加到您的项目后,加载 HDR 图像现在非常简单,如下所示:

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

stb_image.h 自动将 HDR 值映射到浮点值列表:默认情况下,每个通道 32 位和每个颜色 3 个通道。 这就是我们将 equirectangular HDR 环境贴图存储到 2D 浮点纹理中所需的全部内容。

1.3 From Equirectangular to Cubemap (从 Equirectangular 到 Cubemap)

可以直接使用 equirectangular 映射进行environment lookups,但这些操作可能相对昂贵,在这种情况下,直接direct cubemap sample立方体贴图采样的性能更高。 因此,在本章中,我们将首先将 equirectangular 图像转换为立方体贴图以进行进一步处理。 请注意,在此过程中,我们还展示了如何对 equirectangular 地图进行采样,就好像它是 3D 环境地图一样,在这种情况下,您可以自由选择您喜欢的任何解决方案。

将 equirectangular 图像转换为立方体贴图,我们需要渲染一个(单位)立方体并将 equirectangular 映射从内部投影到立方体的所有面上,并为立方体的每个侧面拍摄 6 张图像作为立方体贴图面。 这个立方体的顶点着色器只是简单地渲染立方体并将其本地位置作为 3D 样本向量传递给fragment shader片段着色器

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

对于片段着色器,我们为立方体的每个部分着色,就好像我们将 equirectangular 贴图整齐地折叠到立方体的每一侧一样。 为了实现这一点,我们将片段的sample direction采样方向作为从立方体的局部位置interpolated插值,然后使用这个方向向量和一些三角魔法(spherical to cartesian球面到笛卡尔)对等矩形贴图进行采样,就好像它本身就是一个立方体贴图一样。 我们直接将结果存储到立方体面的片段中,这应该是我们需要做的所有事情:

#version 330 core
out vec4 FragColor;
in vec3 localPos;uniform sampler2D equirectangularMap;const vec2 invAtan = vec2(0.1591, 0.3183);//用数学公式,对球面,进行采样,并转换uv
vec2 SampleSphericalMap(vec3 v)
{vec2 uv = vec2(atan(v.z, v.x), asin(v.y));uv *= invAtan;uv += 0.5;return uv;
}void main()
{       //对球面,进行采样vec2 uv = SampleSphericalMap(normalize(localPos)); // make sure to normalize localPosvec3 color = texture(equirectangularMap, uv).rgb;FragColor = vec4(color, 1.0);
}

如果在给定 HDR equirectangular 贴图的情况下在场景中心渲染一个立方体,您将得到如下所示的内容:

这表明我们有效地将 equirectangular 图像映射到立方体,但还不能帮助我们将源 HDR 图像转换为立方体贴图纹理。 为了实现这一点,我们必须渲染同一个立方体 6 次,查看立方体的每个面,同时使用帧缓冲区对象记录其视觉结果:

配置framebuffer:

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

当然,我们随后还会生成相应的立方体贴图颜色纹理,为其 6 个面中的每一个面预分配内存:

//初始化cubemap
unsigned int envCubemap;
glGenTextures(1, &envCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
//配置每个面
for (unsigned int i = 0; i < 6; ++i)
{// note that we store each face with 16 bit floating point values// 申明每个面都是浮点数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);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

然后剩下要做的就是将 equirectangular 2D 纹理捕捉到立方体贴图面上

类似之前在framebuffer帧缓冲区和point shadows点阴影章节中讨论的代码详细主题,但它实际上归结为设置 6 个不同的视图矩阵(面向立方体的每一侧),设置一个投影矩阵 一个 90 度的fov视野来捕获整个面部,并渲染一个立方体 6 次,将结果存储在浮点帧缓冲区中:

//配置6个视图朝向fov矩阵
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))
};// convert HDR equirectangular environment map to cubemap equivalent
// 初始化渲染球型环境贴图
equirectangularToCubemapShader.use();
equirectangularToCubemapShader.setInt("equirectangularMap", 0);
equirectangularToCubemapShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrTexture);glViewport(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions.
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
//6个视角,分别进行渲染cubemap的6个面
for (unsigned int i = 0; i < 6; ++i)
{equirectangularToCubemapShader.setMat4("view", captureViews[i]);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);renderCube(); // renders a 1x1 cube
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

我们获取帧缓冲区的颜色附件,并为立方体贴图的每个面切换其纹理目标,直接将场景渲染到立方体贴图的一个面中。 一旦这个例程完成(我们只需要做一次),立方体贴图 envCubemap 应该是我们原始 HDR 图像的立方体贴图环境版本。

让我们通过编写一个非常简单的天空盒着色器来测试立方体贴图来显示我们周围的立方体贴图:


#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)); // remove translation from the view matrixvec4 clipPos = projection * rotView * vec4(localPos, 1.0);gl_Position = clipPos.xyww;
}

请注意这里的 xyww 技巧,它确保渲染的立方体片段的深度值始终以 1.0 结束,即最大深度值,如立方体贴图章节中所述。 请注意,我们需要将深度比较函数更改为 GL_LEQUAL

glDepthFunc(GL_LEQUAL);  

(这两个,是cubemap环境贴图必须配置的2步)

然后片段着色器使用立方体的本地片段位置直接对立方体贴图环境贴图进行采样:

#version 330 core
out vec4 FragColor;in vec3 localPos;uniform samplerCube environmentMap;void main()
{vec3 envColor = texture(environmentMap, localPos).rgb;// hdrenvColor = envColor / (envColor + vec3(1.0));// 伽马矫正envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0);
}

我们使用直接对应于正确采样方向向量的插值顶点立方体位置对环境图进行采样。 看到相机的平移组件被忽略,在立方体上渲染这个着色器应该会给你环境贴图作为一个不移动的背景。 此外,由于我们直接将环境贴图的 HDR 值输出到默认的 LDR 帧缓冲区,我们希望正确地对颜色值进行 tone map the color values色调映射。 此外,几乎所有的 HDR 贴图在默认情况下都在线性色彩空间中,因此我们需要在写入默认帧缓冲区之前应用伽马校正

现在在先前渲染的球体上渲染采样的环境贴图应该如下所示:

嗯...我们花了很多设置才到这里,但我们成功地读取了 HDR 环境贴图,将其从等距矩形映射转换为立方体贴图,并将 HDR 立方体贴图作为天空盒渲染到场景中。 此外,我们建立了一个小系统来渲染立方体贴图的所有 6 个面,在convoluting卷积环境贴图时我们将再次需要它。

2. Cubemap convolution(立方体贴图卷积)

如本章开头所述,我们的主要目标是solve the integral for all diffuse indirect lighting given the scene's irradiance in the form of a cubemap environment map以立方体贴图环境贴图的形式在给定场景辐照度的情况下求解所有漫反射间接照明的积分。我们知道,我们可以通过在方向 wi 上对 HDR 环境图进行采样来获得场景 L(p,wi) 在特定方向上的辐射度。为了解决积分问题,我们必须对每个片段从半球 Ω 内所有可能方向的场景辐射进行采样。

然而,在计算上不可能从每个可能的方向(以 Ω 为单位)对环境的光照进行采样,可能的方向的数量理论上是无限的。然而,我们可以approximate the number of directions by taking a finite number of directions or samples, spaced uniformly or taken randomly from within the hemisphere, to get a fairly accurate approximation of the irradiance; effectively solving the integral ∫ discretely通过从半球内均匀分布或随机抽取有限数量的方向或样本来近似方向的数量,以获得相当准确的辐照度近似值;有效地离散地求解积分∫

然而,实时为每个片段执行此操作仍然太昂贵,因为样本数量需要非常大才能获得良好的结果,因此我们希望预先计算。由于半球的方向决定了我们捕获辐照度的位置,因此我们可以pre-calculate the irradiance for every possible hemisphere orientation oriented around all outgoing directions wo预先计算围绕所有出射方向 wo 的每个可能的半球方向的辐照度

给定光照通道中的任何方向向量 wi,然后我们可以对预先计算的辐照度图进行采样,以从方向 wi 检索总漫射辐照度。 为了确定片段表面的间接漫射(辐照)光量,我们检索围绕其表面法线定向的半球的总辐照度。 获取场景的辐照度很简单:

vec3 irradiance = texture(irradianceMap, N).rgb;

现在,要生成辐照度贴图,我们需要将环境的光照转换为立方体贴图。 假设对于每个片段,表面的半球沿法线向量 N 定向,convoluting a cubemap卷积立方体贴图等于计算沿 N 定向的半球 Ω 中每个方向 wi 的total averaged radiance总平均辐射率

值得庆幸的是,本章所有繁琐的设置都不是徒劳的,因为我们现在可以直接获取转换后的立方体贴图,在片段着色器中对其进行卷积,并使用渲染到所有 6 个面的帧缓冲区将其结果捕获到新的立方体贴图中 方向。 由于我们已经将其设置为将 equirectangular 环境贴图转换为立方体贴图,因此我们可以采用完全相同的方法,但使用不同的片段着色器:

#version 330 core
out vec4 FragColor;
in vec3 localPos;uniform samplerCube environmentMap;const float PI = 3.14159265359;void main()
{       // 采样方向 等同于 半球方向// the sample direction equals the hemisphere's orientation vec3 normal = normalize(localPos);vec3 irradiance = vec3(0.0);[...] // convolution codeFragColor = vec4(irradiance, 1.0);
}

environmentMap 是从 equirectangular HDR 环境贴图转换而来的 HDR 立方体贴图。

有很多方法可以对环境贴图进行卷积,但在本章中,我们将为每个立方体贴图纹理像素生成固定数量的样本向量,沿半球 Ω 方向围绕样本方向,并对结果进行平均。 固定数量的样本向量将在半球内均匀分布。 请注意,积分是一个连续函数,并且在给定固定数量的样本向量的情况下对其函数进行离散采样将是一个近似值。 我们使用的样本向量越多,我们就越接近积分。

反射方程的积分∫围绕立体角 dw 旋转,这很难处理。 我们不是在立体角 dw 上积分,而是在其等效球坐标 θ 和 φ 上积分

We use the polar azimuth ϕangle to sample around the ring of the hemisphere between 0 and 2π, and use the inclination zenith θangle between 0and 0.5π to sample the increasing rings of the hemisphere(φ水平自转角度,θ则是仰视角度

(其中cosθ = n * wi中的cosθ,而则是dwi的面积)

求解积分要求我们在半球 Ω 内采集固定数量的离散样本,并对它们的结果进行平均。 这将积分转换为以下离散版本,基于分别在每个球坐标上给出 n1 和 n2 个离散样本的Riemann sum黎曼和:

当我们离散地对两个球面值进行采样时,每个样本将近似或平均半球上的一个区域,如之前的图像所示。 请注意(由于球形的一般特性)当样本区域向中心顶部收敛时,天顶角 θ 越高,半球的离散样本区域就越小。 为了补偿较小的区域,我们通过按 sinθ 缩放区域来衡量其贡献

给定积分的球坐标对半球进行离散采样转换为以下片段代码:

vec3 irradiance = vec3(0.0);  vec3 up    = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(up, normal));
up         = normalize(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){// spherical to cartesian (in tangent space)// 球坐标系转换为,xyz笛卡坐标系,x=sinθ*cosφ, y = sinθ*sinφ, z=cosθvec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));// tangent space to worldvec3 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 delta值来遍历半球; 减少或增加样本增量将分别增加或降低准确性。

在两个循环中,我们将两个球坐标转换为 3D 笛卡尔样本向量,将样本从切线转换为以法线为导向的世界空间,并使用该样本向量直接对 HDR 环境贴图进行采样。 我们将每个样本结果添加到辐照度中,最后除以所采集的样本总数,得到平均采样辐照度。 请注意,我们通过 cos(theta) 缩放采样颜色值,因为光线在较大角度处较弱,并且通过 sin(theta) 来解释较高半球区域中的较小样本区域。

现在剩下要做的就是设置 OpenGL 渲染代码,以便我们可以对之前捕获的 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)存储地图,让 OpenGL 的线性过滤完成大部分工作。 接下来,我们将捕获帧缓冲区重新缩放到新的分辨率

glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);  

Using the convolution shader, we render the environment map in a similar way to how we captured the environment cubemap 使用卷积着色器,我们以与捕获环境立方体贴图类似的方式渲染环境贴图:

//初始化渲染
irradianceShader.use();
irradianceShader.setInt("environmentMap", 0);
irradianceShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);glViewport(0, 0, 32, 32); // don't forget to configure the viewport to the capture dimensions.
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
// 渲染cubemap的6个面
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();
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

现在,在这个例程之后,我们应该有一个预先计算的辐照度图,我们可以直接将其用于基于漫反射图像的照明。 为了查看我们是否成功地卷积了环境贴图,我们将环境贴图替换为辐照度贴图作为天空盒的环境采样器:

如果它看起来像环境贴图的高度模糊版本,则您已经成功地对环境贴图进行了卷积

3.PBR and indirect irradiance lighting (PBR 和间接辐照度照明)

辐照度图表示反射积分的漫反射部分,由所有周围的间接光累积。 鉴于光线不是来自直接光源,而是来自周围环境,我们将漫反射和镜面反射间接照明都视为环境照明,替换我们之前设置的常数项。

首先,确保将预先计算的辐照度图添加为立方体采样器

uniform samplerCube irradianceMap;

给定包含场景所有间接漫反射光的辐照度贴图,检索影响片段的辐照度就像给定表面法线的单个纹理样本一样简单:

// vec3 ambient = vec3(0.03);
vec3 ambient = texture(irradianceMap, N).rgb;

然而,由于间接照明同时包含漫反射和镜面反射部分(正如我们从反射方程的拆分版本中看到的那样),我们需要相应地权衡漫反射部分。 与我们在上一章中所做的类似,我们使用Fresnel菲涅耳方程来确定表面的间接反射率,从中我们可以得出折射(或漫反射)比:

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; 

As the ambient light comes from all directions within the hemisphere oriented around the normal N, there's no single halfway vector to determine the Fresnel response. To still simulate Fresnel, we calculate the Fresnel from the angle between the normal and view vector. However, earlier we used the micro-surface halfway vector, influenced by the roughness of the surface, as input to the Fresnel equation. As we currently don't take roughness into account, the surface's reflective ratio will always end up relatively high. Indirect light follows the same properties of direct light so we expect rougher surfaces to reflect less strongly on the surface edges. Because of this, the indirect Fresnel reflection strength looks off on rough non-metal surfaces (slightly exaggerated for demonstration purposes):(没有roughness粗糙度,导致边缘总是会很亮,不自然)

我们可以通过在 Fresnel-Schlick 方程中注入粗糙度项来缓解这个问题,如 Sébastien Lagarde 所述:

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 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 的威力。

Learn OpenGL 笔记7.3 PBR-IBL-Diffuse irradiance(Image based lighting-漫反射辐照度)相关推荐

  1. Learn OpenGL 笔记7.4 PBR-Specular IBL(Image based lighting-特殊的基于图像的照明)

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

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

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

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

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

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

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

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

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

  6. learn opengl 笔记 1.2

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

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

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

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

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

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

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

最新文章

  1. 教孩子学编程python语言pdf_iOS(iPhone)应用程序开发入门视频教程(35讲)
  2. 3d地球旋转html,echarts 3D地球实现自动旋转
  3. 转移地址在内存中的jmp指令 检测点9.1
  4. bzoj1059: [ZJOI2007]矩阵游戏
  5. Prepared statements(mysqli pdo)
  6. Windows Terminal Preview v0.11 发布:新的字体和主题
  7. 使用Xmanger登陆aix系统桌面时报桌面服务DT未启动问题
  8. java字符如何向float转换_java – 将float转换为字符串分数表示
  9. 6年专注,只因热爱——方创广告设计
  10. IDA 7.0在Mojava更新后打不开的问题
  11. cactiez服务器的系统日志,cactiEZ syslog无数据
  12. 下一代计算机 激光,《Nature》:仅需一束激光,计算机速度有望能再快100万倍...
  13. ASP.NET Core 面试题整理
  14. linux系统下安装wrk和使用
  15. 《objective-c程序设计》学习笔记
  16. 【转】JavaScript面向对象程序设计(6): 封装
  17. 关于AndEngine显示全屏问题
  18. 路由器有信号无网络连接到服务器,路由器信号满格但是没有网络怎么办
  19. 根据三个点的坐标计算三角形面积
  20. D65/TL84/A三种光源以及色温的含义

热门文章

  1. 用Html实现世纪佳缘交友注册页面是什么体验?
  2. Jquery 广告图片轮播切换
  3. 顶部BANNER广告图片放大后再自动缩小消失
  4. 关闭自动降频 linux,iPhone如何关闭降频?iPhone手动关闭降频方法[多图]
  5. [论文解读]Deep active learning for object detection
  6. 计算机应用基础的重点知识,《计算机应用基础知识》重点总结
  7. 不吹不黑!逛 GitHub 没看过这 10 个开源项目,绝对血亏...
  8. 解决springboot无法访问此网站,springboot启动后无法访问网站
  9. java高性能rpc,企业级rpc,zk调度,负载均衡,泛化调用一体的rpc服务框架
  10. 华为rh2288服务器芯片组,华为RH2288H V2服务器内部介绍