1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 12:45:29 +08:00

Compare commits

..

20 Commits

Author SHA1 Message Date
1i9h7_b1u3
d5ded8e505 重翻“阴影映射” & 调整章节顺序 (#337)
* 删除原文已删除的“CSM”小节

* 将“CSM”小节移动到“Guest Article”章节

* 移动"CSM"并更名

* 重翻了一遍阴影映射章节

* 更新阴影映射章节中的图片

* 绝对地址改为了相对地址

* 修改了章节引用地址
2025-07-11 19:33:35 +08:00
Oliver Sun
afb199cc3e Fix typo 2025-06-17 14:39:30 +08:00
lilequan
ed338c7df2 修复纹理重复翻转
页面中的 C++ 示例代码包含了 stbi_set_flip_vertically_on_load(true);,这会在加载纹理时进行Y轴翻转。
然而,页面上展示的顶点着色器代码中,又进行了一次Y轴翻转:TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);。
这两次翻转导致结果被抵消,最终渲染出的纹理仍然是上下颠倒的。
2025-06-17 14:37:47 +08:00
SuperAo
3bead6b3fd 校对 docs/05 Advanced Lighting/02 Gamma Correction.md
1. 补全翻译
2. 调整译注
3. "monitor"译为"显示器"
2025-03-08 12:13:04 +08:00
LiuZengqiang
3956459af8 Fix error in 07-PBR-IBL-Diffuse irradiance.md (#332)
* Update 06 HDR.md

typo

* Update 06 HDR.md

Delete wrong figure insertion.

* Fix error in 07-PBR-IBL-Diffuse irradiance.md

离散积分 kd*c/pi*... -> kd*c*pi...;
方向向量normalize;
2025-03-08 12:01:22 +08:00
LiuZengqiang
ff504c286c Update 06 HDR.md (#331)
typo
2025-03-01 13:26:35 +08:00
Gary Wang
3c3b5dcfb2 CI: also bump python version as well
Python 3.7 has reached it's EOL support: https://devguide.python.org/versions/#status-of-python-versions

See also: https://github.com/actions/setup-python/issues/962
2025-01-26 20:07:18 +08:00
Gary Wang
26d1ed3830 CI: bump referenced action versions in deploy.yml 2025-01-26 20:02:45 +08:00
Gary Wang
b1ee160dd5 Merge pull request #330 from 1012796366/new-theme
更新“高级OpenGL”章节译文以及部分图片
2025-01-26 19:30:44 +08:00
1i9h7_b1u3
c42bb0a832 修改“立方体贴图”的译文 2025-01-26 14:47:08 +08:00
1i9h7_b1u3
3bef01a3a9 更新“抗锯齿”译文+灰度化图片 2025-01-26 14:34:56 +08:00
1i9h7_b1u3
bd752a5030 更新“实例化”中的源代码以及文本排版 2025-01-23 11:58:59 +08:00
1i9h7_b1u3
e18ffe0694 更新“几何着色器”中的图片并调整部分排版 2025-01-22 16:39:53 +08:00
1i9h7_b1u3
4284e545bd 更新“高级GLSL”中的链接并删除多余文本 2025-01-22 15:56:32 +08:00
1i9h7_b1u3
bc00618fd8 更新“高级数据”文本 2025-01-22 15:34:32 +08:00
1i9h7_b1u3
8baeb7179f 更新“立方体贴图”中的图片 2025-01-22 15:17:03 +08:00
1i9h7_b1u3
3175f3c9aa 删除“面剔除”中多余的文本 2025-01-19 20:48:52 +08:00
1i9h7_b1u3
dbadc7c94e 更新“混合”原文部分段落排版+链接 2025-01-19 17:58:00 +08:00
1i9h7_b1u3
0499b92877 更新“模板测试”原文 2025-01-17 18:21:09 +08:00
1i9h7_b1u3
0256023f8f 更新“深度测试”中源代码的变量名和注释 2025-01-15 21:57:26 +08:00
24 changed files with 262 additions and 249 deletions

View File

@@ -6,11 +6,11 @@ jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: set up python - name: set up python
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: 3.7 python-version: 3.9
- name: install dependencies - name: install dependencies
shell: bash shell: bash
run: | run: |

View File

@@ -445,7 +445,7 @@ uniform mat4 transform;
void main() void main()
{ {
gl_Position = transform * vec4(aPos, 1.0f); gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y); TexCoord = vec2(aTexCoord.x, aTexCoord.y);
} }
``` ```

View File

@@ -136,13 +136,13 @@ void main()
首先我们将深度值变换为NDC不是非常困难 首先我们将深度值变换为NDC不是非常困难
```c++ ```c++
float z = depth * 2.0 - 1.0; float ndc = depth * 2.0 - 1.0;
``` ```
接下来使用获取到的z值,应用逆变换来获取线性的深度值: 接下来使用获取到的NDC值,应用逆变换来获取线性的深度值:
```c++ ```c++
float linearDepth = (2.0 * near * far) / (far + near - z * (far - near)); float linearDepth = (2.0 * near * far) / (far + near - ndc * (far - near));
``` ```
这个方程是用投影矩阵推导得出的它使用了方程2来非线性化深度值返回一个<var>near</var>与<var>far</var>之间的深度值。这篇注重数学的[文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)为感兴趣的读者详细解释了投影矩阵,它也展示了这些方程是怎么来的。 这个方程是用投影矩阵推导得出的它使用了方程2来非线性化深度值返回一个<var>near</var>与<var>far</var>之间的深度值。这篇注重数学的[文章](http://www.songho.ca/opengl/gl_projectionmatrix.html)为感兴趣的读者详细解释了投影矩阵,它也展示了这些方程是怎么来的。
@@ -158,7 +158,7 @@ float far = 100.0;
float LinearizeDepth(float depth) 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 * far) / (far + near - z * (far - near)); return (2.0 * near * far) / (far + near - z * (far - near));
} }

View File

@@ -20,7 +20,7 @@
模板缓冲首先会被清除为0之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染其它的都被丢弃了 模板缓冲首先会被清除为0之后在模板缓冲中使用1填充了一个空心矩形。场景中的片段将会只在片段的模板值为1的时候会被渲染其它的都被丢弃了
模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们**写入**了模板缓冲。在同一个(或者接下来的)渲染迭代中,我们可以**读取**这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候你可以尽情发挥,但大体的步骤如下: 模板缓冲操作允许我们在渲染片段时将模板缓冲设定为一个特定的值。通过在渲染时修改模板缓冲的内容,我们**写入**了模板缓冲。在同一个(或者接下来的)中,我们可以**读取**这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候你可以尽情发挥,但大体的步骤如下:
- 启用模板缓冲的写入。 - 启用模板缓冲的写入。
- 渲染物体,更新模板缓冲的内容。 - 渲染物体,更新模板缓冲的内容。
@@ -101,13 +101,14 @@ GL_INVERT | 按位翻转当前的模板缓冲值
物体轮廓所能做的事情正如它名字所描述的那样。我们将会为每个(或者一个)物体在它的周围创建一个很小的有色边框。当你想要在策略游戏中选中一个单位进行操作的,想要告诉玩家选中的是哪个单位的时候,这个效果就非常有用了。为物体创建轮廓的步骤如下: 物体轮廓所能做的事情正如它名字所描述的那样。我们将会为每个(或者一个)物体在它的周围创建一个很小的有色边框。当你想要在策略游戏中选中一个单位进行操作的,想要告诉玩家选中的是哪个单位的时候,这个效果就非常有用了。为物体创建轮廓的步骤如下:
1. 在绘制(需要添加轮廓的)物体之前,将模板函数设置为<var>GL_ALWAYS</var>每当物体的片段被渲染时将模板缓冲更新为1 1. 启用模板写入
2. 渲染物体 2. 在绘制(需要添加轮廓的)物体之前,将模板函数设置为<var>GL_ALWAYS</var>每当物体的片段被渲染时将模板缓冲更新为1
3. 禁用模板写入以及深度测试 3. 渲染物体
4. 将每个物体缩放一点点 4. 禁用模板写入以及深度测试
5. 使用一个不同的片段着色器,输出一个单独的(边框)颜色 5. 将每个物体缩放一点点
6. 再次绘制物体但只在它们片段的模板值不等于1时才绘制 6. 使用一个不同的片段着色器,输出一个单独的(边框)颜色
7. 再次启用模板写入和深度测试 7. 再次绘制物体但只在它们片段的模板值不等于1时才绘制
8. 再次启用模板写入和深度测试。
这个过程将每个物体的片段的模板缓冲设置为1当我们想要绘制边框的时候我们主要绘制放大版本的物体中模板测试通过的部分也就是物体的边框的位置。我们主要使用模板缓冲丢弃了放大版本中属于原物体片段的部分。 这个过程将每个物体的片段的模板缓冲设置为1当我们想要绘制边框的时候我们主要绘制放大版本的物体中模板测试通过的部分也就是物体的边框的位置。我们主要使用模板缓冲丢弃了放大版本中属于原物体片段的部分。
@@ -122,10 +123,15 @@ void main()
我们只想给那两个箱子加上边框,所以我们让地板不参与这个过程。我们希望首先绘制地板,再绘制两个箱子(并写入模板缓冲),之后绘制放大的箱子(并丢弃覆盖了之前绘制的箱子片段的那些片段)。 我们只想给那两个箱子加上边框,所以我们让地板不参与这个过程。我们希望首先绘制地板,再绘制两个箱子(并写入模板缓冲),之后绘制放大的箱子(并丢弃覆盖了之前绘制的箱子片段的那些片段)。
我们首先启用模板测试,并设置测试通过或失败时的行为 我们首先启用模板测试:
```c++ ```c++
glEnable(GL_STENCIL_TEST); glEnable(GL_STENCIL_TEST);
```
并设置在每一帧中模板测试通过或失败时,需要执行的操作:
```c++
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
``` ```
@@ -134,13 +140,14 @@ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
我们将模板缓冲清除为0对箱子中所有绘制的片段将模板值更新为1 我们将模板缓冲清除为0对箱子中所有绘制的片段将模板值更新为1
```c++ ```c++
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲 glStencilFunc(GL_ALWAYS, 1, 0xFF); // 所有的片段都应该更新模板缓冲
glStencilMask(0xFF); // 启用模板缓冲写入 glStencilMask(0xFF); // 启用模板缓冲写入
normalShader.use(); normalShader.use();
DrawTwoContainers(); DrawTwoContainers();
``` ```
通过使用<var>GL_ALWAYS</var>模板测试函数我们保证了箱子的每个片段都会将模板缓冲的模板值更新为1。因为片段永远会通过模板测试在绘制片段的地方模板缓冲会被更新为参考值。 通过使用<var>GL_REPLACE</var>模板测试函数我们保证了箱子的每个片段都会将模板缓冲的模板值更新为1。因为片段永远会通过模板测试在绘制片段的地方模板缓冲会被更新为参考值。
现在模板缓冲在箱子被绘制的地方都更新为1了我们将要绘制放大的箱子但这次要禁用模板缓冲的写入 现在模板缓冲在箱子被绘制的地方都更新为1了我们将要绘制放大的箱子但这次要禁用模板缓冲的写入

View File

@@ -22,13 +22,13 @@ OpenGL中<def>混合</def>(Blending)通常是实现物体<def>透明度</def>
有些图片并不需要半透明只需要根据纹理颜色值显示一部分或者不显示一部分没有中间情况。比如说草如果想不太费劲地创建草这种东西你需要将一个草的纹理贴在一个2D四边形(Quad)上然后将这个四边形放到场景中。然而草的形状和2D四边形的形状并不完全相同所以你只想显示草纹理的某些部分而忽略剩下的部分。 有些图片并不需要半透明只需要根据纹理颜色值显示一部分或者不显示一部分没有中间情况。比如说草如果想不太费劲地创建草这种东西你需要将一个草的纹理贴在一个2D四边形(Quad)上然后将这个四边形放到场景中。然而草的形状和2D四边形的形状并不完全相同所以你只想显示草纹理的某些部分而忽略剩下的部分。
下面这个纹理正是这样的它要么是完全不透明的alpha值为1.0要么是完全透明的alpha值为0.0),没有中间情况。你可以看到,只要不是草的部分,这个图片显示的都是网站的背景颜色而不是它本身的颜色。 下面[这个](../img/04/03/grass.png)纹理正是这样的它要么是完全不透明的alpha值为1.0要么是完全透明的alpha值为0.0),没有中间情况。你可以看到,只要不是草的部分,这个图片显示的都是网站的背景颜色而不是它本身的颜色。
![](../img/04/03/grass.png) ![](../img/04/03/grass.png)
所以当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要<def>丢弃</def>(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。在此之前,我们还要学习如何加载一个透明的纹理。 所以当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要<def>丢弃</def>(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中
要想加载有alpha值的纹理我们并不需要改很多东西`stb_image`在纹理有alpha通道的时候会自动加载但我们仍要在纹理生成过程中告诉OpenGL我们的纹理现在使用alpha通道了 在此之前,我们还要学习如何加载一个透明的纹理。要想加载有alpha值的纹理我们并不需要改很多东西`stb_image`在纹理有alpha通道的时候会自动加载但我们仍要在纹理生成过程中告诉OpenGL我们的纹理现在使用alpha通道了
```c++ ```c++
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
@@ -189,6 +189,8 @@ OpenGL甚至给了我们更多的灵活性允许我们改变方程中源和
- GL_FUNC_ADD默认选项将两个分量相加\(\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}\)。 - GL_FUNC_ADD默认选项将两个分量相加\(\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}\)。
- GL_FUNC_SUBTRACT将两个分量相减 \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\)。 - GL_FUNC_SUBTRACT将两个分量相减 \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\)。
- GL_FUNC_REVERSE_SUBTRACT将两个分量相减但顺序相反\(\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}\)。 - GL_FUNC_REVERSE_SUBTRACT将两个分量相减但顺序相反\(\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}\)。
- GL_MIN取两个分量中的最小值\(\bar{C}_{result} = {min}(\color{red}{Dst}, \color{green}{Src})\)。
- GL_MAX取两个分量中的最大值\(\bar{C}_{result} = {max}(\color{red}{Dst}, \color{green}{Src})\)。
通常我们都可以省略调用<fun>glBlendEquation</fun>,因为<var>GL_FUNC_ADD</var>对大部分的操作来说都是我们希望的混合方程,但如果你真的想打破主流,其它的方程也可能符合你的要求。 通常我们都可以省略调用<fun>glBlendEquation</fun>,因为<var>GL_FUNC_ADD</var>对大部分的操作来说都是我们希望的混合方程,但如果你真的想打破主流,其它的方程也可能符合你的要求。

View File

@@ -48,8 +48,6 @@ float vertices[] = {
在顶点数据中我们将两个三角形都以逆时针顺序定义正面的三角形是1、2、3背面的三角形也是1、2、3如果我们从正面看这个三角形的话。然而如果从观察者当前视角使用1、2、3的顺序来绘制的话从观察者的方向来看背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的它现在是以顺时针顺序渲染的了。这正是我们想要<def>剔除</def>Cull丢弃的不可见面了 在顶点数据中我们将两个三角形都以逆时针顺序定义正面的三角形是1、2、3背面的三角形也是1、2、3如果我们从正面看这个三角形的话。然而如果从观察者当前视角使用1、2、3的顺序来绘制的话从观察者的方向来看背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的它现在是以顺时针顺序渲染的了。这正是我们想要<def>剔除</def>Cull丢弃的不可见面了
在顶点数据中我们定义的是两个逆时针顺序的三角形。然而从观察者的方面看后面的三角形是顺时针的如果我们仍以1、2、3的顺序以观察者当面的视野看的话。即使我们以逆时针顺序定义后面的三角形它现在还是变为顺时针。它正是我们打算剔除丢弃的不可见的面
## 面剔除 ## 面剔除
在本节的开头我们就说过OpenGL能够丢弃那些渲染为背向三角形的三角形图元。既然已经知道如何设置顶点的环绕顺序了我们就可以使用OpenGL的<def>面剔除</def>选项了,它默认是禁用状态的。 在本节的开头我们就说过OpenGL能够丢弃那些渲染为背向三角形的三角形图元。既然已经知道如何设置顶点的环绕顺序了我们就可以使用OpenGL的<def>面剔除</def>选项了,它默认是禁用状态的。

View File

@@ -327,11 +327,11 @@ glDrawArrays(GL_TRIANGLES, 0, 36);
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/6.2.cubemaps_environment_mapping/cubemaps_environment_mapping.cpp)找到完整的源代码。 你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/6.2.cubemaps_environment_mapping/cubemaps_environment_mapping.cpp)找到完整的源代码。
当反射应用到一整个物体上(像是箱子)时,这个物体看起来就像是钢或者铬这样的高反射性材质。如果我们加载[模型加载](../03 Model Loading/03 Model.md)小节中的纳米装模型,我们会得到一种整个套装都是使用铬做成的效果: 当反射应用到一整个物体上(像是箱子)时,这个物体看起来就像是钢或者铬这样的高反射性材质。如果我们加载[模型加载](../03 Model Loading/03 Model.md)小节中的背包模型,我们会得到一种整个套装都是使用铬做成的效果:
![](../img/04/06/cubemaps_reflection_nanosuit.png) ![](../img/04/06/cubemaps_reflection_backpack.png)
这看起来非常棒,但在现实中大部分的模型都不具有完全反射性。我们可以引入<def>反射贴图</def>(Reflection Map),来给模型更多的细节。与漫反射和镜面光贴图一样,反射贴图也是可以采样的纹理图像,它决定这片段的反射性。通过使用反射贴图,我们可以知道模型的哪些部分该以什么强度显示反射。在本节的练习中,将由你来为我们之前创建的模型加载器中引入反射贴图,显著提升纳米装模型的细节。 这看起来非常棒,但在现实中大部分的模型都不具有完全反射性。我们可以引入<def>反射贴图</def>(Reflection Map),来给模型更多的细节。与漫反射和镜面光贴图一样,反射贴图也是可以采样的纹理图像,它决定这片段的反射性。通过使用反射贴图,我们可以知道模型的哪些部分该以什么强度显示反射。在本节的练习中,将由你来为我们之前创建的模型加载器中引入反射贴图,显著提升背包模型的细节。
## 折射 ## 折射
@@ -369,9 +369,9 @@ void main()
} }
``` ```
通过改变折射率,你可以创建完全不同的视觉效果。编译程序并运行,但结果并不是很有趣,因为我们只使用了一个简单的箱子,它不太能显示折射的效果,现在看起来只是有点像一个放大镜。对纳米装使用相同的着色器却能够展现出了我们期待的效果:一个类玻璃的物体。 通过改变折射率,你可以创建完全不同的视觉效果。编译程序并运行,但结果并不是很有趣,因为我们只使用了一个简单的箱子,它不太能显示折射的效果,现在看起来只是有点像一个放大镜。对背包使用相同的着色器却能够展现出了我们期待的效果:一个类玻璃的物体。
![](../img/04/06/cubemaps_refraction.png) ![](../img/04/06/cubemaps_refraction_backpack.png)
你可以想象出有了光照、反射、折射和顶点移动的正确组合,你可以创建出非常漂亮的水。注意,如果要想获得物理上精确的结果,我们还需要在光线离开物体的时候再次折射,现在我们使用的只是单面折射(Single-side Refraction),但它对大部分场合都是没问题的。 你可以想象出有了光照、反射、折射和顶点移动的正确组合,你可以创建出非常漂亮的水。注意,如果要想获得物理上精确的结果,我们还需要在光线离开物体的时候再次折射,现在我们使用的只是单面折射(Single-side Refraction),但它对大部分场合都是没问题的。
@@ -385,7 +385,7 @@ void main()
## 练习 ## 练习
- 尝试在我们之前在[模型加载](../03 Model Loading/01 Assimp.md)小节中创建的模型加载器中引入反射贴图。你可以在[这里](../data/nanosuit_reflection.zip)找到升级后有反射贴图的纳米装模型。仍有几点要注意的: - 尝试在我们之前在[模型加载](../03 Model Loading/01 Assimp.md)小节中创建的模型加载器中引入反射贴图。你可以在[这里](../data/nanosuit_reflection.zip)找到升级后有反射贴图的背包模型。仍有几点要注意的:
- Assimp在大多数格式中都不太喜欢反射贴图所以我们需要欺骗一下它将反射贴图储存为**漫反射贴图**。你可以在加载材质的时候将反射贴图的纹理类型设置为<var>aiTextureType_AMBIENT</var> - Assimp在大多数格式中都不太喜欢反射贴图所以我们需要欺骗一下它将反射贴图储存为**漫反射贴图**。你可以在加载材质的时候将反射贴图的纹理类型设置为<var>aiTextureType_AMBIENT</var>
- 我偷懒直接使用镜面光纹理图像来创建了反射贴图,所以反射贴图在模型的某些地方不能准确地映射:)。 - 我偷懒直接使用镜面光纹理图像来创建了反射贴图,所以反射贴图在模型的某些地方不能准确地映射:)。
- 由于模型加载器本身就已经在着色器中占用了3个纹理单元了你需要将天空盒绑定到第4个纹理单元上因为我们要从同一个着色器中对天空盒采样。 - 由于模型加载器本身就已经在着色器中占用了3个纹理单元了你需要将天空盒绑定到第4个纹理单元上因为我们要从同一个着色器中对天空盒采样。

View File

@@ -6,11 +6,11 @@
翻译 | Krasjet 翻译 | Krasjet
校对 | 暂未校对 校对 | 暂未校对
我们在OpenGL中大量使用缓冲来储存数据已经有很长时间了。操作缓冲其实还有更有意思的方式,而且使用纹理将大量数据传入着色器也有更有趣的方法。这一节中,我们将讨论一些更有意思的缓冲函数,以及我们该如何使用纹理对象来储存大量的数据(纹理的部分还没有完成) 我们在OpenGL中大量使用缓冲来储存数据已经有很长时间了。这一节中,我们将讨论一些其他的操作缓冲的方法
OpenGL中的缓冲只是一个管理特定内存块的对象没有其它更多的功能了。在我们将它绑定到一个<def>缓冲目标</def>(Buffer Target)时,我们才赋予了其意义。当我们绑定一个缓冲到<var>GL_ARRAY_BUFFER</var>时,它就是一个顶点数组缓冲,但我们也可以很容易地将其绑定到<var>GL_ELEMENT_ARRAY_BUFFER</var>。OpenGL内部会为每个目标储存一个缓冲并且会根据目标的不同以不同的方式处理缓冲。 OpenGL中的缓冲只是一个管理特定内存块的对象没有其它更多的功能了。在我们将它绑定到一个<def>缓冲目标</def>(Buffer Target)时,我们才赋予了其意义。当我们绑定一个缓冲到<var>GL_ARRAY_BUFFER</var>时,它就是一个顶点数组缓冲,但我们也可以很容易地将其绑定到<var>GL_ELEMENT_ARRAY_BUFFER</var>。OpenGL内部会为每个目标储存一个缓冲并且会根据目标的不同以不同的方式处理缓冲。
到目前为止,我们一直是调用<fun>glBufferData</fun>函数来填充缓冲对象所管理的内存,这个函数会分配一块内存,并将数据添加到这块内存中。如果我们将它的`data`参数设置为`NULL`,那么这个函数将只会分配内存,但不进行填充。这在我们需要**预留**(Reserve)特定大小的内存,之后回到这个缓冲一点一点填充的时候会很有用。 到目前为止,我们一直是调用<fun>glBufferData</fun>函数来填充缓冲对象所管理的内存,这个函数会分配一块GPU内存,并将数据添加到这块内存中。如果我们将它的`data`参数设置为`NULL`,那么这个函数将只会分配内存,但不进行填充。这在我们需要**预留**(Reserve)特定大小的内存,之后回到这个缓冲一点一点填充的时候会很有用。
除了使用一次函数调用填充整个缓冲之外,我们也可以使用<fun>glBufferSubData</fun>,填充缓冲的特定区域。这个函数需要一个缓冲目标、一个偏移量、数据的大小和数据本身作为它的参数。这个函数不同的地方在于,我们可以提供一个偏移量,指定从**何处**开始填充这个缓冲。这能够让我们插入或者更新缓冲内存的某一部分。要注意的是,缓冲需要有足够的已分配内存,所以对一个缓冲调用<fun>glBufferSubData</fun>之前必须要先调用<fun>glBufferData</fun> 除了使用一次函数调用填充整个缓冲之外,我们也可以使用<fun>glBufferSubData</fun>,填充缓冲的特定区域。这个函数需要一个缓冲目标、一个偏移量、数据的大小和数据本身作为它的参数。这个函数不同的地方在于,我们可以提供一个偏移量,指定从**何处**开始填充这个缓冲。这能够让我们插入或者更新缓冲内存的某一部分。要注意的是,缓冲需要有足够的已分配内存,所以对一个缓冲调用<fun>glBufferSubData</fun>之前必须要先调用<fun>glBufferData</fun>
@@ -69,7 +69,7 @@ glVertexAttribPointer(
注意`stride`参数等于顶点属性的大小因为下一个顶点属性向量能在3个或2个分量之后找到。 注意`stride`参数等于顶点属性的大小因为下一个顶点属性向量能在3个或2个分量之后找到。
这给了我们设置顶点属性的另一种方法。使用哪种方法都不会对OpenGL有什么立刻的好处,它只是设置顶点属性的一种更整洁的方式。具体使用的方法将完全取决于你的喜好与程序类型 这给了我们设置顶点属性的另一种方法。使用哪种方法都是可行的,它只是设置顶点属性的一种更整洁的方式。但是,推荐使用交错方法,因为这样一来,每个顶点着色器运行时所需要的顶点属性在内存中会更加紧密对齐
## 复制缓冲 ## 复制缓冲
@@ -87,7 +87,6 @@ void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoff
接下来<fun>glCopyBufferSubData</fun>会从`readtarget`中读取`size`大小的数据,并将其写入`writetarget`缓冲的`writeoffset`偏移量处。下面这个例子展示了如何复制两个顶点数组缓冲: 接下来<fun>glCopyBufferSubData</fun>会从`readtarget`中读取`size`大小的数据,并将其写入`writetarget`缓冲的`writeoffset`偏移量处。下面这个例子展示了如何复制两个顶点数组缓冲:
```c++ ```c++
float vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1); glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2); glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData)); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sizeof(vertexData));

View File

@@ -14,7 +14,7 @@
着色器都是最简化的如果需要当前着色器以外地方的数据的话我们必须要将数据传进来。我们已经学会使用顶点属性、uniform和采样器来完成这一任务了。然而除此之外GLSL还定义了另外几个以`gl_`为前缀的变量,它们能提供给我们更多的方式来读取/写入数据。我们已经在前面教程中接触过其中的两个了:顶点着色器的输出向量<var>gl_Position</var>,和片段着色器的<var>gl_FragCoord</var> 着色器都是最简化的如果需要当前着色器以外地方的数据的话我们必须要将数据传进来。我们已经学会使用顶点属性、uniform和采样器来完成这一任务了。然而除此之外GLSL还定义了另外几个以`gl_`为前缀的变量,它们能提供给我们更多的方式来读取/写入数据。我们已经在前面教程中接触过其中的两个了:顶点着色器的输出向量<var>gl_Position</var>,和片段着色器的<var>gl_FragCoord</var>
我们将会讨论几个有趣的GLSL内建输入和输出变量并会解释它们能够怎样帮助你。注意我们将不会讨论GLSL中存在的所有内建变量如果你想知道所有的内建变量的话请查看OpenGL的[wiki](http://www.opengl.org/wiki/Built-in_Variable_(GLSL))。 我们将会讨论几个有趣的GLSL内建输入和输出变量并会解释它们能够怎样帮助你。注意我们将不会讨论GLSL中存在的所有内建变量如果你想知道所有的内建变量的话请查看OpenGL的[wiki](https://www.khronos.org/opengl/wiki/Built-in_Variable_(GLSL))。
## 顶点着色器变量 ## 顶点着色器变量
@@ -54,8 +54,6 @@ void main()
整型变量<var>gl_VertexID</var>储存了正在绘制顶点的当前ID。当使用<fun>glDrawElements</fun>)进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当(使用<fun>glDrawArrays</fun>)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。 整型变量<var>gl_VertexID</var>储存了正在绘制顶点的当前ID。当使用<fun>glDrawElements</fun>)进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当(使用<fun>glDrawArrays</fun>)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。
虽然现在它没有什么具体的用途,但知道我们能够访问这个信息总是好的。
## 片段着色器变量 ## 片段着色器变量
在片段着色器中我们也能访问到一些有趣的变量。GLSL提供给我们两个有趣的输入变量<var>gl_FragCoord</var>和<var>gl_FrontFacing</var>。 在片段着色器中我们也能访问到一些有趣的变量。GLSL提供给我们两个有趣的输入变量<var>gl_FragCoord</var>和<var>gl_FrontFacing</var>。

View File

@@ -46,13 +46,13 @@ void main() {
几何着色器同时希望我们设置一个它最大能够输出的顶点数量如果你超过了这个值OpenGL将不会绘制**多出的**顶点),这个也可以在<fun>out</fun>关键字的布局修饰符中设置。在这个例子中,我们将输出一个`line_strip`并将最大顶点数设置为2个。 几何着色器同时希望我们设置一个它最大能够输出的顶点数量如果你超过了这个值OpenGL将不会绘制**多出的**顶点),这个也可以在<fun>out</fun>关键字的布局修饰符中设置。在这个例子中,我们将输出一个`line_strip`并将最大顶点数设置为2个。
如果你不知道什么是线条(Line Strip)线条连接了一组点形成一条连续的线它最少要由两个点来组成。在渲染函数中每多加一个点就会在这个点与前一个点之间形成一条新的线。在下面这张图中我们有5个顶点 !!! important
![](../img/04/09/geometry_shader_line_strip.png) 如果你不知道什么是线条(Line Strip)线条连接了一组点形成一条连续的线它最少要由两个点来组成。在渲染函数中每多加一个点就会在这个点与前一个点之间形成一条新的线。在下面这张图中我们有5个顶点
如果使用的是上面定义的着色器那么这将只能输出一条线段因为最大顶点数等于2。 ![](../img/04/09/geometry_shader_line_strip.png)
为了生成更有意义的结果我们需要某种方式来获取前一着色器阶段的输出。GLSL提供给我们一个<def>内建</def>(Built-in)变量,在内部看起来(可能)是这样的: 为了生成更有意义的结果我们需要某种方式来获取前一着色器阶段的输出。GLSL提供给我们一个名为<fun>gl_in</fun>的<def>内建</def>(Built-in)变量,在内部看起来(可能)是这样的:
```c++ ```c++
in gl_Vertex in gl_Vertex
@@ -267,11 +267,11 @@ in VS_OUT {
!!! Important !!! Important
我们并不是必须要用接口块来向几何着色器传递数据。如果顶点着色器发送的颜色向量是`out vec3 vColor`我们也可以这样写: 我们并不是必须要用接口块来向几何着色器传递数据。我们也可以这样写:
in vec3 vColor[]; in vec3 vColor[];
然而,接口块在几何着色器这样的着色器中会更容易处理一点。实际上,几何着色器的输入能够变得非常大,将它们合并为一个大的接口块数组会更符合逻辑一点。 如果顶点着色器发送的颜色向量是`out vec3 vColor`,那这么写就没问题。然而,接口块在几何着色器这样的着色器中会更容易处理一点。实际上,几何着色器的输入能够变得非常大,将它们合并为一个大的接口块数组会更符合逻辑一点。
接下来我们还需要为下个片段着色器阶段声明一个输出颜色向量: 接下来我们还需要为下个片段着色器阶段声明一个输出颜色向量:
@@ -332,7 +332,7 @@ EndPrimitive();
当我们说**爆破**一个物体时,我们并不是指要将宝贵的顶点集给炸掉,我们是要将每个三角形沿着法向量的方向移动一小段时间。效果就是,整个物体看起来像是沿着每个三角形的法线向量**爆炸**一样。爆炸三角形的效果在纳米装模型上看起来像是这样的: 当我们说**爆破**一个物体时,我们并不是指要将宝贵的顶点集给炸掉,我们是要将每个三角形沿着法向量的方向移动一小段时间。效果就是,整个物体看起来像是沿着每个三角形的法线向量**爆炸**一样。爆炸三角形的效果在纳米装模型上看起来像是这样的:
![](../img/04/09/geometry_shader_explosion.png) ![](../img/04/09/geometry_shader_explosion_backpack.png)
这样的几何着色器效果的一个好处就是,无论物体有多复杂,它都能够应用上去。 这样的几何着色器效果的一个好处就是,无论物体有多复杂,它都能够应用上去。
@@ -491,8 +491,8 @@ void main()
现在,首先使用普通着色器渲染模型,再使用特别的**法线可视化**着色器渲染,你将看到这样的效果: 现在,首先使用普通着色器渲染模型,再使用特别的**法线可视化**着色器渲染,你将看到这样的效果:
![](../img/04/09/geometry_shader_normals.png) ![](../img/04/09/geometry_shader_normals_backpack.png)
尽管我们的纳米装现在看起来像是一个体毛很多而且带着隔热手套的人,它能够很有效地帮助我们判断模型的法线是否确。你可以想象到,这样的几何着色器也经常用于给物体添加<def>毛发</def>(Fur)。 除了让我们的背包变得毛茸茸之外,它还能让我们很好地判断模型的法向量是否确。你可以想象到,这样的几何着色器也经常用于给物体添加<def>毛发</def>(Fur)。
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/9.3.geometry_shader_normals/normal_visualization.cpp)找到源码。 你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/9.3.geometry_shader_normals/normal_visualization.cpp)找到源码。

View File

@@ -107,11 +107,7 @@ for(int y = -10; y < 10; y += 2)
shader.use(); shader.use();
for(unsigned int i = 0; i < 100; i++) for(unsigned int i = 0; i < 100; i++)
{ {
stringstream ss; shader.setVec2(("offsets[" + std::to_string(i) + "]"), translations[i]);
string index;
ss << i;
index = ss.str();
shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
} }
``` ```
@@ -124,11 +120,11 @@ glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100); glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);
``` ```
<fun>glDrawArraysInstanced</fun>的参数和<fun>glDrawArrays</fun>完全一样除了最后多了个参数用来设置需要绘制的实例数量。因为我们想要在10x10网格中显示100个四边形我们将它设置为100.运行代码之后你应该能得到熟悉的100个五彩的四边形。 <fun>glDrawArraysInstanced</fun>的参数和<fun>glDrawArrays</fun>完全一样除了最后多了个参数用来设置需要绘制的实例数量。因为我们想要在10x10网格中显示100个四边形我们将它设置为100运行代码之后你应该能得到熟悉的100个五彩的四边形。
## 实例化数组 ## 实例化数组
虽然之前的实现在目前的情况下能够正常工作但是如果我们要渲染远超过100个实例的时候这其实非常普遍我们最终会超过最大能够发送至着色器的uniform数据大小[上限](http://www.opengl.org/wiki/Uniform_(GLSL)#Implementation_limits)。它的一个代替方案是<def>实例化数组</def>(Instanced Array),它被定义为一个顶点属性(能够让我们储存更多的数据),仅在顶点着色器渲染一个新的实例时才会更新。 虽然之前的实现在目前的情况下能够正常工作但是如果我们要渲染远超过100个实例的时候这其实非常普遍我们最终会超过最大能够发送至着色器的uniform数据大小[上限](https://www.khronos.org/opengl/wiki/GLSL_Uniform#Implementation_limits)。它的一个代替方案是<def>实例化数组</def>(Instanced Array),它被定义为一个顶点属性(能够让我们储存更多的数据),仅在顶点着色器渲染一个新的实例时才会更新。
使用顶点属性时顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。而当我们将顶点属性定义为一个实例化数组时顶点着色器就只需要对每个实例而不是每个顶点更新顶点属性的内容了。这允许我们对逐顶点的数据使用普通的顶点属性而对逐实例的数据使用实例化数组。 使用顶点属性时顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。而当我们将顶点属性定义为一个实例化数组时顶点着色器就只需要对每个实例而不是每个顶点更新顶点属性的内容了。这允许我们对逐顶点的数据使用普通的顶点属性而对逐实例的数据使用实例化数组。
@@ -171,7 +167,7 @@ glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1); glVertexAttribDivisor(2, 1);
``` ```
这段代码很有意思的地方在于最后一行,我们调用了<fun>glVertexAttribDivisor</fun>。这个函数告诉了OpenGL该**什么时候**更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数(Attribute Divisor)。默认情况下属性除数是0告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。将它设置为1时我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。而设置为2时我们希望每2个实例更新一次属性以此类推。我们将属性除数设置为1是在告诉OpenGL处于位置值2的顶点属性是一个实例化数组。 这段代码很有意思的地方在于最后一行,我们调用了<fun>glVertexAttribDivisor</fun>。这个函数告诉了OpenGL该**什么时候**更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是<def>属性除数</def>(Attribute Divisor)。默认情况下属性除数是0告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。将它设置为1时我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。而设置为2时我们希望每2个实例更新一次属性以此类推。我们将属性除数设置为1是在告诉OpenGL处于位置值2的顶点属性是一个实例化数组。
如果我们现在使用<fun>glDrawArraysInstanced</fun>,再次渲染四边形,会得到以下输出: 如果我们现在使用<fun>glDrawArraysInstanced</fun>,再次渲染四边形,会得到以下输出:
@@ -312,7 +308,7 @@ for(unsigned int i = 0; i < rock.meshes.size(); i++)
glEnableVertexAttribArray(3); glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0); glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
glEnableVertexAttribArray(4); glEnableVertexAttribArray(4);
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size)); glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(1 * vec4Size));
glEnableVertexAttribArray(5); glEnableVertexAttribArray(5);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size)); glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
glEnableVertexAttribArray(6); glEnableVertexAttribArray(6);

View File

@@ -36,7 +36,7 @@
由于屏幕像素总量的限制,有些边缘的像素能够被渲染出来,而有些则不会。结果就是我们使用了不光滑的边缘来渲染图元,导致之前讨论到的锯齿边缘。 由于屏幕像素总量的限制,有些边缘的像素能够被渲染出来,而有些则不会。结果就是我们使用了不光滑的边缘来渲染图元,导致之前讨论到的锯齿边缘。
多重采样所做的正是将单一的采样点变为多个采样点这也是它名称的由来。我们不再使用像素中心的单一采样点取而代之的是以特定图案排列的4个子采样点(Subsample)。我们将用这些子采样点来决定像素的遮盖度。当然,这也意味着颜色缓冲的大小会随着子采样点的增加而增加。 多重采样所做的正是将单一的采样点变为多个采样点这也是它名称的由来。我们不再使用像素中心的单一采样点取而代之的是以特定图案排列的4个子采样点(Subsample)。我们将用这些子采样点来决定像素的遮盖度。
![](../img/04/11/anti_aliasing_sample_points.png) ![](../img/04/11/anti_aliasing_sample_points.png)
@@ -48,7 +48,7 @@
从这里开始多重采样就变得有趣起来了。我们知道三角形只遮盖了2个子采样点所以下一步是决定这个像素的颜色。你的猜想可能是我们对每个被遮盖住的子采样点运行一次片段着色器最后将每个像素所有子采样点的颜色平均一下。在这个例子中我们需要在两个子采样点上对被插值的顶点数据运行两次片段着色器并将结果的颜色储存在这些采样点中。幸运的是这并**不是**它工作的方式,因为这本质上说还是需要运行更多次的片段着色器,会显著地降低性能。 从这里开始多重采样就变得有趣起来了。我们知道三角形只遮盖了2个子采样点所以下一步是决定这个像素的颜色。你的猜想可能是我们对每个被遮盖住的子采样点运行一次片段着色器最后将每个像素所有子采样点的颜色平均一下。在这个例子中我们需要在两个子采样点上对被插值的顶点数据运行两次片段着色器并将结果的颜色储存在这些采样点中。幸运的是这并**不是**它工作的方式,因为这本质上说还是需要运行更多次的片段着色器,会显著地降低性能。
MSAA真正的工作方式是无论三角形遮盖了多少个子采样点每个图元中每个像素只运行**一次**片段着色器。片段着色器使用的顶点数据会插值到每个像素**中心**,所得到的结果颜色会被储存在每个被遮盖住的子采样点中。当颜色缓冲的子样本被图元的所有颜色填满时,所有的这些颜色将会在每个像素内部平均化。因为上图的4个采样点中只有2个被遮盖住了这个像素的颜色将会是三角形颜色与其他两个采样点的颜色(在这里是无色)的平均值,最终形成一种淡蓝色。 MSAA真正的工作方式是无论三角形遮盖了多少个子采样点每个图元中每个像素只运行**一次**片段着色器。片段着色器使用插值到像素**中心**的顶点数据然后MSAA使用更大的深度/模板缓冲区来确定子采样点的覆盖率。被覆盖的子采样点数量将决定了像素颜色对帧缓冲的影响程度。因为上图的4个采样点中只有2个被遮盖住了所以三角形颜色会有一半与帧缓冲区的颜色(在这里是无色)进行混合,最终形成一种淡蓝色。
这样子做之后,颜色缓冲中所有的图元边缘将会产生一种更平滑的图形。让我们来看看前面三角形的多重采样会是什么样子: 这样子做之后,颜色缓冲中所有的图元边缘将会产生一种更平滑的图形。让我们来看看前面三角形的多重采样会是什么样子:
@@ -60,9 +60,9 @@ MSAA真正的工作方式是无论三角形遮盖了多少个子采样点
![](../img/04/11/anti_aliasing_rasterization_samples_filled.png) ![](../img/04/11/anti_aliasing_rasterization_samples_filled.png)
对于每个像素来说,越少的子采样点被三角形所覆盖,那么它受到三角形的影响就越小。三角形的不平滑边缘被稍浅的颜色所包围后,从远处观察时就会显得更加平滑了。 三角形的不平滑边缘被稍浅的颜色所包围后,从远处观察时就会显得更加平滑了。
不仅仅是颜色值会受到多重采样的影响,深度和模板测试也能够使用多个采样点。对深度测试来说,每个顶点的深度值会在运行深度测试之前被插值到各个子样本中。对模板测试来说,我们每个子样本,而不是每个像素,存储一个模板值。当然,这意味着深度和模板缓冲的大小会乘以子采样点的个数 深度和模板值会按各个子采样点存储,并且当多个三角形重叠单个像素时,即使我们只运行一次片段着色器,颜色值也依然会按子采样点存储。对深度测试来说,在运行深度测试之前,每个顶点的深度值会被插值到各个子样本中。对模板测试来说,我们会为每个子样本存储模板值,这意味着缓冲的大小会根据每个像素的子采样点数量而相应增加
我们到目前为止讨论的都是多重采样抗锯齿的背后原理,光栅器背后的实际逻辑比目前讨论的要复杂,但你现在应该已经可以理解多重采样抗锯齿的大体概念和逻辑了。 我们到目前为止讨论的都是多重采样抗锯齿的背后原理,光栅器背后的实际逻辑比目前讨论的要复杂,但你现在应该已经可以理解多重采样抗锯齿的大体概念和逻辑了。
@@ -87,15 +87,15 @@ glfwWindowHint(GLFW_SAMPLES, 4);
glEnable(GL_MULTISAMPLE); glEnable(GL_MULTISAMPLE);
``` ```
只要默认的帧缓冲有了多重采样缓冲的附件,我们所要做的只是调用<fun>glEnable</fun>来启用多重采样。因为多重采样的算法都在OpenGL驱动的光栅器中实现了我们不需要再多做什么。如果现在再来渲染本节一开始的那个绿色的立方体我们应该能看到更平滑的边缘 因为多重采样的算法都在OpenGL驱动的光栅器中实现了我们不需要再多做什么。如果现在再来渲染本节一开始的那个绿色的立方体我们应该能看到更平滑的边缘
![](../img/04/11/anti_aliasing_multisampled.png) ![](../img/04/11/anti_aliasing_multisampled.png)
这个箱子看起来的确要平滑多了,如果在场景中有其它的物体,它们也会看起来平滑很多。你可以在[这里](https://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_multisampling)找到这个简单例子的源代码。 这个箱子看起来的确要平滑多了,如果在场景中有其它的物体,它们也会看起来平滑很多。你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/11.1.anti_aliasing_msaa/anti_aliasing_msaa.cpp)找到这个简单例子的源代码。
## 离屏MSAA ## 离屏MSAA
由于GLFW负责了创建多重采样缓冲启用MSAA非常简单。然而如果我们想要使用我们自己的帧缓冲来进行离屏渲染那么我们就必须要自己动手生成多重采样缓冲了。 由于GLFW负责了创建多重采样缓冲启用MSAA非常简单。然而如果我们想要使用我们自己的帧缓冲来进行离屏渲染那么我们就必须要自己动手生成多重采样缓冲了。现在,我们**确实**需要自己创建多重采样缓冲区。
有两种方式可以创建多重采样缓冲,将其作为帧缓冲的附件:纹理附件和渲染缓冲附件,这和在[帧缓冲](05 Framebuffers.md)教程中所讨论的普通附件很相似。 有两种方式可以创建多重采样缓冲,将其作为帧缓冲的附件:纹理附件和渲染缓冲附件,这和在[帧缓冲](05 Framebuffers.md)教程中所讨论的普通附件很相似。
@@ -131,7 +131,7 @@ glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width,
### 渲染到多重采样帧缓冲 ### 渲染到多重采样帧缓冲
渲染到多重采样帧缓冲对象的过程都是自动的。只要我们在帧缓冲绑定时绘制任何东西,光栅器就会负责所有的多重采样运算。我们最终会得到一个多重采样颜色缓冲以及/或深度和模板缓冲。因为多重采样缓冲有一点特别,我们不能直接将它们的缓冲图像用于其他运算,比如在着色器中对它们进行采样。 渲染到多重采样帧缓冲对象的过程非常简单。只要我们在帧缓冲绑定时绘制任何东西,光栅器就会负责所有的多重采样运算。我们最终会得到一个多重采样颜色缓冲以及/或深度和模板缓冲。因为多重采样缓冲有一点特别,我们不能直接将它们的缓冲图像用于其他运算,比如在着色器中对它们进行采样。
一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者<def>还原</def>(Resolve)图像。多重采样帧缓冲的还原通常是通过<fun>glBlitFramebuffer</fun>来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原。 一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者<def>还原</def>(Resolve)图像。多重采样帧缓冲的还原通常是通过<fun>glBlitFramebuffer</fun>来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原。
@@ -147,7 +147,7 @@ glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT,
![](../img/04/11/anti_aliasing_multisampled.png) ![](../img/04/11/anti_aliasing_multisampled.png)
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/11.anti_aliasing_offscreen/anti_aliasing_offscreen.cpp)找到源代码。 你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/11.2.anti_aliasing_offscreen/anti_aliasing_offscreen.cpp)找到源代码。
但如果我们想要使用多重采样帧缓冲的纹理输出来做像是后期处理这样的事情呢我们不能直接在片段着色器中使用多重采样的纹理。但我们能做的是将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中。然后用这个普通的颜色附件来做后期处理从而达到我们的目的。然而这也意味着我们需要生成一个新的FBO作为中介帧缓冲对象将多重采样缓冲还原为一个能在着色器中使用的普通2D纹理。这个过程的伪代码是这样的 但如果我们想要使用多重采样帧缓冲的纹理输出来做像是后期处理这样的事情呢我们不能直接在片段着色器中使用多重采样的纹理。但我们能做的是将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中。然后用这个普通的颜色附件来做后期处理从而达到我们的目的。然而这也意味着我们需要生成一个新的FBO作为中介帧缓冲对象将多重采样缓冲还原为一个能在着色器中使用的普通2D纹理。这个过程的伪代码是这样的
@@ -178,9 +178,9 @@ while(!glfwWindowShouldClose(window))
} }
``` ```
如果现在再实现[帧缓冲](05 Framebuffers.md)教程中的后期处理效果,我们就能够在一个几乎没有锯齿的场景纹理上进行后期处理了。如果施加模糊的核滤镜,看起来将会是这样: 如果现在再实现[帧缓冲](05 Framebuffers.md)教程中的后期处理效果,我们就能够在一个几乎没有锯齿的场景纹理上进行后期处理了。如果让图像灰度化,看起来将会是这样:
![](../img/04/11/anti_aliasing_post_processing.png) ![](../img/04/11/anti_aliasing_post_processing_grayscale.png)
!!! Important !!! Important

View File

@@ -4,13 +4,13 @@
---|--- ---|---
作者 | JoeyDeVries 作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/) 翻译 | [Django](http://bullteacher.com/)
校对 | 暂无 校对 | [AoZhang](https://github.com/SuperAoao)
!!! note !!! note
本节暂未进行完全的重写,错误可能会很多。如果可能的话,请对照原文进行阅读。如果有报告本节的错误,将会延迟至重写之后进行处理。 本节暂未进行完全的重写,错误可能会很多。如果可能的话,请对照原文进行阅读。如果有报告本节的错误,将会延迟至重写之后进行处理。
当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在监视器上。过去,大多数监视器是阴极射线管显示器CRT。这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做监视器Gamma。 当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在显示器上。过去,大多数显示器是阴极射线管显示器CRT。这些显示器有一个物理特性就是两倍的输入电压并不能产生两倍的亮度。输入电压加倍产生的亮度约为输入电压的2.2次幂,这叫做显示器Gamma。
!!! note "译注" !!! 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的二分之一),但是由于这与我们的眼睛感知亮度不完全一致(对比较暗的颜色变化更敏感),所以它看起来有差异。 第一行是人眼所感知到的正常的灰阶亮度要增加一倍比如从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次幂),所以物理亮度通过显示器能够被映射到顶部的非线性亮度;因此看起来效果不错。
监视器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于监视器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图: 显示器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于我们从显示器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图:
![](../img/05/02/gamma_correction_gamma_curves.png) ![](../img/05/02/gamma_correction_gamma_curves.png)
点线代表线性颜色/亮度值译注这表示的是理想状态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监视器显以后实际上都不正确。当使用更高级的光照算法时,这个问题会变得越来越明显,你可以看看下图 因为颜色是根据显示器的输出配置的,所以线性空间中的所有中间(照明)计算在物理上都不正确的。随着更多先进的照明算法的引入,这一点变得更加明显,如下图所示
![](../img/05/02/gamma_correction_example.png) ![](../img/05/02/gamma_correction_example.png)
你可以看到,通过伽玛校正,(更新的)颜色值可以很好地结合在一起,较暗的区域显示更多的细节。总的来说,一个更好的图像质量与一些小的修改。
如果没有适当地纠正这个显示器伽马,照明看起来是错误的,艺术家将很难获得逼真和好看的结果。解决方案正是应用<def>伽马校正</def>
## Gamma校正 ## 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 !!! 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校正的方式 有两种在你的场景中应用gamma校正的方式
使用OpenGL内建的sRGB帧缓冲。 * 使用OpenGL内建的sRGB帧缓冲。
自己在像素着色器中进行gamma校正。 * 自己在像素着色器中进行gamma校正。
第一个选项也许是最简单的方式但是我们也会丧失一些控制权。开启GL_FRAMEBUFFER_SRGB可以告诉OpenGL每个后续的绘制命令里在颜色储存到颜色缓冲之前先校正sRGB颜色。sRGB这个颜色空间大致对应于gamma2.2它也是家用设备的一个标准。开启GL_FRAMEBUFFER_SRGB以后每次像素着色器运行后续帧缓冲OpenGL将自动执行gamma校正包括默认帧缓冲。
第一个选项也许是最简单的方式但是我们也会丧失一些控制权。开启GL_FRAMEBUFFER_SRGB可以告诉OpenGL每个后续的绘制命令里在颜色储存到颜色缓冲之前先校正sRGB颜色。sRGB这个颜色空间大致对应于gamma2.2它也是大多数设备的一个标准。开启GL_FRAMEBUFFER_SRGB以后每次像素着色器运行后续帧缓冲OpenGL将自动执行gamma校正包括默认帧缓冲。
开启GL_FRAMEBUFFER_SRGB简单的调用glEnable就行 开启GL_FRAMEBUFFER_SRGB简单的调用glEnable就行
@@ -58,16 +63,16 @@ Gamma校正(Gamma Correction)的思路是在最终的颜色输出上应用监视
glEnable(GL_FRAMEBUFFER_SRGB); glEnable(GL_FRAMEBUFFER_SRGB);
``` ```
自此你渲染的图像就被进行gamma校正处理你不需要做任何事情硬件就帮你处理了。有时候你应该记得这个建议gamma校正将把线性颜色空间转为非线性空间,所以在最后一步进行gamma校正是极其重要的。如果你在最后输出之前就进行gamma校正所有的后续操作都是在操作不正确的颜色值。例如如果你使用多个帧缓冲你可能打算让两个帧缓冲之间传递的中间结果仍然保持线性空间颜色只是给发送给监视器的最后的那个帧缓冲应用gamma校正。 自此你渲染的图像就被进行gamma校正处理你不需要做任何事情硬件就帮你处理了。使用这种方法(和其他方法)你应该记住的重点是,伽马校正(也)将颜色从线性空间转为非线性空间,所以在最后一步进行伽马校正。如果你在最后输出之前就进行gamma校正所有的后续操作都是在操作不正确的颜色值。例如如果你使用多个帧缓冲你可能打算让两个帧缓冲之间传递的中间结果仍然保持线性空间颜色只是给发送给显示器的最后的那个帧缓冲应用gamma校正。
第二方法稍微复杂点但同时也是我们对gamma操作有完全控制。我们在每个相关像素着色器运行的最后应用gamma校正所以在发送到帧缓冲前颜色就被校正 第二方法需要更多的工作,但也让我们完全控制伽马操作。我们在每个相关像素着色器运行结束时应用伽玛校正,因此最终的颜色在发送到显示器之前结束伽玛校正。
```c++ ```c++
void main() void main()
{ {
// do super fancy lighting // 在线性空间做炫酷的光照效果
[...] [...]
// apply gamma correction // 应用伽马矫正
float gamma = 2.2; float gamma = 2.2;
fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma)); fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
} }
@@ -75,23 +80,23 @@ void main()
最后一行代码将fragColor的每个颜色元素应用有一个1.0/gamma的幂运算校正像素着色器的颜色输出。 最后一行代码将fragColor的每个颜色元素应用有一个1.0/gamma的幂运算校正像素着色器的颜色输出。
方法个问题是为了保持一致,你必须在像素着色器里加上这个gamma校正所以如果你有很多像素着色器它们可能分别用于不同物体那么你就必须每个着色器里都加上gamma校正了。一个更简单的方案是在你的渲染循环中引入后处理阶段,在后处理四边形上应用gamma校正,这样你只要做一次就好了 方法的一个问题是为了保持一致,你必须对每个有助于最终输出的片段着色器应用伽玛校正。如果你有多个对象的十几个片段着色器,你必须每个着色器添加伽马校正代码。一个更简单的解决方案是在你的渲染循环中引入一个后处理阶段,在后处理四边形上应用伽马校正作为最后一步,这样你只要做一次伽马矫正即可
单行代码代表了gamma校正的实现。不令人印象深刻但当你进行gamma校正的时候有一些额外的事情别忘了考虑。 单行代码代表了gamma校正的实现。不那么令人印象深刻但当你进行gamma校正的时候有一些额外的事情别忘了考虑。
### sRGB纹理 ### 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校正纹理显示也没问题。然而现在我们是把所有东西都放在线性空间中展示的纹理颜色就会变坏如下图展示的那样
![](../img/05/02/gamma_correction_srgbtextures.png) ![](../img/05/02/gamma_correction_srgbtextures.png)
纹理图像实在太亮了,发生这种情况是因为它们实际上进行了两次gamma校正想一想当我们基于监视器上看到的情况创建一个图像,我们就已经对颜色值进行了gamma校正所以再次显示在监视器上就没错。由于我们在渲染中又进行了一次gamma校正图片就实在太亮了。 纹理图像太亮了,这是因为它实际上被伽玛校正了两次!想想看,当我们根据在显示器上看到的创建图像,我们有效地对图像的颜色值进行伽玛校正,使其在显示器上看起来正确。因为我们在渲染器中再次进行伽玛校正,图像最终变得太亮了。
为了修复这个问题,我们确保纹理制作者是在线性空间中进行创作的。但是由于大多数纹理制作者并不知道什么是gamma校正并且在sRGB空间中进行创作更简单这也许不是一个好办法 为了解决这个问题,我们必须确保纹理美工在线性空间中工作。然而因为在sRGB空间中工作更容易而且大多数工具甚至不支持线性纹理所以这可能不是首选的解决方案
另一个解决方案是重校或把这些sRGB纹理在进行任何颜色值的计算前变回线性空间。我们可以这样做 另一个解决方案是在对其颜色值进行任何计算之前重新校正或将这些sRGB纹理转换为线性空间。我们可以这样做
```c++ ```c++
float gamma = 2.2; 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); 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纹理时要当心。 因为不是所有纹理都是在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++ ```c++
float attenuation = 1.0 / (distance * distance); float attenuation = 1.0 / (distance * distance);
@@ -131,18 +136,19 @@ float attenuation = 1.0 / distance;
![](../img/05/02/gamma_correction_attenuation.png) ![](../img/05/02/gamma_correction_attenuation.png)
这种差异产生的原因是,光的衰减方程改变了亮度值,而且屏幕上显示出来的也不是线性空间,在监视器上效果最好的衰减方程并不是符合物理的。想想平方衰减方程如果我们使用这个方程而且不进行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 !!! Important
我们在基础光照教程中讨论的更高级的那个衰减方程在有gamma校正的场景中也仍然有用因为它可以让我们对衰减拥有更多准确的控制权不过在进行gamma校正的场景中当然需要不同的参数 我们在基础光照教程中讨论的更高级的那个衰减方程在有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校正时建议只去调整光照参数的原因。 总而言之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校正的内容。 - [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/): 一些额外的实践上的思考。 - [renderwonk.com](http://renderwonk.com/blog/index.php/archive/adventures-with-gamma-correct-rendering/): 一些额外的实践上的思考。

View File

@@ -4,63 +4,59 @@
---|--- ---|---
作者 | JoeyDeVries 作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/) 翻译 | [Django](http://bullteacher.com/)
校对 | gjy_1992 校对 | gjy_1992, [1i9h7_b1u3](https://github.com/1012796366/)
!!! note 阴影是由于遮挡导致光线无法到达而形成的。当一个光源的光线因为被其他物体阻挡而无法照射到某个物体时,该物体便处于阴影之中。阴影为光照场景增添了极强的真实感,让观察者能够更容易感知物体之间的空间关系。为场景赋予了更强的立体感。例如,观察下方两张图,一张有阴影而另外一张没有阴影:
本节暂未进行完全的重写,错误可能会很多。如果可能的话,请对照原文进行阅读。如果有报告本节的错误,将会延迟至重写之后进行处理。
阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。场景和物体的深度感因此能够得到极大提升,下图展示了有阴影和没有阴影的情况下的不同:
![](../../img/05/03/01/shadow_mapping_with_without.png) ![](../../img/05/03/01/shadow_mapping_with_without.png)
你可以看到,有阴影的时候你能更容易地区分出物体之间的位置关系,例如,当使用阴影的时候浮在地板上的立方体的事实更加清晰 你可以看到,有阴影的时候你能更容易地区分出物体之间的位置关系,例如,只有在有阴影的情况下,我们才能明显看到其中一个立方体悬浮于其他立方体之上
阴影还是比较不好实现的,因为当前实时渲染领域还没找到一种完美的阴影算法。目前有几种近似阴影技术,但它们都有自己的弱点和不足,这点我们必须要考虑到 然而,实现阴影绝非易事,主要是因为目前的实时(光栅化图形)研究领域并没有开发出完美的阴影算法,虽然已经有许多优秀的阴影近似算法,但它们都有无法忽略的瑕疵
视频游戏中较多使用的一种技术是阴影贴图(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)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。 阴影映射背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
![](../../img/05/03/01/shadow_mapping_theory.png) ![](../../img/05/03/01/shadow_mapping_theory.png)
这里的所有蓝线代表光源可以看到的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)
![](../../img/05/03/01/shadow_mapping_theory_spaces.png) ![](../../img/05/03/01/shadow_mapping_theory_spaces.png)
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片段是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个\(T\)变换,它可以将任何三维位置转变到光源的可见坐标空间。 左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用来确定片段是否在阴影中。我们使用光源的视图矩阵和投影矩阵,从光源的角度下渲染场景,从而生成深度贴图。这个投影矩阵和视图矩阵一同形成了一个变换矩阵\(T\),它可以将任何三维位置转变到光源的可见坐标空间。
!!! Important !!! 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++ ```c++
GLuint depthMapFBO; unsigned int depthMapFBO;
glGenFramebuffers(1, &depthMapFBO); glGenFramebuffers(1, &depthMapFBO);
``` ```
然后创建一个2D纹理提供给帧缓冲的深度缓冲使用 然后创建一个2D纹理提供给帧缓冲的深度缓冲使用
```c++ ```c++
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024; const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
GLuint depthMap; unsigned int depthMap;
glGenTextures(1, &depthMap); glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap); glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 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_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_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_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这是深度贴图的分辨率。 生成深度贴图不太复杂。因为我们只关心深度值我们要把纹理格式指定为GL_DEPTH_COMPONENT。我们还要把纹理的高宽设置为1024这是深度贴图的分辨率。
@@ -83,7 +79,7 @@ glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
``` ```
我们需要的只是在从光的透视图下渲染场景的时候深度信息,所以颜色缓冲没有用。然而不包含颜色缓冲的帧缓冲对象是不完整的所以我们需要显式告诉OpenGL我们不适用任何颜色数据进行渲染。我们通过将调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事 我们需要从光源角度渲染场景的深度信息,因此不需要使用颜色缓冲。然而不包含颜色缓冲的帧缓冲对象是不完整的所以我们需要显式告诉OpenGL我们不会渲染任何颜色数据。我们用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE。
合理配置将深度值渲染到纹理的帧缓冲后,我们就可以开始第一步了:生成深度贴图。两个步骤的完整的渲染阶段,看起来有点像这样: 合理配置将深度值渲染到纹理的帧缓冲后,我们就可以开始第一步了:生成深度贴图。两个步骤的完整的渲染阶段,看起来有点像这样:
@@ -91,9 +87,9 @@ glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 1. 首选渲染深度贴图 // 1. 首选渲染深度贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT); glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices(); ConfigureShaderAndMatrices();
RenderScene(); RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 像往常一样渲染场景,但这次使用深度贴图 // 2. 像往常一样渲染场景,但这次使用深度贴图
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
@@ -103,55 +99,57 @@ glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene(); RenderScene();
``` ```
这段代码隐去了一些细节但它表达了阴影映射的基本思路。这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景通常是窗口分辨率有着不同的分辨率我们需要改变视口viewport的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。 这段代码隐去了一些细节但它表达了阴影映射的基本思路。这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景通常是窗口分辨率有着不同的分辨率我们需要改变视口(<def>viewport</def>)的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。
### 光源空间的变换 ### 光源空间的变换
前面那段代码中一个不清楚的函数是`ConfigureShaderAndMatrices`。它是用来在第二个步骤确保为每个物体设置了合适的投影和视图矩阵,以及相关的模型矩阵。然而,第一个步骤中,我们从光的位置的视野下使用不同的投影和视图矩阵来渲染场景。 前面那段代码中一个不清楚的函数是`ConfigureShaderAndMatrices`。在第二个步骤中,这和往常一样:确保投影矩阵和视图矩阵都已经正确设置,并且为每个物体设置相应的模型矩阵。然而,第一个步骤中,我们需要使用不同的投影矩阵和视图矩阵来从光源角度渲染场景。
因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形: 因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形:
```c++ ```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); glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
``` ```
这里有个本节教程的demo场景中使用的正交投影矩阵的例。因为投影矩阵间接决定可视区域的范围,以及哪些东西不会被裁切你需要保投影视锥frustum的大小以包含打算在深度贴图中包含的物体。当物体和片段不在深度贴图中,它们就不会产生阴影。 上面的代码是本章演示场景中使用的正交投影矩阵的例。因为投影矩阵间接决定可视区域的范围(例如哪些东西不会被裁切),所以你需要保投影视锥的尺寸包括了应当出现在深度贴图里面的所有物体。当物体或者片段没有出现在深度贴图中的时候,它们就不会产生阴影。
为了创建一个视图矩阵来变换每个物体把它们变换到从光源视角可见的空间中我们将使用glm::lookAt函数这次从光源的位置看向场景中央。 为了创建一个视图矩阵来变换每个物体把它们变换到从光源视角可见的空间中我们将使用glm::lookAt函数这次从光源的位置看向场景中央。
```c++ ```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++ ```c++
glm::mat4 lightSpaceMatrix = lightProjection * lightView; glm::mat4 lightSpaceMatrix = lightProjection * lightView;
``` ```
这个lightSpaceMatrix正是前面我们称为\(T\)的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵,我们就能像往常那样渲染场景了。然而,我们只关心深度值,并非所有片段计算都在我们的着色器中进行。为了提升性能,我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。 这个`lightSpaceMatrix`正是前面我们称为\(T\)的那个变换矩阵。有了`lightSpaceMatrix`,只要给每个着色器提供光空间的投影矩阵和视图矩阵,我们就能像往常那样渲染场景了。然而,我们只关心深度值,并不执行复杂的(照明)片段计算。为了提升性能,我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。
### 渲染至深度贴图 ### 渲染至深度贴图
当我们光的透视图进行场景渲染的时候,我们会用一个比较简单的着色器,这个着色器除了把顶点变换到光空间以外,不会做得更多了。这个简单的着色器叫做`simpleDepthShader`,就是使用下面的这个着色器: 当我们光的角度来渲染场景的时候,我们会用一个比较简单的着色器,这个着色器只会把顶点变换到光空间。这个简单的着色器叫做`simpleDepthShader`,就是使用下面的这个着色器:
```c++ ```c++
#version 330 core #version 330 core
layout (location = 0) in vec3 position; layout (location = 0) in vec3 aPos;
uniform mat4 lightSpaceMatrix; uniform mat4 lightSpaceMatrix;
uniform mat4 model; uniform mat4 model;
void main() void main()
{ {
gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f); gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
} }
``` ```
这个顶点着色器将一个单独模型的一个顶点使用lightSpaceMatrix变换到光空间中。 这个顶点着色器接收模型矩阵和顶点数据,使用`lightSpaceMatrix`变换到光空间中。
由于我们没有颜色缓冲,最后的片段不需要任何处理,所以我们可以简单地使用一个空片段着色器: 由于我们没有颜色缓冲并且禁止了读取和绘制缓冲,因此生成的片段不需要进行任何处理,所以我们可以简单地使用一个空片段着色器:
```c++ ```c++
#version 330 core #version 330 core
@@ -162,136 +160,134 @@ void main()
} }
``` ```
这个空片段着色器什么也不干,运行完后,深度缓冲会被更新。我们可以取消行的注释,来显式设置深度,但是这个(指注释掉那行之后)是更有效率的,因为底层无论如何都会默认去设置深度缓冲 这个空片段着色器什么也不干,运行完后,深度缓冲会被更新。我们可以取消片段着色器中那一行的注释,来显式设置深度,但因为底层总会去设置深度缓冲,所以我们没必要显式设置,直接使用空的片段着色器即可
渲染深度缓冲现在成了 现在,渲染深度/阴影贴图的过程如下所示
```c++ ```c++
simpleDepthShader.Use(); simpleDepthShader.use();
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix)); glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT); glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_DEPTH_BUFFER_BIT);
RenderScene(simpleDepthShader); RenderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
``` ```
这里的RenderScene函数的参数是一个着色器程序(shader program,它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。 这里的`RenderScene`函数接受着色器程序(<def>shader program</def>)作为参数,它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。
后,在光的透视图视角下,很完美地用每个可见片段的最近深度填充了深度缓冲。通过将这个纹理投射到一个2D四边形上和我们在帧缓冲一节做的后处理过程类似就能在屏幕上显示出来,我们会获得这样的东西 终的成品是一个填充完整的深度缓冲区,其中存储了从光源视角可见的所有片段的最近深度值。通过将这个纹理渲染到一个2D四边形上和我们在帧缓冲一节做的后处理过程类似),就能在屏幕上显示出来下面的效果
![](../../img/05/03/01/shadow_mapping_depth_map.png) ![](../../img/05/03/01/shadow_mapping_depth_map.png)
将深度贴图渲染到四边形上的片段着色器 我们使用下面的片段着色器来将深度贴图渲染到四边形上:
```c++ ```c++
#version 330 core #version 330 core
out vec4 color; out vec4 FragColor;
in vec2 TexCoords; in vec2 TexCoords;
uniform sampler2D depthMap; uniform sampler2D depthMap;
void main() void main()
{ {
float depthValue = texture(depthMap, TexCoords).r; 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)获得把场景渲染成深度贴图的源码。 你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/5.advanced_lighting/3.1.1.shadow_mapping_depth/shadow_mapping_depth.cpp)获得把场景渲染成深度贴图的源码。
## 渲染阴影 ## 渲染阴影
正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在片段着色器中执行,用来检验一个片段是否在阴影之中,不过我们在顶点着色器中进行光空间的变换: 正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在片段着色器中执行,用来检验一个片段是否在阴影之中,不过我们在顶点着色器中进行光空间的变换:
```c++ ```c++
#version 330 core #version 330 core
layout (location = 0) in vec3 position; layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 normal; layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 texCoords; layout (location = 2) in vec2 aTexCoords;
out vec2 TexCoords;
out VS_OUT { out VS_OUT {
vec3 FragPos; vec3 FragPos;
vec3 Normal; vec3 Normal;
vec2 TexCoords; vec2 TexCoords;
vec4 FragPosLightSpace; vec4 FragPosLightSpace;
} vs_out; } vs_out;
uniform mat4 projection; uniform mat4 projection;
uniform mat4 view; uniform mat4 view;
uniform mat4 model; uniform mat4 model;
uniform mat4 lightSpaceMatrix; uniform mat4 lightSpaceMatrix;
void main() void main()
{ {
gl_Position = projection * view * model * vec4(position, 1.0f); vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
vs_out.FragPos = vec3(model * vec4(position, 1.0)); vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
vs_out.Normal = transpose(inverse(mat3(model))) * normal; vs_out.TexCoords = aTexCoords;
vs_out.TexCoords = texCoords;
vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0); 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++ ```c++
#version 330 core #version 330 core
out vec4 FragColor; out vec4 FragColor;
in VS_OUT { in VS_OUT {
vec3 FragPos; vec3 FragPos;
vec3 Normal; vec3 Normal;
vec2 TexCoords; vec2 TexCoords;
vec4 FragPosLightSpace; vec4 FragPosLightSpace;
} fs_in; } fs_in;
uniform sampler2D diffuseTexture; uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap; uniform sampler2D shadowMap;
uniform vec3 lightPos; uniform vec3 lightPos;
uniform vec3 viewPos; uniform vec3 viewPos;
float ShadowCalculation(vec4 fragPosLightSpace) float ShadowCalculation(vec4 fragPosLightSpace)
{ {
[...] [...]
} }
void main() void main()
{ {
vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb; vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
vec3 normal = normalize(fs_in.Normal); vec3 normal = normalize(fs_in.Normal);
vec3 lightColor = vec3(1.0); vec3 lightColor = vec3(1.0);
// Ambient // 环境光
vec3 ambient = 0.15 * color; vec3 ambient = 0.15 * lightColor;
// Diffuse // 漫反射
vec3 lightDir = normalize(lightPos - fs_in.FragPos); vec3 lightDir = normalize(lightPos - fs_in.FragPos);
float diff = max(dot(lightDir, normal), 0.0); float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * lightColor; vec3 diffuse = diff * lightColor;
// Specular // 镜面高光
vec3 viewDir = normalize(viewPos - fs_in.FragPos); vec3 viewDir = normalize(viewPos - fs_in.FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = 0.0; float spec = 0.0;
vec3 halfwayDir = normalize(lightDir + viewDir); vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0); spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
vec3 specular = spec * lightColor; vec3 specular = spec * lightColor;
// 计算阴影 // 计算阴影
float shadow = ShadowCalculation(fs_in.FragPosLightSpace); float shadow = ShadowCalculation(fs_in.FragPosLightSpace);
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color; 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++ ```c++
float ShadowCalculation(vec4 fragPosLightSpace) float ShadowCalculation(vec4 fragPosLightSpace)
@@ -302,20 +298,23 @@ float ShadowCalculation(vec4 fragPosLightSpace)
} }
``` ```
返回片段在光空间的-1到1的范围 这将返回片段在光空间中的位置,并将范围限定在[-1,1]
!!! Important !!! Important
当使用正交投影矩阵顶点w元素仍保持不变,所以这一步实际上毫无意义。可是,当使用透视投影的时候就是必须的了,所以为了保证在两种投影矩阵下都有效就得留着这行。 当使用正交投影矩阵向量的w分量仍保持不变,所以这一步实际上毫无意义。可是,当使用透视投影的时候,这一步又变得不可或缺。所以为了保证在两种投影矩阵下都能正常运作,就得留着这行。
因为来自深度贴图的深度在0到1的范围我们也打算使用projCoords从深度贴图中去采样所以我们将NDC坐标变换为0到1的范围 因为来自深度贴图的深度范围是[0,1],我们也打算使用`projCoords`从深度贴图中去采样所以我们将NDC坐标范围变换为[0,1]
译者注这里的意思是上面的projCoords的xyz分量都是[-1,1]下面会指出这对于远平面之类的点才成立而为了和深度贴图的深度相比较z分量需要变换到[0,1]为了作为从深度贴图中采样的坐标xy分量也需要变换到[0,1]。所以整个projCoords向量都需要变换到[0,1]范围。)
```c++ ```c++
projCoords = projCoords * 0.5 + 0.5; 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++ ```c++
float closestDepth = texture(shadowMap, projCoords.xy).r; float closestDepth = texture(shadowMap, projCoords.xy).r;
@@ -333,7 +332,7 @@ float currentDepth = projCoords.z;
float shadow = currentDepth > closestDepth ? 1.0 : 0.0; float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
``` ```
完整的shadowCalculation函数是这样的 完整的`shadowCalculation`函数是这样的:
```c++ ```c++
float ShadowCalculation(vec4 fragPosLightSpace) float ShadowCalculation(vec4 fragPosLightSpace)
@@ -353,31 +352,31 @@ float ShadowCalculation(vec4 fragPosLightSpace)
} }
``` ```
激活这个着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影以及视图矩阵,结果如下图所示: 激活这个着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影矩阵以及视图矩阵,结果如下图所示:
![](../../img/05/03/01/shadow_mapping_shadows.png) ![](../../img/05/03/01/shadow_mapping_shadows.png)
如果你做对了,你会看到地板和上有立方体的阴影。你可以从这里找到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程序的源码
## 改进阴影贴图 ## 改进阴影映射
我们试图让阴影映射工作,但是你也看到了,阴影映射还是有点不真实,我们修复它才能获得更好的效果,这是下面的部分所关注的焦点 我们成功地让阴影映射工作起来了,但是你也看到了,由于若干(清晰可见的)与阴影映射相关的瑕疵,目前的效果还是不够完善,我们得做些修复,接下来的章节中我们将着重解决这些问题
### 阴影失真 ### 阴影失真
前面的图片中明显有不对的地方。放大看会发现明显的线条样式 前面的图片中明显有不对的地方。放大看会发现明显的摩尔纹
![](../../img/05/03/01/shadow_mapping_acne.png) ![](../../img/05/03/01/shadow_mapping_acne.png)
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做**阴影失真(Shadow Acne)**,下图解释了成因: 我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影的不真实感叫做<def>阴影失真</def>(Shadow Acne),下图解释了成因:
![](../../img/05/03/01/shadow_mapping_acne_diagram.png) ![](../../img/05/03/01/shadow_mapping_acne_diagram.png)
因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段从同一个深度值进行采样 阴影贴图的分辨率影响,当多个片段距离光源比较远的时候,它们可能从深度贴图中采样相同的深度值。图片中,每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段会采样相同的深度值
虽然很多时候没问题,但是当光源以个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的多个片段就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片段被认为是在阴影中,有些不在,由此产生了图片中的条纹样式。 虽然很多时候没问题,但是当光源以个角度照射表面的时候就会出问题,这种情况下深度贴图也是从这样的角度下进行渲染的,随后,多个片段就会从同一个斜坡的深度纹理像素中采样,其中一部分在地板上面,另一部分在地板下面;这样我们所得到的阴影就有了差异。也因此,一部分片段位于阴影中,而一部分则不是,最终产生了图片中的条纹样式。
我们可以用一个叫做**阴影偏移**shadow bias的技巧来解决这个问题我们简单的对表面的深度或深度贴图应用一个偏移量这样片段就不会被错误地认为在表面之下了。 我们可以用一个叫做<def>阴影偏移</def>shadow bias的技巧来解决这个问题我们简单的对表面的深度或深度贴图应用一个偏移量这样片段就不会被错误地认为在表面之下了。
![](../../img/05/03/01/shadow_mapping_acne_bias.png) ![](../../img/05/03/01/shadow_mapping_acne_bias.png)
@@ -388,66 +387,72 @@ float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0; float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
``` ```
一个0.005的偏移就能帮到很大的忙,但是有些表面坡度很大,仍然会产生阴影失真。有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量使用点乘: 一个0.005的偏移就能帮到很大的忙,但偏移值高度依赖于光源与表面的夹角,如果倾角特别大的话,那么阴影仍然还是会失真。更稳健的办法根据表面朝向光线的角度更改偏移量使用点乘就可以实现这个办法
```c++ ```c++
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005); float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
``` ```
这里我们有一个偏移量的最大值0.05和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了同一个场景,但使用了阴影偏移,效果的确更好: 这里我们有一个偏移量的最大值0.05和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了使用了阴影偏移后的同一个场景,可以看出效果的确更好:
![](../../img/05/03/01/shadow_mapping_with_bias.png) ![](../../img/05/03/01/shadow_mapping_with_bias.png)
选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。 因为各个场景中合适的偏差值都不尽相同,所以可能需要经过一番调整后才能找到合适的偏移值,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。
### 悬浮 ### 阴影悬浮
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值): 使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移值足够大时,会导致阴影明显地偏离了实际物体,你可以从下图看到这个现象(这是一个夸张的偏移值):
![](../../img/05/03/01/shadow_mapping_peter_panning.png) ![](../../img/05/03/01/shadow_mapping_peter_panning.png)
这个阴影失真叫做悬浮(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在渲染阴影贴图时要剔除正面。
因为我们只需要深度贴图的深度值,对于实心物体来说,无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,即使阴影在物体内部有错误,我们也看不见。
![](../../img/05/03/01/shadow_mapping_culling.png) ![](../../img/05/03/01/shadow_mapping_culling.png)
为了修复peter游移我们要进行正面剔除先必须开启GL_CULL_FACE 为了修复阴影悬浮,在阴影贴图生成阶段,我们要进行正面剔除,先必须开启GL_CULL_FACE
```c++ ```c++
glCullFace(GL_FRONT); glCullFace(GL_FRONT);
RenderSceneToDepthMap(); RenderSceneToDepthMap();
glCullFace(GL_BACK); // 不要忘记设回原先的culling face glCullFace(GL_BACK); // 不要忘记设回原先的面剔除
``` ```
这十分有效地解决了peter panning的问题但只对内部不会对外开口的实物体有效。我们的场景中,在立方体上工作的很好,但在地板上无效,因为正面剔除完全移除了地板。地面是一个单独的平面,不会被完全剔除。如果有人打算使用这个技巧解决peter panning必须考虑到只有剔除物体的正面才有意义 这十分有效地解决了阴影悬浮的问题,但只适用于具有封闭内部空间的实物体。我们的场景中,该方法在立方体上工作的很好,但在地板上无效,这是因为地板是一个平面,因此将被完全剔除。如果打算使用这个技巧解决阴影悬浮,就应当只在合适的物体上进行正面剔除
一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免peter panning 外要注意的是,接近阴影的物体仍然可能会出现不正确的效果。但一般来说,通过常规的偏移值调整就足以解决阴影偏移的问题了
### 采样过多 ### 采样
论你喜不喜欢还有一个视觉差异,就是光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。 论你是否喜欢还有一个视觉差异,光的视锥范围以外的区域也会被判定为处于阴影之中。这是因为当投影坐标超出光的视锥范围时,其值会比1.0大,此时采样的深度纹理就会超出他默认的范围[0,1]。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
![](../../img/05/03/01/shadow_mapping_outside_frustum.png) ![](../../img/05/03/01/shadow_mapping_outside_frustum.png)
你可以在图中看到,光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。 如图所示,存在一个虚构的光照范围,超出该区域的部分就被阴影覆盖;这个区域实际上代表着深度贴图投影到地板上的范围。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了`GL_REPEAT`
我们宁可让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。我们可以储存一个边框颜色然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER 我们更希望让所有超出深度贴图范围的坐标,其深度值是1.0,这样超出的坐标将永远不在阴影之中因为没有物体的深度值是大于1.0的)。我们可以配置一个纹理边界颜色,然后把深度贴图的纹理环绕选项设置为`GL_CLAMP_TO_BORDER`
```c++ ```c++
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 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); glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
``` ```
现在如果我们采样深度贴图0到1坐标范围以外的区域纹理函数总会返回一个1.0的深度值阴影值为0.0。结果看起来会更真实: 现在如果我们采样深度贴图[0,1]范围以外的区域纹理函数总会返回一个1.0的深度值,由此得到的阴影值始终为0.0。结果看起来会更真实:
![](../../img/05/03/01/shadow_mapping_clamp_edge.png) ![](../../img/05/03/01/shadow_mapping_clamp_edge.png)
仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。可以看到这片黑色区域总是出现在光源视锥的极远处 图中仍有一部分是阴影区域,那部分区域的坐标超出了光的正交视锥的远平面。通过阴影方向可以看到这片阴影区域总是出现在光源视锥的远平面外
当一个点比光的远平面还要远时它的投影坐标的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 解决这个问题也很简单只要投影向量的z坐标大于1.0我们就把shadow的值强制设为0.0
@@ -462,25 +467,25 @@ float ShadowCalculation(vec4 fragPosLightSpace)
} }
``` ```
检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,我们最终得到下面我们所追求的效果 通过边界颜色钳制并进行远平面特殊处理,我们最终解决深度贴图的过采样问题。最终得到了预期之内的结果了
![](../../img/05/03/01/shadow_mapping_over_sampling_fixed.png) ![](../../img/05/03/01/shadow_mapping_over_sampling_fixed.png)
些结果意味着只有在深度贴图范围以内的被投影的fragment坐标才有阴影所以任何超出范围的都将会没有阴影。由于在游戏中通常这只发生在远处,就会比我们之前的那个明显的黑色区域效果更真实 样的做法意味着阴影只会出现在深度贴图的覆盖范围内,而光源视锥体以外的区域不会产生阴影。在游戏开发过程中,不产生阴影的部分通常只会出现在远方,相比于此前让远方漆黑一片的做法,这种处理更合理一些
## PCF ## PCF
阴影现在已经附着到场景中了,不过这仍不是我们想要的。如果你放大看阴影,阴影映射对分辨率的依赖很快变得很明显 当前的阴影已经为场景增色不少,但是仍未达到理想水平。如果你放大来看,会发现分辨率对阴影映射造成了非常明显的影响
![](../../img/05/03/01/shadow_mapping_zoom.png) ![](../../img/05/03/01/shadow_mapping_zoom.png)
因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。 因为深度贴图的分辨率固定,一个纹理像素可能覆盖了多个片段,结果就是多个片段会从深度贴图中采样相同的深度值,并得到相同的阴影判定结果,这也就导致了图中的锯齿边
你可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。 你可以通过增加深度贴图的分辨率的方式来减少锯齿块,也可以尝试尽可能的让光的视锥贴合场景。
另一个(并不完整的解决方案叫做PCFpercentage-closer filtering,这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。 另一种(部分解决方案叫做PCF,译作<def>百分比渐进滤波</def>(percentage-closer filtering),这个术语涵盖了多种滤波函数,能生成更柔和阴影,减少锯齿块。其核心思想是多次采样深度贴图,每一次采样的纹理坐标都稍有不同,独立判断每个采样点的阴影状态后,将子结果混合取平均,最终获得相对柔和阴影。
简单的PCF实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来 简单的PCF实现是采样深度贴图周边纹理像素,并取平均值
```c++ ```c++
float shadow = 0.0; float shadow = 0.0;
@@ -496,57 +501,58 @@ for(int x = -1; x <= 1; ++x)
shadow /= 9.0; shadow /= 9.0;
``` ```
这个textureSize返回一个给定采样器纹理0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小,我们用以对纹理坐标进行偏移,确保每个新样本来自不同的深度值。这里我们采样得到9个值它们在投影坐标的x和y值的周围为阴影阻挡进行测试并最终通过样本的总数目将结果平均 此处的`textureSize`返回指定采样器纹理0级mipmap的宽高向量类型为vec2。取其倒数即可得到单一一个纹理像素的大小,可以用来对纹理坐标进行偏移,从而确保每次都采样不同的深度值。本示例在每个投影坐标周围采样9个点来进行阴影判断最终取平均
使用更多的样本,更改texelSize变量你就可以增加阴影的柔和程度。下面你可以看到应用了PCF的阴影 增加采样次数,同时/或是更改`texelSize`变量,可以进一步提升柔化效果。下图展示了应用简易PCF的阴影:
![](../../img/05/03/01/shadow_mapping_soft_shadows.png) ![](../../img/05/03/01/shadow_mapping_soft_shadows.png)
从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影贴图分辨率的不真实感,但通常对于大多数应用来说效果已经很好了。 从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影的不真实感,但通常对于大多数应用来说效果已经很好了。
你可以从[这里](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)矩阵之间存在本质差异。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
![](../../img/05/03/01/shadow_mapping_projection.png) ![](../../img/05/03/01/shadow_mapping_projection.png)
透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。 透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。
另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。发生这个是因为透视投影下,深度变成了非线性的深度值,它的大多数可辨范围都位于近平面附近。为了可以像使用正交投影一样合适地观察深度值,你必须先将非线性深度值转变为线性,如我们在深度测试教程中已经讨论过的那样。 另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。这个是因为透视投影将深度值转换为非线性值,其变化范围大多集中在近平面附近。为了可以像使用正交投影一样合适地观察深度值,你必须先将非线性深度值转变为线性,如我们在[深度测试](../../04 Advanced OpenGL/01 Depth testing.md)教程中已经讨论过的那样。
```c++ ```c++
#version 330 core #version 330 core
out vec4 color; out vec4 FragColor;
in vec2 TexCoords; in vec2 TexCoords;
uniform sampler2D depthMap; uniform sampler2D depthMap;
uniform float near_plane; uniform float near_plane;
uniform float far_plane; uniform float far_plane;
float LinearizeDepth(float depth) 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)); return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
} }
void main() void main()
{ {
float depthValue = texture(depthMap, TexCoords).r; float depthValue = texture(depthMap, TexCoords).r;
color = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // 透视
// color = vec4(vec3(depthValue), 1.0); // orthographic // FragColor = vec4(vec3(depthValue), 1.0); // 正交
} }
``` ```
这个深度值与我们见到的正交投影的很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。 这个深度值与我们见到的正交投影的深度值很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。
## 附加资源 ## 附加资源
- [Tutorial 16 : Shadow mapping](http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/):提供的类似的阴影映射教程,里面有一些额外的解释。 - [Tutorial 16 : Shadow mapping](http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/):提供的类似的阴影映射教程,里面有一些额外的解释。
- [Shadow Mapping Part 1ogldev](http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html):提供的另一个阴影映射教程。 - [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视频教程里面解释了阴影映射及其实现。 - [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):微软的一篇好文章,其中理出了很多提升阴影贴图质量的技术。 - [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关于改进阴影贴图的好视频

View File

@@ -24,7 +24,7 @@ HDR原本只是被运用在摄影上摄影师对同一个场景采取不同
![](../img/05/06/hdr_image.png) ![](../img/05/06/hdr_image.png)
这与我们眼睛工作的原理非常相似也是HDR渲染的基础。当光线很弱的时候,人眼会自动调整从而使过暗和过亮的部分变得更清晰,就像人眼有一个能自动根据场景亮度调整的自动曝光滑块。 这与我们眼睛工作的原理非常相似也是HDR渲染的基础。当光线很弱的时候人眼会自动调整从而使过暗和过亮的部分变得更清晰就像人眼有一个能自动根据场景亮度调整的自动曝光滑块。
HDR渲染和其很相似我们允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。转换HDR值到LDR值得过程叫做色调映射(Tone Mapping)现在现存有很多的色调映射算法这些算法致力于在转换过程中保留尽可能多的HDR细节。这些色调映射算法经常会包含一个选择性倾向黑暗或者明亮区域的参数。 HDR渲染和其很相似我们允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。转换HDR值到LDR值得过程叫做色调映射(Tone Mapping)现在现存有很多的色调映射算法这些算法致力于在转换过程中保留尽可能多的HDR细节。这些色调映射算法经常会包含一个选择性倾向黑暗或者明亮区域的参数。
@@ -159,4 +159,4 @@ HDR渲染的真正优点在庞大和复杂的场景中应用复杂光照算法
## 附加资源 ## 附加资源
- [如果泛光效果不被应用HDR渲染还有好处吗](http://gamedev.stackexchange.com/questions/62836/does-hdr-rendering-have-any-benefits-if-bloom-wont-be-applied): 一个StackExchange问题其中有一个答案非常详细地解释HDR渲染的好处。 - [如果泛光效果不被应用HDR渲染还有好处吗](http://gamedev.stackexchange.com/questions/62836/does-hdr-rendering-have-any-benefits-if-bloom-wont-be-applied): 一个StackExchange问题其中有一个答案非常详细地解释HDR渲染的好处。
- [什么是色调映射? 它与HDR有什么联系](http://photo.stackexchange.com/questions/7630/what-is-tone-mapping-how-does-it-relate-to-hdr): 另一个非常有趣的答案,用了大量图片解释色调映射。 - [什么是色调映射? 它与HDR有什么联系](http://photo.stackexchange.com/questions/7630/what-is-tone-mapping-how-does-it-relate-to-hdr): 另一个非常有趣的答案,用了大量图片解释色调映射。

View File

@@ -375,7 +375,7 @@ $$
$$ $$
L_o(p,\phi_o, \theta_o) = 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\) 的作用。 当我们离散地对两个球坐标轴进行采样时,每个采样近似代表了半球上的一小块区域,如上图所示。注意,由于球的一般性质,当采样区域朝向中心顶部会聚时,天顶角 \(\theta\) 变高,半球的离散采样区域变小。为了平衡较小的区域贡献度,我们使用 \(sin\theta\) 来权衡区域贡献度,这就是多出来的 \(sin\) 的作用。
@@ -386,8 +386,8 @@ $$
vec3 irradiance = vec3(0.0); vec3 irradiance = vec3(0.0);
vec3 up = vec3(0.0, 1.0, 0.0); vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal); vec3 right = normalize(cross(up, normal));
up = cross(normal, right); up = normalize(cross(normal, right));
float sampleDelta = 0.025; float sampleDelta = 0.025;
float nrSamples = 0.0; float nrSamples = 0.0;

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -43,7 +43,6 @@ nav:
- 阴影: - 阴影:
- 阴影映射: '05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md' - 阴影映射: '05 Advanced Lighting/03 Shadows/01 Shadow Mapping.md'
- 点阴影: '05 Advanced Lighting/03 Shadows/02 Point Shadows.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/04 Normal Mapping.md'
- 视差贴图: '05 Advanced Lighting/05 Parallax Mapping.md' - 视差贴图: '05 Advanced Lighting/05 Parallax Mapping.md'
- HDR: '05 Advanced Lighting/06 HDR.md' - HDR: '05 Advanced Lighting/06 HDR.md'
@@ -77,6 +76,8 @@ nav:
- Guest Articles: - Guest Articles:
- 2020: - 2020:
- 骨骼动画: '08 Guest Articles/2020/01 Skeletal Animation.md' - 骨骼动画: '08 Guest Articles/2020/01 Skeletal Animation.md'
- 2021:
- CSM: '08 Guest Articles/2021/01 CSM.md'
- 2022: - 2022:
- 区域光: '08 Guest Articles/2022/03 Area Lights.md' - 区域光: '08 Guest Articles/2022/03 Area Lights.md'
- 历史存档: 'legacy.md' - 历史存档: 'legacy.md'