HTML5画布在使用requestAnimationFrame时出现撕裂问题

3

更新:整个问题最终是与系统图形驱动有关,而不是(似乎)浏览器/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


1
剪切是否总是在同一位置,还是随机移动?这只是一个机器,还是您尝试过的所有机器?它是否与浏览器有关?您不应该期望任何剪切,因此我们需要更多信息来确定发生了什么。 - Blindman67
1
画布已经双缓冲了。避免在需要频繁执行的操作中使用putImageData和getImageData(非常慢)。通过代码创建一个新的画布,只需绘制该结果即可。 - ericjbasti
2
这段代码 setInterval(function() { cached_frame = new ImageData(new Uint8ClampedArray(pixel_data) 会消耗大量内存。垃圾回收器会一直运行,因为每次间隔触发时都会创建新的Image和Uint8ClampedArray。请至少将它们保存为变量并重复使用。 - ericjbasti
1
仅是一些快速的想法,基于 @ericjbasti 的好评论:如有可能,出于性能原因,请避免使用 getImageData / putImageData。rAF 将有助于您的剪切。在同一个 rAF 中执行 getImageData 和 putImageData 操作。使用自动提供给 draw(timestamp) 的时间戳参数来执行定时活动。将 requestAnimationFrame(draw) 放在 draw() 的底部(而不是顶部)。Canvas 本身是双缓冲的,但如果您正在呈现视频,则可能需要缓冲额外的帧。 - markE
1
@markE 哈哈,具有讽刺意味的是,我专门从事计算机图形学。 :) - Blindman67
显示剩余9条评论
1个回答

3

我认为代码并没有按照你的预期运行。

首先,据我所知,您不能将新数据分配给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函数一直在绘图,至少从你发布的代码中可以看出,第2行设置了cached_frame,因此它总是为真并始终在绘制。如果你以某种方式部分更新了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);
<canvas id="canvas" width="1280" height="720"></canvas>


你说得对,ImageData无法更新,我刚刚遇到了这个问题。另外,在我更新代码时,检查cached_frame是一个疏忽。在上述情况下,它是不需要的。 你也是对的,rAF的调用应该放在setInterval的末尾,除非帧已经更新。我将实际的putImageData()调用放在rAF中而不是setInterval中,以避免不必要的调用(例如当页面失去焦点或者CPU负载过重时)。 我现在正在检查所有内容,并将很快进行测试。 - The Busy Wizard
你的撕裂问题解决了吗? - gman
我不知道你从哪里得到的信息,但setInterval不会填充任何调用堆栈。它的工作方式并非如此。至于rAF vs setInterval vs setTimeout,每个都有其适用场景。上面的具体示例模拟接收XHR响应,因此它非常合适。 - gman
关于使用间隔 vs rAF:我需要不断地进行渲染。在许多我想要渲染帧的情况下,rAF并不会触发。这是rAF的(一半)目的;它允许您在用户看不到时不进行任何渲染/绘制。出于这些原因,我使用setInterval来处理后台渲染,而使用rAF来处理实际的绘制部分。 - The Busy Wizard
@gman 天哪!!根据 about:gpu,2D 画布加速和解码加速已被禁用。这令人沮丧,但更重要的是...这是在那台一直运行良好的笔记本电脑上。我得等到今天晚些时候才能检查出现问题的设备。 - The Busy Wizard
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接