计算机图像学(OPENGL):抗锯齿

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

抗锯齿

  在进行一些渲染时,我们可能会得到边缘锯齿感的模型。造成锯齿感的原因就是渲染管线实现的原理。其中有一步是将顶点数据通过光栅化转化为实际的片元,而这就是可能的锯齿感的来源。下面是一个边缘有锯齿感的渲染结果:



  如果将镜头拉近的话,结果会更加明显:



  很明显,这不是我们想要的渲染结果,所以我们需要抗锯齿的技术来平滑边缘。
  首先,有一种称为超级采样抗锯齿(SSAA)的技术,它在渲染时会暂时使用更高分辨率的渲染缓冲(超级采样),在全部场景渲染完成后,分辨率便会降为正常的分辨率,这种额外的分辨率被用来抗锯齿。虽然使用高分辨率渲染可以帮助我们抗锯齿,但这也意味着我们需要绘制更多的片段,所以这种技术现在并不怎么使用。

  上述的SSAA技术催生了一种更先进的技术,称为多重采样抗锯齿(MSAA),概念源自SSAA,但使用了更有效率的方式。

多重采样

  为了了解多重采样的概念,我们首先介绍OpenGL光栅化的原理。
  光栅化是一系列算法和处理的结合,位于最终的顶点处理和片元着色器阶段之间。光栅化将所有属于一个基本体的顶点变换为一系列片段。顶点坐标理论上可以随意制定,但由于片段与屏幕分辨率所关联,所以不能随意决定坐标。在顶点坐标和片段之间,几乎不会有一一对应的映射关系,所以光栅化必须通过某些方法来决定每个顶点会被映射到哪个片段上。



  比如上面的图片,在屏幕像素构成的网格中,每个像素中间有一个采样点,这个采样点用来决定这个像素是否用来构成三角形。上面的红色采样点被三角形包围,那么一个片段会根据对应的像素而生成。即使三角形的某些边穿过的像素,但只要采样点不在三角形内,那么对应的像素就不会参与构成三角形的片段,也就不会被片元着色器影响。
  那么最终屏幕上的三角形会是这样:



  由于屏幕像素数量的限制,一些像素会沿着三角形的边绘制,但有一些不会,这样也就会得到我们所提到的拥有锯齿感的结果。
  那么多重采样做的就是不像上面那样通过一次采样结果就决定三角形覆盖的像素范围,而是进行多次采样点的采样。我们不再只为每个像素分配一个中心采样点,而是为它们分配多个采样点(更多的采样点意味着更精确)。

  上面左边的图是使用单次采样的结果,由于采样点不在三角形内,对应的像素不会包含在三角形的片段中。而右边我们使用多重采样,每个像素包含4个采样点,我们可以看到其中2个采样点在三角形内。
  我们判断有两个采样点在三角形内,接下来就是决定这个像素的颜色。我们最开始的猜想可能是为每个被覆盖的采样点使用片元着色器计算颜色,接着平均每个像素的所有采样点得到最终颜色。这样的话,针对上面的例子,我们就运行了两次片元着色器,并将颜色存储在这些采样点中。但这种猜想并不是多重采样的工作原理,毕竟这么做会损耗大量的性能。
  MSAA的工作原理是:片元着色器只会在每个像素上运行一次,无论三角形覆盖了多少采样点。片元着色器使用的顶点数据来自于像素的中心。接着MSAA会使用比一般情况下更大的深度或模板缓冲来决定采样点的覆盖。采样点的覆盖数量决定了每个像素会向帧缓冲贡献的颜色权重。上面的例子中,只有2个采样点被三角形覆盖,那么对应像素的颜色值会贡献一部分给帧缓冲的颜色,结果就是比较浅的蓝色。
  最终结果就是一个缓冲(高分辨率的深度或模板缓冲),其中所有的基本体的边缘都将变得平滑。下面是使用多重采样来决定三角形的覆盖像素的例子:



  上图中,每个像素包含4个采样点,蓝色的采样点被三角形覆盖,灰色的采样点不被覆盖。所有有采样点被覆盖的对应的像素就会通过运行一次片元着色器将输出的颜色直接存储在帧缓冲中(这里假设没有混合)。位于边缘的像素的采样点不一定全部覆盖在三角形内,那么这些像素只会贡献部分的颜色。
  对于每个像素,属于三角形的采样点越少,用来构成三角形的颜色就越淡(可以这么说,只是程度的大小),采样结果如下:

  现在三角形的边缘被一些浅色的像素所柔和,可以说这个三角形的锯齿感消弱了很多。
  颜色值存储在每个像素中,但深度值和模板值存储在每个采样点中。在深度测试前,顶点的深度值会被插值到每个采样点中;而对于米板测试,我们将模板值存储在采样点中。这也就意味着,由于采样点数量的增加,深度和模板缓冲的大小也成倍增加。

在OpenGL中实现MSAA

  如果想要使用MSAA,我们需要使用能够存储多于1个采样值的缓冲,即多重采样缓冲。
  大多数的窗口系统都提供了一个多重采样缓冲,我们可以通过下面的方法来设置每个像素的采样点的数量:

glfwWindowHint(GLFW_SAMPLES, 4);

  接着我们创建窗口的话,屏幕的每个像素都会包含4个采样点,即缓冲的大小增长为4。
  接着我们告知OpenGL我们要使用多重采样,我们是使用glEnable(GL_MULTISAMPLE)开启。在大多数的OpenGL驱动中,多重采样是默认开启的,所以我们也可以不使用这行命令:

glEnable(GL_MULTISAMPLE);  

  接下来的步骤和之前的渲染没有区别,就这么运行程序我们会得到边缘很柔和的结果:


  这里给出原文参考代码:Code。

离屏MSAA

  因为GLFW为我们自动创建了多重采样缓冲,所以MSAA变得非常简单,但如果我们想要使用自己的帧缓冲的话,我们还是得自己创建多重采样缓冲。就像在帧缓冲那张讲的那样,我们有两种方式创建多重采样缓冲来作为附加项附加到帧缓冲上:纹理附加项和渲染缓冲附加项。

多重采样纹理附加项

  我们使用glTexImage2DMultiSample来创建存储多重采样点的纹理,同时纹理类型改为GL_TEXTURE_2D_MULTISAMPLE:

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

  glTexImage2DMultiSample的第二个参数供我们自定义采样点的数量。如果最后一个参数设为GL_TRUE的话,在纹理中将会使用统一的采样位置,且每个像素的采样点的数量一致。
  接着我们将纹理附加到帧缓冲上,纹理类型为GL_TEXTURE_2D_MULTISAMPLE:

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

  那么当前的帧缓冲就用用多重采样颜色缓冲了,形式为一张纹理图片。

多重采样渲染缓冲对象

  创建多重采样渲染缓冲对象没什么区别,只是我们这次需要使用glRenderbufferStorageMultisample代替glRenderbufferStorage来配置渲染缓冲的内存:

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

  这个方法额外的数字参数是每个像素采样点的数量。

渲染到多重采样帧缓冲

  一张多重采样纹理包含的信息远多于普通的纹理图片,所以我们需要缩小纹理或处理图片。我们通常使用glBlitFramebuffers来处理多重采样帧缓冲,做法是将纹理的一片区域从一个帧缓冲复制到另一个帧缓冲。
  glBlitFramebuffers的源区域和目标区域均由4个屏幕空间坐标定义。在帧缓冲一节提到过,如果我们绑定帧缓冲对象至GL_FRAMEBUFFER,我们同时绑定了读和绘制缓冲区,我们也可以分别绑定至GL_READ_FRAMEBUFFER和GL_DRAW_FRAMEBUFFER。glBlitFramebuffer从这两个缓冲区读取数据并决定谁是源帧缓冲,谁是目标帧缓冲。我们希望将多重采样帧缓冲的某一区域信息传递到默认的帧缓冲,我们可以这么做:

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); 

  如果就这么运行程序会得到与之前一样的结果,这里给出原文代码参考:Code。
  但如果我们想要使用多重采样帧缓冲生成的纹理来进行一些后期处理呢?我们不能直接在片元着色器中使用这张纹理,我们能做的是将多重采样缓冲位块传输到一个拥有非多重采样纹理附加项的FBO,接着我们使用一般的颜色附加项纹理来进行后期处理。这也就意味着我们需要生成一个新的FBO,这个FBO的作用就是将多重采样缓冲转化为我们可以在片元着色器中使用的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();  
  
    ... 
}

  接着我们在片元着色器中进行灰度处理就可以得到这样的结果:



  但接着又存在一个问题,由于我们将多重采样缓冲转化为一张普通的屏幕纹理,一些类似于边缘检测的后期处理仍会造成边缘锯齿化。为了缓解这一问题,我们需要为纹理添加模糊效果,或者,编写我们自己的抗锯齿算法。

自定义抗锯齿算法

  其实我们可以直接将多重采样纹理放到片元着色器中使用,不需要创建一个新的FBO来作为传递中转站。GLSL提供了为每个采样点采样纹理图片的采样器类型,我们可以据此创建自定义的抗锯齿算法。
  我们需要将纹理的采样器类型替换掉:

uniform sampler2DMS screenTextureMS;    

  使用texelFetch方法可以检索每个采样点的颜色:

vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 第四个采样点

  这里暂时就不赘述了。
  最后,给出原文地址供参考:https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing

你可能感兴趣的:(计算机图像学(OPENGL):抗锯齿)