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:
- Create an FBO with a color texture and a depth renderbuffer.
- Bind the FBO, render the scene into it.
- Unbind the FBO (bind default framebuffer).
- 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
- Disable depth testing on the post-process pass — you are drawing a flat fullscreen quad.
- The color texture and renderbuffer are kept alive alongside the FBO — they must outlive the FBO's use.
- The render target size (
RT_W × RT_H) is independent of the window size.