mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 04:35:28 +08:00
Compare commits
5 Commits
ff504c286c
...
new-theme
Author | SHA1 | Date | |
---|---|---|---|
|
d5ded8e505 | ||
|
afb199cc3e | ||
|
ed338c7df2 | ||
|
3bead6b3fd | ||
|
3956459af8 |
@@ -445,7 +445,7 @@ uniform mat4 transform;
|
||||
void main()
|
||||
{
|
||||
gl_Position = transform * vec4(aPos, 1.0f);
|
||||
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
|
||||
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -4,13 +4,13 @@
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | 暂无
|
||||
校对 | [AoZhang](https://github.com/SuperAoao)
|
||||
|
||||
!!! note
|
||||
|
||||
本节暂未进行完全的重写,错误可能会很多。如果可能的话,请对照原文进行阅读。如果有报告本节的错误,将会延迟至重写之后进行处理。
|
||||
|
||||
当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在监视器上。过去,大多数监视器是阴极射线管显示器(CRT)。这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做监视器Gamma。
|
||||
当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在显示器上。过去,大多数显示器是阴极射线管显示器(CRT)。这些显示器有一个物理特性就是两倍的输入电压并不能产生两倍的亮度。将输入电压加倍产生的亮度约为输入电压的2.2次幂,这叫做显示器Gamma。
|
||||
|
||||
!!! note "译注"
|
||||
|
||||
@@ -22,35 +22,40 @@
|
||||
|
||||
第一行是人眼所感知到的正常的灰阶,亮度要增加一倍(比如从0.1到0.2)你才会感觉比原来变亮了一倍(译注:我们在看颜色值从0到1(从黑到白)的过程中,亮度要增加一倍,我们才会感受到明显的颜色变化(变亮一倍)。打个比方:颜色值从0.1到0.2,我们会感受到一倍的颜色变化,而从0.4到0.8我们才能感受到相同程度(变亮一倍)的颜色变化。如果还是不理解,可以参考知乎的[答案](https://www.zhihu.com/question/27467127/answer/37602200))。然而,当我们谈论光的物理亮度,比如光源发射光子的数量的时候,底部(第二行)的灰阶显示出的才是物理世界真实的亮度。如底部的灰阶显示,亮度加倍时返回的也是真实的物理亮度(译注:这里亮度是指光子数量和正相关的亮度,即物理亮度,前面讨论的是人的感知亮度;物理亮度和感知亮度的区别在于,物理亮度基于光子数量,感知亮度基于人的感觉,比如第二个灰阶里亮度0.1的光子数量是0.2的二分之一),但是由于这与我们的眼睛感知亮度不完全一致(对比较暗的颜色变化更敏感),所以它看起来有差异。
|
||||
|
||||
因为人眼看到颜色的亮度更倾向于顶部的灰阶,监视器使用的也是一种指数关系(电压的2.2次幂),所以物理亮度通过监视器能够被映射到顶部的非线性亮度;因此看起来效果不错(译注:CRT亮度是是电压的2.2次幂而人眼相当于2次幂,因此CRT这个缺陷正好能满足人的需要)。
|
||||
因为人眼看到颜色的亮度更倾向于顶部的灰阶,显示器使用的也是一种指数关系(电压的2.2次幂),所以物理亮度通过显示器能够被映射到顶部的非线性亮度;因此看起来效果不错。
|
||||
|
||||
监视器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于监视器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图:
|
||||
显示器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于我们从显示器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图:
|
||||
|
||||

|
||||
|
||||
点线代表线性颜色/亮度值(译注:这表示的是理想状态,Gamma为1),实线代表监视器显示的颜色。如果我们把一个点线线性的颜色翻一倍,结果就是这个值的两倍。比如,光的颜色向量\(\bar{L} = (0.5, 0.0, 0.0)\)代表的是暗红色。如果我们在线性空间中把它翻倍,就会变成\((1.0, 0.0, 0.0)\),就像你在图中看到的那样。然而,由于我们定义的颜色仍然需要输出的监视器上,监视器上显示的实际颜色就会是\((0.218, 0.0, 0.0)\)。在这儿问题就出现了:当我们将理想中直线上的那个暗红色翻一倍时,在监视器上实际上亮度翻了4.5倍以上!
|
||||
点线代表线性颜色/亮度值(译注:这表示的是理想状态,Gamma为1),实线代表显示器器显示的颜色。如果我们把一个点线线性的颜色翻一倍,结果就是这个值的两倍。比如,光的颜色向量\(\bar{L} = (0.5, 0.0, 0.0)\)代表的是半暗红色。如果我们在线性空间中把它翻倍,就会变成\((1.0, 0.0, 0.0)\),就像你在图中看到的那样。然而,由于我们定义的颜色仍然需要输出的显示器上,显示器上显示的实际颜色就会是\((0.218, 0.0, 0.0)\)。在这儿问题就出现了:当我们将理想中直线上的那个半暗红色翻一倍时,在显示器上实际上亮度翻了4.5倍以上!
|
||||
|
||||
直到现在,我们还一直假设我们所有的工作都是在线性空间中进行的(译注:Gamma为1),但最终还是要把所有的颜色输出到监视器上,所以我们配置的所有颜色和光照变量从物理角度来看都是不正确的,在我们的监视器上很少能够正确地显示。出于这个原因,我们(以及艺术家)通常将光照值设置得比本来更亮一些(由于监视器会将其亮度显示的更暗一些),如果不是这样,在线性空间里计算出来的光照就会不正确。同时,还要记住,监视器所显示出来的图像和线性图像的最小亮度是相同的,它们最大的亮度也是相同的;只是中间亮度部分会被压暗。
|
||||
直到现在,我们还一直假设我们所有的工作都是在线性空间中进行的(译注:Gamma为1),但实际上我们一直在显示器的输出空间中工作,所以我们配置的所有颜色和照明变量在物理上都不正确,但在显示器上看起来(有点)正确。出于这个原因,我们(以及艺术家)通常将光照值设置得比本来更亮一些(由于显示器会将其亮度显示的更暗一些),这导致大多数在线性空间里的计算不正确。同时,还要记住,显示器所显示出来的图像和线性图像的最小亮度是相同的,它们最大的亮度也是相同的;只是中间亮度部分会被压暗。
|
||||
|
||||
因为所有中间亮度都是线性空间计算出来的(译注:计算的时候假设Gamma为1)监视器显以后,实际上都会不正确。当使用更高级的光照算法时,这个问题会变得越来越明显,你可以看看下图:
|
||||
因为颜色是根据显示器的输出配置的,所以线性空间中的所有中间(照明)计算在物理上都是不正确的。随着更多先进的照明算法的引入,这一点变得更加明显,如下图所示:
|
||||
|
||||

|
||||
|
||||
你可以看到,通过伽玛校正,(更新的)颜色值可以很好地结合在一起,较暗的区域显示更多的细节。总的来说,一个更好的图像质量与一些小的修改。
|
||||
|
||||
如果没有适当地纠正这个显示器伽马,照明看起来是错误的,艺术家将很难获得逼真和好看的结果。解决方案正是应用<def>伽马校正</def>。
|
||||
|
||||
## Gamma校正
|
||||
|
||||
Gamma校正(Gamma Correction)的思路是在最终的颜色输出上应用监视器Gamma的倒数。回头看前面的Gamma曲线图,你会有一个短划线,它是监视器Gamma曲线的翻转曲线。我们在颜色显示到监视器的时候把每个颜色输出都加上这个翻转的Gamma曲线,这样应用了监视器Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮,所以虽然监视器使它们变暗,但是我们又将其平衡回来了。
|
||||
Gamma校正(Gamma Correction)的思路是在最终的颜色输出到显示器之前先将Gamma的倒数作用到颜色上。回顾本章前面的伽马曲线图,我们看到另一条虚线,它与显示器的伽马曲线相反。我们将每个线性输出颜色乘以这个逆伽马曲线(使它们更亮),一旦颜色显示在显示器上,显示器的伽马曲线就被应用,结果颜色就变成线性的。我们有效地使中间颜色变亮,这样一旦显示器变暗,它们就会平衡。
|
||||
|
||||
我们来看另一个例子。还是那个暗红色\((0.5, 0.0, 0.0)\)。在将颜色显示到监视器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在监视器上相当于降低了\(2.2\)次幂的亮度,所以倒数就是\(1/2.2\)次幂。Gamma校正后的暗红色就会成为\((0.5, 0.0, 0.0)^{1/2.2} = (0.5, 0.0, 0.0)^{0.45} = (0.73, 0.0, 0.0)\)。校正后的颜色接着被发送给监视器,最终显示出来的颜色是\((0.73, 0.0, 0.0)^{2.2} = (0.5, 0.0, 0.0)\)。你会发现使用了Gamma校正,监视器最终会显示出我们在应用中设置的那种线性的颜色。
|
||||
我们来看另一个例子。还是那个暗红色\((0.5, 0.0, 0.0)\)。在将颜色显示到显示器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在显示器上相当于降低了\(2.2\)次幂的亮度,所以倒数就是\(1/2.2\)次幂。Gamma校正后的暗红色就会成为\((0.5, 0.0, 0.0)^{1/2.2} = (0.5, 0.0, 0.0)^{0.45} = (0.73, 0.0, 0.0)\)。校正后的颜色接着被发送给显示器,最终显示出来的颜色是\((0.73, 0.0, 0.0)^{2.2} = (0.5, 0.0, 0.0)\)。你会发现使用了Gamma校正,显示器最终会显示出我们在应用中设置的那种线性的颜色。
|
||||
|
||||
!!! Important
|
||||
|
||||
2.2通常是是大多数显示设备的大概平均gamma值。基于gamma2.2的颜色空间叫做sRGB颜色空间。每个监视器的gamma曲线都有所不同,但是gamma2.2在大多数监视器上表现都不错。出于这个原因,游戏经常都会为玩家提供改变游戏gamma设置的选项,以适应每个监视器(译注:现在Gamma2.2相当于一个标准,后文中你会看到。但现在你可能会问,前面不是说Gamma2.2看起来不是正好适合人眼么,为何还需要校正。这是因为你在程序中设置的颜色,比如光照都是基于线性Gamma,即Gamma1,所以你理想中的亮度和实际表达出的不一样,如果要表达出你理想中的亮度就要对这个光照进行校正)。
|
||||
2.2通常是是大多数显示设备的大概平均gamma值。基于gamma2.2的颜色空间叫做sRGB颜色空间。每个显示器的gamma曲线都有所不同,但是gamma2.2在大多数显示器上表现都不错。出于这个原因,游戏经常都会为玩家提供改变游戏gamma设置的选项,以适应每个显示器(译注:现在Gamma2.2相当于一个标准,后文中你会看到。但现在你可能会问,前面不是说Gamma2.2的曲线不是和人眼识别亮度的特点相同吗,为何还需要校正。其实我们不需要考虑人眼识别亮度的特点,我们重点需要关注保持亮度的线性变化特点。你在程序中设置的颜色,比如光照都是基于线性Gamma,即Gamma1,比如1.0的亮度应该是0.5的二倍,如果不进行gamma矫正,线性0.5的亮度会输出为0.22,线性1.0的亮度会被输出为1.0,亮度相差了很多倍,和你的预期会很不一样)。
|
||||
|
||||
有两种在你的场景中应用gamma校正的方式:
|
||||
|
||||
使用OpenGL内建的sRGB帧缓冲。
|
||||
自己在像素着色器中进行gamma校正。
|
||||
第一个选项也许是最简单的方式,但是我们也会丧失一些控制权。开启GL_FRAMEBUFFER_SRGB,可以告诉OpenGL每个后续的绘制命令里,在颜色储存到颜色缓冲之前先校正sRGB颜色。sRGB这个颜色空间大致对应于gamma2.2,它也是家用设备的一个标准。开启GL_FRAMEBUFFER_SRGB以后,每次像素着色器运行后续帧缓冲,OpenGL将自动执行gamma校正,包括默认帧缓冲。
|
||||
* 使用OpenGL内建的sRGB帧缓冲。
|
||||
* 自己在像素着色器中进行gamma校正。
|
||||
|
||||
第一个选项也许是最简单的方式,但是我们也会丧失一些控制权。开启GL_FRAMEBUFFER_SRGB,可以告诉OpenGL每个后续的绘制命令里,在颜色储存到颜色缓冲之前先校正sRGB颜色。sRGB这个颜色空间大致对应于gamma2.2,它也是大多数设备的一个标准。开启GL_FRAMEBUFFER_SRGB以后,每次像素着色器运行后续帧缓冲,OpenGL将自动执行gamma校正,包括默认帧缓冲。
|
||||
|
||||
开启GL_FRAMEBUFFER_SRGB简单的调用glEnable就行:
|
||||
|
||||
@@ -58,16 +63,16 @@ Gamma校正(Gamma Correction)的思路是在最终的颜色输出上应用监视
|
||||
glEnable(GL_FRAMEBUFFER_SRGB);
|
||||
```
|
||||
|
||||
自此,你渲染的图像就被进行gamma校正处理,你不需要做任何事情硬件就帮你处理了。有时候,你应该记得这个建议:gamma校正将把线性颜色空间转变为非线性空间,所以在最后一步进行gamma校正是极其重要的。如果你在最后输出之前就进行gamma校正,所有的后续操作都是在操作不正确的颜色值。例如,如果你使用多个帧缓冲,你可能打算让两个帧缓冲之间传递的中间结果仍然保持线性空间颜色,只是给发送给监视器的最后的那个帧缓冲应用gamma校正。
|
||||
自此,你渲染的图像就被进行gamma校正处理,你不需要做任何事情硬件就帮你处理了。使用这种方法(和其他方法)你应该记住的重点是,伽马校正(也)将颜色从线性空间转换为非线性空间,所以只在最后一步进行伽马校正。如果你在最后输出之前就进行gamma校正,所有的后续操作都是在操作不正确的颜色值。例如,如果你使用多个帧缓冲,你可能打算让两个帧缓冲之间传递的中间结果仍然保持线性空间颜色,只是给发送给显示器的最后的那个帧缓冲应用gamma校正。
|
||||
|
||||
第二个方法稍微复杂点,但同时也是我们对gamma操作有完全的控制权。我们在每个相关像素着色器运行的最后应用gamma校正,所以在发送到帧缓冲前,颜色就被校正了。
|
||||
第二种方法需要更多的工作,但也让我们完全控制伽马操作。我们在每个相关的像素着色器运行结束时应用伽玛校正,因此最终的颜色在发送到显示器之前结束伽玛校正。
|
||||
|
||||
```c++
|
||||
void main()
|
||||
{
|
||||
// do super fancy lighting
|
||||
// 在线性空间做炫酷的光照效果
|
||||
[...]
|
||||
// apply gamma correction
|
||||
// 应用伽马矫正
|
||||
float gamma = 2.2;
|
||||
fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
|
||||
}
|
||||
@@ -75,23 +80,23 @@ void main()
|
||||
|
||||
最后一行代码,将fragColor的每个颜色元素应用有一个1.0/gamma的幂运算,校正像素着色器的颜色输出。
|
||||
|
||||
这个方法有个问题就是为了保持一致,你必须在像素着色器里加上这个gamma校正,所以如果你有很多像素着色器,它们可能分别用于不同物体,那么你就必须在每个着色器里都加上gamma校正了。一个更简单的方案是在你的渲染循环中引入后处理阶段,在后处理四边形上应用gamma校正,这样你只要做一次就好了。
|
||||
这种方法的一个问题是,为了保持一致性,你必须对每个有助于最终输出的片段着色器应用伽玛校正。如果你有多个对象的十几个片段着色器,你必须为每个着色器添加伽马校正代码。一个更简单的解决方案是在你的渲染循环中引入一个后处理阶段,并在后处理的四边形上应用伽马校正作为最后一步,这样你只需要做一次伽马矫正即可。
|
||||
|
||||
这些单行代码代表了gamma校正的实现。不太令人印象深刻,但当你进行gamma校正的时候有一些额外的事情别忘了考虑。
|
||||
这句单行代码代表了gamma校正的实现。不那么令人印象深刻,但当你进行gamma校正的时候有一些额外的事情别忘了考虑。
|
||||
|
||||
### sRGB纹理
|
||||
|
||||
因为监视器总是在sRGB空间中显示应用了gamma的颜色,无论什么时候当你在计算机上绘制、编辑或者画出一个图片的时候,你所选的颜色都是根据你在监视器上看到的那种。这实际意味着所有你创建或编辑的图片并不是在线性空间,而是在sRGB空间中(译注:sRGB空间定义的gamma接近于2.2),假如在你的屏幕上对暗红色翻一倍,便是根据你所感知到的亮度进行的,并不等于将红色元素加倍。
|
||||
因为显示器总是在sRGB空间中显示应用了gamma的颜色,无论什么时候当你在计算机上绘制、编辑或者绘制一个图片的时候,你所选的颜色都是根据你在显示器上看到的那种。这实际意味着所有你创建或编辑的图片并不是在线性空间,而是在sRGB空间中(译注:sRGB空间定义的gamma接近于2.2),假如在你的屏幕上对暗红色翻一倍,便是根据你所感知到的亮度进行的,并不等于将红色元素加倍。
|
||||
|
||||
结果就是纹理编辑者,所创建的所有纹理都是在sRGB空间中的纹理,所以如果我们在渲染应用中使用这些纹理,我们必须考虑到这点。在我们应用gamma校正之前,这不是个问题,因为纹理在sRGB空间创建和展示,同样我们还是在sRGB空间中使用,从而不必gamma校正纹理显示也没问题。然而,现在我们是把所有东西都放在线性空间中展示的,纹理颜色就会变坏,如下图展示的那样:
|
||||
结果就是纹理编辑者,所创建的所有纹理都是在sRGB空间中的纹理,所以如果我们在渲染应用中使用这些纹理,我们必须考虑到这点。在我们不知道gamma校正之前,这不是个问题,因为纹理在sRGB空间创建和展示,同样我们还是在sRGB空间中使用,从而不必gamma校正纹理显示也没问题。然而,现在我们是把所有东西都放在线性空间中展示的,纹理颜色就会变坏,如下图展示的那样:
|
||||
|
||||

|
||||
|
||||
纹理图像实在太亮了,发生这种情况是因为,它们实际上进行了两次gamma校正!想一想,当我们基于监视器上看到的情况创建一个图像,我们就已经对颜色值进行了gamma校正,所以再次显示在监视器上就没错。由于我们在渲染中又进行了一次gamma校正,图片就实在太亮了。
|
||||
纹理图像太亮了,这是因为它实际上被伽玛校正了两次!想想看,当我们根据在显示器上看到的创建图像时,我们有效地对图像的颜色值进行伽玛校正,使其在显示器上看起来正确。因为我们在渲染器中再次进行伽玛校正,图像最终变得太亮了。
|
||||
|
||||
为了修复这个问题,我们得确保纹理制作者是在线性空间中进行创作的。但是,由于大多数纹理制作者并不知道什么是gamma校正,并且在sRGB空间中进行创作更简单,这也许不是一个好办法。
|
||||
为了解决这个问题,我们必须确保纹理美工在线性空间中工作。然而,因为在sRGB空间中工作更容易,而且大多数工具甚至不支持线性纹理,所以这可能不是首选的解决方案。
|
||||
|
||||
另一个解决方案是重校,或把这些sRGB纹理在进行任何颜色值的计算前变回线性空间。我们可以这样做:
|
||||
另一个解决方案是在对其颜色值进行任何计算之前重新校正或将这些sRGB纹理转换为线性空间。我们可以这样做
|
||||
|
||||
```c++
|
||||
float gamma = 2.2;
|
||||
@@ -106,7 +111,7 @@ vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
|
||||
```
|
||||
|
||||
如果你还打算在你的纹理中引入alpha元素,必究必须将纹理的内部格式指定为GL_SRGB_ALPHA。
|
||||
如果你还打算在你的纹理中引入alpha元素,你必须将纹理的内部格式指定为GL_SRGB_ALPHA。
|
||||
|
||||
因为不是所有纹理都是在sRGB空间中的所以当你把纹理指定为sRGB纹理时要格外小心。比如diffuse纹理,这种为物体上色的纹理几乎都是在sRGB空间中的。而为了获取光照参数的纹理,像specular贴图和法线贴图几乎都在线性空间中,所以如果你把它们也配置为sRGB纹理的话,光照就坏掉了。指定sRGB纹理时要当心。
|
||||
|
||||
@@ -115,7 +120,7 @@ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BY
|
||||
|
||||
## 衰减
|
||||
|
||||
在使用了gamma校正之后,另一个不同之处是光照衰减(Attenuation)。真实的物理世界中,光照的衰减和光源的距离的平方成反比。
|
||||
伽马校正的另一个不同之处在于光照衰减。在真实的物理世界中,光衰减与光源距离的平方成反比。说通俗点,光强度随着离光源距离的平方增加而减少,如下图所示
|
||||
|
||||
```c++
|
||||
float attenuation = 1.0 / (distance * distance);
|
||||
@@ -131,18 +136,19 @@ float attenuation = 1.0 / distance;
|
||||
|
||||

|
||||
|
||||
这种差异产生的原因是,光的衰减方程改变了亮度值,而且屏幕上显示出来的也不是线性空间,在监视器上效果最好的衰减方程,并不是符合物理的。想想平方衰减方程,如果我们使用这个方程,而且不进行gamma校正,显示在监视器上的衰减方程实际上将变成\((1.0 / distance^2)^{2.2}\)。若不进行gamma校正,将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实,因为它实际变成了\((1.0 / distance)^{2.2} = 1.0 / distance^{2.2}\)。这和物理公式是很相似的。
|
||||
这种差异产生的原因是,光的衰减方程改变了亮度值,而且屏幕上显示出来的也不是线性空间,在显示器上效果最好的衰减方程,并不是符合物理的。想想平方衰减方程,如果我们使用这个方程,而且不进行gamma校正,显示在监视器上的衰减方程实际上将变成\((1.0 / distance^2)^{2.2}\)。若不进行gamma校正,将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实,因为它实际变成了\((1.0 / distance)^{2.2} = 1.0 / distance^{2.2}\)。这和物理公式是很相似的。
|
||||
|
||||
!!! Important
|
||||
|
||||
我们在基础光照教程中讨论的更高级的那个衰减方程在有gamma校正的场景中也仍然有用,因为它可以让我们对衰减拥有更多准确的控制权(不过,在进行gamma校正的场景中当然需要不同的参数)。
|
||||
|
||||
我创建的这个简单的demo场景,你可以在这里找到源码以及顶点和像素着色器。按下空格就能在有gamma校正和无gamma校正的场景进行切换,两个场景使用的是相同的纹理和衰减。这不是效果最好的demo,不过它能展示出如何应用所有这些技术。
|
||||
我创建的这个简单的demo场景,你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/5.advanced_lighting/2.gamma_correction/gamma_correction.cpp)找到源码。按下空格就能在有gamma校正和无gamma校正的场景进行切换,两个场景使用的是相同的纹理和衰减。这不是效果最好的demo,不过它能展示出如何应用所有这些技术。
|
||||
|
||||
总而言之,gamma校正使你可以在线性空间中进行操作。因为线性空间更符合物理世界,大多数物理公式现在都可以获得较好效果,比如真实的光的衰减。你的光照越真实,使用gamma校正获得漂亮的效果就越容易。这也正是为什么当引进gamma校正时,建议只去调整光照参数的原因。
|
||||
|
||||
## 附加资源
|
||||
|
||||
- [What every coder should know about gamma](https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/)John Novak写的一篇关于伽马校正的深度文章。
|
||||
- [cambridgeincolour.com](http://www.cambridgeincolour.com/tutorials/gamma-correction.htm):更多关于gamma和gamma校正的内容。
|
||||
- [wolfire.com](http://blog.wolfire.com/2010/02/Gamma-correct-lighting): David Rosen关于在渲染领域使用gamma校正的好处。
|
||||
- [blog.wolfire.com](http://blog.wolfire.com/2010/02/Gamma-correct-lighting): 这是David Rosen的一篇关于伽马校正在图形渲染中的好处的博客。
|
||||
- [renderwonk.com](http://renderwonk.com/blog/index.php/archive/adventures-with-gamma-correct-rendering/): 一些额外的实践上的思考。
|
||||
|
@@ -4,63 +4,59 @@
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | gjy_1992
|
||||
校对 | gjy_1992, [1i9h7_b1u3](https://github.com/1012796366/)
|
||||
|
||||
!!! note
|
||||
|
||||
本节暂未进行完全的重写,错误可能会很多。如果可能的话,请对照原文进行阅读。如果有报告本节的错误,将会延迟至重写之后进行处理。
|
||||
|
||||
阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。场景和物体的深度感因此能够得到极大提升,下图展示了有阴影和没有阴影的情况下的不同:
|
||||
阴影是由于遮挡导致光线无法到达而形成的。当一个光源的光线因为被其他物体阻挡而无法照射到某个物体时,该物体便处于阴影之中。阴影为光照场景增添了极强的真实感,让观察者能够更容易感知物体之间的空间关系。为场景赋予了更强的立体感。例如,观察下方两张图,一张有阴影而另外一张没有阴影:
|
||||
|
||||

|
||||
|
||||
你可以看到,有阴影的时候你能更容易地区分出物体之间的位置关系,例如,当使用阴影的时候浮在地板上的立方体的事实更加清晰。
|
||||
你可以看到,有阴影的时候你能更容易地区分出物体之间的位置关系,例如,只有在有阴影的情况下,我们才能明显看到其中一个立方体悬浮于其他立方体之上。
|
||||
|
||||
阴影还是比较不好实现的,因为当前实时渲染领域还没找到一种完美的阴影算法。目前有几种近似阴影技术,但它们都有自己的弱点和不足,这点我们必须要考虑到。
|
||||
然而,实现阴影绝非易事,主要是因为目前的实时(光栅化图形)研究领域并没有开发出完美的阴影算法,虽然已经有许多优秀的阴影近似算法,但它们都有无法忽略的瑕疵。
|
||||
|
||||
视频游戏中较多使用的一种技术是阴影贴图(shadow mapping),效果不错,而且相对容易实现。阴影贴图并不难以理解,性能也不会太低,而且非常容易扩展成更高级的算法(比如 [Omnidirectional Shadow Maps](http://learnopengl.com/#!Advanced-Lighting/Shadows/Point-Shadows)和 [Cascaded Shadow Maps](http://learnopengl.com/#!Advanced-Lighting/Shadows/CSM))。
|
||||
大多数电子游戏中使用的一种技术是<def>阴影映射</def>(shadow mapping),效果不错,而且相对容易实现。阴影映射并不难以理解,性能开销不算太高,而且非常容易扩展成更高级的算法(比如[全向阴影贴图](02 Point Shadows.md)和[级联阴影贴图](../../08 Guest Articles/2021/01 CSM.md))。
|
||||
|
||||
## 阴影映射
|
||||
|
||||
阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
|
||||
阴影映射背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于从光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
|
||||
|
||||

|
||||
|
||||
这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment:它们应该渲染为带阴影的。如果我们绘制一条从光源出发,到达最右边盒子上的一个片段上的线段或射线,那么射线将先击中悬浮的盒子,随后才会到达最右侧的盒子。结果就是悬浮的盒子被照亮,而最右侧的盒子将处于阴影之中。
|
||||
这里的所有蓝线代表光源可以看到的片段。黑线代表被遮挡的片段:它们会被渲染为处于阴影中的片段。如果我们绘制一条从光源出发,到达最右边盒子上的一个片段上的线段或<def>射线</def>(ray),那么射线将先击中悬浮的盒子,随后才会到达最右侧的盒子。结果就是悬浮的盒子被照亮,而最右侧的盒子将处于阴影之中。
|
||||
|
||||
我们希望得到射线第一次击中的那个物体,然后用这个最近点和射线上其他点进行对比。然后我们将测试一下看看射线上的其他点是否比最近点更远,如果是的话,这个点就在阴影中。对从光源发出的射线上的成千上万个点进行遍历是个极端消耗性能的举措,实时渲染上基本不可取。我们可以采取相似举措,不用投射出光的射线。我们所使用的是非常熟悉的东西:深度缓冲。
|
||||
我们希望得到射线首次击中物体时的交点,然后用这个最近的点和射线上其他点进行对比。随后我们将测试一下,如果一个测试点比最近点更远的话,那么这个点就在阴影中。然而,若从这类光源发射出成千上万条光线并逐一遍历,是一种极为低效的方法,实时渲染上基本不可取。不过,我们可以采取相似举措,不用投射出光的射线,而是使用我们非常熟悉的东西:深度缓冲。
|
||||
|
||||
你可能记得在[深度测试](http://learnopengl.com/#!Advanced-OpenGL/Depth-testing)教程中,在深度缓冲里的一个值是摄像机视角下,对应于一个片段的一个0到1之间的深度值。如果我们从光源的透视图来渲染场景,并把深度值的结果储存到纹理中会怎样?通过这种方式,我们就能对光源的透视图所见的最近的深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片段了。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)或阴影贴图。
|
||||
你可能记得在[深度测试](../../04 Advanced OpenGL/01 Depth testing.md)教程中,在深度缓冲里的一个值对应于片段在摄像机视角下的深度值,其范围在0到1之间。如果我们从光源的视角来渲染场景,并把生成的深度值储存到纹理中会怎样?通过这种方式,我们就能从光源的视角采样最近的深度值。最终便可获得该方向上第一个可见片段的深度值。我们将所有的深度值存储到一个纹理中,称之为<def>深度贴图</def>(depth map)或是<def>阴影贴图</def>(shadow map)。
|
||||
|
||||

|
||||
|
||||
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片段是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个\(T\)变换,它可以将任何三维位置转变到光源的可见坐标空间。
|
||||
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用来确定片段是否在阴影中。我们使用该光源的视图矩阵和投影矩阵,从光源的角度下渲染场景,从而生成深度贴图。这个投影矩阵和视图矩阵一同形成了一个变换矩阵\(T\),它可以将任何三维位置转变到光源的可见坐标空间。
|
||||
|
||||
!!! Important
|
||||
|
||||
定向光并没有位置,因为它被规定为无穷远。然而,为了实现阴影贴图,我们得从一个光的透视图渲染场景,这样就得在光的方向的某一点上渲染场景。
|
||||
因为定向光源被设定为无限远,所以它没有具体的位置。然而,为了实现阴影映射,我们得从光源的某个虚拟位置,沿着定向光源方向来渲染场景。
|
||||
|
||||
在右边的图中我们显示出同样的平行光和观察者。我们渲染一个点\(\bar{\color{red}{P}}\)处的片段,需要决定它是否在阴影中。我们先得使用\(T\)把\(\bar{\color{red}{P}}\)变换到光源的坐标空间里。既然点\(\bar{\color{red}{P}}\)是从光的透视图中看到的,它的z坐标就对应于它的深度,例子中这个值是0.9。使用点\(\bar{\color{red}{P}}\)在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点\(\bar{\color{green}{C}}\),最近的深度是0.4。因为索引深度贴图的结果是一个小于点\(\bar{\color{red}{P}}\)的深度,我们可以断定\(\bar{\color{red}{P}}\)被挡住了,它在阴影中了。
|
||||
我们可以看到,在右边的图中,平行光和观察者位置都与左图相同。我们渲染一个在点\(\bar{\color{red}{P}}\)处的片段,需要确定它是否在阴影中。我们得先使用变换矩阵\(T\)把点\(\bar{\color{red}{P}}\)变换到光源的坐标空间里。既然现在是从光的角度来看点\(\bar{\color{red}{P}}\)的,那么该点的z坐标就相当于它的深度值,本例中这个值是0.9。 通过点\(\bar{\color{red}{P}}\)的坐标,我们可以采样深度/阴影贴图,获得从光源视角中可见的最近深度值,结果是点\(\bar{\color{green}{C}}\),本例中,最近的深度值是0.4。因为采样深度贴图的结果是一个小于点\(\bar{\color{red}{P}}\)的深度值,我们可以断定\(\bar{\color{red}{P}}\)被挡住了,它在阴影中了。
|
||||
|
||||
阴影映射由两个步骤组成:首先,我们渲染深度贴图,然后我们像往常一样渲染场景,使用生成的深度贴图来计算片段是否在阴影之中。听起来有点复杂,但随着我们一步一步地讲解这个技术,就能理解了。
|
||||
因此,阴影映射由两个步骤组成:首先,我们渲染深度贴图,然后我们像往常一样渲染场景,使用生成的深度贴图来计算片段是否在阴影之中。听起来有点复杂,但随着我们一步一步地讲解这个技术,就能理解了。
|
||||
|
||||
## 深度贴图
|
||||
|
||||
第一步我们需要生成一张深度贴图(Depth Map)。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,我们将再次需要帧缓冲。
|
||||
第一步我们需要生成一张深度贴图(<def>Depth Map</def>)。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,我们将再次需要帧缓冲。
|
||||
|
||||
首先,我们要为渲染的深度贴图创建一个帧缓冲对象:
|
||||
|
||||
```c++
|
||||
GLuint depthMapFBO;
|
||||
unsigned int depthMapFBO;
|
||||
glGenFramebuffers(1, &depthMapFBO);
|
||||
```
|
||||
|
||||
然后,创建一个2D纹理,提供给帧缓冲的深度缓冲使用:
|
||||
|
||||
```c++
|
||||
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
|
||||
|
||||
GLuint depthMap;
|
||||
const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
|
||||
|
||||
unsigned int depthMap;
|
||||
glGenTextures(1, &depthMap);
|
||||
glBindTexture(GL_TEXTURE_2D, depthMap);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
|
||||
@@ -68,7 +64,7 @@ glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
```
|
||||
|
||||
生成深度贴图不太复杂。因为我们只关心深度值,我们要把纹理格式指定为GL_DEPTH_COMPONENT。我们还要把纹理的高宽设置为1024:这是深度贴图的分辨率。
|
||||
@@ -83,7 +79,7 @@ glReadBuffer(GL_NONE);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
```
|
||||
|
||||
我们需要的只是在从光的透视图下渲染场景的时候深度信息,所以颜色缓冲没有用。然而,不包含颜色缓冲的帧缓冲对象是不完整的,所以我们需要显式告诉OpenGL我们不适用任何颜色数据进行渲染。我们通过将调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事。
|
||||
我们只需要从光源角度渲染场景时的深度信息,因此不需要使用颜色缓冲。然而,不包含颜色缓冲的帧缓冲对象是不完整的,所以我们需要显式告诉OpenGL我们不会渲染任何颜色数据。我们用glDrawBuffer和glReadBuffer来把读和绘制缓冲设置为GL_NONE。
|
||||
|
||||
合理配置将深度值渲染到纹理的帧缓冲后,我们就可以开始第一步了:生成深度贴图。两个步骤的完整的渲染阶段,看起来有点像这样:
|
||||
|
||||
@@ -91,9 +87,9 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// 1. 首选渲染深度贴图
|
||||
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
ConfigureShaderAndMatrices();
|
||||
RenderScene();
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
ConfigureShaderAndMatrices();
|
||||
RenderScene();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// 2. 像往常一样渲染场景,但这次使用深度贴图
|
||||
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
|
||||
@@ -103,55 +99,57 @@ glBindTexture(GL_TEXTURE_2D, depthMap);
|
||||
RenderScene();
|
||||
```
|
||||
|
||||
这段代码隐去了一些细节,但它表达了阴影映射的基本思路。这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景(通常是窗口分辨率)有着不同的分辨率,我们需要改变视口(viewport)的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。
|
||||
这段代码隐去了一些细节,但它表达了阴影映射的基本思路。这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景(通常是窗口分辨率)有着不同的分辨率,我们需要改变视口(<def>viewport</def>)的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。
|
||||
|
||||
### 光源空间的变换
|
||||
|
||||
前面那段代码中一个不清楚的函数是`ConfigureShaderAndMatrices`。它是用来在第二个步骤确保为每个物体设置了合适的投影和视图矩阵,以及相关的模型矩阵。然而,第一个步骤中,我们从光的位置的视野下使用了不同的投影和视图矩阵来渲染的场景。
|
||||
前面那段代码中一个不清楚的函数是`ConfigureShaderAndMatrices`。在第二个步骤中,这和往常一样:确保投影矩阵和视图矩阵都已经正确设置,并且为每个物体设置相应的模型矩阵。然而,在第一个步骤中,我们需要使用不同的投影矩阵和视图矩阵来从光源角度渲染场景。
|
||||
|
||||
因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形:
|
||||
|
||||
```c++
|
||||
GLfloat near_plane = 1.0f, far_plane = 7.5f;
|
||||
float near_plane = 1.0f, far_plane = 7.5f;
|
||||
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
|
||||
```
|
||||
|
||||
这里有个本节教程的demo场景中使用的正交投影矩阵的例子。因为投影矩阵间接决定可视区域的范围,以及哪些东西不会被裁切,你需要保证投影视锥(frustum)的大小,以包含打算在深度贴图中包含的物体。当物体和片段不在深度贴图中时,它们就不会产生阴影。
|
||||
上面的代码是本章演示场景中所使用的正交投影矩阵的示例。因为该投影矩阵间接决定可视区域的范围(例如哪些东西不会被裁切),所以你需要确保投影视锥的尺寸包括了应当出现在深度贴图里面的所有物体。当物体或者片段没有出现在深度贴图中的时候,它们就不会产生阴影。
|
||||
|
||||
为了创建一个视图矩阵来变换每个物体,把它们变换到从光源视角可见的空间中,我们将使用glm::lookAt函数;这次从光源的位置看向场景中央。
|
||||
|
||||
```c++
|
||||
glm::mat4 lightView = glm::lookAt(glm::vec3(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
glm::mat4 lightView = glm::lookAt(glm::vec3(-2.0f, 4.0f, -1.0f),
|
||||
glm::vec3( 0.0f, 0.0f, 0.0f),
|
||||
glm::vec3( 0.0f, 1.0f, 0.0f));
|
||||
```
|
||||
|
||||
二者相结合为我们提供了一个光空间的变换矩阵,它将每个世界空间坐标变换到光源处所见到的那个空间;这正是我们渲染深度贴图所需要的。
|
||||
二者相结合为我们提供了一个光源空间的变换矩阵,它将每个世界空间向量变换到在光源位置可以看到的的空间;这正是我们渲染深度贴图所需要的。
|
||||
|
||||
```c++
|
||||
glm::mat4 lightSpaceMatrix = lightProjection * lightView;
|
||||
```
|
||||
|
||||
这个lightSpaceMatrix正是前面我们称为\(T\)的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵,我们就能像往常那样渲染场景了。然而,我们只关心深度值,并非所有片段计算都在我们的着色器中进行。为了提升性能,我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。
|
||||
这个`lightSpaceMatrix`正是前面我们称为\(T\)的那个变换矩阵。有了`lightSpaceMatrix`,只要给每个着色器提供光源空间的投影矩阵和视图矩阵,我们就能像往常那样渲染场景了。然而,我们只关心深度值,并不执行复杂的(照明)片段计算。为了提升性能,我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。
|
||||
|
||||
### 渲染至深度贴图
|
||||
|
||||
当我们以光的透视图进行场景渲染的时候,我们会用一个比较简单的着色器,这个着色器除了把顶点变换到光空间以外,不会做得更多了。这个简单的着色器叫做`simpleDepthShader`,就是使用下面的这个着色器:
|
||||
当我们从光的角度来渲染场景的时候,我们会用一个比较简单的着色器,这个着色器只会把顶点变换到光空间。这个简单的着色器叫做`simpleDepthShader`,就是使用下面的这个着色器:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 position;
|
||||
|
||||
layout (location = 0) in vec3 aPos;
|
||||
|
||||
uniform mat4 lightSpaceMatrix;
|
||||
uniform mat4 model;
|
||||
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
|
||||
}
|
||||
gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
这个顶点着色器将一个单独模型的一个顶点,使用lightSpaceMatrix变换到光空间中。
|
||||
这个顶点着色器接收模型矩阵和顶点数据,使用`lightSpaceMatrix`变换到光源空间中。
|
||||
|
||||
由于我们没有颜色缓冲,最后的片段不需要任何处理,所以我们可以简单地使用一个空片段着色器:
|
||||
由于我们没有颜色缓冲并且禁止了读取和绘制缓冲,因此生成的片段不需要进行任何处理,所以我们可以简单地使用一个空片段着色器:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
@@ -162,136 +160,134 @@ void main()
|
||||
}
|
||||
```
|
||||
|
||||
这个空片段着色器什么也不干,运行完后,深度缓冲会被更新。我们可以取消那行的注释,来显式设置深度,但是这个(指注释掉那行之后)是更有效率的,因为底层无论如何都会默认去设置深度缓冲。
|
||||
这个空片段着色器什么也不干,运行完后,深度缓冲会被更新。我们可以取消片段着色器中那一行的注释,来显式设置深度,但因为底层总会去设置深度缓冲,所以我们没必要显式设置,直接使用空的片段着色器即可。
|
||||
|
||||
渲染深度缓冲现在成了:
|
||||
现在,渲染深度/阴影贴图的过程如下所示:
|
||||
|
||||
```c++
|
||||
simpleDepthShader.Use();
|
||||
simpleDepthShader.use();
|
||||
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));
|
||||
|
||||
|
||||
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
RenderScene(simpleDepthShader);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
RenderScene(simpleDepthShader);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
```
|
||||
|
||||
这里的RenderScene函数的参数是一个着色器程序(shader program),它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。
|
||||
这里的`RenderScene`函数接受着色器程序(<def>shader program</def>)作为参数,它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。
|
||||
|
||||
最后,在光的透视图视角下,很完美地用每个可见片段的最近深度填充了深度缓冲。通过将这个纹理投射到一个2D四边形上(和我们在帧缓冲一节做的后处理过程类似),就能在屏幕上显示出来,我们会获得这样的东西:
|
||||
最终的成品是一个填充完整的深度缓冲区,其中存储了从光源视角可见的所有片段的最近深度值。通过将这个纹理渲染到一个2D四边形上(和我们在帧缓冲一节做的后期处理过程类似),就能在屏幕上显示出来下面的效果:
|
||||
|
||||

|
||||
|
||||
将深度贴图渲染到四边形上的片段着色器:
|
||||
我们使用下面的片段着色器来将深度贴图渲染到四边形上:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
out vec4 color;
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec2 TexCoords;
|
||||
|
||||
|
||||
uniform sampler2D depthMap;
|
||||
|
||||
|
||||
void main()
|
||||
{
|
||||
float depthValue = texture(depthMap, TexCoords).r;
|
||||
color = vec4(vec3(depthValue), 1.0);
|
||||
}
|
||||
FragColor = vec4(vec3(depthValue), 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
要注意的是当用透视投影矩阵取代正交投影矩阵来显示深度时,有一些轻微的改动,因为使用透视投影时,深度是非线性的。本节教程的最后,我们会讨论这些不同之处。
|
||||
要注意的是当用透视投影矩阵而不是正交投影矩阵来显示深度时,存在一些细微的差异,因为使用透视投影时,深度是非线性的。本节教程的最后,我们会讨论这些不同之处。
|
||||
|
||||
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/5.advanced_lighting/3.1.1.shadow_mapping_depth/shadow_mapping_depth.cpp)获得把场景渲染成深度贴图的源码。
|
||||
|
||||
## 渲染阴影
|
||||
|
||||
正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在片段着色器中执行,用来检验一个片段是否在阴影之中,不过我们在顶点着色器中进行光空间的变换:
|
||||
正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在片段着色器中执行,用来检验一个片段是否在阴影之中,不过我们在顶点着色器中进行光源空间的变换:
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
layout (location = 0) in vec3 position;
|
||||
layout (location = 1) in vec3 normal;
|
||||
layout (location = 2) in vec2 texCoords;
|
||||
|
||||
out vec2 TexCoords;
|
||||
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aNormal;
|
||||
layout (location = 2) in vec2 aTexCoords;
|
||||
|
||||
out VS_OUT {
|
||||
vec3 FragPos;
|
||||
vec3 Normal;
|
||||
vec2 TexCoords;
|
||||
vec4 FragPosLightSpace;
|
||||
} vs_out;
|
||||
|
||||
|
||||
uniform mat4 projection;
|
||||
uniform mat4 view;
|
||||
uniform mat4 model;
|
||||
uniform mat4 lightSpaceMatrix;
|
||||
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = projection * view * model * vec4(position, 1.0f);
|
||||
vs_out.FragPos = vec3(model * vec4(position, 1.0));
|
||||
vs_out.Normal = transpose(inverse(mat3(model))) * normal;
|
||||
vs_out.TexCoords = texCoords;
|
||||
{
|
||||
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
|
||||
vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
|
||||
vs_out.TexCoords = aTexCoords;
|
||||
vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
|
||||
gl_Position = projection * view * vec4(vs_out.FragPos, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
这儿的新的地方是FragPosLightSpace这个输出向量。我们用同一个lightSpaceMatrix,把世界空间顶点位置转换为光空间。顶点着色器传递一个普通的经变换的世界空间顶点位置vs_out.FragPos和一个光空间的vs_out.FragPosLightSpace给片段着色器。
|
||||
这段代码里的新内容是`FragPosLightSpace`这个输出向量。我们用和之前一样的`lightSpaceMatrix`(即生成深度贴图时,用于将世界空间中的顶点坐标变换到光源空间的矩阵),把世界空间中的顶点位置转换到光源空间,方便之后在片段着色器中使用。
|
||||
|
||||
片段着色器使用Blinn-Phong光照模型渲染场景。我们接着计算出一个shadow值,当fragment在阴影中时是1.0,在阴影外是0.0。然后,diffuse和specular颜色会乘以这个阴影元素。由于阴影不会是全黑的(由于散射),我们把ambient分量从乘法中剔除。
|
||||
我们用来渲染场景的主片段着色器使用了<def>Blinn-Phong</def>光照模型。在片段着色器中,我们计算阴影分量,当片段处于阴影中时,其值为1.0,当片段不在阴影中时,其值为0.0,然后将得到的漫反射分量和镜面分量乘以这个阴影分量。因为光散射的缘故,阴影很少是完全黑暗的,所以我们并没有让环境分量乘以阴影分量。
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
out vec4 FragColor;
|
||||
|
||||
|
||||
in VS_OUT {
|
||||
vec3 FragPos;
|
||||
vec3 Normal;
|
||||
vec2 TexCoords;
|
||||
vec4 FragPosLightSpace;
|
||||
} fs_in;
|
||||
|
||||
|
||||
uniform sampler2D diffuseTexture;
|
||||
uniform sampler2D shadowMap;
|
||||
|
||||
|
||||
uniform vec3 lightPos;
|
||||
uniform vec3 viewPos;
|
||||
|
||||
|
||||
float ShadowCalculation(vec4 fragPosLightSpace)
|
||||
{
|
||||
[...]
|
||||
}
|
||||
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
|
||||
vec3 normal = normalize(fs_in.Normal);
|
||||
vec3 lightColor = vec3(1.0);
|
||||
// Ambient
|
||||
vec3 ambient = 0.15 * color;
|
||||
// Diffuse
|
||||
// 环境光
|
||||
vec3 ambient = 0.15 * lightColor;
|
||||
// 漫反射
|
||||
vec3 lightDir = normalize(lightPos - fs_in.FragPos);
|
||||
float diff = max(dot(lightDir, normal), 0.0);
|
||||
vec3 diffuse = diff * lightColor;
|
||||
// Specular
|
||||
// 镜面高光
|
||||
vec3 viewDir = normalize(viewPos - fs_in.FragPos);
|
||||
vec3 reflectDir = reflect(-lightDir, normal);
|
||||
float spec = 0.0;
|
||||
vec3 halfwayDir = normalize(lightDir + viewDir);
|
||||
spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
|
||||
vec3 specular = spec * lightColor;
|
||||
// 计算阴影
|
||||
// 计算阴影值
|
||||
float shadow = ShadowCalculation(fs_in.FragPosLightSpace);
|
||||
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
|
||||
|
||||
FragColor = vec4(lighting, 1.0f);
|
||||
FragColor = vec4(lighting, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
片段着色器大部分是从高级光照教程中复制过来,只不过加上了个阴影计算。我们声明一个shadowCalculation函数,用它计算阴影。片段着色器的最后,我们我们把diffuse和specular乘以(1-阴影元素),这表示这个片段有多大成分不在阴影中。这个片段着色器还需要两个额外输入,一个是光空间的片段位置和第一个渲染阶段得到的深度贴图。
|
||||
片段着色器大部分是从[高级光照](../01 Advanced Lighting.md/)教程中复制过来的,只不过加上了个阴影计算。我们声明一个`shadowCalculation`函数,用它计算阴影。片段着色器的最后,我们我们把漫反射分量和镜面分量乘以阴影分量的反值(1.0-`shadow`),这表示该片段有不受阴影遮挡的程度。这个片段着色器还需要两个额外输入,一个是变换到光源空间后的片段位置,另一个是第一个渲染阶段得到的深度贴图。
|
||||
|
||||
首先要检查一个片段是否在阴影中,把光空间片段位置转换为裁切空间的标准化设备坐标。当我们在顶点着色器输出一个裁切空间顶点位置到gl_Position时,OpenGL自动进行一个透视除法,将裁切空间坐标的范围-w到w转为-1到1,这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到片段着色器里,我们必须自己做透视除法:
|
||||
要检查一个片段是否在阴影中,首先要把光源空间片段位置转换为裁切空间的标准化设备坐标。当我们在顶点着色器输出一个裁切空间顶点位置到`gl_Position`时,OpenGL自动进行一个透视除法,例如,将裁切空间坐标的范围[-w,w]转为[-1,1],这要将x、y、z分量除以向量的w分量来实现。由于裁切空间的`FragPosLightSpace`并不会通过`gl_Position`传到片段着色器里,我们必须自己做透视除法:
|
||||
|
||||
```c++
|
||||
float ShadowCalculation(vec4 fragPosLightSpace)
|
||||
@@ -302,20 +298,23 @@ float ShadowCalculation(vec4 fragPosLightSpace)
|
||||
}
|
||||
```
|
||||
|
||||
返回了片段在光空间的-1到1的范围。
|
||||
这将返回片段在光源空间中的位置,并将范围限定在[-1,1]。
|
||||
|
||||
!!! Important
|
||||
|
||||
当使用正交投影矩阵,顶点w元素仍保持不变,所以这一步实际上毫无意义。可是,当使用透视投影的时候就是必须的了,所以为了保证在两种投影矩阵下都有效就得留着这行。
|
||||
当使用正交投影矩阵时,向量的w分量仍保持不变,所以这一步实际上毫无意义。可是,当使用透视投影的时候,这一步又变得不可或缺。所以为了保证在两种投影矩阵下都能正常运作,就得留着这行。
|
||||
|
||||
因为来自深度贴图的深度在0到1的范围,我们也打算使用projCoords从深度贴图中去采样,所以我们将NDC坐标变换为0到1的范围。
|
||||
(译者注:这里的意思是,上面的projCoords的xyz分量都是[-1,1](下面会指出这对于远平面之类的点才成立),而为了和深度贴图的深度相比较,z分量需要变换到[0,1];为了作为从深度贴图中采样的坐标,xy分量也需要变换到[0,1]。所以整个projCoords向量都需要变换到[0,1]范围。)
|
||||
因为来自深度贴图的深度范围是[0,1],我们也打算使用`projCoords`从深度贴图中去采样,所以我们将NDC坐标范围变换为[0,1]。
|
||||
|
||||
```c++
|
||||
projCoords = projCoords * 0.5 + 0.5;
|
||||
```
|
||||
|
||||
有了这些投影坐标,我们就能从深度贴图中采样得到0到1的结果,从第一个渲染阶段的projCoords坐标直接对应于变换过的NDC坐标。我们将得到光的位置视野下最近的深度:
|
||||
!!! note "译注"
|
||||
|
||||
这里的意思是,上面的projCoords的xyz分量都是[-1,1](下面会指出这对于远平面之类的点才成立),而为了和深度贴图的深度相比较,z分量需要变换到[0,1];为了作为从深度贴图中采样的坐标,xy分量也需要变换到[0,1]。所以整个projCoords向量都需要变换到[0,1]范围。
|
||||
|
||||
通过此变换,投影坐标projCoords的[0,1]范围坐标将与第一个渲染阶段生成的NDC坐标精确对应。这样我们就能从深度贴图中获取光源视角下的最近深度值:
|
||||
|
||||
```c++
|
||||
float closestDepth = texture(shadowMap, projCoords.xy).r;
|
||||
@@ -333,7 +332,7 @@ float currentDepth = projCoords.z;
|
||||
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
|
||||
```
|
||||
|
||||
完整的shadowCalculation函数是这样的:
|
||||
完整的`shadowCalculation`函数是这样的:
|
||||
|
||||
```c++
|
||||
float ShadowCalculation(vec4 fragPosLightSpace)
|
||||
@@ -353,31 +352,31 @@ float ShadowCalculation(vec4 fragPosLightSpace)
|
||||
}
|
||||
```
|
||||
|
||||
激活这个着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影以及视图矩阵,结果如下图所示:
|
||||
激活这个着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影矩阵以及视图矩阵,结果如下图所示:
|
||||
|
||||

|
||||
|
||||
如果你做对了,你会看到地板和上有立方体的阴影。你可以从这里找到demo程序的[源码](https://learnopengl.com/code_viewer_gh.php?code=src/5.advanced_lighting/3.1.2.shadow_mapping_base/shadow_mapping_base.cpp)。
|
||||
如果你做对了,你会看到地板和上有立方体的阴影(尽管还是有不少瑕疵)。你可以从[这里](https://learnopengl.com/code_viewer_gh.php?code=src/5.advanced_lighting/3.1.2.shadow_mapping_base/shadow_mapping_base.cpp)找到demo程序的源码。
|
||||
|
||||
## 改进阴影贴图
|
||||
## 改进阴影映射
|
||||
|
||||
我们试图让阴影映射工作,但是你也看到了,阴影映射还是有点不真实,我们修复它才能获得更好的效果,这是下面的部分所关注的焦点。
|
||||
我们成功地让阴影映射工作起来了,但是你也看到了,由于若干(清晰可见的)与阴影映射相关的瑕疵,目前的效果还是不够完善,我们得做些修复,接下来的章节中我们将着重解决这些问题。
|
||||
|
||||
### 阴影失真
|
||||
|
||||
前面的图片中明显有不对的地方。放大看会发现明显的线条样式:
|
||||
前面的图片中明显有不对的地方。放大看会发现明显的摩尔纹:
|
||||
|
||||

|
||||
|
||||
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做**阴影失真(Shadow Acne)**,下图解释了成因:
|
||||
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影的不真实感叫做<def>阴影失真</def>(Shadow Acne),下图解释了成因:
|
||||
|
||||

|
||||
|
||||
因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段从同一个深度值进行采样。
|
||||
受阴影贴图的分辨率影响,当多个片段距离光源比较远的时候,它们可能从深度贴图中采样相同的深度值。图片中,每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段会采样相同的深度值。
|
||||
|
||||
虽然很多时候没问题,但是当光源以一个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的。多个片段就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片段被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。
|
||||
虽然很多时候没问题,但是当光源以某个角度照射表面的时候就会出问题,这种情况下深度贴图也是从这样的角度下进行渲染的,随后,多个片段就会从同一个斜坡的深度纹理像素中采样,其中一部分在地板上面,另一部分在地板下面;这样我们所得到的阴影就有了差异。也因此,一部分片段位于阴影中,而一部分则不是,最终产生了图片中的条纹样式。
|
||||
|
||||
我们可以用一个叫做**阴影偏移**(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。
|
||||
我们可以用一个叫做<def>阴影偏移</def>(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。
|
||||
|
||||

|
||||
|
||||
@@ -388,66 +387,72 @@ float bias = 0.005;
|
||||
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
|
||||
```
|
||||
|
||||
一个0.005的偏移就能帮到很大的忙,但是有些表面坡度很大,仍然会产生阴影失真。有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量:使用点乘:
|
||||
一个0.005的偏移就能帮到很大的忙,但偏移值高度依赖于光源与表面的夹角,如果倾角特别大的话,那么阴影仍然还是会失真。更稳健的办法是根据表面朝向光线的角度来更改偏移量,使用点乘就可以实现这个办法:
|
||||
|
||||
```c++
|
||||
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
|
||||
```
|
||||
|
||||
这里我们有一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了同一个场景,但使用了阴影偏移,效果的确更好:
|
||||
这里我们有一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了使用了阴影偏移后的同一个场景,可以看出效果的确更好:
|
||||
|
||||

|
||||
|
||||
选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。
|
||||
因为各个场景中合适的偏差值都不尽相同,所以可能需要经过一番调整后才能找到合适的偏移值,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。
|
||||
|
||||
### 悬浮
|
||||
### 阴影悬浮
|
||||
|
||||
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):
|
||||
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移值足够大时,会导致阴影明显地偏离了实际物体,你可以从下图看到这个现象(这是一个夸张的偏移值):
|
||||
|
||||

|
||||
|
||||
这个阴影失真叫做悬浮(Peter Panning),因为物体看起来轻轻悬浮在表面之上(译注Peter Pan就是童话彼得潘,而panning有平移、悬浮之意,而且彼得潘是个会飞的男孩…)。我们可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling)你也许记得在面剔除教程中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。
|
||||
这个阴影失真叫做<def>阴影悬浮</def>(Peter Panning),因为物体看起来轻轻悬浮在表面之上。
|
||||
|
||||
因为我们只需要深度贴图的深度值,对于实体物体无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。
|
||||
!!! note "译注"
|
||||
|
||||
Peter Pan就是长篇小说《彼得·潘》中的人物,panning有平移、悬浮之意,而且彼得潘恰好是个会飞的男孩。
|
||||
|
||||
我们可以使用一个叫技巧解决大部分的阴影悬浮问题:当渲染深度贴图时候使用<def>正面剔除</def>(front face culling)。你也许记得在[面剔除](../../04 Advanced OpenGL/04 Face culling.md)教程中OpenGL默认是背面剔除。而现在我们要告诉OpenGL,在渲染阴影贴图时要剔除正面。
|
||||
|
||||
因为我们只需要深度贴图的深度值,对于实心物体来说,无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,即使阴影在物体内部有错误,我们也看不见。
|
||||
|
||||

|
||||
|
||||
为了修复peter游移,我们要进行正面剔除,先必须开启GL_CULL_FACE:
|
||||
为了修复阴影悬浮,在阴影贴图生成阶段,我们要进行正面剔除,首先必须开启GL_CULL_FACE:
|
||||
|
||||
```c++
|
||||
glCullFace(GL_FRONT);
|
||||
RenderSceneToDepthMap();
|
||||
glCullFace(GL_BACK); // 不要忘记设回原先的culling face
|
||||
glCullFace(GL_BACK); // 不要忘记设回原先的面剔除
|
||||
```
|
||||
|
||||
这十分有效地解决了peter panning的问题,但只对内部不会对外开口的实体物体有效。我们的场景中,在立方体上工作的很好,但在地板上无效,因为正面剔除完全移除了地板。地面是一个单独的平面,不会被完全剔除。如果有人打算使用这个技巧解决peter panning必须考虑到只有剔除物体的正面才有意义。
|
||||
这十分有效地解决了阴影悬浮的问题,但只适用于具有封闭内部空间的实心物体。在我们的场景中,该方法在立方体上工作的很好,但在地板上无效,这是因为地板是一个平面,因此将被完全剔除。如果打算使用这个技巧解决阴影悬浮,就应当只在合适的物体上进行正面剔除。
|
||||
|
||||
另一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免peter panning。
|
||||
另外要注意的是,接近阴影面的物体仍然可能会出现不正确的效果。但一般来说,通过常规的偏移值调整就足以解决阴影偏移的问题了。
|
||||
|
||||
### 采样过多
|
||||
### 过采样
|
||||
|
||||
无论你喜不喜欢还有一个视觉差异,就是光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
|
||||
不论你是否喜欢,还有一个视觉差异,即光的视锥范围以外的区域也会被判定为处于阴影之中。这是因为当投影坐标超出光的视锥范围时,其值会比1.0大,此时采样的深度纹理就会超出他默认的范围[0,1]。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
|
||||
|
||||

|
||||
|
||||
你可以在图中看到,光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。
|
||||
如图所示,存在一个虚构的光照范围,超出该区域的部分就被阴影覆盖;这个区域实际上代表着深度贴图投影到地板上的范围。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了`GL_REPEAT`。
|
||||
|
||||
我们宁可让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。我们可以储存一个边框颜色,然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER:
|
||||
我们更希望让所有超出深度贴图范围的坐标,其深度值是1.0,这样超出的坐标将永远不在阴影之中(因为没有物体的深度值是大于1.0的)。我们可以配置一个纹理边界颜色,然后把深度贴图的纹理环绕选项设置为`GL_CLAMP_TO_BORDER`:
|
||||
|
||||
```c++
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
|
||||
float borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
|
||||
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
|
||||
```
|
||||
|
||||
现在如果我们采样深度贴图0到1坐标范围以外的区域,纹理函数总会返回一个1.0的深度值,阴影值为0.0。结果看起来会更真实:
|
||||
现在如果我们采样深度贴图[0,1]范围以外的区域,纹理函数总会返回一个1.0的深度值,由此得到的阴影值始终为0.0。结果看起来会更真实:
|
||||
|
||||

|
||||
|
||||
仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。
|
||||
图中仍有一部分是阴影区域,那部分区域的坐标超出了光的正交视锥的远平面。通过阴影方向可以看到,这片阴影区域总是出现在光源视锥的远平面外。
|
||||
|
||||
当一个点比光的远平面还要远时,它的投影坐标的z坐标大于1.0。这种情况下,GL_CLAMP_TO_BORDER环绕方式不起作用,因为我们把坐标的z元素和深度贴图的值进行了对比;它总是为大于1.0的z返回true。
|
||||
当一个点在光源空间的z坐标大于1.0时,表示该点超出了远平面。这种情况下,`GL_CLAMP_TO_BORDER`环绕方式不起作用,这是因为在进行深度比较时,z>1.0的结果始终为真值。
|
||||
|
||||
解决这个问题也很简单,只要投影向量的z坐标大于1.0,我们就把shadow的值强制设为0.0:
|
||||
|
||||
@@ -462,25 +467,25 @@ float ShadowCalculation(vec4 fragPosLightSpace)
|
||||
}
|
||||
```
|
||||
|
||||
检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,我们最终会得到下面我们所追求的效果:
|
||||
通过边界颜色钳制并进行远平面特殊处理,我们最终解决了深度贴图的过采样问题。最终得到了预期之内的结果了:
|
||||
|
||||

|
||||
|
||||
这些结果意味着,只有在深度贴图范围以内的被投影的fragment坐标才有阴影,所以任何超出范围的都将会没有阴影。由于在游戏中通常这只发生在远处,就会比我们之前的那个明显的黑色区域效果更真实。
|
||||
这样的做法意味着阴影只会出现在深度贴图的覆盖范围内,而光源视锥体以外的区域不会产生阴影。在游戏开发过程中,不产生阴影的部分通常只会出现在远方,相比于此前让远方漆黑一片的做法,这种处理更合理一些。
|
||||
|
||||
## PCF
|
||||
|
||||
阴影现在已经附着到场景中了,不过这仍不是我们想要的。如果你放大看阴影,阴影映射对分辨率的依赖很快变得很明显。
|
||||
当前的阴影已经为场景增色不少,但是仍未达到理想水平。如果你放大来看,会发现分辨率对阴影映射造成了非常明显的影响。
|
||||
|
||||

|
||||
|
||||
因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。
|
||||
因为深度贴图的分辨率固定,一个纹理像素可能覆盖了多个片段,结果就是多个片段会从深度贴图中采样相同的深度值,并得到相同的阴影判定结果,这也就导致了图中的锯齿边缘。
|
||||
|
||||
你可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。
|
||||
你可以通过增加深度贴图的分辨率的方式来减少锯齿块,也可以尝试尽可能的让光的视锥贴合场景。
|
||||
|
||||
另一个(并不完整的)解决方案叫做PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。
|
||||
另一种(部分)解决方案叫做PCF,译作<def>百分比渐进滤波</def>(percentage-closer filtering),这个术语涵盖了多种滤波函数,能生成更柔和的阴影,减少锯齿块。其核心思想是多次采样深度贴图,每一次采样的纹理坐标都稍有不同,独立判断每个采样点的阴影状态后,将子结果混合取平均,最终获得相对柔和的阴影。
|
||||
|
||||
一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:
|
||||
一种简单的PCF实现是采样深度贴图周边纹理像素,并取平均值:
|
||||
|
||||
```c++
|
||||
float shadow = 0.0;
|
||||
@@ -496,57 +501,58 @@ for(int x = -1; x <= 1; ++x)
|
||||
shadow /= 9.0;
|
||||
```
|
||||
|
||||
这个textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小,我们用以对纹理坐标进行偏移,确保每个新样本,来自不同的深度值。这里我们采样得到9个值,它们在投影坐标的x和y值的周围,为阴影阻挡进行测试,并最终通过样本的总数目将结果平均化。
|
||||
此处的`textureSize`返回指定采样器纹理在0级mipmap的宽高向量,类型为vec2。取其倒数,即可得到单一一个纹理像素的大小,可以用来对纹理坐标进行偏移,从而确保每次都采样不同的深度值。本示例在每个投影坐标周围采样9个点来进行阴影判断,最终取平均值。
|
||||
|
||||
使用更多的样本,更改texelSize变量,你就可以增加阴影的柔和程度。下面你可以看到应用了PCF的阴影:
|
||||
增加采样次数,同时/或是更改`texelSize`变量,可以进一步提升柔化效果。下图展示了应用简易PCF后的阴影:
|
||||
|
||||

|
||||
|
||||
从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影贴图分辨率的不真实感,但通常对于大多数应用来说效果已经很好了。
|
||||
从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影的不真实感,但通常对于大多数应用来说效果已经很好了。
|
||||
|
||||
你可以从[这里](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping)找到这个例子的全部源码和第二个阶段的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping&type=fragment)着色器。
|
||||
你可以从[这里](https://learnopengl.com/code_viewer_gh.php?code=src/5.advanced_lighting/3.1.3.shadow_mapping/shadow_mapping.cpp)找到这个例子的全部源码。
|
||||
|
||||
实际上PCF还有更多的内容,以及很多技术要点需要考虑以提升柔和阴影的效果,但处于本章内容长度考虑,我们将留在以后讨论。
|
||||
实际上PCF技术体系包含了更多的柔化阴影边缘的方案,但限于本章篇幅,我们将留在以后讨论。
|
||||
|
||||
|
||||
### 正交 vs 投影
|
||||
### 正交投影 vs 透视投影
|
||||
|
||||
在渲染深度贴图的时候,正交(Orthographic)和投影(Projection)矩阵之间有所不同。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
|
||||
在渲染深度贴图的时候,正交(orthographic)矩阵和透视(perspective)矩阵之间存在本质差异。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
|
||||
|
||||

|
||||
|
||||
透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。
|
||||
|
||||
另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。发生这个是因为透视投影下,深度变成了非线性的深度值,它的大多数可辨范围都位于近平面附近。为了可以像使用正交投影一样合适地观察深度值,你必须先将非线性深度值转变为线性的,如我们在深度测试教程中已经讨论过的那样。
|
||||
另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。这个是因为透视投影将深度值转换为非线性值,其变化范围大多集中在近平面附近。为了可以像使用正交投影一样合适地观察深度值,你必须先将非线性深度值转变为线性值,如我们在[深度测试](../../04 Advanced OpenGL/01 Depth testing.md)教程中已经讨论过的那样。
|
||||
|
||||
```c++
|
||||
#version 330 core
|
||||
out vec4 color;
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec2 TexCoords;
|
||||
|
||||
|
||||
uniform sampler2D depthMap;
|
||||
uniform float near_plane;
|
||||
uniform float far_plane;
|
||||
|
||||
|
||||
float LinearizeDepth(float depth)
|
||||
{
|
||||
float z = depth * 2.0 - 1.0; // Back to NDC
|
||||
float z = depth * 2.0 - 1.0; // 转换为 NDC
|
||||
return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
|
||||
}
|
||||
|
||||
|
||||
void main()
|
||||
{
|
||||
float depthValue = texture(depthMap, TexCoords).r;
|
||||
color = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
|
||||
// color = vec4(vec3(depthValue), 1.0); // orthographic
|
||||
}
|
||||
FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // 透视
|
||||
// FragColor = vec4(vec3(depthValue), 1.0); // 正交
|
||||
}
|
||||
```
|
||||
|
||||
这个深度值与我们见到的用正交投影的很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。
|
||||
这个深度值与我们见到的正交投影的深度值很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。
|
||||
|
||||
## 附加资源
|
||||
|
||||
- [Tutorial 16 : Shadow mapping](http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/):提供的类似的阴影映射教程,里面有一些额外的解释。
|
||||
- [Shadow Mapping – Part 1:ogldev](http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html):提供的另一个阴影映射教程。
|
||||
- [How Shadow Mapping Works](https://www.youtube.com/watch?v=EsccgeUpdsM):的一个第三方YouTube视频教程,里面解释了阴影映射及其实现。
|
||||
- [Shadow Mapping – Part 1](http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html):提供的另一个阴影映射教程。
|
||||
- [How Shadow Mapping Works](https://www.youtube.com/watch?v=EsccgeUpdsM):一个第三方YouTube视频教程,里面解释了阴影映射及其实现。
|
||||
- [Common Techniques to Improve Shadow Depth Maps](https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324%28v=vs.85%29.aspx):微软的一篇好文章,其中理出了很多提升阴影贴图质量的技术。
|
||||
- [How I Implemented Shadows in my Game Engine](https://www.youtube.com/watch?v=uueB2kVvbHo):ThinMatrix关于改进阴影贴图的好视频
|
@@ -375,7 +375,7 @@ $$
|
||||
|
||||
$$
|
||||
L_o(p,\phi_o, \theta_o) =
|
||||
k_d\frac{c}{\pi} \frac{1}{n1 n2} \sum_{\phi = 0}^{n1} \sum_{\theta = 0}^{n2} L_i(p,\phi_i, \theta_i) \cos(\theta) \sin(\theta) d\phi d\theta
|
||||
k_d \frac{c\pi}{n1 n2} \sum_{\phi = 0}^{n1} \sum_{\theta = 0}^{n2} L_i(p,\phi_i, \theta_i) \cos(\theta) \sin(\theta) d\phi d\theta
|
||||
$$
|
||||
|
||||
当我们离散地对两个球坐标轴进行采样时,每个采样近似代表了半球上的一小块区域,如上图所示。注意,由于球的一般性质,当采样区域朝向中心顶部会聚时,天顶角 \(\theta\) 变高,半球的离散采样区域变小。为了平衡较小的区域贡献度,我们使用 \(sin\theta\) 来权衡区域贡献度,这就是多出来的 \(sin\) 的作用。
|
||||
@@ -386,8 +386,8 @@ $$
|
||||
vec3 irradiance = vec3(0.0);
|
||||
|
||||
vec3 up = vec3(0.0, 1.0, 0.0);
|
||||
vec3 right = cross(up, normal);
|
||||
up = cross(normal, right);
|
||||
vec3 right = normalize(cross(up, normal));
|
||||
up = normalize(cross(normal, right));
|
||||
|
||||
float sampleDelta = 0.025;
|
||||
float nrSamples = 0.0;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 11 KiB |
@@ -43,7 +43,6 @@ nav:
|
||||
- 阴影:
|
||||
- 阴影映射: '05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md'
|
||||
- 点阴影: '05 Advanced Lighting/03 Shadows/02 Point Shadows.md'
|
||||
- CSM: '05 Advanced Lighting/03 Shadows/03 CSM.md'
|
||||
- 法线贴图: '05 Advanced Lighting/04 Normal Mapping.md'
|
||||
- 视差贴图: '05 Advanced Lighting/05 Parallax Mapping.md'
|
||||
- HDR: '05 Advanced Lighting/06 HDR.md'
|
||||
@@ -77,6 +76,8 @@ nav:
|
||||
- Guest Articles:
|
||||
- 2020:
|
||||
- 骨骼动画: '08 Guest Articles/2020/01 Skeletal Animation.md'
|
||||
- 2021:
|
||||
- CSM: '08 Guest Articles/2021/01 CSM.md'
|
||||
- 2022:
|
||||
- 区域光: '08 Guest Articles/2022/03 Area Lights.md'
|
||||
- 历史存档: 'legacy.md'
|
||||
|
Reference in New Issue
Block a user