【问题标题】:Why does my canvas go blank after converting to image?为什么我的画布在转换为图像后变为空白?
【发布时间】:2023-04-06 13:22:01
【问题描述】:

我正在尝试使用以下 sn-p 将 this page 上的 canvas 元素转换为 png(例如,在 JavaScript 控制台中输入):

(function convertCanvasToImage(canvas) {
  var image = new Image();
  image.src = canvas.toDataURL("image/png");
  return image;
})($$('canvas')[0]);

不幸的是,我得到的 png 是完全空白的。另请注意,调整页面大小后原始画布变为空白。

为什么canvas 变为空白?如何将此canvas 转换为 png?

【问题讨论】:

  • 您有任何错误吗? $$ 是拼写错误,还是您的 jQuery 使用双美元符号而不是单美元符号?
  • $$ 是 Chrome JavaScript 控制台的内置函数,其行为类似于 jQuery。我没有收到任何错误,没有。
  • 啊,不知道...我在想那个页面有问题。如果我在该页面的画布上执行toDataURL(),我会得到一个 500x500 的空白图像并且画布会清除。如果我在使用画布的网站上执行完全相同的代码,我会得到正确的数据并且画布不会清晰。

标签: javascript canvas webgl


【解决方案1】:

Kevin Reid 的preserveDrawingBuffer 建议是正确的,但(通常)有更好的选择。 tl;dr 是最后的代码。

将渲染网页的最终像素放在一起并与渲染 WebGL 内容进行协调可能会很昂贵。通常的流程是:

  1. JavaScript 向 WebGL 上下文发出绘图命令
  2. JavaScript 返回,将控制权返回给主浏览器事件循环
  3. WebGL 上下文将绘图缓冲区(或其内容)转换为合成器,以便集成到当前在屏幕上呈现的网页中
  4. 页面,带有 WebGL 内容,显示在屏幕上

请注意,这与大多数 OpenGL 应用程序不同。其中,渲染的内容通常直接显示,而不是与页面上的一堆其他内容合成,其中一些实际上可能位于 WebGL 内容之上并与 WebGL 内容混合。

WebGL 规范已更改为在第 3 步之后将绘图缓冲区视为基本上是空的。您在 devtools 中运行的代码在第 4 步之后出现,这就是您得到一个空缓冲区的原因。对规范的这种更改允许在第 3 步之后的消隐基本上是硬件中实际发生的情况(如在许多移动 GPU 中)的平台上实现显着的性能改进。如果您希望在第 3 步之后有时会制作 WebGL 内容的副本,则浏览器必须在第 3 步之前始终制作绘图缓冲区的副本,这会使您的帧率下降在某些平台上急剧下降。

您可以完全做到这一点,并通过将preserveDrawingBuffer 设置为true 来强制浏览器制作副本并保持图像内容可访问。来自规范:

可以通过设置 WebGLContextAttributes 对象的 preserveDrawingBuffer 属性来更改此默认行为。如果此标志为真,则绘图缓冲区的内容将被保留,直到作者清除或覆盖它们。如果此标志为假,则在渲染函数返回后尝试使用此上下文作为源图像执行操作可能会导致未定义的行为。这包括 readPixels 或 toDataURL 调用,或将此上下文用作另一个上下文的 texImage2D 或 drawImage 调用的源图像。

在您提供的示例中,代码只是更改了上下文创建行:

gl = canvas.getContext("experimental-webgl", {preserveDrawingBuffer: true});

请记住,它会在某些浏览器中强制使用较慢的路径,并且性能会受到影响,具体取决于您渲染的内容和方式。在大多数桌面浏览器中应该没问题,实际上不需要制作副本,而且这些浏览器构成了绝大多数支持 WebGL 的浏览器……但仅限于现在。

但是,还有另一种选择(在规范的下一段中有些令人困惑地提到)。

基本上,您在第 2 步之前自己制作副本:在所有绘制调用完成之后,但在您从代码将控制权返回给浏览器之前。这是 WebGL 绘图缓冲区仍然完好且可访问的时候,您应该可以轻松访问像素。您使用与其他方式相同的 toDataUrlreadPixels 调用,只是时机很重要。

在这里,您可以两全其美。您获得了绘图缓冲区的副本,但您无需在每一帧中都为它付费,即使是那些您不需要副本的帧(可能是其中的大多数),就像您将 preserveDrawingBuffer 设置为真的。

在您提供的示例中,只需将您的代码添加到 drawScene 的底部,您应该会在下方看到画布的副本:

function drawScene() {
  ...

  var webglImage = (function convertCanvasToImage(canvas) {
    var image = new Image();
    image.src = canvas.toDataURL('image/png');
    return image;
  })(document.querySelectorAll('canvas')[0]);

  window.document.body.appendChild(webglImage);
}

【讨论】:

  • 谢谢。这解释了为什么 png 图像是空白的,但它不能解释为什么 原始图像 会变成空白。
  • 这对我来说只是 {preserveDrawingBuffer: true} 部分,但答案很好
  • 我是编码新手,我猜我在这里遗漏了一些东西......是否有可能以更无聊的术语得到答案? :(
【解决方案2】:

这里有一些东西可以尝试。我不知道这些应该中的任何一个是否是完成这项工作所必需的,但它们可能会有所作为。

  • preserveDrawingBuffer: true 添加到getContext 属性。
  • 尝试在稍后的动画教程中执行此操作;即在画布上重复绘制而不是只绘制一次。

【讨论】:

    【解决方案3】:

    toDataURL()从Buffer中读取数据。

    我们不需要使用preserveDrawingBuffer: true

    在读取数据之前,我们还需要使用render()

    终于:

    renderer.domElement.toDataURL();
    

    【讨论】: