实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_anti-alias-test
锯齿边缘的产生和光栅器将顶点数据转化为片段的方式有关。
anti_aliasing_zoomed.png
有多种抗锯齿技术,能够缓解锯齿。

一、超采样抗锯齿(SSAA)

Super Sample Anti-aliasing,SSAA。
它会使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。但这会带来很大的性能开销,已经不适合现代需求。

二、多重采样抗锯齿

Multisample Anti-aliasing,MSAA。
先学习一下OpenGL光栅器(Raster)的工作方式,光栅器是以一个图元的所有顶点作为输入,并转换为一系列的片段。顶点坐标可以是任意值,但片段不行,它受限于窗口的有限分辨率。顶点和坐标之间几乎不是一一对应的映射,光栅器是以某种方式将无限的顶点坐标映射成有限的片段/屏幕坐标。

1、单采样点


anti_aliasing_rasterization.png
每个像素的中心有一个采样点(Sample Point),如果一个像素的采样点被三角形覆盖,则表示该像素被三角形覆盖,会在该像素位置生成一个三角形颜色的片段。如果三角形覆盖了像素的一部分但是没有覆盖到中心的采样点,则表示该像素未被三角形覆盖,不会在对应像素生成片段。 这就是锯齿形成的原因,按照如上采样图,三角形光栅化后输出结果如下:
anti_aliasing_rasterization_filled.png
单采样点简单说就是判断像素要么被覆盖,要么没被覆盖。

2、多采样点

多重采样,显然就是将单一的采样点变为多个采样点(多重采样叫法的原因)。假设是4个采样点,我们通过这4个采样点来决定像素的遮盖度,覆盖的采样点越多,片段颜色越接近三角形的颜色。
anti_aliasing_sample_points.png
上图就是单采样点和多采样点的差别。采样点的数量可以是任意的,更多采样点可以更精准表达覆盖率。
注意,多采样点的像素依然只会运行一次片段着色器。片段着色器所使用的顶点数据会插值到每个像素的中心,得到的结果颜色会被储存在每个被遮盖的子采样点中,没有被覆盖的采样点则为原本颜色缓冲的颜色。将这四个采样点的颜色值平均一下就得到了这个片段的最终颜色。如下图:
anti_aliasing_rasterization_samples.pnganti_aliasing_rasterization_samples_filled.png
在三角形内部(完全被覆盖)的像素,片段着色器只运行一次,颜色输出存储到全部的4个采样点中,边缘的像素,则颜色输出只会保存到被覆盖的采样点中。
不仅颜色值有多重采样,深度值和模板测试也可以多采样点。原理类似。这就是多重采样的基本原理,光栅器的实际逻辑远比这复杂。

三、OpenGL中的MSAA

默认的颜色缓冲,一个像素只会保存一个颜色值,多重采样则要求保存多于1个颜色值,因此我们需要新的缓冲类型来保存采样点,这个缓冲叫做多重采样缓冲(Multisample Buffer)。GLFW库的开启默认缓冲的多重采样抗锯齿方法如下:

  1. glfwInit(); // 初始化GLFW
  2. ......
  3. glfwWindowHint(GLFW_SAMPLES, 4); // 告诉OpenGL,期望一个4采样点的缓冲
  4. ......
  5. glEnable(GL_MULTISAMPLE); // 开启多重采样
  6. ......

1、离屏MSAA

上述是开启默认缓冲的多重采样,如果我们需要创建自己的帧缓冲进行离屏渲染时,则需要手动创建多重采样缓冲绑定为帧缓冲的附件(纹理附件或者渲染缓冲附件)。

2、纹理类型帧缓冲附件

下面是创建多重采样纹理附件示例。

  1. // 颜色附件
  2. GLuint textureMS;
  3. glGenTextures( 1, &textureMS );
  4. glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, textureMS); //多重采样纹理类型
  5. glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, // glTexImage2DMultisample方法,多重采样纹理类型
  6. 4, // 采样点数
  7. GL_RGB, // internalformat
  8. ScreenWidth, ScreenHeight, // 纹理像素宽高
  9. GL_TRUE ); // 采用相同的采样点位置和数目
  10. glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, 0 ); // 回复默认绑定
  11. glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, // 绑定到帧缓冲颜色附件
  12. GL_TEXTURE_2D_MULTISAMPLE, // 多重采样纹理类型
  13. textureMS, // 纹理ID
  14. 0 ); // mipmap level

glTexImage2DMultisample

  1. // available only if the GL version is 3.2 or greater.
  2. void glTexImage2DMultisample(GLenum target,
  3. GLsizei samples,
  4. GLint internalformat,
  5. GLsizei width,
  6. GLsizei height,
  7. GLboolean fixedsamplelocations);
  8. // 创建多重采样纹理:开辟内存,指定格式、维度、和采样点数量。
  9. // target: 指定操作目标,可能值如下:
  10. // GL_TEXTURE_2D_MULTISAMPLE
  11. // GL_PROXY_TEXTURE_2D_MULTISAMPLE
  12. // samples: 采样点数量,取值范围[0, GL_MAX_SAMPLES - 1]
  13. // interformat: 内部存储格式
  14. // width: 宽度,取值范围[0, GL_MAX_TEXTURE_SIZE - 1]
  15. // height: 高度
  16. // fixedsamplelocations : 所有纹理像素是否采样相同的采样位置和采样数量。

当在着色器中访问多样本纹理时,该访问将使用一个vector<整数>(描述要提取哪个纹理像素)和一个整数(对应于样本编号)来描述要提取的纹理像素中的哪个样本。 多样本纹理不同于普通纹理,不能在片段着色器使用标准采样函数。
GL_INVALID_OPERATION is generated if internalformat is a depth- or stencil-renderable format and samples is greater than the value of GL_MAX_DEPTH_TEXTURE_SAMPLES.
GL_INVALID_OPERATION is generated if internalformat is a color-renderable format and samples is greater than the value of GL_MAX_COLOR_TEXTURE_SAMPLES.
GL_INVALID_OPERATION is generated if internalformat is a signed or unsigned integer format and samples is greater than the value of GL_MAX_INTEGER_SAMPLES.
GL_INVALID_VALUE is generated if either width or height negative or is greater than GL_MAX_TEXTURE_SIZE.
GL_INVALID_VALUE is generated if samples is greater than GL_MAX_SAMPLES.

3、渲染缓冲帧缓冲附件

下面是创建多重采样渲染缓冲附件示例:

  1. // 渲染缓冲附件,深度和模板缓冲常用渲染缓冲类型,一般都不会去采样读取数据
  2. GLuint depthStencilAttachment;
  3. glGenRenderbuffers( 1, &depthStencilAttachment );
  4. glBindRenderbuffer( GL_RENDERBUFFER, depthStencilAttachment );
  5. glRenderbufferStorageMultisample( GL_RENDERBUFFER, // glRenderbufferStorageMultisample方法
  6. 4, // 采样点数
  7. GL_DEPTH24_STENCIL8, // 内部格式
  8. ScreenWidth, ScreenHeight ); // 像素宽高
  9. glBindRenderbuffer( GL_RENDERBUFFER, 0 );
  10. glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthStencilAttachment );

glRenderbufferStorageMultisample

  1. void glRenderbufferStorageMultisample(GLenum target,
  2. GLsizei samples,
  3. GLenum internalformat,
  4. GLsizei width,
  5. GLsizei height);
  6. // multisample版本的glRenderbufferStorage。
  7. target: // 必须是GL_RENDERBUFFER
  8. samples: // 采样点数量,取值范围[0, GL_MAX_SAMPLES],
  9. internalformat: // 内部格式
  10. width,height: // 缓冲宽高,像素单位,取值范围[0, GL_MAX_RENDERBUFFER_SIZE]

如果internalformat 是一个整型,则samples的取值范围[0, GL_MAX_INTEGER_SAMPLES]。
glRenderbufferStorageMultisample调用成功之后,any existing data store for the renderbuffer image and the contents of the data store will be deleted。
GL_INVALID_ENUM is generated if target is not GL_RENDERBUFFER.
GL_INVALID_VALUE is generated if samples is greater than GL_MAX_SAMPLES.
GL_INVALID_ENUM is generated if internalformat is not a color-renderable, depth-renderable, or stencil-renderable format.
GL_INVALID_OPERATION is generated if internalformat is a signed or unsigned integer format and samples is greater than the value of GL_MAX_INTEGER_SAMPLES
GL_INVALID_VALUE is generated if either of width or height is negative, or greater than the value of GL_MAX_RENDERBUFFER_SIZE.
GL_OUT_OF_MEMORY is generated if the GL is unable to create a data store of the requested size.

4、多重采样帧缓缓冲例子

将上面的两种附件整合起来:

  1. GLuint fbo_ms;
  2. glGenFramebuffers( 1, &fbo_ms );
  3. glBindFramebuffer( GL_FRAMEBUFFER, fbo_ms );
  4. // 颜色附件(纹理附件类型)
  5. GLuint textureMS;
  6. glGenTextures( 1, &textureMS );
  7. glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, //多重采样纹理类型
  8. textureMS );
  9. glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, // glTexImage2DMultisample方法,多重采样纹理类型
  10. 4, // 采样点数
  11. GL_RGB, // internalformat
  12. ScreenWidth, ScreenHeight, // 纹理像素宽高
  13. GL_TRUE ); // 采用相同的采样点位置和数目
  14. glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, 0 ); // 回复默认绑定
  15. glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, // 绑定到帧缓冲颜色附件
  16. GL_TEXTURE_2D_MULTISAMPLE, // 多重采样纹理类型
  17. textureMS, // 纹理ID
  18. 0 ); // mipmap level
  19. // 深度与模板附件(渲染缓冲类型)
  20. GLuint depthStencilAttachment;
  21. glGenRenderbuffers( 1, &depthStencilAttachment );
  22. glBindRenderbuffer( GL_RENDERBUFFER, depthStencilAttachment ); // 绑定为当前渲染缓冲
  23. glRenderbufferStorageMultisample( GL_RENDERBUFFER, // glRenderbufferStorageMultisample方法
  24. 4, // 采样点数
  25. GL_DEPTH24_STENCIL8, // 内部格式
  26. ScreenWidth, ScreenHeight ); // 像素宽高
  27. glBindRenderbuffer( GL_RENDERBUFFER, 0 ); // 恢复为默认
  28. glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthStencilAttachment );
  29. if( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE ) // 检查完整性
  30. {
  31. cout << "ERROR::FRAMEBUFFER::FRAMEBUFFER is not complete1.";
  32. }
  33. glBindFramebuffer( GL_FRAMEBUFFER, 0 );

5、渲染多重采样纹理

前面我们提到,多重采样的图像不能用标准采样函数来采样。我们可以通过glBlitFramebuffer来将多重采样缓冲缩小或还原(Resolve)为普通2D纹理数据。方法如下:

  1. // 将multisampledFBO中的多重采样图像还原到普通帧缓冲中
  2. glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
  3. glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
  4. glBlitFramebuffer(0, 0, width, height, // 从read framebuffer拷贝的区域
  5. 0, 0, width, height, // 拷贝到draw framebuufer的哪个区域
  6. GL_COLOR_BUFFER_BIT, // 拷贝那个缓冲(颜色、深度、模板),这里只拷贝颜色缓冲
  7. GL_NEAREST); // 两个区域的size不相同时,采样方法为GL_NEAREST
  8. // 获取fbo的颜色附件即可获得普通2D纹理,这样我们即可对有抗锯齿的图像调用片段着色器进行逐片段处理(post process 后期处理)

glBiltFramebuffer

  1. void glBlitFramebuffer(GLint srcX0, // Specify the bounds of the source rectangle within
  2. GLint srcY0, // the read buffer of the read framebuffer.
  3. GLint srcX1,
  4. GLint srcY1,
  5. GLint dstX0, // Specify the bounds of the destination rectangle within the
  6. GLint dstY0, // write buffer of the write framebuffer.
  7. GLint dstX1,
  8. GLint dstY1,
  9. GLbitfield mask,
  10. GLenum filter);
  11. // 从read framebuffer拷贝一个像素块到write framebuffer
  12. // srcX0, srcY0, srcX1, srcY1: 源区域(read framebuffer),具体是[(srcX0, srcY0), (srcX1, srcY1)),左闭右开区间
  13. // dstX0, dstY0, dstX1, dstY1: 目标区域(write framebuffer),具体是[(dstX0, dstY0), (dstX1, dstY1)),左闭右开区间
  14. // mask: 决定拷贝哪个缓冲数据,位或运算,可能值如下:
  15. // GL_COLOR_BUFFER_BIT
  16. // GL_DEPTH_BUFFER_BIT
  17. // GL_STENCIL_BUFFER_BIT
  18. GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT,表示拷贝颜色和深度缓冲。
  19. // filter: 指定源和目的的size不相等时,图像被延展的插值方法。可能值为GL_NEAREST或者GL_LINEAR。
  20. GL_LINEAR仅适用于COLOR_BUFFER

当filter = GL_LINEAR,mask又包含了GL_DEPTH_BUFFER_BIT或者GL_STENCIL_BUFFER_BIT时,将不会拷贝数据,并报错GL_INVALID_OPERATION。
当filter = GL_LINEAR,如果需要采样源的边界之外的地方,将采样GL_CLAMP_TO_EDGE的环绕方式(warpping)。
当是拷贝COLOR_BUFFER时,将拷贝到目标帧缓冲的每个draw buffer中。
如果指定的源和目的区域有覆盖或者相同,且源和目的是同一个帧缓冲,行为将未定义。
GL_INVALID_OPERATION is generated if mask contains any of the GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT and filter is not GL_NEAREST.
GL_INVALID_OPERATION is generated if mask contains GL_COLOR_BUFFER_BIT and any of the following conditions hold:

  • The read buffer contains fixed-point or floating-point values and any draw buffer contains neither fixed-point nor floating-point values.
  • The read buffer contains unsigned integer values and any draw buffer does not contain unsigned integer values.
  • The read buffer contains signed integer values and any draw buffer does not contain signed integer values.

GL_INVALID_OPERATION is generated if mask contains GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT and the source and destination depth and stencil formats do not match.
GL_INVALID_OPERATION is generated if filter is GL_LINEAR and the read buffer contains integer data.
GL_INVALID_OPERATION is generated if the value of GL_SAMPLES for the read and draw buffers is not identical.
GL_INVALID_OPERATION is generated if GL_SAMPLE_BUFFERS for both read and draw buffers greater than zero and the dimensions of the source and destination rectangles is not identical.
GL_INVALID_FRAMEBUFFER_OPERATION is generated if the objects bound to GL_DRAW_FRAMEBUFFER_BINDING or GL_READ_FRAMEBUFFER_BINDING are not framebuffer complete.

四、自定义抗锯齿算法

我们可以使用特定的采样函数,在片段着色器中对多重采样纹理数据的每个子样本进行采样,这样我们就可以实现自己的抗锯齿算法。

  1. #version 330 core
  2. in vec2 texCoords; // 纹理坐标
  3. uniform sampler2DMS textureMS; // 多重采样图像的多重采样器
  4. void main()
  5. {
  6. vec4 colorSample = texelFetch(textureMS,
  7. texCoords,
  8. 3); // 采样第4个采样点
  9. }