走样(Aliasing)就是锯齿化,反走样(Anti-aliasing)就是抗锯齿
只要玩过游戏,那么都应该对抗锯齿不陌生,不少游戏也都有关于抗锯齿的设置
如上图,放大的部分能很明显的看到“锯齿”边,如果了解光栅化的过程,那么也很容易理解锯齿是怎样产生的,这不是什么底层的BUG,正是完全正确的流程会出现这中“锯齿”现象,本质原因是场景的定义在三维空间中是连续的,而最终显示的像素则却是一个离散的二维数组,所以在判断一个点到底没有被某个像素覆盖的时候不应该单纯是一个“有”或者“没有"问题,也因此,抗锯齿一定是采用优化手段而无法根治
(参考于learnopengl.com)
光栅化将属于一个基本图形的所有顶点转化为一系列片段,顶点坐标理论上可以含有任何坐标,但片段却不是这样,因为它与你的窗口的解析度有关,几乎永远都不会有顶点坐标和片段的一对一映射,所以光栅化必须以某种方式决定每个特定顶点最终结束于哪个片段/屏幕坐标上
每个像素中心会包含一个采样点(sample point),它被用来决定一个像素是否被三角形所覆盖,红色的采样点如果在三角形内部,那么就会为这个被覆盖像素生成一个片段,否则就算三角形覆盖了部分屏幕像素,只要采样点没被覆盖这个像素就不会被处理
如果你的顶点组成的线段刚好与屏幕平行,那么这个时候就会好很多,但事实上只要出现斜边,就必然出现上图的情况
为了解决,一个很容易想到的方法是:每个像素不再只有中心一个采样点,而是设置多个采样点,假设最终有x%个采样点被覆盖,那么这个像素的颜色就按照对应比例x%进行平均化
上面所说的方法正是多重采样抗锯齿(MultiSampling Anti-Aliasing),也是最常用的抗锯齿算法,使用n个采样点意味着需要n倍的颜色缓冲区空间,但是对于这n个采样点,仍然只需要执行一次着色器,着色器使用的顶点数据会通过插值锁定在像素的中间,然后在计算最终颜色的时候乘上覆盖率,若执行多次着色器,会很显著的降低性能
上图就是MSAA优化后的效果,其实采样点数量是可以任意指定的,不过如果你的分辨率过低又或者采样点过少,就会产生一个新的问题:边缘模糊
好在是,这些东西GPU已经帮我们做了,如果是在openGL中进行最简单的应用,只需要添加2行代码就ok:
再看看效果,应该就是没问题了
前置:OpenGL基础33:帧缓冲之离屏渲染
很可惜,如果用了自己的帧缓冲,那么仅用上面的2行代码就不可以了,GLFW并不会对你自己创建的缓冲负责,在这种情况下就需要自己生成多采样缓冲以实现MSAA
可以参考前置章节,用同样的方法(纹理附件和渲染缓冲附件)创建多采样缓冲,并使其成为帧缓冲的附件,主要逻辑是在中间插入一个新的FBO,专门用于用于处理抗锯齿,之后再转入之前用于显示的FBO中进行我们想要的后处理:
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中,然后用这个普通的颜色附件来做后期处理,从而达到我们实际的目的:
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,也就是多采样抗锯齿,事实上,抗锯齿方法还有很多,效率和效果也都不太一样:
自定义抗锯齿算法:
因为屏幕纹理重新变回了只有一个采样点的普通纹理,有些后处理过滤器,比如边检测(edge-detection)将会再次导致锯齿边问题,为了修正此问题,往往要对纹理进行模糊处理,又或者创建自己的抗锯齿算法。将一个多重采样的纹理图像不进行还原直接传入着色器也是可行的,GLSL提供了这样的选项,以让我们能够对纹理图像的每个子样本进行采样
要想获取每个子样本的颜色值,需要将纹理uniform采样器设置为sampler2DMS,而不是平常使用的sampler2D:
uniform sampler2DMS screenTextureMS;
void main()
{
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);
}