Ake Koomsin

Offscreen Rendering and Multisampling With OpenGL

It has been for a while since my last post. I was enjoying with my senior project, “Accelerating Map Rendering with GPU”. In this project, my friend and I modified Mapnik, an opensource map rendering, to utilize Nvidia’s Path Rendering. Path Rendering is an OpenGL extension provided by Nvidia for vector graphic rendering. Nvidia claims that its extensions aims to reduce overhead from traditional APIs when using them to draw vector graphics. In the end, we are able to make the map production 30-60% faster. Our implementation can be found on https://github.com/ake-koomsin/mapnik_nvpr

There are two important things that we had to achieve in order to use Path Rendering extension for map rendering, offscreen rendering and multisampling. Offscreen rendering is important because we don’t want the renderer to show the intermediate result. Multisampling is for producing a quality map.

I think offscreen rendering combining with multisampling is an important piece of knowledge. It is also hard to find a complete refernce about these two. Therefore, I think it is worth writing about it.

Offscreen Rendering

Offscreen rendering is a technique that are commonly found in game development. Sometimes, there are situations that you want to generate a texture at runtime.

To set up offscreen rendering, you have to create your own “Framebuffer Object (FBO)”. Actually, OpenGL has its own default FBO. A result stored in the default FBO will be shown onto the screen while the result stored in our own FBO will be not. The code below demonstrates how to set up our own FBO.

Setting our own FBO for offscreen rendering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int fbo, colorBuffer, depthBuffer;

// Create and bind the FBO
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// Create color render buffer
glGenRenderbuffers(1, &colorBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);

// Create depth render buffer (This is optional)
glGenRenderbuffers(1, &depthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);

// Bind Texture assuming we have created a texture
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, aTexture, 0);

// Draw something
drawGraphic();

It is straightforward. First, we create a FBO. After that, we create render buffers and textures and attach them to our FBO.

Multisampling

By default, OpenGL does not care about antialiasing. As a result, the output contains stair-like artifacts which degrade visual quality. We have to enable multisampling by the code below.

Enabling multisampling
1
glEanble(GL_MULTISAMPLE);

Combining Offscreen Rendering and Multisampling Together

It turns out that to combine them together, we need additional set up which are multisample framebuffer storage and multisample texture. The code below demonstrates how to do.

Setting up FBO with Multisampling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Create multisample texture
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, aMultisampleTexture);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 16, GL_RGBA, width, height, GL_TRUE);

int fbo, colorBuffer, depthBuffer;

// Create and bind the FBO
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// Create color render buffer
glGenRenderbuffers(1, &colorBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 16, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);

// Create depth render buffer (This is optional)
glGenRenderbuffers(1, &depthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 16, GL_DEPTH24_STENCIL8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);

// Bind Texture assuming we have created a texture
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, aTexture, 0);

// Enable multisampling
glEanble(GL_MULTISAMPLE);

// Draw something
drawGraphic();

Retrieving the result

After offscreen rendering, you may want to display the result onto the screen. When you are using multisampling FBO, you are not able to use the result stored in the texture directly. You have to do “Blitting” which transfer the result from one FBO to another. The code below shows how to do.

Blitting
1
2
3
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, normalFBO); // Normal FBO can be the default FBO too.
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);

I hope these snippets are a useful reference.