LearnOpenGL学习笔记—高级OpenGL 11:抗锯齿
【项目地址:点击这里这里这里】
本节对应官网学习内容:抗锯齿
1 知识回顾
关于抗锯齿的话题在我的博客里老生常谈了
看这一节笔记: GAMES101课程学习笔记—Lec 05~06:Rasterization 光栅化
就能对抗锯齿或者说是反走样以及走样的产生的原理理解的很透彻了
官网主要详细介绍了下MSAA的原理,可以点进去结合起来看看,不再赘述。
2 OpenGL中的MSAA
如果我们想要在OpenGL中使用MSAA,我们必须要使用一个能在每个像素中存储大于1个颜色值的颜色缓冲(因为多重采样需要我们为每个采样点都储存一个颜色)。
所以,我们需要一个新的缓冲类型,来存储特定数量的多重采样样本,它叫做多重采样缓冲(Multisample Buffer)。
GLFW和大多数的窗口系统一样,提供了一个多重采样缓冲,用以代替默认的颜色缓冲。
我们所要做的只是提示(Hint) GLFW,我们希望使用一个包含N个样本的多重采样缓冲。
这可以在创建窗口之前调用glfwWindowHint来完成。
glfwWindowHint(GLFW_SAMPLES, 4);
现在再调用glfwCreateWindow创建渲染窗口时,每个屏幕坐标就会使用一个包含4个子采样点的颜色缓冲了。
GLFW会自动创建一个每像素4个子采样点的深度和样本缓冲。这也意味着所有缓冲的大小都增长了4倍。
现在我们已经向GLFW请求了多重采样缓冲,我们还需要调用glEnable并启用GL_MULTISAMPLE,来启用多重采样。
在大多数OpenGL的驱动上,多重采样都是默认启用的,所以这个调用可能会有点多余,但显式地调用一下会更保险一点。这样子不论是什么OpenGL的实现都能够正常启用多重采样了。
glEnable(GL_MULTISAMPLE);
只要默认的帧缓冲有了多重采样缓冲的附件,我们所要做的只是调用glEnable来启用多重采样。
因为多重采样的算法都在OpenGL驱动的光栅器中实现了,我们不需要再多做什么。
我们为了让这两行代码正常工作,把之前帧缓冲的部分都注释掉
我们先看看未开启MSAA时的锯齿效果
我们用上两行代码开启MSAA
3 离屏MSAA
由于GLFW负责了创建多重采样缓冲,启用MSAA非常简单。
然而,如果我们想要使用我们自己的帧缓冲来进行离屏渲染,那么我们就必须要自己动手生成多重采样缓冲了。
有两种方式可以创建多重采样缓冲,将其作为帧缓冲的附件:纹理附件和渲染缓冲附件,这和在帧缓冲教程中所讨论的普通附件很相似。
多重采样纹理附件
为了创建一个支持储存多个采样点的纹理,我们使用glTexImage2DMultisample来替代glTexImage2D,它的纹理目标是GL_TEXTURE_2D_MULTISAPLE。
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
它的第二个参数设置的是纹理所拥有的样本个数。
如果最后一个参数为GL_TRUE,图像将会对每个纹素使用相同的样本位置以及相同数量的子采样点个数。
我们使用glFramebufferTexture2D将多重采样纹理附加到帧缓冲上,这里纹理类型使用的是GL_TEXTURE_2D_MULTISAMPLE。
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
当前绑定的帧缓冲现在就有了一个纹理图像形式的多重采样颜色缓冲。
多重采样渲染缓冲对象
和纹理类似,创建一个多重采样渲染缓冲对象并不难。我们所要做的只是在指定(当前绑定的)渲染缓冲的内存存储时,将glRenderbufferStorage的调用改为glRenderbufferStorageMultisample就可以了。
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
函数中,渲染缓冲对象后的参数我们将设定为样本的数量,在当前的例子中是4。
渲染到多重采样帧缓冲
渲染到多重采样帧缓冲对象的过程都是自动的。
只要我们在帧缓冲绑定时绘制任何东西,光栅器就会负责所有的多重采样运算。
我们最终会得到一个多重采样颜色缓冲以及/或深度和模板缓冲。
因为多重采样缓冲有一点特别,我们不能直接将它们的缓冲图像用于其他运算,比如在着色器中对它们进行采样。
一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者还原(Resolve)图像。
多重采样帧缓冲的还原通常是通过glBlitFramebuffer来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原。
glBlitFramebuffer会将一个用4个屏幕空间坐标所定义的源区域,复制到一个同样用4个屏幕空间坐标所定义的目标区域中。
在帧缓冲教程中,当我们绑定到GL_FRAMEBUFFER时,我们是同时绑定了读取和绘制的帧缓冲目标。
我们也可以将帧缓冲分开绑定至GL_READ_FRAMEBUFFER与GL_DRAW_FRAMEBUFFER。
glBlitFramebuffer函数会根据这两个目标,决定哪个是源帧缓冲,哪个是目标帧缓冲。
接下来,我们可以将图像位块传送(Blit)到默认的帧缓冲中,将多重采样的帧缓冲传送到屏幕上。
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
- void glBlitFramebuffer( GLint srcX0,
GLint srcY0,
GLint srcX1,
GLint srcY1,
GLint dstX0,
GLint dstY0,
GLint dstX1,
GLint dstY1,
GLbitfield mask,
GLenum filter); - srcX0, srcY0, srcX1, srcY1
Specify the bounds of the source rectangle within the read buffer of the read framebuffer.
指定读帧缓冲区的矩形范围 - dstX0, dstY0, dstX1, dstY1
Specify the bounds of the destination rectangle within the write buffer of the write framebuffer.
指定写帧缓冲区的矩形范围 - mask
The bitwise OR of the flags indicating which buffers are to be copied. The allowed flags are GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT.
指定要读取的缓冲区,可以取GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT。GL_COLOR_BUFFER_BIT表示颜色缓冲区, GL_DEPTH_BUFFER_BIT表示深度缓冲区 and GL_STENCIL_BUFFER_BIT表示模板缓冲区。 - filter
Specifies the interpolation to be applied if the image is stretched. Must be GL_NEAREST or GL_LINEAR.
指定伸缩变形时的插值方法,可以取GL_NEAREST or GL_LINEAR。
但如果我们想要使用多重采样帧缓冲的纹理输出来做像是后期处理这样的事情呢?
我们不能直接在片段着色器中使用多重采样的纹理。
但我们能做的是将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中。
然后用这个普通的颜色附件来做后期处理,从而达到我们的目的。
然而,这也意味着我们需要生成一个新的FBO,作为中介帧缓冲对象,将多重采样缓冲还原为一个能在着色器中使用的普通2D纹理。
这个过程的伪代码是这样的:
unsigned int msFBO = CreateFBOWithMultiSampledAttachments();
// 使用普通的纹理颜色附件创建一个新的FBO
...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
...
while(!glfwWindowShouldClose(window))
{
...
glBindFramebuffer(msFBO);
ClearFrameBuffer();
DrawScene();
// 将多重采样缓冲还原到中介FBO上
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
// 现在场景是一个2D纹理缓冲,可以将这个图像用来后期处理
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ClearFramebuffer();
glBindTexture(GL_TEXTURE_2D, screenTexture);
DrawPostProcessingQuad();
...
}
如果现在再来渲染这个程序,我们会得到与之前完全一样的抗锯齿结果。
完整具体的来说 就是我们把fbo的建立过程改成这样
#pragma region FBO / RBO
//配置MSAA帧缓冲区
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
//创建多采样颜色附加纹理
unsigned int textureColorBufferMultiSampled;
glGenTextures(1, &textureColorBufferMultiSampled);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGB, 800, 600, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled, 0);
//为深度和模板附件创建(也是多采样)renderbuffer对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// 将它附加当前绑定的帧缓冲对象
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 配置第二个正常的中介的帧缓冲
unsigned int intermediateFBO;
glGenFramebuffers(1, &intermediateFBO);
glBindFramebuffer(GL_FRAMEBUFFER, intermediateFBO);
// 创建颜色附加纹理
unsigned int screenTexture;
glGenTextures(1, &screenTexture);
glBindTexture(GL_TEXTURE_2D, screenTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0); // we only need a color buffer
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Intermediate framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#pragma endregion
在渲染循环中,我们这么处理
while (!glfwWindowShouldClose(window))
{
......
//第一阶段处理,渲染到自己建立的fbo
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
......
//第二阶段 位块传送(Blit)到默认的帧缓冲中
//将多重采样缓冲还原到中介FBO上
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
glBlitFramebuffer(0, 0, 800, 600, 0, 0, 800, 600, GL_COLOR_BUFFER_BIT, GL_NEAREST);
//现在场景是一个2D纹理缓冲,可以将这个图像用来后期处理
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.0f, 1.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
screenShader->use();
glBindVertexArray(quadVAO);
glBindTexture(GL_TEXTURE_2D, screenTexture); // use the color attachment texture as the texture of the quad plane
glDisable(GL_DEPTH_TEST);
glUniform1i(glGetUniformLocation(screenShader->ID, "screenTexture"), 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
......
如果现在再实现帧缓冲教程中的后期处理效果,我们就能够在一个几乎没有锯齿的场景纹理上进行后期处理了。
比如边缘检测看起来将会是这样:
- 当然因为屏幕纹理又变回了一个只有单一采样点的普通纹理,像是边缘检测这样的后期处理滤镜会重新导致锯齿。为了补偿这一问题,可以之后对纹理进行模糊处理,或者想出自己的抗锯齿算法。
如果将多重采样与离屏渲染结合起来,我们需要自己负责一些额外的细节。
但所有的这些细节都是值得额外的努力的,因为多重采样能够显著提升场景的视觉质量。
当然,要注意,如果使用的采样点非常多,启用多重采样会显著降低程序的性能。在本节中,通常采用的是4采样点的MSAA。
自定义抗锯齿算法
将一个多重采样的纹理图像不进行还原直接传入着色器也是可行的。
GLSL提供了这样的选项,让我们能够对纹理图像的每个子样本进行采样,所以我们可以创建我们自己的抗锯齿算法。在大型的图形应用中通常都会这么做。
要想获取每个子样本的颜色值,我们需要将纹理uniform采样器设置为sampler2DMS,而不是平常使用的sampler2D:
uniform sampler2DMS screenTextureMS;
使用texelFetch函数就能够获取每个子样本的颜色值了:
`vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);// 第4个子样本
我们不会深入探究自定义抗锯齿技术的细节,这里仅仅是给一点启发。