提高画布效果的实时性能

Luk*_* B. 5 javascript optimization html5 canvas

我试图在HTML5游戏中使用以下效果:http://somethinghitme.com/projects/metaballs/

但由于它是一个游戏(而不是图形演示),我有更严格的FPS要求,我需要时间来计算物理和其他一些东西,我最大的瓶颈是元球的代码.

以下代码是我在剥离原始代码后获得的性能,它不是很漂亮,但它足以满足我的目的:

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);
}
Run Code Online (Sandbox Code Playgroud)

我的代码上有另一个循环,我设法通过使用以下链接http://www.fatagnus.com/unrolling-your-loop-for-better-performance-in-javascript/中描述的技术来提高其性能,但是在这上使用相同实际上会降低性能(也许我做错了?)

我还研究了网络工作者,看看我是否可以分割负载(因为代码分别针对每个像素运行)但是我在这个链接上找到的例子http://blogs.msdn.com/b/eternalcoding/archive/2012/09 /20/using-web-workers-to-improve-performance-of-image-manipulation.aspx在使用Web worker时实际运行速度较慢.

我还可以做些什么?有没有办法从循环中删除分支?展开它的另一种方式?或者这是我能做的最好的?

编辑:

这是一些周围的代码:

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

}
Run Code Online (Sandbox Code Playgroud)

和处理方法:

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

};
Run Code Online (Sandbox Code Playgroud)

可以看出,我尝试使用图像而不是渐变形状,但性能更差,我也尝试使用ctx.drawImage而不是putImageData,但它失去了alpha并且不会更快.我想不出能达到预期效果的替代方案.目前的代码在谷歌浏览器上运行完美,但Safari和Firefox真的很慢.还有什么我可以尝试的吗?我应该放弃这些浏览器吗?

小智 9

更新

一些可以应用的技术

以下是一些优化技术,可以应用于使这项工作在FF和Safari中更加流畅.

话虽这么说:Chrome的canvas实现非常好,而且比Firefox和Safari提供的骨头更快(目前).新的Opera使用与Chrome相同的引擎,并且(大约?)同Chrome一样快.

为了实现良好的跨浏览器工作,需要做出一些妥协,并且质量将始终受到影响.

我试图演示的技术是:

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

瓶颈

为每个元球生成梯度的成本很高.因此,当我们一劳永逸地缓存这个问题时,我们只会注意到性能的巨大提升.

另一点是getImageData,putImageData而且我们需要使用高级语言来迭代低级字节数组.幸运的是,阵列是类型阵列,因此有一点帮助,但除非我们牺牲更多质量,否则我们将无法从中获得更多.

当你需要挤压所有东西时,所谓的微观优化就变得至关重要(这些有着不应有的坏名声IMO).

从你的帖子的印象:你似乎非常接近这个工作,但从提供的代码我不能看到什么问题这么说.

在任何情况下 - 这是一个实际的实现(基于您引用的代码):

小提琴演示

在初始步骤中预先计算变量 - 我们可以预先计算的所有内容在以后帮助我们,因为我们可以直接使用该值:

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,

...
Run Code Online (Sandbox Code Playgroud)

我们在这里使用一个因子来减小实际大小并将最终渲染缩放到屏幕画布上.对于每个2因子,您可以指数级地保存4x像素.我在演示中将其预设为2,这适用于Chrome,适用于Firefox.您甚至可以在比我的更好的规格机器上的两个浏览器中运行因子1(1:1比率)(Atom CPU).

初始化各种画布的尺寸:

// 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
Run Code Online (Sandbox Code Playgroud)

然后生成一个渐变的单个实例,稍后将为其他球缓存.值得注意的是:我最初只使用它来绘制所有球但后来决定将每个球缓存为图像(画布)而不是绘图和缩放.

这会造成内存损失但会提高性能.如果记忆很重要,你可以在生成它们的循环中跳过渲染球的缓存,而只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();
Run Code Online (Sandbox Code Playgroud)

然后在循环中生成各种元球.

缓存计算和渲染的元球:

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
    });
}
Run Code Online (Sandbox Code Playgroud)

然后我们关闭正在缩放的​​图像的插值 - 这会获得更快的速度.

请注意,您也可以在某些浏览器中使用CSS来执行与此处相同的操作.

禁用图像平滑:

// disable image smoothing for sake of speed
ctx.webkitImageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.oImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;  // future...
Run Code Online (Sandbox Code Playgroud)

现在完成了非关键部分.其余的代码利用这些调整来表现更好.

主循环现在看起来像这样:

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);
}
Run Code Online (Sandbox Code Playgroud)

使用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);
}
Run Code Online (Sandbox Code Playgroud)

一些微观优化实际上有助于这种情况.

我们缓存alpha通道的像素值,因为我们使用它超过两次.而不是潜水,6我们乘以,0.1667因为乘法更快一点.

我们已经缓存了tresholdQ价值(25%threshold).将缓存的值放在函数中可以提高速度.

不幸的是,由于此方法基于alpha通道,我们还需要清除主画布.在这种情况下,这会产生(相对)巨大的惩罚.最佳的是能够使用你可以直接"blit"的纯色,但我没有在这里看到这个方面.

您也可以将点数据放在数组中而不是作为对象.但是,由于这种情况很少,在这种情况下可能不值得.

结论

我可能错过了一两个(或更多)可以进一步优化的地方,但你明白了.

正如您所看到的,修改后的代码运行速度比原始代码快几倍,主要是因为我们在这里做出了质量和一些优化,特别是梯度的优化.