easy-gl › Offscreen Rendering

Offscreen Rendering

Render a scene to a texture (FBO), then display it on a fullscreen quad as a post-processing step.

Overview

The pattern is:

  1. Create an FBO with a color texture and a depth renderbuffer.
  2. Bind the FBO, render the scene into it.
  3. Unbind the FBO (bind default framebuffer).
  4. Draw a fullscreen quad, sampling from the color texture.

Step 1 — Create the render target

constexpr int RT_W = 1280, RT_H = 720;

// Color attachment
easygl::Texture colorTex;
colorTex.create();
colorTex.bind(easygl::TextureTarget::Texture2D);
colorTex.set_parameter(easygl::TextureTarget::Texture2D,
                         easygl::TextureParameter::MinFilter,
                         static_cast<int>(easygl::TextureMinFilter::Linear));
colorTex.set_parameter(easygl::TextureTarget::Texture2D,
                         easygl::TextureParameter::MagFilter,
                         static_cast<int>(easygl::TextureMagFilter::Linear));
colorTex.set_storage_2d(easygl::TextureTarget::Texture2D, 1,
                          easygl::InternalFormat::Rgba8, RT_W, RT_H);

// Depth attachment
easygl::Renderbuffer depthRbo;
depthRbo.create();
depthRbo.bind();
depthRbo.set_storage(easygl::InternalFormat::DepthComponent24, RT_W, RT_H);

// FBO
easygl::Framebuffer fbo;
fbo.create();
fbo.bind();
fbo.attach_texture_2d(
    easygl::FramebufferTarget::Framebuffer,
    easygl::FramebufferAttachment::Color0,
    easygl::TextureTarget::Texture2D,
    colorTex.native_handle(), 0);
fbo.attach_renderbuffer(
    easygl::FramebufferTarget::Framebuffer,
    easygl::FramebufferAttachment::Depth,
    depthRbo.native_handle());

if (!fbo.is_complete()) {
    throw std::runtime_error("FBO not complete");
}
easygl::Framebuffer::unbind();

Post-processing shader (inversion)

const char* postVertSrc = R"(#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
out vec2 vUV;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); vUV = aUV; })";

const char* postFragSrc = R"(#version 330 core
in vec2 vUV;
uniform sampler2D uScreen;
out vec4 FragColor;
void main() {
    vec4 col = texture(uScreen, vUV);
    FragColor = vec4(1.0 - col.rgb, 1.0); // colour inversion
})";

easygl::Program postProg(postVertSrc, postFragSrc);

Fullscreen quad

constexpr float fsQuad[] = {
    -1.0f, -1.0f,  0.0f, 0.0f,
     1.0f, -1.0f,  1.0f, 0.0f,
     1.0f,  1.0f,  1.0f, 1.0f,
    -1.0f, -1.0f,  0.0f, 0.0f,
     1.0f,  1.0f,  1.0f, 1.0f,
    -1.0f,  1.0f,  0.0f, 1.0f,
};
easygl::VertexArray fsVao;
easygl::Buffer      fsVbo;
// ... create, bind, upload, set_attribute same as before ...

Render loop

// ---- Pass 1: render scene into FBO ----
fbo.bind();
device.set_viewport(0, 0, RT_W, RT_H);
device.set_depth_test_enabled(true);
device.set_clear_color(0.2f, 0.2f, 0.2f, 1.0f);
device.clear(easygl::ClearFlags::Color | easygl::ClearFlags::Depth);

sceneProg.use();
// ... draw scene objects ...

// ---- Pass 2: post-process onto default framebuffer ----
easygl::Framebuffer::unbind();
device.set_viewport(0, 0, windowW, windowH);
device.set_depth_test_enabled(false);
device.set_clear_color(1.0f, 1.0f, 1.0f, 1.0f);
device.clear(easygl::ClearFlags::Color);

postProg.use();
colorTex.active_bind(easygl::TextureUnit::Texture0,
                       easygl::TextureTarget::Texture2D);
postProg.set_uniform(postProg.uniform_location("uScreen"), 0);

fsVao.bind();
device.draw_arrays(easygl::PrimitiveType::Triangles, 0, 6);

Key points