mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 04:35:28 +08:00
rewrite 04-11
This commit is contained in:
@@ -266,7 +266,7 @@ $$
|
||||
|
||||

|
||||
|
||||
记住,OpenGL通常是在3D空间进行操作的,对于2D的情况我们可以把z轴缩放1倍,这样z轴的值就不变了。我们刚刚的缩放操作是<def>不均匀(Non-uniform)<def>缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放因子都一样那么就叫<def>均匀缩放</def>(Uniform Scale)。
|
||||
记住,OpenGL通常是在3D空间进行操作的,对于2D的情况我们可以把z轴缩放1倍,这样z轴的值就不变了。我们刚刚的缩放操作是<def>不均匀(Non-uniform)</def>缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放因子都一样那么就叫<def>均匀缩放</def>(Uniform Scale)。
|
||||
|
||||
我们下面会构造一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素会分别与向量的对应元素相乘。如果我们把1变为3会怎样?这样子的话,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3倍。如果我们把缩放变量表示为\((\color{red}{S_1}, \color{green}{S_2}, \color{blue}{S_3})\)我们可以为任意向量\((x,y,z)\)定义一个缩放矩阵:
|
||||
|
||||
|
@@ -3,104 +3,105 @@
|
||||
原文 | [Anti Aliasing](http://learnopengl.com/#!Advanced-OpenGL/Anti-Aliasing)
|
||||
---|---
|
||||
作者 | JoeyDeVries
|
||||
翻译 | [Django](http://bullteacher.com/)
|
||||
校对 | [Geequlim](http://geequlim.com),[KenLee](https://hellokenlee.github.io/)
|
||||
翻译 | Meow J, [Django](http://bullteacher.com/)
|
||||
校对 | 暂未校对
|
||||
|
||||
在你的渲染大冒险中,你可能会遇到模型边缘有锯齿的问题。**锯齿边缘(Jagged Edge)**出现的原因是由顶点数据光栅化成为片段(fragment)的方式所引起的。举个例子,我们随手绘制一个简单的正方体就已经能很清楚地看到锯齿边缘的效果:
|
||||
在学习渲染的旅途中,你可能会时不时遇到模型边缘有锯齿的情况。这些<def>锯齿边缘</def>(Jagged Edges)的产生和光栅器将顶点数据转化为片段的方式有关。在下面的例子中,你可以看到,我们只是绘制了一个简单的立方体,你就能注意到它存在锯齿边缘了:
|
||||
|
||||

|
||||
|
||||
尽管可能不会被立刻察觉到,如果你更近的看看立方体的边,你就会发现锯齿状的图案。如果我们放大就会看到下面的情境:
|
||||
可能不是非常明显,但如果你离近仔细观察立方体的边缘,你就应该能够看到锯齿状的图案。如果放大的话,你会看到下面的图案:
|
||||
|
||||

|
||||
|
||||
这当然不是我们在实际应用中想要的效果。这个效果,我们很明显能看到边缘像素的形态,这种现象被称之为**走样(Aliasing)**。有很多技术能够减少走样现象,产生更平滑的边缘,这些技术叫做**抗锯齿技术**(Anti-aliasing,也被称为反走样技术)。
|
||||
这很明显不是我们想要在最终程序中所实现的效果。你能够清楚看见形成边缘的像素。这种现象被称之为<def>走样</def>(Aliasing)。有很多种抗锯齿(Anti-aliasing,也被称为反走样)的技术能够帮助我们缓解这种现象,从而产生更**平滑**的边缘。
|
||||
|
||||
最开始我们有一个叫做**超采样抗锯齿技术(Super Sample Anti-aliasing, SSAA)**,它暂时使用一个更高的分辨率(以超采样方式)来渲染场景,当输出图像在帧缓冲中被更新时,图像的分辨率会被下采样(down sample)回原来的分辨率。它使用额外的分辨率来防止锯齿边缘。虽然它确实为我们提供了一种解决走样问题的方案,但却由于必须绘制比平时更多的片段而降低了性能。所以这个技术只流行了一段时间。
|
||||
最开始我们有一种叫做<def>超采样抗锯齿</def>(Super Sample Anti-aliasing, SSAA)的技术,它会使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。这些**额外的**分辨率会被用来防止锯齿边缘的产生。虽然它确实能够解决走样的问题,但是由于这样比平时要绘制更多的片段,它也会带来很大的性能开销。所以这项技术只拥有了短暂的辉煌。
|
||||
|
||||
这个技术的基础上诞生了更为现代的技术,叫做**多采样抗锯齿(Multisample Anti-aliasing)**或叫**MSAA**,虽然它借用了SSAA的理念,但却以更加高效的方式实现了它。这节教程我们会展开讨论这个OpenGL内建的MSAA技术。
|
||||
然而,在这项技术的基础上也诞生了更为现代的技术,叫做<def>多重采样抗锯齿</def>(Multisample Anti-aliasing, MSAA)。它借鉴了SSAA背后的理念,但却以更加高效的方式实现了抗锯齿。我们在这一节中会深度讨论OpenGL中内建的MSAA技术。
|
||||
|
||||
## 多重采样
|
||||
|
||||
为了理解什么是多重采样(Multisampling),以及它是如何解决锯齿问题的,我们先要更深入了解一下OpenGL光栅化的工作方式。
|
||||
为了理解什么是多重采样(Multisampling),以及它是如何解决锯齿问题的,我们有必要更加深入地了解OpenGL光栅器的工作方式。
|
||||
|
||||
光栅化表示在我们输出的顶点(Vertices)和片段着色器(fragment shader)中间的所有算法和处理过程的集合(译者注: 在OpenGL中,光栅化步骤在几何着色器后,片段着色器前)。光栅化将属于一个图元的所有顶点转化为一系列片段。顶点坐标理论上可以是任意值,但片段却不是这样,这是因为它们受你的窗口的分辨率限制。几乎永远都不会有顶点坐标和片段的一对一映射,所以光栅化必须以某种方式决定每个顶点最终位于哪个片段/屏幕坐标上。
|
||||
光栅器是位于最终处理过的顶点之后到片段着色器之前所经过的所有的算法与过程的总和。光栅器会将一个图元的所有顶点作为输入,并将它转换为一系列的片段。顶点坐标理论上可以取任意值,但片段不行,因为它们受限于你窗口的分辨率。顶点坐标与片段之间几乎永远也不会有一对一的映射,所以光栅器必须以某种方式来决定每个顶点最终所在的片段/屏幕坐标。
|
||||
|
||||

|
||||
|
||||
这里我们看到一个屏幕像素网格,每个像素中心包含一个采样点(sample point),它被用来决定一个像素是否被三角形所覆盖。红色的采样点表示该点被三角形所覆盖,片段着色器会给该屏幕像素着色。不过即使三角形覆盖了某一个屏幕像素的一部分,但是中心的采样点没被覆盖到,这个像素仍然不会受到片段着色器的影响。
|
||||
这里我们可以看到一个屏幕像素的网格,每个像素的中心包含有一个<def>采样点</def>(Sample Point),它会被用来决定这个三角形是否遮盖了某个像素。图中红色的采样点被三角形所遮盖,在每一个遮住的像素处都会生成一个片段。虽然三角形边缘的一些部分也遮住了某些屏幕像素,但是这些像素的采样点并没有被三角形**内部**所遮盖,所以它们不会受到片段着色器的影响。
|
||||
|
||||
你可能现在已经明白走样的原因是什么了。上述三角形渲染后在你的屏幕上会是这样的:
|
||||
你现在可能已经清楚走样的原因了。完整渲染后的三角形在屏幕上会是这样的:
|
||||
|
||||

|
||||
|
||||
由于屏幕像素总量的限制,有些边上的像素能被渲染出来,而有些则不会。结果就是我们渲染出的图元的边缘产生了上图的锯齿。
|
||||
由于屏幕像素总量的限制,有些边缘的像素能够被渲染出来,而有些则不会。结果就是我们使用了不光滑的边缘来渲染图元,导致之前讨论到的锯齿边缘。
|
||||
|
||||
多采样所做的正是不再使用单一采样点来决定三角形的覆盖范围,而是采用多个采样点(这大概就是它名字的由来)。我们不再使用每个像素中心的采样点,取而代之的是4个子采样(subsample),用它们来决定像素是否被覆盖。这意味着颜色缓冲的大小也由于每个像素的子样本的增加而增加了。
|
||||
多重采样所做的正是将单一的采样点变为多个采样点(这也是它名称的由来)。我们不再使用像素中心的单一采样点,取而代之的是以特定图案排列的4个子采样点(Subsample)。我们将用这些子采样点来决定像素的遮盖度。当然,这也意味着颜色缓冲的大小会随着子采样点的增加而增加。
|
||||
|
||||

|
||||
|
||||
上图的左侧显示了我们普通决定一个三角形是否覆盖屏幕像素的方式(译者注: 中心点,单采样)。这个像素并不会被一个片段着色器着色(因此它保持空白),因为它的采样点没有被三角形所覆盖。右边示了多采样的版本,每个像素包含4个采样点。这里我们可以看到只有2个采样点被三角形覆盖。
|
||||
上图的左侧展示了正常情况下判定三角形是否遮盖的方式。在例子中的这个像素上不会运行片段着色器(所以它会保持空白)。因为它的采样点并未被三角形所覆盖。上图的右侧展示的是实施多重采样之后的版本,每个像素包含有4个采样点。这里,只有两个采样点遮盖住了三角形。
|
||||
|
||||
!!! Important
|
||||
|
||||
采样点的数量是任意的,更多的采样点能带来更精确的覆盖率。
|
||||
采样点的数量可以是任意的,更多的采样点能带来更精确的遮盖率。
|
||||
|
||||
从这儿开始我们的多重采样变得有趣起来了。我们知道了只有2个子采样被三角覆盖,下一步就是决定这个像素的颜色。我们可以猜测一下,我们会为每个被覆盖的子采样运行片段着色器,然后对每个像素的所有子采样的颜色进行平均化。在这种情况下,我们需要为每一个被插值后的顶点数据的每一个子采样运行两次两次片段着色器,然后把采样点的颜色储存起来。幸好,它不是这么运作的,因为这等于说我们必须运行更多的片段着色器,会明显降低性能。
|
||||
从这里开始多重采样就变得有趣起来了。我们知道三角形只遮盖了2个子采样点,所以下一步是决定这个像素的颜色。你的猜想可能是,我们对每个被遮盖住的子采样点运行一次片段着色器,最后将每个像素所有子采样点的颜色平均一下。在这个例子中,我们需要在两个子采样点上对被插值的顶点数据运行两次片段着色器,并将结果的颜色储存在这些采样点中。(幸运的是)这并**不是**它工作的方式,因为这本质上说还是需要运行更多次的片段着色器,会显著地降低性能。
|
||||
|
||||
MSAA的真正工作方式是,每个像素只运行一次片段着色器,无论该像素有多少子样本被三角形所覆盖。片段着色器接受的顶点数据是被插值到每一个像素的**中心**坐标,而其着色的颜色会被每个被三角形覆盖的子采样储存。一旦一个我们绘制的图元的子采样颜色缓冲被填满了,每个像素对应的所有颜色将会被平均化,这使得每个像素最终有了一个唯一颜色。比如在前面的图片中4个子采样中只有2个被三角形覆盖,像素的颜色事实上是一个均值,该均值由三角形的颜色和其他2个子采样的颜色(aka. 背景色)平均化而成,最后该像素被着色为一种浅蓝色。
|
||||
MSAA真正的工作方式是,无论三角形遮盖了多少个子采样点,(每个图元中)每个像素只运行**一次**片段着色器。片段着色器所使用的顶点数据会插值到每个像素的**中心**,所得到的结果颜色会被储存在每个被遮盖住的子采样点中。当颜色缓冲的子样本被图元的所有颜色填满时,所有的这些颜色将会在每个像素内部平均化。因为上图的4个采样点中只有2个被遮盖住了,这个像素的颜色将会是三角形颜色与其他两个采样点的颜色(在这里是无色)的平均值,最终形成一种淡蓝色。
|
||||
|
||||
结果是,颜色缓冲中所有图元的边变得更加平滑了。让我们看看多重采样对前面的一个三角形来说是怎样做的:
|
||||
这样子做之后,颜色缓冲中所有的图元边缘将会产生一种更平滑的图形。让我们来看看前面三角形的多重采样会是什么样子:
|
||||
|
||||

|
||||
|
||||
这里每个像素包含着4个子采样(不相关采样点的没有被标注出来)蓝色的子采样是被三角形覆盖了的,灰色的则没有被覆盖。三角形内部区域中的所有像素都会运行一次片段着色器,它输出的颜色由4个子采样决定。三角形的边缘并不是所有的子采样都会被覆盖,所以片段着色器的结果仅由部分的子采样决定。根据被覆盖子采样的数量,最终的像素颜色由三角形颜色和其他子采样所储存的颜色所决定。(译者注: 其实有点类似于Blending的原理。)
|
||||
这里,每个像素包含4个子采样点(不相关的采样点都没有标注),蓝色的采样点被三角形所遮盖,而灰色的则没有。对于三角形的内部的像素,片段着色器只会运行一次,颜色输出会被存储到全部的4个子样本中。而在三角形的边缘,并不是所有的子采样点都被遮盖,所以片段着色器的结果将只会储存到部分的子样本中。根据被遮盖的子样本的数量,最终的像素颜色将由三角形的颜色与其它子样本中所储存的颜色来决定。
|
||||
|
||||
大致上来说,如果更多的采样点被覆盖,那么像素的颜色就会更接近于三角形。如果我们用这种方去给我们前面的三角形的填充像素颜色,我们会获得这样的结果:
|
||||
简单来说,一个像素中如果有更多的采样点被三角形遮盖,那么这个像素的颜色就会更接近于三角形的颜色。如果我们给上面的三角形填充颜色,就能得到以下的效果:
|
||||
|
||||

|
||||
|
||||
对于每个像素来说,被三角形覆盖的子采样越少,像素受到三角形的颜色的影响也越少。现在原本三角形不平滑的边被比实际颜色浅一些的颜色像素所包围,因此观察者从远处看上去就比较平滑了。
|
||||
对于每个像素来说,越少的子采样点被三角形所覆盖,那么它受到三角形的影响就越小。三角形的不平滑边缘被稍浅的颜色所包围后,从远处观察时就会显得更加平滑了。
|
||||
|
||||
不仅颜色值会被多重采样技术影响,深度测试和模板测试也同样使用了多重采样技术。比如深度测试,顶点的深度值在运行深度测试前被插值到每个子采样中,对于模板测试,我们为每个子采样储存模板值,而不是每个像素。这意味着深度和模板缓冲的大小随着像素子样本的增加也增加了。
|
||||
不仅仅是颜色值会受到多重采样的影响,深度和模板测试也能够使用多个采样点。对深度测试来说,每个顶点的深度值会在运行深度测试之前被插值到各个子样本中。对模板测试来说,我们对每个子样本,而不是每个像素,存储一个模板值。当然,这也意味着深度和模板缓冲的大小会乘以子采样点的个数。
|
||||
|
||||
到目前为止我们所讨论的不过是多重采样技术的工作原理。光栅化背后实际的逻辑要比我们讨论的复杂,但你现在可以理解MSAA背后的概念和逻辑了。
|
||||
(译者注: 如果看到这里还是对原理似懂非懂,可以简单看看知乎上[@文刀秋二](https://www.zhihu.com/people/edliu/answers) 对抗锯齿技术的[精彩介绍](https://www.zhihu.com/question/20236638/answer/14438218))
|
||||
我们到目前为止讨论的都是多重采样抗锯齿的背后原理,光栅器背后的实际逻辑比目前讨论的要复杂,但你现在应该已经可以理解多重采样抗锯齿的大体概念和逻辑了。
|
||||
|
||||
(译者注: 如果看到这里还是对原理似懂非懂,可以简单看看知乎上[@文刀秋二](https://www.zhihu.com/people/edliu/answers)对抗锯齿技术的[精彩介绍](https://www.zhihu.com/question/20236638/answer/14438218))
|
||||
|
||||
|
||||
## OpenGL中的MSAA
|
||||
|
||||
如果我们打算在OpenGL中使用MSAA,那么我们必须使用一个可以为每个像素储存一个以上的颜色值的颜色缓冲(因为多采样需要我们为每个采样点储存一个颜色)。我们这就需要一个新的缓冲类型,它可以储存要求数量的多重采样样本,它叫做**多样本缓冲(Multisample Buffer)**。
|
||||
如果我们想要在OpenGL中使用MSAA,我们必须要使用一个能在每个像素中存储大于1个颜色值的颜色缓冲(因为多重采样需要我们为每个采样点都储存一个颜色)。所以,我们需要一个新的缓冲类型,来存储特定数量的多重采样样本,它叫做<def>多重采样缓冲</def>(Multisample Buffer)。
|
||||
|
||||
多数窗口系统可以为我们提供一个多样本缓冲,以代替默认的颜色缓冲。GLFW同样给了我们这个功能,我们所要作的就是提示GLFW,我们希望使用一个带有N个样本的多样本缓冲,而不是普通的颜色缓冲,这要在创建窗口前调用`glfwWindowHint`来完成:
|
||||
大多数的窗口系统都应该提供了一个多重采样缓冲,用以代替默认的颜色缓冲。GLFW同样给了我们这个功能,我们所要做的只是**提示**(Hint) GLFW,我们希望使用一个包含N个样本的多重采样缓冲。这可以在创建窗口之前调用<fun>glfwWindowHint</fun>来完成。
|
||||
|
||||
```c++
|
||||
glfwWindowHint(GLFW_SAMPLES, 4);
|
||||
```
|
||||
|
||||
当我们现在调用`glfwCreateWindow`,用于渲染的窗口就被创建了,这次每个屏幕坐标使用一个包含4个子样本的颜色缓冲。这意味着所有缓冲的大小都增长4倍。
|
||||
现在再调用<fun>glfwCreateWindow</fun>创建渲染窗口时,每个屏幕坐标就会使用一个包含4个子采样点的颜色缓冲了。GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
|
||||
|
||||
现在我们请求GLFW提供了多样本缓冲,我们还要调用`glEnable`来开启多采样,参数是 `GL_MULTISAMPLE`。大多数OpenGL驱动,多采样默认是开启的,所以这个调用有点多余,但通常记得开启它是个好主意。这样所有OpenGL实现的多采样都开启了。
|
||||
现在我们已经向GLFW请求了多重采样缓冲,我们还需要调用<fun>glEnable</fun>并启用<var>GL_MULTISAMPLE</var>,来启用多重采样。在大多数OpenGL的驱动上,多重采样都是默认启用的,所以这个调用可能会有点多余,但显式地调用一下会更保险一点。这样子不论是什么OpenGL的实现都能够正常启用多重采样了。
|
||||
|
||||
```c++
|
||||
glEnable(GL_MULTISAMPLE);
|
||||
```
|
||||
|
||||
当默认帧缓冲有了多采样缓冲附件的时候,我们所要做的全部就是调用 `glEnable`开启多采样。因为实际的多采样算法在OpenGL驱动光栅化里已经实现了,所以我们无需再做什么了。如果我们现在来渲染教程开头的那个绿色立方体,我们会看到边缘变得平滑了:
|
||||
只要默认的帧缓冲有了多重采样缓冲的附件,我们所要做的只是调用<fun>glEnable</fun>来启用多重采样。因为多重采样的算法都在OpenGL驱动的光栅器中实现了,我们不需要再多做什么。如果现在再来渲染本节一开始的那个绿色的立方体,我们应该能看到更平滑的边缘:
|
||||
|
||||

|
||||
|
||||
这个箱子看起来平滑多了,在场景中绘制任何物体都可以利用这个技术。可以[从这里找到](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_multisampling)这个简单的例子。
|
||||
这个箱子看起来的确要平滑多了,如果在场景中有其它的物体,它们也会看起来平滑很多。你可以在[这里](https://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_multisampling)找到这个简单例子的源代码。
|
||||
|
||||
## 离屏MSAA
|
||||
|
||||
因为GLFW负责创建多采样缓冲,开启MSAA非常简单。如果我们打算使用我们自己的帧缓冲,来进行离屏渲染,那么我们就必须自己生成多采样缓冲了;现在我们需要自己负责创建多采样缓冲。
|
||||
由于GLFW负责了创建多重采样缓冲,启用MSAA非常简单。然而,如果我们想要使用我们自己的帧缓冲来进行离屏渲染,那么我们就必须要自己动手生成多重采样缓冲了。
|
||||
|
||||
有两种方式可以创建多采样缓冲,并使其成为帧缓冲的附件:纹理附件和渲染缓冲附件,和[帧缓冲教程](05 Framebuffers.md)里讨论过的普通的附件很相似。
|
||||
有两种方式可以创建多重采样缓冲,将其作为帧缓冲的附件:纹理附件和渲染缓冲附件,这和在[帧缓冲](05 Framebuffers.md)教程中所讨论的普通附件很相似。
|
||||
|
||||
### 多采样纹理附件
|
||||
### 多重采样纹理附件
|
||||
|
||||
为了创建一个支持储存多采样点的纹理,我们使用 `glTexImage2DMultisample`来替代 `glTexImage2D`,它的纹理目标是**`GL_TEXTURE_2D_MULTISAMPLE`**:
|
||||
为了创建一个支持储存多个采样点的纹理,我们使用<fun>glTexImage2DMultisample</fun>来替代<fun>glTexImage2D</fun>,它的纹理目标是<var>GL_TEXTURE_2D_MULTISAPLE</var>。
|
||||
|
||||
```c++
|
||||
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
|
||||
@@ -108,33 +109,33 @@ glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, heigh
|
||||
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
|
||||
```
|
||||
|
||||
第二个参数现在设置了我们打算让纹理拥有的样本数。如果最后一个参数等于 **`GL_TRUE`**,图像上的每一个纹理像素(texel)将会使用相同的样本位置,以及同样的子样本数量。
|
||||
它的第二个参数设置的是纹理所拥有的样本个数。如果最后一个参数为<var>GL_TRUE</var>,图像将会对每个纹素使用相同的样本位置以及相同数量的子采样点个数。
|
||||
|
||||
为将多采样纹理附加到帧缓冲上,我们使用`glFramebufferTexture2D`,不过这次纹理类型是**`GL_TEXTURE_2D_MULTISAMPLE`**:
|
||||
我们使用<fun>glFramebufferTexture2D</fun>将多重采样纹理附加到帧缓冲上,但这里纹理类型使用的是<var>GL_TEXTURE_2D_MULTISAMPLE</var>。
|
||||
|
||||
```c++
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
|
||||
```
|
||||
|
||||
当前绑定的帧缓冲现在有了一个纹理图像形式的多采样颜色缓冲。
|
||||
当前绑定的帧缓冲现在就有了一个纹理图像形式的多重采样颜色缓冲。
|
||||
|
||||
### 多采样渲染缓冲对象
|
||||
### 多重采样渲染缓冲对象
|
||||
|
||||
和纹理一样,创建一个多采样渲染缓冲对象(Multisampled Renderbuffer Objects)不难。而且还很简单,因为我们所要做的全部就是当我们指定渲染缓冲的内存的时候将`glRenderbuffeStorage`改为`glRenderbufferStorageMuiltisample`:
|
||||
和纹理类似,创建一个多重采样渲染缓冲对象并不难。我们所要做的只是在指定(当前绑定的)渲染缓冲的内存存储时,将<fun>glRenderbufferStorage</fun>的调用改为<fun>glRenderbufferStorageMultisample</fun>就可以了。
|
||||
|
||||
```c++
|
||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
|
||||
```
|
||||
|
||||
有一样东西在这里有变化,就是缓冲目标后面那个额外的参数,我们将其设置为样本数量,当前的例子中应该是4.
|
||||
函数中,渲染缓冲对象后的参数我们将设定为样本的数量,在当前的例子中是4。
|
||||
|
||||
### 渲染到多采样帧缓冲
|
||||
### 渲染到多重采样帧缓冲
|
||||
|
||||
渲染到多采样帧缓冲对象是自动的。当我们绘制任何东西时,帧缓冲对象就绑定了,光栅化会对负责所有多采样操作。我们接着得到了一个多采样颜色缓冲,以及深度和模板缓冲。因为多采样缓冲有点特别,我们不能为其他操作直接使用它们的缓冲图像,比如在着色器中进行采样。
|
||||
渲染到多重采样帧缓冲对象的过程都是自动的。只要我们在帧缓冲绑定时绘制任何东西,光栅器就会负责所有的多重采样运算。我们最终会得到一个多重采样颜色缓冲以及/或深度和模板缓冲。因为多重采样缓冲有一点特别,我们不能直接将它们的缓冲图像用于其他运算,比如在着色器中对它们进行采样。
|
||||
|
||||
一个多采样图像包含了比普通图像更多的信息,所以我们需要做的是压缩或还原图像。还原一个多采样帧缓冲,通常用`glBlitFramebuffer`来完成,它从一个帧缓冲中复制一个区域粘贴另一个里面,同时也将任何多采样缓冲还原。
|
||||
一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者<def>还原</def>(Resolve)图像。多重采样帧缓冲的还原通常是通过<fun>glBlitFramebuffer</fun>来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原。
|
||||
|
||||
`glBlitFramebuffer`把一个4屏幕坐标源区域传递到一个也是4空间坐标的目标区域。你可能还记得帧缓冲教程中,如果我们绑定到`GL_FRAMEBUFFER`,我们实际上就同时绑定到了读和写的帧缓冲目标。我们还可以通过`GL_READ_FRAMEBUFFER`和`GL_DRAW_FRAMEBUFFER`绑定到各自的目标上。`glBlitFramebuffer`函数从这两个目标读取,并决定哪一个是源哪一个是目标帧缓冲。接着我们就可以通过把图像位块传送(Blitting)到默认帧缓冲里,将多采样帧缓冲输出传递到实际的屏幕了:
|
||||
<fun>glBlitFramebuffer</fun>会将一个用4个屏幕空间坐标所定义的<def>源</def>区域复制到一个同样用4个屏幕空间坐标所定义的<def>目标</def>区域中。你可能记得在[帧缓冲](05 Framebuffers.md)教程中,当我们绑定到<var>GL_FRAMEBUFFER</var>时,我们是同时绑定了读取和绘制的帧缓冲目标。我们也可以将帧缓冲分开绑定至<var>GL_READ_FRAMEBUFFER</var>与<var>GL_DRAW_FRAMEBUFFER</var>。<fun>glBlitFramebuffer</fun>函数会根据这两个目标,决定哪个是源帧缓冲,哪个是目标帧缓冲。接下来,我们可以将图像<def>位块传送</def>(Blit)到默认的帧缓冲中,将多重采样的帧缓冲传送到屏幕上。
|
||||
|
||||
```c++
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
|
||||
@@ -142,67 +143,65 @@ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
```
|
||||
|
||||
如果我们渲染应用,我们将得到和没用帧缓冲一样的结果:一个绿色立方体,它使用MSAA显示出来,但边缘锯齿明显少了:
|
||||
如果现在再来渲染这个程序,我们会得到与之前完全一样的结果:一个使用MSAA显示出来的橄榄绿色的立方体,而且锯齿边缘明显减少了:
|
||||
|
||||

|
||||
|
||||
你可以[在这里找到源代码](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_framebuffers)。
|
||||
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/11.anti_aliasing_offscreen/anti_aliasing_offscreen.cpp)找到源代码。
|
||||
|
||||
但是如果我们打算使用一个多采样帧缓冲的纹理结果来做这件事,就像后处理一样会怎样?我们不能在片段着色器中直接使用多采样纹理。我们可以做的事情是把多缓冲位块传送(Blit)到另一个带有非多采样纹理附件的FBO中。之后我们使用这个普通的颜色附件纹理进行后处理,通过多采样来对一个图像渲染进行后处理效率很高。这意味着我们必须生成一个新的FBO,它仅作为一个将多采样缓冲还原为一个我们可以在片段着色器中使用的普通2D纹理中介。伪代码是这样的:
|
||||
但如果我们想要使用多重采样帧缓冲的纹理输出来做像是后期处理这样的事情呢?我们不能直接在片段着色器中使用多重采样的纹理。但我们能做的是将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中。然后用这个普通的颜色附件来做后期处理,从而达到我们的目的。然而,这也意味着我们需要生成一个新的FBO,作为中介帧缓冲对象,将多重采样缓冲还原为一个能在着色器中使用的普通2D纹理。这个过程的伪代码是这样的:
|
||||
|
||||
```c++
|
||||
GLuint msFBO = CreateFBOWithMultiSampledAttachments();
|
||||
// Then create another FBO with a normal texture color attachment
|
||||
unsigned int msFBO = CreateFBOWithMultiSampledAttachments();
|
||||
// 使用普通的纹理颜色附件创建一个新的FBO
|
||||
...
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
|
||||
...
|
||||
while(!glfwWindowShouldClose(window))
|
||||
{
|
||||
...
|
||||
|
||||
|
||||
glBindFramebuffer(msFBO);
|
||||
ClearFrameBuffer();
|
||||
DrawScene();
|
||||
// Now resolve multisampled buffer(s) into intermediate FBO
|
||||
// 将多重采样缓冲还原到中介FBO上
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
// Now scene is stored as 2D texture image, so use that image for post-processing
|
||||
// 现在场景是一个2D纹理缓冲,可以将这个图像用来后期处理
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
ClearFramebuffer();
|
||||
glBindTexture(GL_TEXTURE_2D, screenTexture);
|
||||
DrawPostProcessingQuad();
|
||||
|
||||
...
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
如果我们实现帧缓冲教程中讲的后处理代码,我们就能创造出没有锯齿边的所有效果很酷的后处理特效。使用模糊kernel过滤器,看起来会像这样:
|
||||
如果现在再实现[帧缓冲](05 Framebuffers.md)教程中的后期处理效果,我们就能够在一个几乎没有锯齿的场景纹理上进行后期处理了。如果施加模糊的核滤镜,看起来将会是这样:
|
||||
|
||||

|
||||
|
||||
你可以[在这里找到所有MSAA版本的后处理源码](http://learnopengl.com/code_viewer.php?code=advanced/anti_aliasing_post_processing)。
|
||||
|
||||
!!! Important
|
||||
|
||||
因为屏幕纹理重新变回了只有一个采样点的普通纹理,有些后处理过滤器,比如边检测(edge-detection)将会再次导致锯齿边问题。为了修正此问题,之后你应该对纹理进行模糊处理,或者创建你自己的抗锯齿算法。
|
||||
因为屏幕纹理又变回了一个只有单一采样点的普通纹理,像是**边缘检测**这样的后期处理滤镜会重新导致锯齿。为了补偿这一问题,你可以之后对纹理进行模糊处理,或者想出你自己的抗锯齿算法。
|
||||
|
||||
当我们希望将多采样和离屏渲染结合起来时,我们需要自己负责一些细节。所有细节都是值得付出这些额外努力的,因为多采样可以明显提升场景视频输出的质量。要注意,开启多采样会明显降低性能,样本越多越明显。本文写作时,MSAA4样本很常用。
|
||||
你可以看到,如果将多重采样与离屏渲染结合起来,我们需要自己负责一些额外的细节。但所有的这些细节都是值得额外的努力的,因为多重采样能够显著提升场景的视觉质量。当然,要注意,如果使用的采样点非常多,启用多重采样会显著降低程序的性能。在本节写作时,通常采用的是4采样点的MSAA。
|
||||
|
||||
## 自定义抗锯齿算法
|
||||
|
||||
可以直接把一个多采样纹理图像传递到着色器中,以取代必须先还原的方式。GLSL给我们一个选项来为每个子样本进行纹理图像采样,所以我们可以创建自己的抗锯齿算法,在比较大的图形应用中,通常这么做。
|
||||
将一个多重采样的纹理图像不进行还原直接传入着色器也是可行的。GLSL提供了这样的选项,让我们能够对纹理图像的每个子样本进行采样,所以我们可以创建我们自己的抗锯齿算法。在大型的图形应用中通常都会这么做。
|
||||
|
||||
为获取每个子样本的颜色值,你必须将纹理uniform采样器定义为sampler2DMS,而不是使用sampler2D:
|
||||
要想获取每个子样本的颜色值,你需要将纹理uniform采样器设置为<fun>sampler2DMS</fun>,而不是平常使用的<fun>sampler2D</fun>:
|
||||
|
||||
```c++
|
||||
uniform sampler2DMS screenTextureMS;
|
||||
```
|
||||
|
||||
使用texelFetch函数,就可以获取每个样本的颜色值了:
|
||||
使用<fun>texelFetch</fun>函数就能够获取每个子样本的颜色值了:
|
||||
|
||||
```c++
|
||||
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3); // 4th subsample
|
||||
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3); // 第4个子样本
|
||||
```
|
||||
|
||||
我们不会深究自定义抗锯齿技术的创建细节,但是会给你自己去实现它提供一些提示。
|
||||
我们不会深入探究自定义抗锯齿技术的细节,这里仅仅是给你一点启发。
|
Reference in New Issue
Block a user