openglframebuffermultisamplingdeferred-renderingdeferred-shading

Resolution of multi-sampled frame-buffer with multiple color attachments


Trying to implement Anti-aliasing on top of deferred shading, I'm attempting to use multi-sampled render buffers and then resolve the samples with a buffer-blit pass.

  1. As is traditional in deferred shading, I am rendering the scene with a dedicated shader issuing 3 color outputs:

    • Positions
    • Normals
    • Diffuse & specular
  2. Those are then used in a lighting-computation pass resulting in the final scene texture

  3. The scene texture is rendered to the screen on a full-screen quad using a simplistic shader

As you probably guessed, the on-screen MSAA is not applied to the content of scene texture when being rendered to the screen: to achieve anti-aliasing I thus chose to use multi-sampled render buffers in step 1) and introduced an extra step 1.1) for resolution. Of course the multi-sampling is only necessary/useful for the color map, not the other 2 maps.

My problem & question is that apparently, a frame buffer with multiple render-buffers/color attachments can only be defined for the same types of attachments ; meaning that if one attachment is multi-sampled then all others must be.

This becomes a problem for Positions and Normals buffers during resolution, because the geometry and lighting is affected as a result of anti-aliasing.

    // Create the frame buffer for deferred shading: 3 color attachments and a depth buffer
    glGenFramebuffers(1, &gBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    {
        // - Position color buffer
        glGenRenderbuffers(1, &gPosition);
        glBindRenderbuffer(GL_RENDERBUFFER, gPosition);
        glRenderbufferStorageMultisample(GL_RENDERBUFFER, 8, GL_RGBA16F, w, h);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, gPosition);

        // - Normal color buffer
        glGenRenderbuffers(1, &gNormal);
        glBindRenderbuffer(GL_RENDERBUFFER, gNormal);
        glRenderbufferStorageMultisample(GL_RENDERBUFFER, 8, GL_RGBA16F, w, h);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_RENDERBUFFER, gNormal);

        // - Color + specular color buffer
        glGenRenderbuffers(1, &gColorSpec);
        glBindRenderbuffer(GL_RENDERBUFFER, gColorSpec);
        glRenderbufferStorageMultisample(GL_RENDERBUFFER, 8, GL_RGBA, w, h);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_RENDERBUFFER, gColorSpec);

        unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
        glDrawBuffers(3, attachments);

        // - Generate the depth buffer for rendering
        glGenRenderbuffers(1, &sceneDepth);
        glBindRenderbuffer(GL_RENDERBUFFER, sceneDepth);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h);
        glRenderbufferStorageMultisample(GL_RENDERBUFFER, 8, GL_DEPTH_COMPONENT, w, h);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, sceneDepth);
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // Create a frame buffer with 3 attachments for sample resolution
    glGenFramebuffers(1, &gFrameRes);
    glBindFramebuffer(GL_FRAMEBUFFER, gFrameRes);
    {
        glGenTextures(1, &gPositionRes);
        glBindTexture(GL_TEXTURE_2D, gPositionRes);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, w, h, 0, GL_RGB, GL_FLOAT, nullptr);
        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, gPositionRes, 0);

        glGenTextures(1, &gNormalRes);
        glBindTexture(GL_TEXTURE_2D, gNormalRes);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, nullptr);
        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_ATTACHMENT1, GL_TEXTURE_2D, gNormalRes, 0);

        glGenTextures(1, &gColorSpecRes);
        glBindTexture(GL_TEXTURE_2D, gColorSpecRes);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gColorSpecRes, 0);
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);


    // ...
    //
    // Once the scene is rendered, resolve:
    glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gFrameRes);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
    glBlitFramebuffer(0, 0, sw, sh, 0, 0, sw, sh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
    glReadBuffer(GL_COLOR_ATTACHMENT1);
    glDrawBuffer(GL_COLOR_ATTACHMENT1);
    glBlitFramebuffer(0, 0, sw, sh, 0, 0, sw, sh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
    glReadBuffer(GL_COLOR_ATTACHMENT2);
    glDrawBuffer(GL_COLOR_ATTACHMENT2);
    glBlitFramebuffer(0, 0, sw, sh, 0, 0, sw, sh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

The result of the code sample above is that lit object's edges are showing artifacts of inappropriately dark/black or bright/white pixels, presumably because their position and/or normal have been altered in the process.


Solution

  • This becomes a problem for Positions and Normals buffers during resolution, because the geometry and lighting is affected as a result of anti-aliasing.

    It's supposed to be.

    It is logically incoherent to have positions and normals not use multisampling, while the diffuse/specular colors acquired are. Remember what multisampling is: each pixel has multiple samples, and different data from overlapping triangles may write to different samples in the same pixel. So you can have diffuse/specular colors from two or more triangles all in the same pixel. But that also means you should have positions and normals associated with each of those sub-pixel colors too. Your lighting pass won't make sense otherwise; you'd be using position&normal values for colors that didn't generate them.

    Proper multisampling with deferred rendering is expensive. The only way to make it work is to multisample everything, and then perform your lighting pass computations on a per-sample level. Since most of the performance gain of multisampling compared to super-sampling is not doing computations per-sample, you're only getting the benefits of multisampling (rather than supersampling) in your geometry pass, not your lighting pass.

    This is why people try to avoid multisampling when using deferred rendering. This is why pseudo-antialiasing techniques like FXAA and whatever exist.