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