-
In the last exhibit, we only used one post-processing filter a time. In this exhibit, we chain multiple filters together.
-
Multi-step image processing requires several drawing passes which each read the output of the last filtering step. How do we manage this sort of structure?
- If there are n steps, we can allocate n+1 buffers for all inputs and outputs.
- Alternatively, we can use only 2 buffers with the help of double buffering.
-
Double buffering, aka ping-pong buffering, is a technique to simplify programming
when multiple operations are applied to the same image in succession.
-
At any time, we only have two buffers: the write buffer and the read buffer. You can only read from the read buffer, and you can only write to the write buffer.
- We can swap the buffer to exchange their roles.
-
Applying multiple operations to the same image with double buffering:
- Copy the source image to the read buffer.
- Perform operation 1, reading from the read buffer and writing to the write buffer.
- Swap the buffers.
- Perform operation 2, reading from the read buffer and writing to the write buffer.
- Swap the buffers.
- Perform operation 3, reading from the read buffer and writing to the write buffer.
- And so on.
-
Here's how we implemented a double buffer in JavaScript:
function createDoubleBuffer(gl, width, height) {
var output = {
// An array of textures, containing only two textures.
textures: [],
// An integer index telling which one is the read buffer.
readBufferIndex: 0,
// Return the read buffer.
getReadBuffer: function() {
return this.textures[this.readBufferIndex];
},
// Return the write buffer.
getWriteBuffer: function() {
return this.textures[1 - this.readBufferIndex];
},
// Exchange the roles of the buffers.
swap: function() {
this.readBufferIndex = 1 - this.readBufferIndex;
}
};
// Allocate the buffers.
output.textures.push(createFloatTexture(gl, width, height));
output.textures.push(createFloatTexture(gl, width, height));
return output;
}
var buffer = createDoubleBuffer(gl, 512, 512);
-
Here's a convenience function to render something to a write buffer of a double buffer,
and then swap it.
function drawToBufferAndSwap(gl, fbo, doubleBuffer, drawFunc) {
// Bind the FBO.
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// Attach the write buffer to the color attachment.
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
doubleBuffer.getWriteBuffer(),
0);
// Let the input function draw as necessary.
drawFunc();
// Detach the write buffer.
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
null,
0);
// Unbind the FBO.
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Swap the buffers.
doubleBuffer.swap();
// After this point, what was written to the write buffer is now in the read buffer and is ready for the next step.
}
-
Here's how we implemented the filter chaining:
// Draw scene to frame buffer.
drawToBufferAndSwap(gl, fbo, buffer, function() {
gl.clearColor(0.75, 0.75, 0.75, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Code elided for brevity.
gl.flush();
});
if ($("#blurXCheckBox").is(":checked")) {
drawToBufferAndSwap(gl, fbo, buffer, function() {
gl.useProgram(blurXProgram);
// Code elided for brevity.
drawFullScreenQuad(gl, blurXProgram);
gl.useProgram(null);
gl.flush();
});
}
if ($("#blurYCheckBox").is(":checked")) {
drawToBufferAndSwap(gl, fbo, buffer, function() {
gl.useProgram(blurYProgram);
// Code elided for brevity.
drawFullScreenQuad(gl, blurYProgram);
gl.useProgram(null);
gl.flush();
});
}
if ($("#srgbCheckBox").is(":checked")) {
drawToBufferAndSwap(gl, fbo, buffer, function() {
gl.useProgram(srgbProgram);
// Code elided for brevity.
drawFullScreenQuad(gl, srgbProgram);
gl.useProgram(null);
gl.flush();
});
}
// Copy pixel from read buffer to the monitor.
{
gl.useProgram(textureCopyProgram);
if (textureCopyProgram.texture != null) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, buffer.getReadBuffer());
gl.uniform1i(textureCopyProgram.texture, 0);
}
drawFullScreenQuad(gl, textureCopyProgram);
}