提高画布效果的实时性能

5
我正在尝试在一个HTML5游戏中使用以下效果:http://somethinghitme.com/projects/metaballs/。但由于这是一个游戏(而不是图形演示),我有更紧密的FPS要求,需要时间计算物理和其他一些事情,而我的最大瓶颈是metaballs的代码。以下代码是我在性能方面剥离原始代码后得到的,它不太美观,但对我的目的足够了。
ParticleSpawner.prototype.metabilize = function(ctx) {
    var imageData = this._tempCtx.getImageData(0,0,900,675),
    pix = imageData.data;
    this._tempCtx.putImageData(imageData,0,0);

    for (var i = 0, n = pix.length; i <n; i += 4) {
        if(pix[i+3]<210){
            pix[i+3] = 0;
        }
    }

    //ctx.clearRect(0,0,900,675);
    //ctx.drawImage(this._tempCanvas,0,0);
    ctx.putImageData(imageData, 0, 0);
}

我在我的代码中使用了另一个循环,并通过使用以下链接描述的技术 http://www.fatagnus.com/unrolling-your-loop-for-better-performance-in-javascript/ 来提高其性能,但是在这个循环上使用相同的技术实际上会降低性能(也许我做错了?)
我还研究了Web Workers以查看是否可以分割负载(因为代码针对每个像素单独运行),但是我在此链接中找到的示例http://blogs.msdn.com/b/eternalcoding/archive/2012/09/20/using-web-workers-to-improve-performance-of-image-manipulation.aspx 实际上会使速度更慢。
还有什么其他方法可以尝试吗? 有没有办法从循环中删除分支? 另一种展开它的方法? 还是这已经是我所能做到的最好的了?
编辑:
这是周围部分代码:
ParticleSpawner.prototype.drawParticles = function(ctx) {
    this._tempCtx.clearRect(0,0,900,675);

    var iterations = Math.floor(this._particles.getNumChildren() / 8);
    var leftover = this._particles.getNumChildren() % 8;
    var i = 0;

    if(leftover > 0) {
        do {
            this.process(i++);
        } while(--leftover > 0);
    }

    do {
        this.process(i++);
        this.process(i++);
        this.process(i++);
        this.process(i++);
        this.process(i++);
        this.process(i++);
        this.process(i++);
        this.process(i++);
    } while(--iterations > 0);

    this.metabilize(ctx);

}

以及处理方法:

ParticleSpawner.prototype.process = function(i) {
    if(!this._particles.getChildAt(i)) return;
    var bx = this._particles.getChildAt(i).x;
    var by = this._particles.getChildAt(i).y;

    if(bx > 910 || bx < -10 || by > 685) {
        this._particles.getChildAt(i).destroy();
        return;
    }

    //this._tempCtx.drawImage(this._level._queue.getResult("particleGradient"),bx-20,by-20);

    var grad = this._tempCtx.createRadialGradient(bx,by,1,bx,by,20);
    this._tempCtx.beginPath();

    var color = this._particles.getChildAt(i).color;
    var c = "rgba("+color.r+","+color.g+","+color.b+",";

    grad.addColorStop(0, c+'1.0)');
    grad.addColorStop(0.6, c+'0.5)');
    grad.addColorStop(1, c+'0)');

    this._tempCtx.fillStyle = grad;
    this._tempCtx.arc(bx, by, 20, 0, Math.PI*2);
    this._tempCtx.fill();

};

可以看到,我尝试使用图像代替渐变形状,但性能更差,我还尝试使用ctx.drawImage代替putImageData,但它会失去alpha通道并且不会更快。我无法想到其他方法来实现所需的效果。当前代码在Google Chrome上运行完美,但Safari和Firefox非常慢。我还有什么尝试的余地吗?我应该放弃这些浏览器吗?
3个回答

9

更新

可应用的一些技术

以下是一些可应用的优化技术,可以使此工作在FF和Safari中更加流畅。

话虽如此:Chrome的画布实现非常好,并且比Firefox和Safari提供的更快(目前)。新版Opera使用与Chrome相同的引擎,与Chrome大致相当快。

为了在各种浏览器上正常工作,需要做出一些妥协,通常会影响质量

我试图演示的技术有:

  • 缓存一个用作元球基础的渐变
  • 尽可能缓存所有东西
  • 以半分辨率渲染
  • 使用drawImage()更新主画布
  • 禁用图像平滑处理
  • 使用整数坐标和大小
  • 使用requestAnimationFrame()
  • 尽可能使用while循环

瓶颈

生成每个元球的渐变成本很高。因此,当我们对其进行一次性缓存时,我们将只通过这样做就会注意到性能大幅提升。

另一个问题是getImageDataputImageData以及我们需要使用高级语言迭代低级字节数组的事实。幸运的是,该数组是类型化数组,因此这有一些帮助,但除非我们牺牲更多质量,否则我们将无法从中获得更多。

当您需要尽可能挤出所有东西时,所谓的微优化就变得至关重要(在我看来,这些并没有应得的不良声望)。

根据您的帖子印象:您似乎非常接近使其正常工作,但从提供的代码中,我无法看出哪里出错了。

无论如何-这里是实际实现(基于您提到的代码):

演示

在初始步骤中预先计算变量-我们可以直接使用值,这可以帮助我们稍后:

var ...,

// multiplicator for resolution (see comment below)
factor = 2,
width = 500,
height = 500,

// some dimension pre-calculations
widthF = width / factor,
heightF = height / factor,

// for the pixel alpha
threshold = 210,
thresholdQ = threshold * 0.25,

// for gradient (more for simply setting the resolution)
grad,
dia = 500 / factor,
radius = dia * 0.5,

...

我们在这里使用一个因子来减小实际大小并将最终渲染缩放到屏幕画布上。每个2倍因子可以指数级地节省4倍像素。在演示中,我将其预设为2,在Chrome浏览器中效果很好,在Firefox浏览器中效果也不错。如果您的机器配置比我的(Atom CPU)更好,甚至可以在两个浏览器上运行1倍因子(1:1比例)。
初始化各种画布的大小:
// set sizes on canvases
canvas.width = width;
canvas.height = height;

// off-screen canvas
tmpCanvas.width = widthF;
tmpCanvas.height = heightF;

// gradient canvas
gCanvas.width = gCanvas.height = dia

首先生成一个渐变实例,它将被缓存并用于后续的所有球。值得注意的是:我最初只使用这个来绘制所有的球,但后来决定将每个球缓存为一个图像(画布),而不是绘制和缩放。

这样会有一些内存开销,但可以提高性能。如果内存很重要,您可以跳过在生成球时缓存渲染球,并在需要绘制球时只drawImage渐变画布。

生成渐变:

var grad = gCtx.createRadialGradient(radius, radius, 1, radius, radius, radius);
grad.addColorStop(0, 'rgba(0,0,255,1)');
grad.addColorStop(1, 'rgba(0,0,255,0)');
gCtx.fillStyle = grad;
gCtx.arc(radius, radius, radius, 0, Math.PI * 2);
gCtx.fill();

在生成各种“代谢球”的循环中。

缓存计算和渲染的“代谢球”:

for (var i = 0; i < 50; i++) {

    // all values are rounded to integer values
    var x = Math.random() * width | 0,
        y = Math.random() * height | 0,
        vx = Math.round((Math.random() * 8) - 4),
        vy = Math.round((Math.random() * 8) - 4),
        size = Math.round((Math.floor(Math.random() * 200) + 200) / factor),

        // cache this variant as canvas
        c = document.createElement('canvas'),
        cc = c.getContext('2d');

    // scale and draw the metaball
    c.width = c.height = size;
    cc.drawImage(gCanvas, 0, 0, size, size);

    points.push({
        x: x,
        y: y,
        vx: vx,
        vy: vy,
        size: size,
        maxX: widthF + size,
        maxY: heightF + size,
        ball: c  // here we add the cached ball
    });
}

然后,对于正在缩放的图像,我们关闭插值 - 这样可以获得更快的速度。

请注意,您也可以在某些浏览器中使用CSS来实现与此处相同的效果。

禁用图像平滑处理:

// disable image smoothing for sake of speed
ctx.webkitImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.oImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;  // future...

现在非关键部分已经完成。其余的代码利用这些微调来提高性能。
主循环现在如下所示:
function animate() {

    var len = points.length,
        point;

    // clear the frame of off-sceen canvas
    tmpCtx.clearRect(0, 0, width, height);

    while(len--) {
        point = points[len];
        point.x += point.vx;
        point.y += point.vy;

        // the checks are now exclusive so only one of them is processed    
        if (point.x > point.maxX) {
            point.x = -point.size;
        } else if (point.x < -point.size) {
            point.x = point.maxX;
        }

        if (point.y > point.maxY) {
            point.y = -point.size;
        } else if (point.y < -point.size) {
            point.y = point.maxY;
        }

        // draw cached ball onto off-screen canvas
        tmpCtx.drawImage(point.ball, point.x, point.y, point.size, point.size);
    }

    // trigger levels
    metabalize();

    // low-level loop
    requestAnimationFrame(animate);
}

使用requestAnimationFrame可以更好地利用浏览器,因为它比仅使用setTimeout更低级和更高效。
原始代码检查了两个边缘 - 这是不必要的,因为球只能在每个轴上越过一个边缘。
代谢函数被修改如下:
function metabalize(){

    // cache what can be cached
var imageData = tmpCtx.getImageData(0 , 0, widthF, heightF),
        pix = imageData.data,
        i = pix.length - 1,
        p;

    // using a while loop here instead of for is beneficial
    while(i > 0) {
    p = pix[i];
        if(p < threshold) {
    pix[i] = p * 0.1667; // multiply is faster than div
    if(p > thresholdQ){
        pix[i] = 0;
    }
        }
    i -= 4;
    }

    // put back data, clear frame and update scaled
    tmpCtx.putImageData(imageData, 0, 0);
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(tmpCanvas, 0, 0, width, height);
}

一些微小的优化可以在这种情况下实际帮助。
我们将像素值的alpha通道缓存,因为我们使用它超过两次。我们用0.1667乘法代替6除法,因为乘法速度稍快。
我们已经缓存了tresholdQ值(threshold的25%)。将缓存值放在函数内部会更快一点。
不幸的是,由于此方法基于alpha通道,我们还需要清除主画布。在这种情况下,这会有一个(相对)巨大的惩罚。最理想的是能够直接“blit”的纯色,但我在这里没有考虑这个方面。
你也可以将点数据放在数组中而不是作为对象。然而,由于这样的点很少,所以在这种情况下可能不值得。
总之,我可能错过了一两个(或更多)可以进一步优化的地方,但你明白我的意思。
正如你所看到的,由于我们在质量和某些优化方面做出的妥协,特别是在渐变方面,修改后的代码运行速度比原始代码快了几倍。

什么是视频?我会看看asm.js,但asm能做哪些优化,手写不能做到呢? - Luke B.
啊,元球是由物理控制的,想法是制造一些假流体,而元球是使其看起来不像一堆圆圈,更像真正的流体的一种方式。我不认为我能记录每种可能的组合的数据或视频 :P。感谢提供的链接,我会尝试使用asm.js编译一些东西,看看它是否更快。 - Luke B.
@LukeB 看看新的回答是否能给你提供解决方案。我认为这已经是从画布中挤出的最大限度,如果这不能帮助你,恐怕除非有一个超级聪明的 metaball 算法或内存使用不成问题,否则无能为力。 - user1693593
谢谢你的回答,我尝试了所有方法,但Safari拒绝以可接受的速度运行游戏。不过,在Chrome上我确实看到了一些改善。 - Luke B.
1
@LukeB。Safari不行。Safari(原始的WebKit)是所有浏览器中画布实现最慢的。很遗憾,没有办法让Safari画布表现得更好。我不会感到惊讶,如果这是Google分叉并走自己的路线的原因之一(而Opera在其新版本中采用Chrome代码库而不是Safari代码库)。这就像让一辆小菲亚特赛车和一辆法拉利(或者大众汽车)竞争一样。这就像从石头里挤血一样难以做到。(除非你想用石头敲人的头) :-) - user1693593
显示剩余2条评论

1

这个循环已经非常简单,使用了JIT喜欢的稳定类型,因此我认为你无法获得显著的改进。

我已经消除了+3并稍微展开了它(假设width*height可被4整除)。我添加了|0 "cast"到整数,使其在V8中略微更快。

总体而言,这提高了10%:

var i = (3 - 4)|0;
var n = (pix.length - 16)|0;
while(i < n) {
    if (pix[i+=4] < 210){
        pix[i] = 0;
    }
    if (pix[i+=4] < 210){
        pix[i] = 0;
    }
    if (pix[i+=4] < 210){
        pix[i] = 0;
    }
    if (pix[i+=4] < 210){
        pix[i] = 0;
    }
}

如果您需要它运行得更快,也许可以使用低分辨率的画布来实现效果?

1

编程中绘制粒子部分有改进的空间。

可以使用而不是使用

if(leftover > 0) {
        do {
            this.process(i++);
        } while(--leftover > 0);
    }

你可以直接使用这个。
while(leftover > 0) {
        this.process(i++);
        leftover --;
   }

这将减少if语句的一个判断步骤,也会减少(--)操作符的使用。这将降低代码的复杂度。
使用do while循环时,可以去掉(--)操作符,用简单的语句来降低代码的圈复杂度,使代码更快。
最终,这将提高代码的性能和处理速度,并减少CPU和资源的使用。虽然Ken的答案也可以工作,但我创建了一个类似于你的样例站点的更快的fiddle。
如果有任何问题,请留言,并更新fiddle以进行性能检查。fiddle

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