此系列记录我跟随 LearnOpenGL 学习的历程。

# 高级 OpenGL

# 抗锯齿

锯齿现象的产生主要是由于光栅器将顶点数据转化为片段的方式

# SSAA

超采样抗锯齿 (Super Sample Anti-aliasing, SSAA),使用比正常分辨率更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样 (Downsample) 至正常的分辨率。虽然它确实能够解决走样的问题,但是由于这样比平时要绘制更多的片段,它也会带来很大的性能开销。

# MSAA

多重采样抗锯齿 (Multisample Anti-aliasing, MSAA),它借鉴了 SSAA 背后的理念,但却以更加高效的方式实现了抗锯齿。
多重采样将每个像素单一的采样点变为多个采样点(这也是它名称的由来)。不再使用像素中心的单一采样点,取而代之的是以特定图案排列的 4 个子采样点(Subsample,采样点的数量可以是任意的,更多的采样点能带来更精确的遮盖率)。用这些子采样点来决定像素的遮盖度。
无论三角形遮盖了多少个子采样点,(每个图元中)每个像素只运行一次片段着色器。片段着色器使用插值到像素中心的顶点数据,然后,MSAA 使用更大的深度 / 模板缓冲区来确定子采样点的覆盖率。被覆盖的子采样点数量将决定了像素颜色对帧缓冲的影响程度。

# OpenGL 中的 MSAA

在不启用帧缓冲的情况下,在 OpenGL 中启用 MSAA 只需要加入如下代码

p
glfwWindowHint(GLFW_SAMPLES, 4);// 使用包含 4 个子样本的多重采样缓冲
glEnable(GL_MULTISAMPLE);// 开启多重采样,一般默认开启

# 离屏 MSAA

如果使用我们自己的帧缓冲,则必须手动生成多重采样缓冲。
我们可以设置多重采样纹理附件与多重采样渲染缓冲对象,将其作为附件绑定到帧缓冲中。

l
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// 创造并绑定多重采样纹理附件
glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, textureColorbuffer);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, SCR_WIDTH, SCR_HEIGHT, GL_TRUE);
glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, textureColorbuffer, 0);
// 创造并绑定多重采样渲染缓冲对象
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_DEPTH24_STENCIL8, SCR_WIDTH, SCR_HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

如果直接将我们的帧缓冲输出到屏幕上,我们可以将帧缓冲分开绑定至 GL_READ_FRAMEBUFFER 与 GL_DRAW_FRAMEBUFFER。glBlitFramebuffer 函数会根据这两个目标,决定哪个是源帧缓冲,哪个是目标帧缓冲。接下来,我们可以将图像位块传送 (Blit) 到默认的帧缓冲中,将多重采样的帧缓冲传送到屏幕上。

l
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);

但是如果想用多重采样缓冲的纹理来做后期处理,我们可以将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的 FBO 中,然后用这个普通的颜色附件来做后期处理,也就是生成一个新的 FBO,作为中介帧缓冲对象,将多重采样缓冲还原为一个能在着色器中使用的普通 2D 纹理。

l
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();  
    ... 
}

因为屏幕纹理又变回了一个只有单一采样点的普通纹理,像是边缘检测这样的后期处理滤镜会重新导致锯齿。为了补偿这一问题,可以在之后对纹理进行模糊处理,或者创造自己的抗锯齿算法。
将一个多重采样的纹理图像不进行还原直接传入着色器也是可行的。GLSL 提供了这样的选项,让我们能够对纹理图像的每个子样本进行采样,所以我们可以创建我们自己的抗锯齿算法。

l
// 将多重采样纹理传入,采样器定义为 sampler2DMS
uniform sampler2DMS screenTextureMS;
// 使用 texelFetch 函数获得每个子样本的值
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3);  // 第 4 个子样本

比如下面代码进行了模糊化的后期处理

l
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2DMS screenTextureMS;
const float offset = 1.0 / 300.0;  
void main()
{
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // 左上
        vec2( 0.0f,    offset), // 正上
        vec2( offset,  offset), // 右上
        vec2(-offset,  0.0f),   // 左
        vec2( 0.0f,    0.0f),   // 中
        vec2( offset,  0.0f),   // 右
        vec2(-offset, -offset), // 左下
        vec2( 0.0f,   -offset), // 正下
        vec2( offset, -offset)  // 右下
    );
    float kernel[9] = float[](
    1.0 / 16, 2.0 / 16, 1.0 / 16,
    2.0 / 16, 4.0 / 16, 2.0 / 16,
    1.0 / 16, 2.0 / 16, 1.0 / 16  
    );
    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        // 对每个采样点取所有 MSAA 样本的平均值
        vec3 color = vec3(0.0);
        for(int s = 0; s < 4; s++)
        {
            color += texelFetch(screenTextureMS, ivec2((TexCoords.st + offsets[i]) * textureSize(screenTextureMS)), s).rgb;
        }
        sampleTex[i] = color / float(4);
    }
    vec3 col = vec3(0.0);
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];
    FragColor = vec4(col, 1.0);
}