1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

Merge pull request #330 from 1012796366/new-theme

更新“高级OpenGL”章节译文以及部分图片
This commit is contained in:
Gary Wang
2025-01-26 19:30:44 +08:00
committed by GitHub
15 changed files with 61 additions and 61 deletions

View File

@@ -136,13 +136,13 @@ void main()
首先我们将深度值变换为NDC不是非常困难
```c++
float z = depth * 2.0 - 1.0;
float ndc = depth * 2.0 - 1.0;
```
接下来使用获取到的z值,应用逆变换来获取线性的深度值:
接下来使用获取到的NDC值,应用逆变换来获取线性的深度值:
```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)为感兴趣的读者详细解释了投影矩阵,它也展示了这些方程是怎么来的。
@@ -158,7 +158,7 @@ float far = 100.0;
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));
}

View File

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

View File

@@ -22,13 +22,13 @@ OpenGL中<def>混合</def>(Blending)通常是实现物体<def>透明度</def>
有些图片并不需要半透明只需要根据纹理颜色值显示一部分或者不显示一部分没有中间情况。比如说草如果想不太费劲地创建草这种东西你需要将一个草的纹理贴在一个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)
所以当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要<def>丢弃</def>(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。在此之前,我们还要学习如何加载一个透明的纹理。
所以当添加像草这样的植被到场景中时,我们不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分。我们想要<def>丢弃</def>(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中
要想加载有alpha值的纹理我们并不需要改很多东西`stb_image`在纹理有alpha通道的时候会自动加载但我们仍要在纹理生成过程中告诉OpenGL我们的纹理现在使用alpha通道了
在此之前,我们还要学习如何加载一个透明的纹理。要想加载有alpha值的纹理我们并不需要改很多东西`stb_image`在纹理有alpha通道的时候会自动加载但我们仍要在纹理生成过程中告诉OpenGL我们的纹理现在使用alpha通道了
```c++
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_SUBTRACT将两个分量相减 \(\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}\)。
- 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>对大部分的操作来说都是我们希望的混合方程,但如果你真的想打破主流,其它的方程也可能符合你的要求。

View File

@@ -48,8 +48,6 @@ float vertices[] = {
在顶点数据中我们将两个三角形都以逆时针顺序定义正面的三角形是1、2、3背面的三角形也是1、2、3如果我们从正面看这个三角形的话。然而如果从观察者当前视角使用1、2、3的顺序来绘制的话从观察者的方向来看背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的它现在是以顺时针顺序渲染的了。这正是我们想要<def>剔除</def>Cull丢弃的不可见面了
在顶点数据中我们定义的是两个逆时针顺序的三角形。然而从观察者的方面看后面的三角形是顺时针的如果我们仍以1、2、3的顺序以观察者当面的视野看的话。即使我们以逆时针顺序定义后面的三角形它现在还是变为顺时针。它正是我们打算剔除丢弃的不可见的面
## 面剔除
在本节的开头我们就说过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)找到完整的源代码。
当反射应用到一整个物体上(像是箱子)时,这个物体看起来就像是钢或者铬这样的高反射性材质。如果我们加载[模型加载](../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),但它对大部分场合都是没问题的。
@@ -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>
- 我偷懒直接使用镜面光纹理图像来创建了反射贴图,所以反射贴图在模型的某些地方不能准确地映射:)。
- 由于模型加载器本身就已经在着色器中占用了3个纹理单元了你需要将天空盒绑定到第4个纹理单元上因为我们要从同一个着色器中对天空盒采样。

View File

@@ -6,11 +6,11 @@
翻译 | Krasjet
校对 | 暂未校对
我们在OpenGL中大量使用缓冲来储存数据已经有很长时间了。操作缓冲其实还有更有意思的方式,而且使用纹理将大量数据传入着色器也有更有趣的方法。这一节中,我们将讨论一些更有意思的缓冲函数,以及我们该如何使用纹理对象来储存大量的数据(纹理的部分还没有完成)
我们在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>
@@ -69,7 +69,7 @@ glVertexAttribPointer(
注意`stride`参数等于顶点属性的大小因为下一个顶点属性向量能在3个或2个分量之后找到。
这给了我们设置顶点属性的另一种方法。使用哪种方法都不会对OpenGL有什么立刻的好处,它只是设置顶点属性的一种更整洁的方式。具体使用的方法将完全取决于你的喜好与程序类型
这给了我们设置顶点属性的另一种方法。使用哪种方法都是可行的,它只是设置顶点属性的一种更整洁的方式。但是,推荐使用交错方法,因为这样一来,每个顶点着色器运行时所需要的顶点属性在内存中会更加紧密对齐
## 复制缓冲
@@ -87,7 +87,6 @@ void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoff
接下来<fun>glCopyBufferSubData</fun>会从`readtarget`中读取`size`大小的数据,并将其写入`writetarget`缓冲的`writeoffset`偏移量处。下面这个例子展示了如何复制两个顶点数组缓冲:
```c++
float vertexData[] = { ... };
glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
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>
我们将会讨论几个有趣的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>)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。
虽然现在它没有什么具体的用途,但知道我们能够访问这个信息总是好的。
## 片段着色器变量
在片段着色器中我们也能访问到一些有趣的变量。GLSL提供给我们两个有趣的输入变量<var>gl_FragCoord</var>和<var>gl_FrontFacing</var>。

View File

@@ -46,13 +46,13 @@ void main() {
几何着色器同时希望我们设置一个它最大能够输出的顶点数量如果你超过了这个值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++
in gl_Vertex
@@ -267,11 +267,11 @@ in VS_OUT {
!!! Important
我们并不是必须要用接口块来向几何着色器传递数据。如果顶点着色器发送的颜色向量是`out 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)找到源码。

View File

@@ -107,11 +107,7 @@ for(int y = -10; y < 10; y += 2)
shader.use();
for(unsigned int i = 0; i < 100; i++)
{
stringstream ss;
string index;
ss << i;
index = ss.str();
shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
shader.setVec2(("offsets[" + std::to_string(i) + "]"), translations[i]);
}
```
@@ -124,11 +120,11 @@ glBindVertexArray(quadVAO);
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获取新一组适用于当前顶点的属性。而当我们将顶点属性定义为一个实例化数组时顶点着色器就只需要对每个实例而不是每个顶点更新顶点属性的内容了。这允许我们对逐顶点的数据使用普通的顶点属性而对逐实例的数据使用实例化数组。
@@ -171,7 +167,7 @@ glBindBuffer(GL_ARRAY_BUFFER, 0);
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>,再次渲染四边形,会得到以下输出:
@@ -312,7 +308,7 @@ for(unsigned int i = 0; i < rock.meshes.size(); i++)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
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);
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
glEnableVertexAttribArray(6);

View File

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

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