【问题标题】:HTML5 canvas tearing with requestAnimationFrame使用 requestAnimationFrame 撕裂 HTML5 画布
【发布时间】:2017-01-05 05:10:54
【问题描述】:

更新:这整个问题最终是系统图形驱动程序的问题,而不是(看似)浏览器/API问题。撕裂的框架归结为实际的显示更新。再次感谢那些参与讨论并试图提供帮助的人。


我的页面使用画布和 2d 上下文以 720p 显示预渲染帧。我正在单独渲染帧并使用新的 ImageData 更新变量。然后,在 requestAnimationFrame 中,我只需执行 context.putImageData(cached_image_data);。尽管提前完全渲染了帧并有效地进行了双重缓冲,但我仍然经常撕裂。我在 SO 上发现了一些其他问题,但它们都以“使用 RAF”结尾。代码归结为:

var canvas = document.getElementById("canvas");
var cached_frame = new ImageData(new Uint8ClampedArray(canvas.width * canvas.height * 4), canvas.width, canvas.height);
var context = canvas.getContext("2d");
var framerate = 30;

function draw() {
    if (cached_frame)
        context.putImageData(cached_frame, 0, 0);

    requestAnimationFrame(draw);
}

setInterval(function() {
    var frame = context.getImageData(0, 0, canvas.width, canvas.height);

    // Do things to manipulate frame.data.

    // Save the resultant pixel data for the cached_frame.
    cached_frame.data = frame.data;
}, 1000 / framerate);

draw();

如果不使用 webgl,我还能做些什么吗?

任何建议表示赞赏。泰都:D

【问题讨论】:

  • 剪毛是一直在同一个位置还是随机移动?它只是一台机器,还是您尝试过的所有机器?它依赖于浏览器吗?您不应该期望任何剪切,因此我们需要更多信息来弄清楚发生了什么。
  • 画布已经被双缓冲了。对于需要经常发生的任何事情(超级慢),请避免使用 putImageData 和 getImageData。通过代码创建一个新画布并绘制其结果。
  • 这部分代码setInterval(function() { cached_frame = new ImageData(new Uint8ClampedArray(pixel_data) 将杀死内存。垃圾收集器将一直触发,因为每次间隔触发时您都会创建一个新的 Image 和 Uint8ClampedArray。将它们保存为变量并至少重复使用它们。
  • 只是基于@ericjbasti 的优秀 cmets 的一些快速思考: 如果可能,出于性能原因避免使用 getImageData / putImageData。 rAF 将帮助您剪毛。在同一个 rAF 中执行 getImageData 和 putImageData 操作。使用自动输入draw(timestamp) 的时间戳参数来执行定时活动。将requestAnimationFrame(draw) 放在draw() 的底部(不是顶部)。画布本身是双缓冲的,但如果您正在展示视频,您可能需要缓冲额外的帧。
  • @markE LOL,讽刺的是我专攻计算机图形学。 :)

标签: javascript html canvas vsync


【解决方案1】:

我不认为代码正在做你认为它正在做的事情

首先,据我所知,您不能将新数据分配给 ImageData 所以这一行

cached_frame.data = frame.data;

什么都不做。我们可以测试表明它不起作用的那个

var ctx = document.createElement("canvas").getContext("2d");
document.body.appendChild(ctx.canvas);

var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var data = new Uint8ClampedArray(imageData.length);

// fill imageData.data with red
fillWithColor(255, 0, 0, 255, imageData.data);

// fill data with green
fillWithColor(0, 255, 0, 255, data);

// assign imageData.data to data
imageData.data = data;

// Draw. If assigning imageData.data works result will
// be green, if not result will be red
ctx.putImageData(imageData, 0, 0);

function fillWithColor(r, g, b, a, dst) {
  for (ii = 0; ii < dst.length; ii += 4) {
    dst[ii + 0] = r;
    dst[ii + 1] = g;
    dst[ii + 2] = b;
    dst[ii + 3] = a;
  }
}
  

其次,您的 draw 函数正在连续绘制,至少从您发布的代码中 cached_frame 设置在第 2 行,因此它总是正确的并且总是会绘制。如果您以某种方式对cached_frame 中的实际数据进行了部分更新,那么当只有部分结果时它会绘制。

我想你想要这样的东西

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;

function draw() {
   context.putImageData(frame, 0, 0);
}

setInterval(function() {
    frame = context.getImageData(0, 0, canvas.width, canvas.height);

    // Do things to manipulate frame.data

    // frame is ready, draw it at next rAF
    requestAnimationFrame(draw);
}, 1000 / framerate);

如果您认为解码速度会比 raf 更快,您可能需要检查抽签是否已经排队。不过,在这种情况下,我认为您实际上并不需要 rAF。我很确定您可以在 setInterval 结束时进行绘制,它会显示在下一帧,不会撕裂。

这是一个测试,它不会让我流泪。

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
var frameCount = 0;

setInterval(function() {
  ++frameCount;
  frame = context.getImageData(0, 0, canvas.width, canvas.height);
  var data = frame.data;
  var width = frame.width;
  var height = frame.height;
  // Do things to manipulate frame.data
  for (var yy = 0; yy < height; ++yy) {
    for (var xx = 0; xx < width; ++xx) {
      var offset = (yy * width + xx) * 4;
      data[offset + 0] = ((xx >> 2 & 0x1) ^ frameCount & 0x1) ? 255 : 0;
      data[offset + 3] = 255;
    }
  }

  // frame is ready, draw it at next rAF
  context.putImageData(frame, 0, 0);
}, 1000 / framerate);
&lt;canvas id="canvas" width="1280" height="720"&gt;&lt;/canvas&gt;

【讨论】:

  • 您说得对,ImageData 无法更新,这是我刚才遇到的问题。此外,当我更新代码时,对 cached_frame 的检查是一个疏忽。在上面的场景中不需要它。对 rAF 的调用应该在 setInterval 结束时进行也是正确的,因为除非框架已更新,否则不需要。我将 putImageData() 的实际调用放在 rAF 而不是 setInterval 中,以避免不必要地调用它(例如当页面失焦时,或者只是在繁重的 CPU 负载下)。我现在正在经历所有事情,很快就会进行测试。
  • 你的眼泪消失了吗?
  • 使用setInterval 可能会在无法处理负载的设备上出现一些非常糟糕的行为。由于 setInterval 将调用放在调用堆栈上,而不管最后一次调用是否退出,它会慢慢填满调用堆栈并最终崩溃,页面变慢的原因有很多。还有一个问题是 rAF 可以停止发射而 setTimeout 没有。您不能保证每个区间都会通过 rAF 调用绘图函数。如果您确实需要使用计时器,请使用 setTimeout。
  • 我不知道你从哪里得到你的信息,但 setInterval 不会填满任何调用堆栈。这不是它的工作方式。至于 rAF、setInterval 和 setTimeout 各有各的位置。上面的具体示例是模拟接收 XHR 响应,因此非常适合。
  • 关于使用间隔与 rAF:我需要不断渲染。在我想要渲染帧的许多情况下,rAF 不会触发。这是 rAF 点的(一半);当用户看不到它时,它允许您不进行任何渲染/绘图。由于这些原因,我使用 setInterval 来处理幕后的渲染,并使用 rAF 来处理实际的绘图部分。
猜你喜欢
  • 2011-04-09
  • 1970-01-01
  • 1970-01-01
  • 2011-07-10
  • 2011-08-13
  • 1970-01-01
  • 2011-01-08
  • 2012-12-09
  • 1970-01-01
相关资源
最近更新 更多