抗锯齿化(Anti Aliasing)
- 锯齿形边缘(jagged edges) 出现的原因取决于光栅化时如何将顶点数据转换为实际片元。
- 最早,有一种叫做超分辨率抗锯齿化(super sample anti-aliasing, SSAA) 的技术,它使用一个更高分辨率的渲染缓冲区来渲染场景,然后当整个场景渲染完成时再通过降采样将分辨率恢复正常。因为这项技术在性能上有重大缺点,因此也只应用一时。
- 从SSAA技术发展了更现代的抗锯齿化技术:多重采样抗锯齿化(multisample anti-aliasing, MSAA),它借用了SSAA的一些概念但以更高效的方式实现。本章节重点介绍OpenGL内置的MSAA技术。
1. 多重采样
- 想要了解多重采样和它如何解决锯齿化问题,我们需要更深入的了解OpenGL的光栅化程序(rasterizer)。光栅化程序接收单个基元的顶点数据将其转换为片元集合。顶点坐标理论上可以是任何坐标,但是由于显示屏幕分辨率的缘故,片元却不能。因此顶点和片元之间的坐标几乎不可能进行一对一转换,所以光栅化程序需要以某种方式将指定顶点坐标转换为最终的片元/屏幕坐标。见下图:(图片取自书中)
- 如上图所示,在一个像素网格中,每个像素的中心都有一个采样点(sample point),采样点决定了该像素是否包含在三角形中。如果采样点包含在三角形中(图中红色采样点)则为对应的像素生成片元。但是在三角形的边上,虽然三角形经过屏幕像素,可采样点并不包含在三角形中,因此片元着色器并不会影响到该屏幕像素。
- 对于单个采样点,上图中的三角形最终渲染结果大致如下:(图片取自书中)
- 多重采样所做的,就是不使用单个采样点决定基元是否包含某个屏幕像素,而是多个采样点。例如我们以常规方式放置四个子采样(subsamples)来决定是否包含像素。从下图我们可以看出,左侧单个采样点像素未包含在三角形中,因此该像素不会运行片元着色器输出颜色;右侧四个采样点中有两个包含在三角形中,因此像素运行片元着色器产生颜色。(图片取自书中)
- 注意:采样的数量可以是任何数字,更多的采样点能更精确地决定部分包含的像素的渲染结果。
- 在MSAA中,对于每个像素片元着色器只运行一次,不管该像素中多少个子采样点被基元包含。片元着色器使用插值到像素中心的顶点数据运行,然后MSAA使用一个更大的深度/模板缓冲区来决定是否包含子采样点,最后被包含的子采样点的数量决定了将多少三角形的颜色写入到帧缓冲区。例如上面的图中,四个子采样点中有两个被三角形包含,那么像素最终的颜色就是一半三角形的颜色与一半帧缓冲区的颜色的混合。
- 最终的结果就是使用更高分辨率的颜色缓冲区(和更高分辨率的深度/模板缓冲区)来产生光滑的边缘。(图片取自书中)
- 最后三角形边缘的渲染结果大致如下,注意边缘像素的颜色:(图片取自书中)
- 注意:虽然片元着色器每个像素只运行一次,但是颜色值、深度值和模板值则是按子采样点数量存储。
2. OpenGL中的MSAA
- 如果我们想要使用OpenGL的MSAA,我们需要一个能存储每个像素不止一个采样值的缓冲区——一种能存储指定数量采样的缓冲区类型,多重采样缓冲区(multisample buffer)。
- 大部分窗体系统都为我们提供了一个多重采样缓冲区。在GLFW中,我们只需在创建窗体之前调用
glfwWindowHint
函数指示GLFW我们需要N采样的多重采样缓冲区来代替常规缓冲区即可。
glfwWindowHint(GLFW_SAMPLES, 4);
- 在OpenGL中我们则只需调用
glEnable
函数启动多重采用即可(不过OpenGL一般默认启动多重采样)。
glEnable(GL_MULTISAMPLE);
-
下面是单采样和多采样的绿色立方体渲染效果。
3. 离屏MSAA
- 因为GLFW内置了多重采样缓冲区的创建,因此启动多重采样很容易。但是如果我们想使用自己的帧缓冲区,我们必须自己生成多重采样缓冲区。有两种方式可以生成多重采样缓冲区来作为帧缓冲区的附件:纹理附件和渲染缓冲区附件。与我们在帧缓冲区章节讨论的常规附件相似。
3.1 多重采样纹理附件
- 要创建支持存储多个采样点的纹理我们使用
glTexImage2DMultisample
而不是glTexImage2D
。
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
// samples: 采样点数
// 最后一个参数GL_TRUE表示:对纹理像素采用相同的采用位置和相同的子采样数量
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
- 要将多重采样纹理附加到帧缓冲区,我们使用
glFramebufferTexture2D
函数。
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
3.2 多重采样渲染缓冲区对象
- 与纹理相似,创建多重采样渲染缓冲区对象并不困难,我只需将
glRenderbufferStorage
函数更换为glRenderbufferStorageMultisample
,并配置(当前绑定)渲染缓冲区的内存。
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
3.3 渲染到多重采样帧缓冲区
- 因为多重采样帧缓冲区比较特别,对于一些操作如着色器中的采样,我们不能直接使用。解析一个多重采样的帧缓冲区一般使用
glBlitFramebuffer
函数,该函数从一个帧缓冲区中的一个区域将数据拷贝到另外一个缓冲区中,同时对多重采样缓冲区进行了解析。从帧缓冲区章节我们知道有GL_READ_FRAMEBUFFER
和GL_DRAW_FRAMEBUFFER
两个缓冲区目标。glBlitFramebuffer
函数能够从这两个目标读取数据并自动确定哪一个是源哪一个是目标帧缓冲区。因此我们可以通过将图像块传输到默认帧缓冲区来将多重采样帧缓冲区数据输出到屏幕。
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampleFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
- 综上所述,离屏MSAA的渲染循环中的大致过程如下:
// framebuffer:多重采样的帧缓冲区
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
ClearScene();
DrawScene();
// 将多重采样帧缓冲区输出到默认帧缓冲区(屏幕显示的缓冲区)
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
-
渲染效果如下,与直接在窗体系统设置多重采样相似:
- 如果我们想对多重采样帧缓冲区进行后处理,因为我们不能直接使用当前片元着色器中的多重采样纹理,因此我们需要再创建一个非多重采样的中间帧缓冲区。我们先将多重采样帧缓冲区的数据拷贝到中间帧缓冲区,然后对中间帧缓冲区的常规2D纹理进行后处理,再将其显示到屏幕上。大致过程如下:
CreateSceneAndSetData();
CreateMultisampleFramebuffer();
CreateScreenAndSetData();
CreateIntermediaFramebuffer();
SetScreenTexture();
while (!glfwWindowShouldClose(window))
{
BindMultisampleFramebuffer();
DrawScene();
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
// 目标设置为中间帧缓冲区
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
glBlitFramebuffer(0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
DrawScreenTexture();
glfwSwapBuffers(window);
glfwPollEvents();
}
-
一个灰度化的立方体效果。