2

I want to get the pixel-data from a Three.js demo. As far as I know, there are 2 way to proceed :

1) draw the webGl-canvas inside a 2D-canvas and use Context2D.getImageData like that :

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(renderer.domElement,0,0);
var data = ctx.getImageData(0,0,w,h).data;

2) use directly the context3D with readPixels, like that :

var ctx = renderer.domElement.getContext("webgl");
var data = new UInt8Array(w*h*4);
ctx.readPixels(0, 0, w,h, ctx.RGBA, ctx.UNSIGNED_BYTE, data);

These 2 way to proceed works and give the same results but the second one is almost 2 times slower than the one using context2d.getImageData.

Sounds very weird to me. How the fact to draw the 3D-stuff into a 2D-canvas could be faster than using the context3D directly ? I don't understand and I'm almost sure I don't use gl.readPixels correctly.

Then my question is : how to use gl.readPixels in order to be faster than context2d.drawImage + context2d.getImageData ?

I tryed to used a Float32Array like that

var ctx = renderer.domElement.getContext("webgl");
var data = new Float32Array(w*h*4);
ctx.readPixels(0, 0, w,h, ctx.RGBA, ctx.FLOAT, data);

I thought it should be faster since there is no conversion from Float to UInt8 but it looks like it doesn't work like that because my 'data' array stay empty after the call of ctx.readPixels

Thank you for your help !

(please excuse me if my english is not perfect, it's not my native language)

Tom Lecoz
  • 75
  • 1
  • 9
  • Just checking but in your example above you're not setting the size of the 2D canvas to match the size of the WebGL canvas. Are you in reality? because otherwise that might explain the speed difference. – gman Jan 08 '18 at 01:11
  • Hello ! The variables 'w' and 'h' represents the width & height of the canvas. Both canvas have the same size (512x512) – Tom Lecoz Jan 08 '18 at 05:24
  • 1
    you don't show that in your code about. You show creating a canvas, calling getContext, calling drawImage, calling getImageData. Without setting the size of that canvas itself that canvas is 300x150 unless you set it somewhere else you're not showing – gman Jan 08 '18 at 05:25
  • Could you elaborate on that first option? How does one "draw a WebGL canvas inside a 2D canvas" in such a way that the 2D canvas will hold the pixel data of the first one? Can't seem to get that to work... – Arnaud H Mar 26 '19 at 10:41
  • Hello ! the method using canvas.getImageData with webgl have been optimised in chrome between the day I wrote this post and now because it's now possible to check the pixels of canvas containing a webgl-movie at 60fps even with a big resolution. If you use pure webgl, everything is draw inside this "context-3d" of a canvas. Then you just need to create another canvas and do var ctx = otherCanvas.getContext("2d"); ctx.drawImage(webglCanvas,0,0); var pixels = ctx.getImageData(0,0,otherCanvas.width,otherCanvas.height).data – Tom Lecoz Mar 26 '19 at 14:26
  • (I never found the correct way to use gl.readPixels . Contrary to the "answer" defined as "accepted" , it's very slow if the webglCanvas contains something (it's not the case in the example made by gman) – Tom Lecoz Mar 26 '19 at 14:28

1 Answers1

3

On my machine I get readPixels as 2x to 20x faster than drawImage/getImageData. Tested on MacOS Chrome, Firefox, well as Windows 10 Chrome, and Firefox. Safari came out readPixels as slower. Sounds like a bug in Safari and in fact checking Safari Technology Preview Release 46, as expected, readPixels is 3x to 1.2x faster than drawImage/getImageData

const gl = document.createElement("canvas").getContext("webgl");
const ctx = document.createElement("canvas").getContext("2d");

const w = 512;
const h = 512;

gl.canvas.width = w;
gl.canvas.height = h;
ctx.canvas.width = w;
ctx.canvas.height = h;

const readPixelBuffer = new Uint8Array(w * h * 4);

const tests = [
 { fn: withReadPixelsPreAlloc, msg: "readPixelsPreAlloc", },
 { fn: withReadPixels, msg: "readPixels", },
 { fn: withDrawImageGetImageData, msg: "drawImageGetPixels", },
];

let ndx = 0;
runNextTest();

function runNextTest() {
  if (ndx >= tests.length) {
    return;
  }
  const test = tests[ndx++];
  
  // use setTimeout to give the browser a change to 
  // do something between tests
  setTimeout(function() {
    log(test.msg, "iterations in 5 seconds:", runTest(test.fn));
    runNextTest();
  }, 0);
}

function runTest(fn) {
  const start = performance.now();
  let count = 0;
  for (;;) {
    const elapsed = performance.now() - start;
    if (elapsed > 5000) {
      break;
    }
    fn();
    ++count;
  }
  return count;
}

function withReadPixelsPreAlloc() {
  gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, readPixelBuffer);
}

function withReadPixels() {
  const readPixelBuffer = new Uint8Array(w * h * 4);
  gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, readPixelBuffer);
}

function withDrawImageGetImageData() {
  ctx.drawImage(gl.canvas, 0, 0);
  ctx.getImageData(0, 0, w, h);
}

function log(...args) {
  const elem = document.createElement("pre");
  elem.textContent = [...args].join(' ');
  document.body.appendChild(elem);
}

As for converting to float the canvas itself is stored in bytes. There is no conversion to float and you likely got a GL error

const gl = document.createElement("canvas").getContext("webgl");
const buf = new Float32Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, buf);
log("ERROR:", glEnumToString(gl, gl.getError()));

function log(...args) {
  const elem = document.createElement("pre");
  elem.textContent = [...args].join(' ');
  document.body.appendChild(elem);
}

function glEnumToString(gl, val) {
  if (val === 0) { return 'NONE'; }
  for (key in gl) {
    if (gl[key] === val) { 
      return key;
    }
  }
  return `0x${val.toString(16)}`;
}

Checking the console I see the error is

WebGL: INVALID_ENUM: readPixels: invalid type
gman
  • 100,619
  • 31
  • 269
  • 393
  • Thank you a lot for you message ! I will study it a little bit because your results are very different from what I got. I launched your test and getImageData appears to be slower but it's not the case with my code. It's weird because I don't see any difference right now but, for sure, I missed something :) Thank you again ! – Tom Lecoz Jan 08 '18 at 17:20
  • Unfortunately, I have not the time to make some test tonight, but I will. Actually, I am not totally sure that your code represents the reality because the webgl-canvas contains nothing. In my code, I used a buffer created once for all as you do in your withReadPixelsPreAlloc but the results are very very different. With your code and preAlloc I get 2785 iteration in 5 second, it means that each iteration is done in less than 2 ms for 512x512. Sounds incredible to me. In my case, the size of the texture is 32x32 in reality and it need 5-6ms , 512x512 take around 50ms. – Tom Lecoz Jan 08 '18 at 23:27
  • The main difference I see between our code is that my webgl-canvas contains a texture. – Tom Lecoz Jan 08 '18 at 23:30
  • I confirm : if I remove every mesh on the screen and try to use gl.readPixels, performance are not at all the same but it's totally useless... With one single mesh and a 512x512 canvas-size => around 45-50ms Without any mesh => 1-2 ms. If i use a canvas-size of 32x32, getImageData is at least 3x faster than gl.readPixels. I still don't understand how it's possible.. – Tom Lecoz Jan 09 '18 at 03:06
  • but how do u get the pixel data of a scene? If I have a simple rotating cube the pixels are always 0,0,0,255. couldnt find anwer on here – Yeets Sep 04 '22 at 20:22
  • @Yeets, Are you reading the pixels in the same event that you're rendering them? You need to post your code somewhere. [example](https://jsgist.org/?src=5d2786ca811c881e882aad78a8e3919c). – gman Sep 04 '22 at 21:45
  • thanks I read your answer again then double checked, it was reading it but I was also trying to encode i to a PNG but te librry wasnt working, I saved it as a RAW and opened with any program and it worked :) – Yeets Sep 04 '22 at 21:57