如何从我不拥有的画布中获取像素数据?

时间:2019-01-04 23:46:17

标签: javascript html canvas webgl webgl2

我正在尝试从画布获取像素RGBA数据以进行进一步处理。我认为,如果这有所不同,画布实际上就是Unity游戏。

我正在尝试使用游戏Shakes and Fidget的画布进行此操作。我使用readPixels中的context方法。

这是我尝试过的:

var example = document.getElementById('#canvas');
var context = example.getContext('webgl2');      // Also doesn't work with: ', {preserveDrawingBuffer: true}'
var pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4); 
context.readPixels(0, 0, context.drawingBufferWidth, context.drawingBufferHeight, context.RGBA, context.UNSIGNED_BYTE, pixels);

但是所有像素显然都是黑色的(显然不是真的)。​​

编辑:另外,我想多次读取像素。 谢谢大家的回答。 @Kaiido提供的答案非常适合我:)

3 个答案:

答案 0 :(得分:3)

您只能要求一次Canvas上下文。如果您将相同的选项传递给null,则以下所有请求均将返回getContext(),或者返回之前创建的上下文。

现在,您链接到的页面在创建上下文时未通过preserveDrawingBuffer选项,这意味着要能够从此处获取像素信息,您必须将其链接事件循环与游戏循环一样发生。
幸运的是,这个精确的游戏确实使用了一个简单的requestAnimationFrame循环,因此要挂接到同一个事件循环,我们要做的就是将代码包装在requestAnimationFrame调用中。

由于回调是堆叠的,并且确实需要此类回调中的下一帧才能创建循环,因此我们可以确保我们的调用将在它们之后堆叠。

我现在意识到这可能并不明显,所以我将尝试进一步解释 requestAnimationFrame 的作用,以及如何确定在Unity的回调之后将调用回调。

requestAnimationFrame(fn)fn回调推送到一堆回调中,所有这些回调将在浏览器执行其 paint之前以先进先出的顺序同时被调用屏幕上的操作。在最接近的事件循环结束时,偶尔会发生这种情况(通常每秒60次)。
可以理解为setTimeout(fn , time_remaining_until_next_paint)的一种,主要区别是可以保证 requestAnimationFrame 回调执行程序将在事件循环结束时以及在其他js执行之后被调用事件循环的内容。
因此,如果我们在与调用回调的事件循环相同的事件循环中调用requestAnimationFrame(fn),则伪造的time_remaining_until_next_paint将是0,而fn将被推送在堆栈的底部(后进后出)。
而且,当从回调执行器本身内部调用requestAnimationFrame(fn)时,time_remaining_until_next_paint可能会围绕16,并且fn将在下一帧的第一个调用中被调用。

因此,确保从 requestAnimationFrame 的回调执行程序外部进行的对requestAnimationFrame(fn)的任何调用都可以在与 requestAnimationFrame支持的循环,然后被调用。

因此,我们要获取这些像素,就是将对 readPixels 的调用包装在 requestAnimationFrame 调用中,并在之后调用它Unity的循环开始了。

var example = document.getElementById('#canvas');
var context = example.getContext('webgl2') || example.getContext('webgl');
var pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4);
requestAnimationFrame(() => {
  context.readPixels(0, 0, context.drawingBufferWidth, context.drawingBufferHeight, context.RGBA, context.UNSIGNED_BYTE, pixels);
  // here `pixels` has the correct data
});

答案 1 :(得分:1)

可能您需要在渲染像素的同时读取像素,或者需要强制画布使用preserveDrawingBuffer: true,以便您可以随时读取画布。

要做第二个覆盖getContext

HTMLCanvasElement.prototype.getContext = function(origFn) {
  const typesWeCareAbout = {
    "webgl": true,
    "webgl2": true,
    "experimental-webgl": true,
  };
  return function(type, attributes = {}) {
    if (typesWeCareAbout[type]) {
      attributes.preserveDrawingBuffer = true;
    }
    return origFn.call(this, type, attributes);
  };
}(HTMLCanvasElement.prototype.getContext);

将其放在Unity游戏之前的文件顶部,或者将其放在单独的脚本文件中,然后将其包含在Unity游戏之前。

您现在应该可以在Unity制作的任何画布上获取上下文,并随时随地调用gl.readPixels

对于另一种方法,在同一事件中获取像素,您可以换行requestAnimationFrame以便在Unity使用gl.readPixels后插入requestAnimationFrame

window.requestAnimationFrame = function(origFn) {
  return function(callback) {
    return origFn(this, function(time) {
      callback(time);
      gl.readPixels(...);
    };
  };
}(window.requestAnimationFrame);

另一种解决方案是使用虚拟webgl上下文。 This library shows an example of implementing a virtual webgl context并显示an example of post processing the unity output

请注意,在某些时候Unity可能会切换为使用OffscreenCanvas。届时,它可能需要除上述解决方案以外的其他解决方案。

答案 2 :(得分:0)

或者,您可以将画布的内容流式传输到视频元素,将视频内容绘制到另一个画布并读取那里的像素。

这应该独立于由 requestAnimationFrame 绘制的框架,但是是异步的。

我们需要一个视频、另一个画布和一个流:

var example = document.getElementById('#canvas');
var stream=example.captureStream(0);//0 fps

var vid=document.createElement("video");
vid.width=example.width;
vid.height=example.height;
vid.style="display:none;";
document.body.appendChild(vid);

var canvas2=document.createElement("canvas");
canvas2.width=example.width;
canvas2.height=example.height;
canvas2.style="display:none;";
var width=example.width;
var height=example.height;
body.appendChild(canvas2);

var ctx = canvas2.getContext('2d');

现在您可以通过从流中请求一个帧,将其推入视频并将视频绘制到我们的画布上来读取游戏画布:

stream.requestFrame();
//wait for the game to draw a frame
vid.srcObject=stream;
//wait
ctx.drawImage(vid, 0, 0, width, height, 0, 0, width, height);
var pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4); 
ctx.readPixels(0, 0, ctx.drawingBufferWidth, ctx.drawingBufferHeight, ctx.RGBA, ctx.UNSIGNED_BYTE, pixels);