实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_anti-alias-test
锯齿边缘的产生和光栅器将顶点数据转化为片段的方式有关。
有多种抗锯齿技术,能够缓解锯齿。
一、超采样抗锯齿(SSAA)
Super Sample Anti-aliasing,SSAA。
它会使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率。但这会带来很大的性能开销,已经不适合现代需求。
二、多重采样抗锯齿
Multisample Anti-aliasing,MSAA。
先学习一下OpenGL光栅器(Raster)的工作方式,光栅器是以一个图元的所有顶点作为输入,并转换为一系列的片段。顶点坐标可以是任意值,但片段不行,它受限于窗口的有限分辨率。顶点和坐标之间几乎不是一一对应的映射,光栅器是以某种方式将无限的顶点坐标映射成有限的片段/屏幕坐标。
1、单采样点
每个像素的中心有一个采样点(Sample Point),如果一个像素的采样点被三角形覆盖,则表示该像素被三角形覆盖,会在该像素位置生成一个三角形颜色的片段。如果三角形覆盖了像素的一部分但是没有覆盖到中心的采样点,则表示该像素未被三角形覆盖,不会在对应像素生成片段。 这就是锯齿形成的原因,按照如上采样图,三角形光栅化后输出结果如下:
单采样点简单说就是判断像素要么被覆盖,要么没被覆盖。
2、多采样点
多重采样,显然就是将单一的采样点变为多个采样点(多重采样叫法的原因)。假设是4个采样点,我们通过这4个采样点来决定像素的遮盖度,覆盖的采样点越多,片段颜色越接近三角形的颜色。
上图就是单采样点和多采样点的差别。采样点的数量可以是任意的,更多采样点可以更精准表达覆盖率。
注意,多采样点的像素依然只会运行一次片段着色器。片段着色器所使用的顶点数据会插值到每个像素的中心,得到的结果颜色会被储存在每个被遮盖的子采样点中,没有被覆盖的采样点则为原本颜色缓冲的颜色。将这四个采样点的颜色值平均一下就得到了这个片段的最终颜色。如下图:
在三角形内部(完全被覆盖)的像素,片段着色器只运行一次,颜色输出存储到全部的4个采样点中,边缘的像素,则颜色输出只会保存到被覆盖的采样点中。
不仅颜色值有多重采样,深度值和模板测试也可以多采样点。原理类似。这就是多重采样的基本原理,光栅器的实际逻辑远比这复杂。
三、OpenGL中的MSAA
默认的颜色缓冲,一个像素只会保存一个颜色值,多重采样则要求保存多于1个颜色值,因此我们需要新的缓冲类型来保存采样点,这个缓冲叫做多重采样缓冲(Multisample Buffer)。GLFW库的开启默认缓冲的多重采样抗锯齿方法如下:
glfwInit(); // 初始化GLFW
......
glfwWindowHint(GLFW_SAMPLES, 4); // 告诉OpenGL,期望一个4采样点的缓冲
......
glEnable(GL_MULTISAMPLE); // 开启多重采样
......
1、离屏MSAA
上述是开启默认缓冲的多重采样,如果我们需要创建自己的帧缓冲进行离屏渲染时,则需要手动创建多重采样缓冲绑定为帧缓冲的附件(纹理附件或者渲染缓冲附件)。
2、纹理类型帧缓冲附件
下面是创建多重采样纹理附件示例。
// 颜色附件
GLuint textureMS;
glGenTextures( 1, &textureMS );
glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, textureMS); //多重采样纹理类型
glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, // glTexImage2DMultisample方法,多重采样纹理类型
4, // 采样点数
GL_RGB, // internalformat
ScreenWidth, ScreenHeight, // 纹理像素宽高
GL_TRUE ); // 采用相同的采样点位置和数目
glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, 0 ); // 回复默认绑定
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, // 绑定到帧缓冲颜色附件
GL_TEXTURE_2D_MULTISAMPLE, // 多重采样纹理类型
textureMS, // 纹理ID
0 ); // mipmap level
glTexImage2DMultisample
// available only if the GL version is 3.2 or greater.
void glTexImage2DMultisample(GLenum target,
GLsizei samples,
GLint internalformat,
GLsizei width,
GLsizei height,
GLboolean fixedsamplelocations);
// 创建多重采样纹理:开辟内存,指定格式、维度、和采样点数量。
// target: 指定操作目标,可能值如下:
// GL_TEXTURE_2D_MULTISAMPLE
// GL_PROXY_TEXTURE_2D_MULTISAMPLE
// samples: 采样点数量,取值范围[0, GL_MAX_SAMPLES - 1]
// interformat: 内部存储格式
// width: 宽度,取值范围[0, GL_MAX_TEXTURE_SIZE - 1]
// height: 高度
// 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、渲染缓冲帧缓冲附件
下面是创建多重采样渲染缓冲附件示例:
// 渲染缓冲附件,深度和模板缓冲常用渲染缓冲类型,一般都不会去采样读取数据
GLuint depthStencilAttachment;
glGenRenderbuffers( 1, &depthStencilAttachment );
glBindRenderbuffer( GL_RENDERBUFFER, depthStencilAttachment );
glRenderbufferStorageMultisample( GL_RENDERBUFFER, // glRenderbufferStorageMultisample方法
4, // 采样点数
GL_DEPTH24_STENCIL8, // 内部格式
ScreenWidth, ScreenHeight ); // 像素宽高
glBindRenderbuffer( GL_RENDERBUFFER, 0 );
glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthStencilAttachment );
glRenderbufferStorageMultisample
void glRenderbufferStorageMultisample(GLenum target,
GLsizei samples,
GLenum internalformat,
GLsizei width,
GLsizei height);
// multisample版本的glRenderbufferStorage。
target: // 必须是GL_RENDERBUFFER
samples: // 采样点数量,取值范围[0, GL_MAX_SAMPLES],
internalformat: // 内部格式
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、多重采样帧缓缓冲例子
将上面的两种附件整合起来:
GLuint fbo_ms;
glGenFramebuffers( 1, &fbo_ms );
glBindFramebuffer( GL_FRAMEBUFFER, fbo_ms );
// 颜色附件(纹理附件类型)
GLuint textureMS;
glGenTextures( 1, &textureMS );
glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, //多重采样纹理类型
textureMS );
glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, // glTexImage2DMultisample方法,多重采样纹理类型
4, // 采样点数
GL_RGB, // internalformat
ScreenWidth, ScreenHeight, // 纹理像素宽高
GL_TRUE ); // 采用相同的采样点位置和数目
glBindTexture( GL_TEXTURE_2D_MULTISAMPLE, 0 ); // 回复默认绑定
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, // 绑定到帧缓冲颜色附件
GL_TEXTURE_2D_MULTISAMPLE, // 多重采样纹理类型
textureMS, // 纹理ID
0 ); // mipmap level
// 深度与模板附件(渲染缓冲类型)
GLuint depthStencilAttachment;
glGenRenderbuffers( 1, &depthStencilAttachment );
glBindRenderbuffer( GL_RENDERBUFFER, depthStencilAttachment ); // 绑定为当前渲染缓冲
glRenderbufferStorageMultisample( GL_RENDERBUFFER, // glRenderbufferStorageMultisample方法
4, // 采样点数
GL_DEPTH24_STENCIL8, // 内部格式
ScreenWidth, ScreenHeight ); // 像素宽高
glBindRenderbuffer( GL_RENDERBUFFER, 0 ); // 恢复为默认
glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthStencilAttachment );
if( glCheckFramebufferStatus( GL_FRAMEBUFFER ) != GL_FRAMEBUFFER_COMPLETE ) // 检查完整性
{
cout << "ERROR::FRAMEBUFFER::FRAMEBUFFER is not complete1.";
}
glBindFramebuffer( GL_FRAMEBUFFER, 0 );
5、渲染多重采样纹理
前面我们提到,多重采样的图像不能用标准采样函数来采样。我们可以通过glBlitFramebuffer来将多重采样缓冲缩小或还原(Resolve)为普通2D纹理数据。方法如下:
// 将multisampledFBO中的多重采样图像还原到普通帧缓冲中
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glBlitFramebuffer(0, 0, width, height, // 从read framebuffer拷贝的区域
0, 0, width, height, // 拷贝到draw framebuufer的哪个区域
GL_COLOR_BUFFER_BIT, // 拷贝那个缓冲(颜色、深度、模板),这里只拷贝颜色缓冲
GL_NEAREST); // 两个区域的size不相同时,采样方法为GL_NEAREST
// 获取fbo的颜色附件即可获得普通2D纹理,这样我们即可对有抗锯齿的图像调用片段着色器进行逐片段处理(post process 后期处理)
glBiltFramebuffer
void glBlitFramebuffer(GLint srcX0, // Specify the bounds of the source rectangle within
GLint srcY0, // the read buffer of the read framebuffer.
GLint srcX1,
GLint srcY1,
GLint dstX0, // Specify the bounds of the destination rectangle within the
GLint dstY0, // write buffer of the write framebuffer.
GLint dstX1,
GLint dstY1,
GLbitfield mask,
GLenum filter);
// 从read framebuffer拷贝一个像素块到write framebuffer
// srcX0, srcY0, srcX1, srcY1: 源区域(read framebuffer),具体是[(srcX0, srcY0), (srcX1, srcY1)),左闭右开区间
// dstX0, dstY0, dstX1, dstY1: 目标区域(write framebuffer),具体是[(dstX0, dstY0), (dstX1, dstY1)),左闭右开区间
// mask: 决定拷贝哪个缓冲数据,位或运算,可能值如下:
// GL_COLOR_BUFFER_BIT
// GL_DEPTH_BUFFER_BIT
// GL_STENCIL_BUFFER_BIT
如GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT,表示拷贝颜色和深度缓冲。
// filter: 指定源和目的的size不相等时,图像被延展的插值方法。可能值为GL_NEAREST或者GL_LINEAR。
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.
四、自定义抗锯齿算法
我们可以使用特定的采样函数,在片段着色器中对多重采样纹理数据的每个子样本进行采样,这样我们就可以实现自己的抗锯齿算法。
#version 330 core
in vec2 texCoords; // 纹理坐标
uniform sampler2DMS textureMS; // 多重采样图像的多重采样器
void main()
{
vec4 colorSample = texelFetch(textureMS,
texCoords,
3); // 采样第4个采样点
}