5

I’m using WebGL to run some image processing (similar to Brad Larson’s GPUImage or the tutorial WebGL Fundamentals), followed by a CPU step that consumes the pixels. Unfortunately, when I call gl.readPixels, the buffer is all black in Firefox. Why?

Here’s a standalone example: http://jsfiddle.net/yonran/8C3n5/ In Chrome, the result is, “Within the framebuffer, there are 924 black pixels and 100 blue pixels,” while in Firefox, the result is, “Within the framebuffer, there are 1024 black pixels and 0 blue pixels.”

This is how I set up the framebuffer:

function createTextureAndFramebuffer(gl, width, height) {
  var outputTexture = createAndSetupTexture(gl);
  // Make the texture the same size as the image.
  gl.bindTexture(gl.TEXTURE_2D, outputTexture);
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);
  gl.bindTexture(gl.TEXTURE_2D, null);

  // create a framebuffer
  var fbo = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
  // attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0);
  return {texture: outputTexture, framebuffer: fbo};
}
function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
    // This is necessary for non-power-of-two textures
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

This is how I draw to the framebuffer:

ImageFilter.prototype = {
  ...,
  draw: function() {
    var gl = this.gl;
    // Use the texture
    gl.bindTexture(gl.TEXTURE_2D, this.texture);

    // make this the framebuffer we are rendering to.
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Tell webgl the viewport setting needed for framebuffer.
    gl.viewport(0, 0, this.outputWidth, this.outputHeight);

    gl.useProgram(this.program);
    this.applyUniforms();

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    gl.flush();
  },
  vertexShaderString:
      "attribute vec4 position;\n" +
      "attribute vec4 inputTextureCoordinate;\n" +
      "\n" +
      "varying vec2 textureCoordinate;\n" +
      "\n" +
      "void main()\n" +
      "{\n" +
      "     gl_Position = position;\n" +
      "     textureCoordinate = inputTextureCoordinate.xy;\n" +
      "}\n",
  fragmentShaderString:
      "varying highp vec2 textureCoordinate;\n" +
      "\n" +
      "uniform sampler2D inputImageTexture;\n" +
      "\n" +
      "void main()\n" +
      "{\n" +
      "  gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +
      "}\n"
};

And then I read from the framebuffer like this:

gl.bindFramebuffer(gl.FRAMEBUFFER, imageFilter.framebuffer);
var width = imageFilter.outputWidth, height = imageFilter.outputHeight;
var array = new Uint8ClampedArray(width * height * 4);
gl.finish();
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, array);
var blackPixels = 0, bluePixels = 0;
var firstBlueX, firstBlueY;
for (var y = 0; y < height; y++) {
  for (var x = 0; x < width; x++) {
    var idx = 4*(x + y*width);
    var r = array[idx], g = array[idx+1], b = array[idx+2], a = array[idx+3];
    if (r === 0 && g === 0 && b === 0)
      blackPixels++;
    else if (b === 255) {
      bluePixels++;
      if (firstBlueX == null) {
        firstBlueX = x;
        firstBlueY = y;
      }
    }
  }
}
var el = document.createElement("div");
document.body.appendChild(el);
el.textContent = "The first canvas is drawn to a framebuffer and then drawn again on the second canvas. " +
  "Within the framebuffer, there are " + blackPixels + " black pixels and " +
  bluePixels + " blue pixels The first blue pixel is at (" + firstBlueX+ "," + firstBlueY + ") " +
  "(origin is at bottom left).";

i thought this might be a synchronization problem, but adding gl.finish() or a setTimeout before reading doesn’t help. When searching, I found that multisampling can cause the framebuffer to be 0, but setting antialias: false within the canvas.getContext call doesn’t help.

Edit: I just tried setting a pixel before readPixels, and Firefox doesn’t unset the pixel. Perhaps readPixels is not implemented yet and is a no-op as of 14.0.1?

yonran
  • 18,156
  • 8
  • 72
  • 97
  • Could you include the relevant portion of your code here, for quick viewing and in case jsfiddle goes away next year? – Kevin Reid Jul 25 '12 at 16:03

1 Answers1

2

The offending line is here:

var array = new Uint8ClampedArray(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, array);

When you specify the type as GL_UNSIGNED_BYTE you must pass in the correctly typed array or FF fails. The correct type here is Uint8Array, not Uint8ClampedArray.

If you check the FF console log you will see this is the error message, resolved by swapping these array types:

Error: WebGL: readPixels: Mismatched type/pixels types
fuzic
  • 2,492
  • 16
  • 22
  • Thanks @fuzic. Changing from Uint8ClampedArray to Uint8Array worked. I probably didn’t see the error earlier because it was not in the Firebug console; it was only in the Javascript Error Console. – yonran Sep 17 '12 at 21:31