-
This exhibit is a bit different from the last one.
- In the last one, we can only use one post-processing filter a time.
- In this exhibit, we chain the filters together, and we can turn each one on and off.
-
Doing multi-step image processing requires reading the output of the last filtering step.
How do you actually manage it?
- 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.
- We have buffers.
-
At any time, one buffer is the write buffer and the other is the read buffer.
The operation you can perform on the buffer is as indicated by the name.
You can only read from the read buffer, and you can only write from 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 integer index telling which one is the read buffer.
readBufferIndex: 0,
// An array of textures, containing only two textures.
textures: [],
// 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 user code do its magic.
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, what was written to the write buffer is 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);
}