OpenGL基础43:抗锯齿

一、走样与反走样

走样(Aliasing)就是锯齿化,反走样(Anti-aliasing)就是抗锯齿

只要玩过游戏,那么都应该对抗锯齿不陌生,不少游戏也都有关于抗锯齿的设置

如上图,放大的部分能很明显的看到“锯齿”边,如果了解光栅化的过程,那么也很容易理解锯齿是怎样产生的,这不是什么底层的BUG,正是完全正确的流程会出现这中“锯齿”现象,本质原因是场景的定义在三维空间中是连续的,而最终显示的像素则却是一个离散的二维数组,所以在判断一个点到底没有被某个像素覆盖的时候不应该单纯是一个“有”或者“没有"问题,也因此,抗锯齿一定是采用优化手段而无法根治

二、采样

(参考于learnopengl.com

光栅化将属于一个基本图形的所有顶点转化为一系列片段,顶点坐标理论上可以含有任何坐标,但片段却不是这样,因为它与你的窗口的解析度有关,几乎永远都不会有顶点坐标和片段的一对一映射,所以光栅化必须以某种方式决定每个特定顶点最终结束于哪个片段/屏幕坐标上

每个像素中心会包含一个采样点(sample point),它被用来决定一个像素是否被三角形所覆盖,红色的采样点如果在三角形内部,那么就会为这个被覆盖像素生成一个片段,否则就算三角形覆盖了部分屏幕像素,只要采样点没被覆盖这个像素就不会被处理

如果你的顶点组成的线段刚好与屏幕平行,那么这个时候就会好很多,但事实上只要出现斜边,就必然出现上图的情况

为了解决,一个很容易想到的方法是:每个像素不再只有中心一个采样点,而是设置多个采样点,假设最终有x%个采样点被覆盖,那么这个像素的颜色就按照对应比例x%进行平均化

三、MSAA

上面所说的方法正是多重采样抗锯齿(MultiSampling Anti-Aliasing),也是最常用的抗锯齿算法,使用n个采样点意味着需要n倍的颜色缓冲区空间,但是对于这n个采样点,仍然只需要执行一次着色器,着色器使用的顶点数据会通过插值锁定在像素的中间,然后在计算最终颜色的时候乘上覆盖率,若执行多次着色器,会很显著的降低性能

上图就是MSAA优化后的效果,其实采样点数量是可以任意指定的,不过如果你的分辨率过低又或者采样点过少,就会产生一个新的问题:边缘模糊

好在是,这些东西GPU已经帮我们做了,如果是在openGL中进行最简单的应用,只需要添加2行代码就ok:

  • glfwWindowHint(GLFW_SAMPLES, n):提示GLFW,希望使用一个带有n个样本的多样本缓冲(Multisample Buffer)以代替普通的颜色缓冲,需要在创建窗口前完成
  • glEnable(GL_MULTISAMPLE):开启多采样,一般是默认开启

再看看效果,应该就是没问题了

四、多采样缓冲

前置:OpenGL基础33:帧缓冲之离屏渲染

很可惜,如果用了自己的帧缓冲,那么仅用上面的2行代码就不可以了,GLFW并不会对你自己创建的缓冲负责,在这种情况下就需要自己生成多采样缓冲以实现MSAA

可以参考前置章节,用同样的方法(纹理附件和渲染缓冲附件)创建多采样缓冲,并使其成为帧缓冲的附件,主要逻辑是在中间插入一个新的FBO,专门用于用于处理抗锯齿,之后再转入之前用于显示的FBO中进行我们想要的后处理:

  • glTexImage2DMultisample:创建一个支持储存多采样点的纹理,第2个参数现在设置了纹理拥有的样本数,如果最后一个参数等于GL_TRUE,图像上的每一个纹理像素(texel)都会使用相同的样本位置,以及同样的子样本数量
  • glRenderbufferStorageMultisample:和之前的glRenderbufferStorage方法一样,缓冲目标后面多了一个参数,为样本数量
  • GL_TEXTURE_2D_MULTISAMPLE:多采样目标纹理,之前都是GL_TEXTURE_2D
GLuint FBO, RBO;
glGenFramebuffers(1, &FBO);
glBindFramebuffer(GL_FRAMEBUFFER, FBO);

GLuint textureColorBufferMultiSampled = getMultiSampleTexture(4);           //MSAA 4x抗锯齿
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled, 0);

glGenRenderbuffers(1, &RBO);
glBindRenderbuffer(GL_RENDERBUFFER, RBO);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, WIDTH, HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, RBO);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

GLuint screenFBO;
glGenFramebuffers(1, &screenFBO);
glBindFramebuffer(GL_FRAMEBUFFER, screenFBO);
GLuint textureColorBuffer = getAttachmentTexture();
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorBuffer, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    cout << "ERROR::FRAMEBUFFER:: Intermediate framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

GLuint getMultiSampleTexture(GLuint samples)
{
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
    glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, WIDTH, HEIGHT, GL_TRUE);
    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
    return texture;
}

渲染到多采样帧缓冲对象是自动的,只要我们在帧缓冲绑定时绘制任何东西,光栅器就会负责所有的多重采样运算。我们最终会得到一个多重采样颜色缓冲以及/或深度和模板缓冲,不过多采样缓冲有点特别,我们不能为其他操作直接使用它们的缓冲图像,比如在着色器中进行采样

当然我们其实根本不需要采样图象,也不需要拿多采样缓冲来做什么,只要底层帮我们解决,那么剩下的只需要还原图像就好,也就是我们要将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中,然后用这个普通的颜色附件来做后期处理,从而达到我们实际的目的:

  • glBlitFramebuffer:把一个4屏幕坐标源区域传送(Blitting)到一个也是4空间坐标的目标区域,其中源缓冲为GL_READ_FRAMEBUFFER绑定的目标,目标缓冲为GL_DRAW_FRAMEBUFFER绑定的缓冲,前4个参数为指定读帧缓冲区的矩形范围,第5-8个参数为指定写帧缓冲区的矩形范围,第9个参数为指定要读取的缓冲区,第10个参数为指定伸缩变形时的插值方法
glBindFramebuffer(GL_READ_FRAMEBUFFER, FBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, screenFBO);
glBlitFramebuffer(0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
        
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
shaderScreen.Use();
glBindVertexArray(quadVAO);
glBindTexture(GL_TEXTURE_2D, textureColorBuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);

这样就可以顺利的使用之前所说的kernel过滤器了

五、扩展

其它抗锯齿技术:

上面详细介绍了MSAA,也就是多采样抗锯齿,事实上,抗锯齿方法还有很多,效率和效果也都不太一样:

  • SSAA:超级采样抗锯齿,使用比正常分辨率更高的分辨率来渲染场景,当图像输出在帧缓冲中更新时,分辨率再被下采样(Downsample)至正常的分辨率,性能巨耗,但是效果最好(理论最完美解决方法)
  • FXAA:快速近似抗锯齿,相对于MSAA速度更快、显存占用更低,但是效果也更差,可以配合锐化使用
  • CSAA:覆盖采样抗锯齿,原理是将边缘多边形里需要采样的子像素坐标覆盖掉,将原像素坐标强制安置在硬件和驱动程序预告算好的坐标中,这就好比采样标准统一的MSAA,能够最高效率地执行边缘采样,当然现在很多时候已经不再支持CSAA了,也逐渐冷门
  • FSAA:全屏抗锯齿,SSAA的特殊考虑

自定义抗锯齿算法:

因为屏幕纹理重新变回了只有一个采样点的普通纹理,有些后处理过滤器,比如边检测(edge-detection)将会再次导致锯齿边问题,为了修正此问题,往往要对纹理进行模糊处理,又或者创建自己的抗锯齿算法。将一个多重采样的纹理图像不进行还原直接传入着色器也是可行的,GLSL提供了这样的选项,以让我们能够对纹理图像的每个子样本进行采样

要想获取每个子样本的颜色值,需要将纹理uniform采样器设置为sampler2DMS,而不是平常使用的sampler2D:

  • texelFetch:获取每个子样本的颜色值,注意样本标签从0开始
uniform sampler2DMS screenTextureMS;
void main()
{
    vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);
}

猜你喜欢

转载自blog.csdn.net/Jaihk662/article/details/107821098