1、前言
1.1、一些感慨
2023年来了,令人闹心伤身的疫情也暂告一段落了。感慨之余,其实我也挺惆怅,这个系列教程还能继续下去吗?或者我自己还能坚持多久,我不知道。因为我也天天徘徊在失业的边缘,年纪大了被人嫌弃,学历低被人嫌弃,身体稍差也被人嫌弃,忽然发现我已不是当初那个少年了,却还始终怀揣着少年时的梦想,依旧挣扎在理想与现实之间,或者只是挣扎在温饱线上,已然是一身债,半条命了。当然幸运的是到现在我都还没有阳过,属于少数那部分,我身边的朋友们也很惊奇。然而我的家人也都阳过了,他们也很惊奇我居然一直没有阳过,算是一点小小的慰藉吧。
年轻时走错的弯路,经受过的磨难,造成了今天的困境,索性的是我现在在尽一切努力纠错,只是错过的光阴却永远无法找回了。生活可能就是这样,当你经历了很多,看透了很多,甚至都觉得人生其实只是短暂的一瞬间,与整个世界其实也毫无意义,我们不断重复和追寻的不过就是在思考、探讨或实践所谓的人生意义而已,更多的其实只是为了生存。无论你是工作、学习、生活、休闲、旅游、陪伴家人等等,都不过如此,始终你所能影响的就是那么一部分人,能记住你的也就是那一部分人,其中可能还包括你所谓的敌人或者对手,或许也没什么人会记得你。其实归根结底我们改变不了什么,甚至连我们自己的某些卑劣的人性都无法克服,只是一个平凡的自己而已。当然这样的生死又会有什么意义呢?
然而,当我在对自己感兴趣的自己的这个领域深耕下去时,并且一次次突破瓶颈,恍然大悟时,我才真正感觉到内啡肽带给我的那种“极乐”,我始终认为这或许才是我自己真正的人生的意义,钻研下去,分享出去,获得真正的“极乐”。
将来,不论怎样,我想只要我还活着,我就将这个系列继续下去,直到生命的尽头,不为别的,只为自己内心的“极乐”!在此也非常感谢各位网友一路的陪伴与支持!
1.2、运行效果展示
在对技术的执著与深深的情怀支撑下,经过了一段时间的编程和调试后,终于 IBL 的基础示例代码也编写上传 Github 完成了( GRSD3D12Sample/25-IBL-MultiInstance-Sphere )。当这个例子第一次运行起来后,画面直接将我自己也惊艳到了。瞬间感觉“内啡肽”分泌量达到峰值,感觉真的是太好了,瞬间觉得所有的努力与汗水都十分的值得。当然如果各位能够顺利的从该系列教程的开始看到这里时,并且当你自己调试运行所有示例并完全搞明白的时候,也同样会感受到我说的那种快乐。其实本章示例代码也是很早就上传到了GitHub上,只是最近才得一点宽裕的时间,把这一章的教程内容补齐。
从本质上来说,我们现在学习和讨论的是关于 Shader 的话题,与完全的D3D12接口编程是有点区别的,当然使用 D3D12 接口编程的目的之一就是为了更好的进行 Shader 编程。最终本章的内容不但可以用于 D3D,还可以用于 OpenGL,Vulkan,甚至手机渲染上,只要你搞明白了基本的 PBR Shader 的数学原理和基本代码,就可以把它们用在你想要使用的任何地方,甚至包括光追渲染。
因为 IBL 的话题有些复杂,涉及内容较多(有D3D12编程的,也有Shader编程方面的,还有自身PBR的,更牵扯到很多数学、物理方面的知识),所以这一块的知识,也将被分成几部分来介绍。本章核心是让大家对整个 IBL 先有个整体的认识,尤其是其数学原理,并且搞清楚 IBL 作为 PBR 的一部分核心解决了什么问题。这是继续后续很多高级话题的基础,比如 Probe(探针),Pathtracing(路径追踪),球谐函数、距离场等等。所以这一章和前两章的内容希望大家都能够牢固掌握和理解,当然我会尽力为各位梳理清楚,方便各位深入学习。
按照惯例,在正式开始介绍之前,先来看几幅效果截图:
从示例中已经可以明显的看出明亮光滑的金属球表面倒映出了环境光的样子,并且每种材质反射率表现出了不同的材质的基础属性,比如黄金看上去已经很逼真,而且黄铜,紫铜也表现出不同的反射色差,这一切都是 PBR 本身要表达的令人惊艳的光照效果。而这些还只是通过多种“近似”方法模拟出来的。当然本章展示的也只是 PBR 的基础效果而已,后面看有时间的话,就继续展开讲讲其它的那些分支,比如大气散射、皮肤渲染、玻璃折射等等,当然这要看我时间和精力的情况。只是希望我还能撑下去吧。
1.3、示例简介
本章的示例,是在前一章 DirectX12(D3D12)基础教程(十九)—— 多实例渲染_ 示例的基础上,更进一步多实例化,用到了“间接绘制(ExecuteIndirect)”方法,并且示例中的小球阵列按照几种主要的材质(涉及金、银、铜、铁、水、玻璃等等)不同的反射率形成多个平面,每个平面又按照从上到下粗糙度变大,从左到右金属度变小的顺序排列成密集的的材质球演示立方阵列。并且横向排列比较宽,主要是因为我的显示器是 HDR 的“带鱼屏”,所以就这样设置。大家可以通过调整程序开头的几个参数来自己手动设置材质球阵列的行列大小。
同时一定要注意的是,因为 IBL 中就开始牵扯到了复杂的光照积分运算,所以最原始的基于简单数值积分的 Shader 文件 “GRS_IBL_Diffuse_Irradiance_Convolution_With_Integration_PS.hlsl” 仅供大家参考学习之用,请千万不要在代码中引用! 实际我们使用的是结合了蒙特卡洛积分和重要性采样的版本“GRS_IBL_Diffuse_Irradiance_Convolution_With_Monte_Carlo_PS.hlsl”。根本原因是因为最基本的数值积分版本在单像素上计算量过于庞大,在加载和计算积分的过程中如果分辨率过高会导致GPU过载,如果你的系统没有保护机制,那么可能造成显卡损伤。
在此郑重声明,由于你误操作引用了原始积分版本而导致的一切后果自负!
1.4、示例操作说明
因为示例中引用了几个纹理,并且使用的是 HDR 纹理,为了让大家方便的引用不同的环境光照映射,所以程序会弹出几个文件选择框。第一个文件选择框是选择 “Image Base Lights” 也就是 HDR版的环境光源图片(扩展名为hdr,并且文件名中会有3k的字样说明),目前我们使用的是“hdr”格式的“等距柱面环境映射”贴图,示例代码中附带了几个环境映射。更多的环境映射,大家可以去 “sIBL Archive (hdrlabs.com)” 这里免费下载。在此感谢 hdrlabs 的慷慨分享!
第二个文件选择框是为了选择 “等距柱面映射”版本的 Skybox,在 sIBL 下载的时候会附带至少 8k 分辨率的高清 SkyBox 贴图,为了感受更加逼真的效果建议加载扩展名为“jpg”的高清版环境映射。需要注意的是,实际代码中使用的是和加载 HDR 环境映射一样的 stb 库来加载这个高清的 jpg 图片,而没有使用我们一直使用的 WIC 库。因为WIC库自身的限制,这么高分辨率的图片它已经不能加载了。而关于 stb 库的用法,将在后续的教程中详细的介绍,本章教程将还是集中在 IBL 数学原理的介绍上。
需要注意的是,高分辨率的环境映射加载需要一定的时间,并且因为程序并没有做优化的缘故,所以也会占用一定的内存和显存,当你显卡的显存少于 2G 时运行可能会有问题,请考虑升级显卡,或加载 Preview 版本的低分辨率版本环境映射。
程序运行中可以用按键来控制一些效果,统一在下表中说明:
按键 | 效果 | 说明 |
---|---|---|
Tab键 | 打开/关闭 中间贴图显示框 | 所有的中间解算或计算的CubeMap贴图、预积分贴图(漫反射和镜面反射积分图)、LUT等,使用了HLSL中的动态上限纹理数组 |
+/- | 切换不同反射率材质球的平面 | 当全部切换完一遍后,会将所有的材质球平面全部显示出来,形成一个密集的阵列,有密集恐惧症的请慎重运行示例! |
w | 前 | 向前移动镜头 |
s | 后 | 向后移动镜头 |
a | 左 | 向左移动镜头 |
d | 右 | 向右移动镜头 |
PageUp | 上 | 向上移动镜头 |
PageDown | 下 | 向下移动镜头 |
Shift | 启/停 小球阵列旋转 | 启动或停止小球阵列的旋转动画 |
未在上表中出现的操作方式,请详见代码中的 WndProc 函数中关于键盘消息处理的部分,这里就不在赘述了。
在编译和运行之后,并且能够操作的情况下,建议反复运行并轮流加载本章教程附带的4组精选的 HDR 环境映射,以感受不同的效果和氛围。并且对 PBR 的 IBL 渲染方式有一个最直接的感性认识。
另外需要注意的是,示例中带的名为“UV-Testgrid” 纹理是用于验证和理解“等距柱面映射”的原理和解算结果的,请在全部学习完毕后再运行,以加深对整个IBL知识的理解。运行这个验证环境纹理时,两个纹理都选择 testgrid.jpg 文件即可。其本质就是特意设定的黑白方格纹理,加载计算后可以根据方格及文字标记识别解算后的纹理坐标变化情况(拓扑变换),以及计算过程中各种贴图都是经历了什么样的变化。
1.5、本章内容的简述
在实际学习 IBL 的整个过程中,我深深的感受到,如果其数学原理不搞清楚的话,学习 IBL 就只会停留于皮毛,而根本无法触及其深刻的本质,尤其是其中一些极具创造力的数学技巧。不得不说,当我彻底搞懂了 IBL 的数学原理之后,就被他其中的那些非常有创意的数学技巧所深深吸引。当然触及到 IBL 自身的数学原理的资料,无论是书籍还是资料都是非常少的,或者就是分散在各处,其中有些还语焉不详,错漏百出。
正因为这样的感触,所以我就专门整理一整套的 IBL 的基础数学原理成为此章教程。并且尽力将整个推算简化近似的过程都讲清楚,当然过于基础的内容限于篇幅就不多讲解了。数学基础较弱同学,请自行复习微积分、概率论、线性代数、向量运算即向量微积分等内容,尤其是概率论及微积分是彻底搞懂 IBL 原理的基础支撑性知识。在阅读本篇教程前,建议各位先阅读一下: 3D数学系列之——再谈特卡洛积分和重要性采样 对蒙特卡洛积分有个初步的了解。
最终如果你暂时看不懂这一章的内容也没有关系,先收藏,等你复习完必要的数学知识后再折回头好好消化下本章的内容,直到能够彻底掌握 IBL 的数学原理为止,因为这是通向PBR 渲染中的其他高级话题的第一座高峰。
2、什么是IBL
2.1、“Cook-Torrance” 模型解决的问题
实际在 DirectX12(D3D12)基础教程(十八)—— PBR基础从物理到艺术(下) 中,我们只是解决反射方程中的
f
r
(
p
⃗
,
ω
i
⃗
,
ω
o
⃗
)
{f}_{r}( \vec{p} , \vec{\omega_i} , \vec{\omega_o})
fr(p,ωi,ωo) 函数的近似形式问题。或者说我们找到了一个物理与艺术相结合的近似解析表达式(基于迪士尼原则),也就是基于微平面理论的“Cook-Torrance”模型的 BRDF 函数。整体上来说该模型使得的渲染方程更进一步的解析表达如下(忽略自发光项):
L
o
(
p
⃗
,
ω
o
⃗
)
=
∫
Ω
(
k
d
c
π
+
k
s
D
F
G
4
(
ω
o
⃗
⋅
n
⃗
)
(
ω
i
⃗
⋅
n
⃗
)
)
L
i
(
p
⃗
,
ω
i
⃗
)
n
⃗
⋅
ω
i
⃗
d
ω
i
⃗
L_o(\vec{p},\vec{\omega_o}) = \int\limits_{\Omega} (k_d \cfrac{c}{\pi} + k_s \cfrac{ D F G }{ 4 (\vec{\omega_o} \cdot \vec{n} )(\vec{\omega_i} \cdot \vec{n})}) L_i(\vec{p},\vec{\omega_i}) \vec{n} \cdot \vec{\omega_i} \mathrm{d} \vec{\omega_i}
Lo(p,ωo)=Ω∫(kdπc+ks4(ωo⋅n)(ωi⋅n)DFG)Li(p,ωi)n⋅ωidωi
上述方程是完整的积分方程形式,也被称为渲染方程,在之前的教程中都没有正式的提到过,现在正式将其列出,作为复习总结和本章开始的基础。
之前之所以没有出现这个形式的总体方程,是因为前面的重点是放在 BRDF 函数的解析与应用上,也就是 DFG 部分的表达式的介绍与应用上。并且是基于点光源的简单原理性应用,并没有涉及到复杂的积分究竟如何计算和编程上。即还没有真正的考虑全部复杂的入射光源的问题。也就是说前面的教程实质上是简化了光源(其实只使用了几个点光源而已,从而就不必要考虑复杂的积分问题),而将重点放在了 BRDF 函数的讲解和说明上。那么在这一章中,我们就需要来思考复杂光源或复杂入射光情况下,如何来完整的做这个积分运算。
2.3、光源问题
在之前示例的几个点光源模型下,上述的 “Cook-Torrance” 模型积分其实就退化为按每个点光源分别计算 BRDF,以及对应一条光线的反射效果,然后再根据光源的参数计算一个点引起的光照反射效果之后,最终求和并求平均值即可。这使的复杂的积分计算退化成了求和问题(与黎曼和有本质的区别不要搞混)。这样的简化,其目的是搞明白基于微平面理论的 “Cook-Torrance” 模型 BRDF 函数的解析形式及 Shader 代码如何编写的问题。
在有限点光源的示例中,其实本质上来说,积分方程中的 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数其实就被简化为视点 p 、光源颜色、和入射方向向量(实际是从光源位置计算得到)等几个具体向量而已。这其实在实际应用中是不存在的情况。因为这就好像用简单的几道单光子激光照射一个物体表面上的情况一样,除了实验室中可能会有这种场景外,实在想不出还会有什么地方会有这样的情况。
而在现实的情况中,基于对万事万物都处在普遍联系中这个朴素真理的认识,我们知道物体间的光照关系是异常复杂的。也就是说代表光源的 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数本身是异常复杂的。这就导致整个积分方程的计算也会异常复杂,从而无法使用解析积分的方法求解其原函数。实际计算中只能退回到黎曼和的方式来计算这个积分,而且是对每一个像素点使用所有的入射光进行复杂的数值积分计算。这样反射方程的整体计算量对于实时 PBR 来说依然是非常巨大而无法执行的。
并且积分中的入射辐照度函数
L
i
(
p
⃗
,
ω
i
⃗
)
L_i(\vec{p},\vec{\omega_i})
Li(p,ωi) ,往往还无法简单的找到解析表达式,也就是说没法用一个解析式来表达它。这样实际上对于复杂光照场景来说 “Cook-Torrance” 模型积分虽然有了近似的 BRDF 函数支撑,但其计算依然是非常复杂的。
2.4、IBL 模型
在现实光照的场景中入射辐照度 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数的表达式一般是无法知道的,主要是因为对于具体的物体表面的一点 p ⃗ \vec{p} p 来说,其入射光不止来源于直接光源,还可能来源于其它物体的反光以及大气散射后的光源等等(想想阴天见不到太阳时的情景,这时基本没有直射光源)。
但是可以换个角度思考这个问题,既然万事万物都处于普遍联系中,那么也就是说一事物上受到的光照一定是来自于其环境中所有的其它事物。这些其它事物就包括光源、反光体等等。这时聪明的图形程序员就想到,如果把包围物体的环境映射(通常是一个以被照射物体中心点为原点的 CubeMap 映射,这也是插入 DirectX12(D3D12)基础教程(二十)—— 纹理数组(Texture Array)非DDS初始化操作)这一章教程的本意。)然后将这个环境映射作为 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数的数值表示(图形即函数!)那么 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数的计算就成了一个针对 CubeMap 映射的 3D Sample 操作,而采样的方向就是入射光的反方向 − ω i ⃗ -\vec{\omega_i} −ωi 。这就是 IBL (Image Based Lighting)的真实含义。
也就是说我们用代表所有环境光,包括光源、反光体等的图片作为 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数并进行采样操作,就相当于进行了 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数计算。这样一来 “Cook-Torrance” 模型积分方程中最重要的 BRDF 计算问题 和入射辐照度 L i ( p ⃗ , ω i ⃗ ) L_i(\vec{p},\vec{\omega_i}) Li(p,ωi) 函数计算问题就都解决了,剩下的就是找到合适的 环境映射 CubeMap Image,加载并进行最终的数值积分计算即可。
而在实际 IBL 应用中,一般使用的是将原本需要6个面来存储的环境映射CubeMap 贴图,投影压缩到一张被称为 “等距柱面”贴图的映射,通常像下面的样子:
这个图形看上去很扭曲,其实可以简单的将他理解为我们常见的世界地图,本质上就是将本应是一个球面的图形拓扑平铺到了一个平面上。只是等距柱面贴图是按照圆柱面映射球面,最后再把圆柱面展开平铺成平面图形。这样做的好处就是比起 6 个面的 cubemap 来说,存储上面占有较大优势。同时这也方便一些 360 度全景相机拍摄存储处理。当然图形细节及精度方面就有所损失,尤其是靠近图形的上下边界部分。但是它在中间水平线(地平线)附近几乎保留了全部细节,所以在重映射回球面时,因为通常场景中,视方向主要还是集中在中间线部分,所以实际中看上去效果还是令人满意的。在 “sIBL Archive (hdrlabs.com)” 网站上就有大量的精美的等距柱面环境映射贴图供大家免费下载。
实际使用的时候,就需要先将这个 “等距柱面贴图” 解算为一个具有 6 个面的 CubeMap纹理上。具体解算的方法放在后续教程中再详细介绍。本章示例代码中那些小方块中的第1幅图片就是加载后的等距柱面原始图片,紧随其后的 第2-7幅图片就是结算后的CubeMap 6个面的图片:
从上图中可以看出,解算后的6个面的CubeMap的效果还是令人满意的,也就是说实际上等距柱面贴图的缺陷并不是那么明显,比起其优点来说几乎可以忽略不计,这也是等距柱面贴图比较流行的原因。在此复习下6个面的顺序以及位置关系(最好对照上面的顺序和下图自行脑补一下最终的CubeMap的样子):
在后续要介绍的漫反射项预积分和镜面反射预积分计算中,实际使用的都将是解算出的6个面的 CubeMap(本章教程中使用了 GS 一次性渲染到六个面的方法,后续教程会详细介绍,大家先理解原理即可)。
同时在示例代码的开头处,用宏定义预设值定义了结算后的 CubeMap每个面的大小如下:
#define GRS_RTA_DEFAULT_SIZE_W 512
#define GRS_RTA_DEFAULT_SIZE_H 512
对于设备比较好,尤其是显卡比较好的同学来说,可以尝试将这个预设值调高一点,但是一定要注意这两个值必须一样大小,因为实际上这6个面围成了一个正方体的6个面,可以直观的理解为是一个SkyBox,关于SkyBox 和 CubeMap操作的更多基础知识请参阅:DirectX12(D3D12)基础教程(二十)—— 纹理数组(Texture Array)非DDS初始化操作 。
另外除了等距柱面贴图外,IBL 中还经常使用另一大类被称之为“双抛物面环境映射”的贴图。这里需要提醒大家注意的是,不论使用哪种环境映射来作为整体的环境光照的光源,包括后续可能会讲到的所谓全局光照中使用的探针(Probe),并实时生成的环境映射中,其格式都应该是 HDR 线性的。这是因为根据对 PBR 本质的了解,我们知道 PBR 核心的思想就是物理真实的,所以其包含的作为光源光线的信息也应当是物理真实的,而现实中的光源能量值范围是非常宽泛的,并且远超出一般 RGB 图形中所能表示的范围的。所以这样的光源能量信息就必须使用更宽范围的 HDR 图片格式来存储。有些情况下,甚至需要自定义格式的 HDR 格式图片来存储物理真实的光源能量信息。
另外,如果环境映射使用的不是 HDR 格式时,或者严格的说是被衰减并压缩了范围的光源能量信息时,将会导致整个 IBL 效果失真,最终效果甚至看上去还不如传统光照时的效果。这是大多数时候被忽视的一个问题,即虽然渲染内核使用的是 PBR 的算法,但是使用的光源、光线信息却不是 “物理真实的” 从而导致实际效果大打折扣。这个问题是包括图形程序员和技美人员,都需要高度重视的一个问题。
特别需要提醒的是,即使是使用所谓 HDR 功能的一些摄影摄像设备采集真实环境的映射时,依然要仔细查看它实际上所能表达和记录的光线的能量范围。因为有些 HDR 设备虽然称之为 HDR, 可能实际上只是比普通的RGB表达范围高一点点而已。而这时辐射度测量仪器是非常好的帮手。
关于 HDR 的进一步话题,将在后续的教程中详细介绍。这里大家先可以简单的理解为 HDR 就是每个颜色范围都可以远超过 [0.0f,1.0f] 范围的图片。基于这个理解,大家就会明白在之前的点光源 PBR 示例中,为每个光源设置的 RGB 范围是远远大于1.0的含义了,其实就是为了使用更高能量表达的光源,尽可能的照亮材质球。文章来源:https://www.toymoban.com/news/detail-403815.html
最后要提示大家的是,其实整个 IBL 属于被称为 PBL (Physically Based Lighting)的范畴,也就是基于物理的光照,即使用物理真实的光源光照信息来渲染整个场景的方法。文章来源地址https://www.toymoban.com/news/detail-403815.html
到了这里,关于DirectX12(D3D12)基础教程(二十一)—— PBR:IBL 的数学原理(1/5)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!