我试图在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/中描述的技术来提高其性能,但在此使用相同的方法实际上会降低性能(也许我做错了?)< / p>
我还研究了网络工作者,看看我是否可以分割负载(因为代码分别针对每个像素运行)但是我在这个链接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并且不会更快。我无法想到达到预期效果的替代方案。目前的代码在谷歌浏览器上运行完美,但Safari和Firefox真的很慢。还有什么我可以尝试的吗?我应该放弃这些浏览器吗?
答案 0 :(得分:9)
<强>更新强>
以下是一些优化技术,可用于使这项工作在FF和Safari中更加流畅。
话虽如此:Chrome浏览器的画布实现非常好,而且比Firefox和Safari提供的骨头快得多(目前)。新的Opera使用与Chrome相同的引擎,并且(大约?)与Chrome的速度一样快。
为了实现良好的跨浏览器,需要做出一些妥协,并且始终质量会受到影响。
我尝试演示的技巧是:
drawImage()
更新主画布requestAnimationFrame()
while
循环为每个元球生成渐变的成本很高。因此,当我们一劳永逸地缓存这一点时,我们只会注意到性能的巨大提升。
另一点是getImageData
和putImageData
以及我们需要使用高级语言迭代低级字节数组的事实。幸运的是,阵列是类型阵列,所以有一点帮助,但除非我们牺牲更多的质量,否则我们无法从中获得更多。
当你需要挤压所有东西时,所谓的微观优化就变得至关重要(这些有着不应有的不良声誉IMO)。
从你的帖子的印象:你似乎非常接近这个工作,但从提供的代码我看不出会出现什么问题。
在任何情况下 - 这是一个实际的实现(基于您所引用的代码):
<强> Fiddle demo 强>
在初始步骤中预先计算变量 - 我们可以预先计算的所有内容有助于我们以后直接使用该值:
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因子,您可以指数级地保存4x像素。我在演示中将其预设为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通道的像素值,因为我们使用它超过两次。 6
上的潜水不是与<{1}}相乘,因为乘法会快一点。
我们已经缓存了0.1667
值(tresholdQ
的25%)。将缓存值放在函数中可以提高速度。
不幸的是,由于此方法基于alpha通道,我们还需要清除主画布。在这种情况下,这会产生(相对)巨大的惩罚。最佳的是能够使用纯色,你可以&#34; blit&#34;直接,但我没有在这里看到这个方面。
您也可以将点数据放在数组而不是对象中。但是,由于这种情况很少,在这种情况下可能不值得。
我可能错过了一两个(或更多)可以进一步优化的地方,但你明白了。
正如您所看到的,修改后的代码运行速度比原始代码快几倍,这主要是由于我们在这里做出的妥协,质量和一些优化,尤其是渐变。
答案 1 :(得分:1)
在绘制粒子部分时,编程有一定的改进。
而不是使用
if(leftover > 0) {
do {
this.process(i++);
} while(--leftover > 0);
}
你可以使用这个
while(leftover > 0) {
this.process(i++);
leftover --;
}
这将减少if的一个条件检查步骤以及减少一个值并检查的( - )运算符。这将降低复杂性
尽管你可以删除( - ),但只需简单的语句就可以减少这个特定代码的Cyclomatic Complexity并使代码更快。
最终,这将通过更快地处理代码并减少CPU和资源的使用来提高性能。虽然肯的答案也是有效的,但我创造了一个与你的样本网站相似的小提琴,速度更快。
如果有任何问题请发表评论,并更新游戏代码以进行性能检查。
答案 2 :(得分:1)
这个循环已经非常简单,使用JIT喜欢的稳定类型,所以我认为你不会得到显着的改进。
我已经删除了+3
并将其展开了一些(假设宽度*高度可以被4整除)。我已经将|0
“cast”添加到整数中,这使得它在V8中更加快速。
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;
}
}
如果你需要它的速度更快,那么可能会使用分辨率较低的画布来实现效果吗?
答案 3 :(得分:0)
这可能有助于查看以下内容
<script type="text/javascript" src="dat.gui.js"></script>
<script type="text/javascript">
var FizzyText = function() {
this.message = 'dat.gui';
this.speed = 0.8;
this.displayOutline = false;
this.explode = function() { ... };
// Define render logic ...
};
window.onload = function() {
var text = new FizzyText();
var gui = new dat.GUI();
gui.add(text, 'message');
gui.add(text, 'speed', -5, 5);
gui.add(text, 'displayOutline');
gui.add(text, 'explode');
};
</script>