5

我正在尝试对 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);
}

我的代码有另一个循环,我设法通过使用以下链接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在使用网络工作者时实际上运行速度较慢。

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

编辑:

这是一些周围的代码:

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 真的很慢。还有什么我可以尝试的吗?我应该放弃那些浏览器吗?

4

3 回答 3

9

更新

一些可以应用的技术

这里有一些优化技术可以用来使这项工作在 FF 和 Safari 中也更加流畅。

话虽这么说:Chrome 的画布实现非常好,而且(目前)比 Firefox 和 Safari 提供的框架要快得多。新的 Opera 使用与 Chrome 相同的引擎,并且(大约?)与 Chrome 一样快。

为了让它在跨浏览器上正常工作,需要做出一些妥协,并且一如既往的质量会受到影响。

我尝试演示的技术是:

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

瓶颈

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

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

当你需要尽可能地压缩一切时,所谓的微优化就变得至关重要(这些在 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,

...

我们在这里使用一个因子来减小实际大小并将最终渲染缩放到屏幕画布上。对于每 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.

检查两个边缘的原始代码 - 这不是必需的,因为球一次只能穿过一个边缘(每个轴)。

metabolize 函数修改如下:

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 通道的像素值。我们乘法而不是潜水,因为乘法要快一点。60.1667

我们已经缓存了tresholdQ值(25% threshold)。将缓存的值放入函数中会提高一点速度。

不幸的是,由于此方法基于 Alpha 通道,我们还需要清除主画布。在这种情况下,这有(相对)巨大的惩罚。最佳方案是能够使用可以直接“blit”的纯色,但我没有在这里研究这方面。

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

综上所述

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

正如你所看到的,修改后的代码运行速度比原始代码快几倍,这主要是由于我们在质量和一些优化方面做出的妥协,特别是在梯度方面。

于 2013-06-18T20:37:18.583 回答
1

编程方面有改进的余地,在绘制粒子部分。

而不是使用

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

你可以用这个

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

这将减少对 if 的条件检查的一步,以及减少一个值并检查的 (--) 运算符。这将降低复杂性

当你有 (--) 可以被删除时,所有做的事情,通过简单的语句,这将降低这个特定代码的循环复杂度,并使这个代码更快。

最终,这将通过更快的代码处理和更少的 CPU 和资源使用来提高性能。尽管 Ken 的回答也是有效的,但我又创建了一个类似于您的示例站点的小提琴,速度更快。

fiddle

如果有任何问题,请发表评论,并使用游戏代码更新小提琴以进行性能检查。

于 2013-08-29T07:08:50.190 回答
1

这个循环已经很简单了,使用了 JIT 喜欢的稳定类型,所以我认为你不会得到显着的改进。

我已经消除+3并展开了一点(假设宽度*高度可以被 4 整除)。我在|0整数中添加了“强制转换”,使其在 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;
    }
}

如果您需要它更快,那么也许使用较低分辨率的画布来获得效果?

于 2013-09-01T21:50:59.077 回答