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

How do I obtain pixel data from a canvas I don't own?

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

我正在尝试使用游戏 Shakes and Fidget. I use the readPixels method from the context 的 canvas 来执行此操作。

这是我试过的:

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 提供的答案非常适合我:)

您只能要求 Canvas 上下文一次。如果您将相同的选项传递给 getContext().

,则以下所有请求将是 return null,或者之前创建的相同上下文

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

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

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

requestAnimationFrame(fn)fn 回调推送到回调堆栈中,这些回调将在浏览器执行其 [= 之前​​以先进先出的顺序同时调用42=]绘制到屏幕 操作。这种情况偶尔发生一次(通常每秒 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) 的任何调用都保证在与 [=42= 相同的事件循环中被调用]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
});

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

进行第二次覆盖 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 游戏之前包含它。

您现在应该能够获得有关 canvas Unity 制作的任何内容的上下文,并随时调用 gl.readPixels

对于另一种方法,在同一事件中获取像素,您可以换行 requestAnimationFrame,这样您就可以在 Unity 使用 requestAnimationFrame[=20= 之后插入您的 gl.readPixels ]

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 and shows an example of post processing the unity output

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

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

这应该独立于 requestAnimationFrame 绘制的帧,但它是异步的。

我们需要一个视频,另一个 canvas 和一个流:

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');

现在您可以阅读游戏 canvas,方法是从流中请求一帧,将其推入视频并将视频绘制到我们的 canvas:

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);