Github: https://github.com/RenaudRohlinger/webgl-compute-pbo
This example showcases a technique similar to compute shading using a
transform feedback program and a Pixel Buffer Object (PBO) to simulate
compute shader effects in environments where they are not available.
Every second, a 'compute' operation processes vertex data through
transform feedback. The result stored in an array buffer on the GPU, is
then transferred to a texture using a PBO (gl.PIXEL_UNPACK_BUFFER) and
then used as input in the next frame. This setup keeps the processed data
entirely on the GPU for enhanced performance.
Shaders can access the updated data in the texture through a texture uniform, allowing for efficient data manipulation and comparison using texelFetch. This approach minimizes CPU-GPU data transfer, optimizing performance for GPU-based computations like dynamic sorting or simulations.
The compute operation in this example incrementally updates vertex data values, transferring them to a texture using a GPU-to-GPU Pixel Buffer Object (PBO) transfer. This method avoids CPU involvement, improving performance. For debugging, the texture's contents are read back to the CPU using gl.readPixels.
Pixel Buffer Object Data:
Code:
<!-- Path: ./index.js -->
<script>
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl2');
// Updated vertex shader for transform feedback
const vsTransform = /* glsl */ `#version 300 es
uniform highp sampler2D u_data;
out vec4 o_data;
void main() {
vec4 i_data = texelFetch(u_data, ivec2(gl_VertexID, 0), 0);
o_data = mod(i_data + 1., 10.);
}`;
// Fragment shader for transform feedback (not used but required)
const fsTransform = `#version 300 es
precision highp float;
void main() {
}`;
// Create shaders and programs
const transformProgram = createProgram(gl, vsTransform, fsTransform, [
'o_data',
]); // Add the varyings parameter
const drawProgram = createProgram(gl, vsDraw, fsDraw);
const size = 2;
const dataArray = new Float32Array(size * 4);
dataArray
.map((_, i) => i)
.forEach((v, i) => {
dataArray[i] = v;
});
const width = size;
const height = 1;
canvas.width = width * 4;
canvas.height = height;
canvas.style['image-rendering'] = 'pixelated';
canvas.style.display = 'block';
canvas.style.width = `${window.innerWidth - 14}px`;
canvas.style.height = `${height * 20}px`;
const datas = new Float32Array(dataArray);
// Setup transform feedback
const tf = gl.createTransformFeedback();
// Create a framebuffer
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
const dataTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, dataTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA32F,
width,
height,
0,
gl.RGBA,
gl.FLOAT,
datas
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const dataBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, dataBuffer);
gl.bufferData(gl.ARRAY_BUFFER, datas, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// Attach the texture to the framebuffer
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
dataTexture,
0
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const dataLocation = gl.getAttribLocation(transformProgram, 'a_data');
const drawDataLocation = gl.getAttribLocation(drawProgram, 'a_data');
function tick() {
// Use the transform program
gl.useProgram(transformProgram);
// gl.bindBuffer(gl.ARRAY_BUFFER, null);
// Set the write buffer as the transform feedback buffer
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, dataBuffer);
// Perform transform feedback
gl.enable(gl.RASTERIZER_DISCARD);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, width); // Adjust the count as needed
gl.endTransformFeedback();
gl.disable(gl.RASTERIZER_DISCARD);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// Bind PBO and transfer data to texture
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, dataBuffer);
gl.bindTexture(gl.TEXTURE_2D, dataTexture);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.FLOAT, 0);
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
debug();
}
setInterval(tick, 1000);
</script>
Author: Renaud Rohlinger @onirenaud