OpenGL学习笔记31-Anti Aliasing

Anti Aliasing 反混叠

Advanced-OpenGL/Anti-Aliasing

在您冒险的渲染过程中,您可能会在模型的边缘遇到一些锯齿状的图案。这些锯齿状边缘出现的原因是由于光栅化器将顶点数据转换为场景背后的实际片段。当绘制一个简单的立方体时,这些锯齿状的边缘看起来就像一个例子:

虽然不能立即看到,但如果你仔细观察立方体的边缘,你会看到锯齿状的图案。如果我们放大,你会看到以下内容:

这显然不是我们想要的应用程序的最终版本。这种清晰地看到由边缘组成的像素构成的效果称为混叠。有相当多的技术被称为反锯齿技术,通过产生平滑的边缘来对抗这种锯齿行为。

首先,我们有一个技术称为超级样本反混叠(SSAA),临时使用一个高分辨率的渲染缓冲区渲染场景(超级采样)。然后,当整个场景被渲染,分辨率下降采样回到正常的分辨率。这个额外的分辨率是用来防止锯齿状的边缘。虽然它确实为我们提供了一个解决混叠问题的方案,但它带来了一个主要的性能缺陷,因为我们必须绘制比通常多得多的片段。因此,这项技术只有短暂的辉煌时刻。

这种技术确实产生了一种更现代的技术,称为多重反混叠或MSAA,它借鉴了SSAA背后的概念,同时实现了一种更有效的方法。在本章中,我们将广泛讨论OpenGL中内置的MSAA技术。

Multisampling 多重采样

为了理解多采样是什么以及它是如何解决混叠问题的,我们首先需要深入研究一下OpenGL光栅化器的内部工作原理。

光栅化器是所有算法和进程的组合,它位于你最终处理的顶点和片段着色器之间。光栅化器获取属于单个原语的所有顶点,并将其转换为一组片段。顶点坐标理论上可以有任何坐标,但是片段不能,因为它们受到屏幕分辨率的限制。顶点坐标和片段之间几乎不会有一对一的映射,所以光栅化器必须以某种方式确定片段/屏幕坐标每个特定的顶点最终会在哪里。

这里我们看到一个屏幕像素网格,其中每个像素的中心包含一个样本点,用于确定一个像素是否被三角形覆盖。红色的采样点被三角形覆盖,并为覆盖的像素生成一个片段。即使三角形边缘的某些部分仍然进入某些屏幕像素,像素的样本点没有被三角形的内部覆盖,所以这个像素不会受到任何碎片着色器的影响。

你现在可能已经知道混叠的起源了。这个三角形的完整渲染版本在你的屏幕上看起来是这样的:

由于屏幕像素的数量有限,一些像素会沿着边缘渲染,而一些不会。结果是我们渲染了非光滑边缘的原语,产生了我们之前看到的锯齿状边缘。

多采样所做的,不是使用单个采样点来确定三角形的覆盖率,而是使用多个采样点(猜猜它的名字是从哪里来的)。我们将在一个通用模式中放置4个子样本,而不是在每个像素的中心放置一个单一的样本点,并使用它们来确定像素的覆盖率。

图像的左边显示了我们通常如何确定三角形的覆盖范围。这个特定的像素不会运行一个片段着色器(因此保持空白),因为它的样本点没有被三角形覆盖。图像的右边显示了一个多采样版本,其中每个像素包含4个采样点。在这里,我们可以看到只有两个采样点覆盖三角形。

样本点的数量可以是我们想要的任意数目,更多的样本可以给我们更好的覆盖精度。

这就是多采样变得有趣的地方。我们确定三角形覆盖了2个子样本,所以下一步是确定这个特定像素的颜色。我们最初的猜测是我们为每个被覆盖的子样本运行片段着色器,然后平均每个子样本的每个像素的颜色。在这个例子中,我们在每个子样本的插值顶点数据上运行了两次片段着色器,并将结果颜色存储在这些样本点上。幸运的是,这并不是它工作的方式,因为这将意味着我们需要运行更多的片段着色器,而不是没有多次采样,这将大大降低性能。

MSAA如何真正工作是片段着色器只运行一次每像素(为每个原语)不管多少子样本的三角形覆盖;片段着色器运行顶点数据插值到像素的中心。然后,MSAA使用更大的深度/模板缓冲区来确定子样本覆盖率。所覆盖的子样本的数量决定了像素颜色对framebuffer的贡献。因为4个样本中只有2个在前面的图像中被覆盖,所以三角形的一半颜色与framebuffer颜色(在本例中是clear颜色)混合在一起,从而形成浅蓝色。

结果是一个更高分辨率的缓冲区(具有更高分辨率的深度/模板),其中所有原始边缘现在产生一个更平滑的模式。让我们看看当我们确定早期三角形的覆盖范围时,多重采样是什么样子的:

这里每个像素包含4个子样本(不相关的样本被隐藏),其中蓝色的子样本被三角形覆盖,而灰色的样本点没有。在三角形的内部区域中,所有像素都将运行fragment shader,一旦它的颜色输出被直接存储在framebuffer中(假设没有混合)。在三角形的内部边缘,然而,并不是所有的子样本将被覆盖,所以碎片着色器的结果将不会完全贡献给framebuffer。基于被覆盖样本的数量,三角形碎片的颜色或多或少会在那个像素处结束。

对于每个像素,三角形的子样本越少,它获取的三角形颜色就越少。如果我们要填充实际的像素颜色,我们得到以下图像:

三角形的硬边现在被比实际边缘颜色稍浅的颜色包围,这使得边缘从远处看时显得平滑。

深度和模板值存储在每个子样例中,即使我们只运行片段着色器一次,颜色值存储在每个子样例以及多个三角形重叠单个像素的情况下。对于深度测试,在运行深度测试之前将顶点的深度值插入到每个子样本中,对于模板测试,我们存储每个子样本的模板值。这意味着缓冲区的大小现在增加了每个像素的子样本数量。

到目前为止,我们所讨论的是一个基本的概述,多采样反走样是如何在幕后工作的。光栅化器背后的实际逻辑有点复杂,但这个简短的描述应该足以理解多层反走样背后的概念和逻辑;足够深入到实际的方面。

MSAA in OpenGL

如果我们想在OpenGL中使用MSAA,我们需要使用一个缓冲区,该缓冲区能够存储每个像素的一个以上的采样值。我们需要一种新的类型的缓冲区,可以存储给定数量的多字节,这被称为多字节缓冲区。

大多数窗口系统能够为我们提供一个多窗口缓冲区,而不是一个默认缓冲区。GLFW也给了我们这个功能,我们所需要做的就是提示GLFW,我们想要使用一个有N个样本的多级缓冲区,而不是一个普通的缓冲区,在创建窗口之前调用glfwWindowHint:


glfwWindowHint(GLFW_SAMPLES, 4);

当我们现在调用glfwCreateWindow时,我们创建了一个呈现窗口,但是这次使用了一个包含每个屏幕坐标4个子样本的缓冲区。这意味着缓冲区的大小增加了4。

既然我们向GLFW请求了多采样缓冲区,那么我们需要通过使用GL_MULTISAMPLE调用glEnable来启用多采样。在大多数OpenGL驱动程序中,多级采样是默认启用的,所以这个调用有点多余,但通常启用它是一个好主意。这样,所有的OpenGL实现都启用了多级采样。


glEnable(GL_MULTISAMPLE);  

因为实际的多采样算法是在OpenGL驱动程序的光栅化器中实现的,所以我们不需要做太多其他的事情。如果我们在本章开始的时候渲染绿色立方体,我们会看到更平滑的边缘:

立方体确实看起来更平滑了,同样的效果也适用于你在场景中绘制的其他物体。您可以在这里here. 找到这个简单示例的源代码。

Off-screen MSAA

因为GLFW负责创建多采样缓冲区,所以启用MSAA非常容易。但是,如果我们想使用自己的framebuffer,我们必须自己生成多数据的buffer;现在我们确实需要创建多数据的缓冲区。

有两种方法可以创建多采样缓冲区来作为framebuffer的附件:纹理附件和renderbuffer附件。与我们在framebuffers一章中讨论的普通附件非常相似。

Multisampled texture attachments

为了创建一个支持存储多个样本点的纹理,我们使用glTexImage2DMultisample而不是glTexImage2D,后者接受GL_TEXTURE_2D_MULTISAPLE作为纹理目标:


glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);  

第二个参数设置了我们希望纹理拥有的样本数量。如果最后一个参数设置为GL_TRUE,图像将为每个texel使用相同的样本位置和相同数量的子样本。

我们使用glFramebufferTexture2D来附加一个多层纹理到framebuffer,但是这次使用GL_TEXTURE_2D_MULTISAMPLE作为纹理类型:


glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0); 

当前绑定的framebuffer现在有一个纹理图像形式的多采样颜色缓冲区。

Multisampled renderbuffer objects

像纹理一样,创建一个多采样的renderbuffer对象并不困难。它甚至很容易,因为我们需要改变的是glRenderbufferStorage到glRenderbufferStorageMultisample时,我们配置(当前绑定)renderbuffer的内存存储:


glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);  

这里改变的一件事是额外的秒参数我们设置了我们想要使用的样本数量;在这个特殊的情况下。

Render to multisampled framebuffer

呈现到多数据帧缓冲区是很简单的。每当我们在绑定framebuffer对象时绘制任何内容时,光栅化程序就会处理所有的多帧操作。然而,因为多采样缓冲区有点特殊,我们不能直接使用缓冲区进行其他操作,比如在着色器中采样。

多采样图像比普通图像包含更多的信息,所以我们需要做的是缩小或解析图像。解析一个多采样的framebuffer通常是通过glBlitFramebuffer来完成的,它将一个区域从一个framebuffer复制到另一个framebuffer,同时也解析任何多采样的缓冲区。

glBlitFramebuffer将由4个屏幕空间坐标定义的给定源区域传输到同样由4个屏幕空间坐标定义的给定目标区域。您可能还记得,在framebuffers一章中,如果我们绑定到GL_FRAMEBUFFER,就同时绑定到读取和绘制framebuffer目标。我们还可以通过将framebuffer分别绑定到GL_READ_FRAMEBUFFER和GL_DRAW_FRAMEBUFFER来单独绑定到这些目标。glBlitFramebuffer函数从这两个目标读取数据,以确定哪个是源framebuffer,哪个是目标framebuffer。然后,我们可以将多采样的framebuffer输出传输到实际屏幕,方法是将图像blitting的默认framebuffer,如下所示:


glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); 

如果我们渲染相同的应用程序,我们应该得到相同的输出:一个用MSAA显示的青绿色立方体,并且再次显示明显较少的锯齿边:

您可以在这里here. 找到源代码。

但是,如果我们想要使用多采样帧缓冲区的纹理结果来做诸如后处理之类的事情呢?我们不能直接在碎片着色器中使用多层叠纹理。然而,我们可以做的是用一个非多采样纹理附件将多采样缓冲区blit到一个不同的FBO。然后我们使用这种普通的彩色附着纹理进行后处理,有效的后处理通过多采样渲染的图像。这意味着我们必须生成一个新的FBO,它仅仅作为一个中间framebuffer对象来解析多采样的缓冲区;我们可以在碎片着色器中使用一个普通的2D纹理。这个过程看起来有点像这个伪代码:


unsigned int msFBO = CreateFBOWithMultiSampledAttachments();
// then create another FBO with a normal texture color attachment
[...]
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
    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
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    ClearFramebuffer();
    glBindTexture(GL_TEXTURE_2D, screenTexture);
    DrawPostProcessingQuad();  
  
    [...] 
}

如果我们在framebuffers一章的后期处理代码中实现这一点,我们就能够在(几乎)没有锯齿边缘的场景的纹理上创建各种各样很酷的后期处理效果。用一个灰度后处理滤镜,它看起来像这样:

因为屏幕纹理还是一个普通的(非多层次化)纹理,一些后处理过滤器(如边缘检测)将再次引入锯齿状边缘。为了适应这一点,你可以事后模糊纹理或创建自己的抗锯齿算法。

您可以看到,当我们想要结合多采样和屏幕外渲染时,我们需要注意一些额外的步骤。这些步骤值得付出额外的努力,因为多次采样可以显著提高场景的视觉质量。请注意,使用的示例越多,启用多采样就会显著降低性能。

Custom Anti-Aliasing algorithm

有可能直接传递一个多层叠纹理图像到一个片段着色器,而不是首先解决它。GLSL为我们提供了每个子样本采样纹理图像的选项,这样我们就可以创建自己的自定义抗锯齿算法。

为了获得每个子样本的纹理值,你必须将纹理均匀采样器定义为sampler2DMS而不是通常的sampler2D:


uniform sampler2DMS screenTextureMS;    

使用texelFetch函数可以检索每个样本的颜色值:


vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 4th subsample

我们不会在这里详细介绍创建自定义反锯齿技术,但这可能已经足够开始自己构建一个了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值