在HTML5 Canvas中提升10,000个粒子的性能

3

我有两个JS Fiddles,都有10,000个雪花在移动,但使用了两种不同的方法。

第一个fiddle: http://jsfiddle.net/6eypdhjp/

使用fillRect和4x4白色正方形,每秒提供大约60帧@ 10,000个雪花。

所以我想知道能否改进这一点,并在HTML5Rocks网站上找到了一些关于画布性能的信息。其中一个建议是将雪花预先渲染到画布上,然后使用drawImage绘制画布。

建议在此处http://www.html5rocks.com/en/tutorials/canvas/performance/,即在标题Pre-render to an off-screen canvas下。使用Ctrl + f查找该部分。

所以我尝试了他们的建议,使用这个fiddle:http://jsfiddle.net/r973sr7c/

然而,当有10,000个雪花时,我只能得到大约3帧每秒。这非常奇怪,因为jsPerf甚至显示在使用相同方法时性能有所提升http://jsperf.com/render-vs-prerender

我用于预渲染的代码在这里:

//snowflake particles
var mp = 10000; //max particles
var particles = [];
for(var i = 0; i < mp; i++) {
    var m_canvas        = document.createElement('canvas');
        m_canvas.width  = 4;
        m_canvas.height = 4;
    var tmp             = m_canvas.getContext("2d");
        tmp.fillStyle   = "rgba(255,255,255,0.8)";
        tmp.fillRect(0,0,4,4);
    
    particles.push({
        x  : Math.random()*canvas.width, //x-coordinate
        y  : Math.random()*canvas.height, //y-coordinate
        r  : Math.random()*4+1, //radius
        d  : Math.random()*mp, //density
        img: m_canvas //tiny canvas
    })
}   
//Lets draw the flakes
function draw()    {   
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for(var i = 0; i < particles.length; i++)       {
        var flake = particles[i];
        ctx.drawImage(flake.img, flake.x,flake.y);    
    }
}

所以我想知道为什么我的帧率这么低?有没有更好的方法在屏幕上移动更多的粒子同时保持60帧每秒?

1
我可能错了,但是创建1000个“canvas”元素对我来说不太对。应该运行“document.createElement('canvas')” 1000次吗? - James Taylor
@JamesTaylor 嗯,那就是我对html5Rock的例子的理解…可能我误解了吗? - Sir
@topheman 所以预渲染只在特定情况下使用? - Sir
1
不过在html5rocks的例子中,他们会在画布上预渲染1张图像,然后重复使用这个相同的图像作为指针来创建场景。你应该做同样的事情(我怀疑你不需要10000种粒子,如果只是半径在1到4之间变化的话)。你的例子的问题在于你随机改变颜色,所以很难统一粒子。 - topheman
@topheman 我不改变颜色?也不改变它们的大小。只改变它们的位置。请注意,mp 的 for 循环仅在 windows.onload 时运行。这只是为了设置粒子。 - Sir
显示剩余3条评论
3个回答

5
最佳帧速率是通过绘制预渲染图像(或预渲染画布)来实现的。
您可以重构代码:
- 创建大约2-3个离屏(内存中的)画布,每个画布上绘制1/3的粒子。 - 为每个画布分配一个下落速度和漂移速度。 - 在每个动画帧中,将每个离屏画布(根据其自己的下落速度和漂移速度偏移)绘制到屏幕画布上。 结果应该达到大约60帧每秒。
这种技术以增加的内存使用量换取了最大的帧速率。
以下是示例代码和演示:

        var canvas=document.getElementById("canvas");
        var ctx=canvas.getContext("2d");
        var cw=canvas.width;
        var ch=canvas.height;

var mp=10000;
var particles=[];
var panels=[];
var panelCount=2;
var pp=panelCount-.01;
var maxFallrate=2;
var minOffsetX=-parseInt(cw*.25);
var maxOffsetX=0;

// create all particles
for(var i=0;i<mp;i++){
  particles.push({
   x: Math.random()*cw*1.5,  //x-coordinate
   y: Math.random()*ch, //y-coordinate
   r: 1, //radius
   panel: parseInt(Math.random()*pp) // panel==0 thru panelCount
  })
}

// create a canvas for each panel
var drift=.25;
for(var p=0;p<panelCount;p++){
    var c=document.createElement('canvas');
    c.width=cw*1.5;
    c.height=ch*2;
    var offX=(drift<0)?minOffsetX:maxOffsetX;
    panels.push({
        canvas:c,
        ctx:c.getContext('2d'),
        offsetX:offX,
        offsetY:-ch,
        fallrate:2+Math.random()*(maxFallrate-1),
        driftrate:drift
    });
    // change to opposite drift direction for next panel
    drift=-drift;
}

// pre-render all particles
// on the specified panel canvases
for(var i=0;i<particles.length;i++){
    var p=particles[i];
    var cctx=panels[p.panel].ctx;
    cctx.fillStyle='white';
    cctx.fillRect(p.x,p.y,1,1);
}

// duplicate the top half of each canvas
// onto the bottom half of the same canvas
for(var p=0;p<panelCount;p++){
    panels[p].ctx.drawImage(panels[p].canvas,0,ch);
}

// begin animating
drawStartTime=performance.now();
requestAnimationFrame(animate);


function draw(time){
    ctx.clearRect(0,0,cw,ch);
    for(var i=0;i<panels.length;i++){
        var panel=panels[i];
        ctx.drawImage(panel.canvas,panel.offsetX,panel.offsetY);
    }
}

function animate(time){
    for(var i=0;i<panels.length;i++){

        var p=panels[i];

        p.offsetX+=p.driftrate;
        if(p.offsetX<minOffsetX || p.offsetX>maxOffsetX){
            p.driftrate*=-1;
            p.offsetX+=p.driftrate;
        }

        p.offsetY+=p.fallrate;
        if(p.offsetY>=0){p.offsetY=-ch;}

        draw(time);

    }
    requestAnimationFrame(animate);
}
body{ background-color:#6b92b9; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>


有趣的是,如果我提高帧率,雪会移动得更快=/ 我正在尝试推动它的极限,所以我把RAF切换回了setTimeout。 - Sir
你可以调整fallrate属性以适应不同的帧速率,使你的雪花落下的效果符合你的期望。干杯! - markE

1
我认为你不想每次创建一个新的canvas元素。这样做会导致性能严重下降。
当我将此代码移出循环时,性能立即得到改善。我认为这样做将使您能够优化代码以实现预期的行为:
var m_canvas = document.createElement('canvas');
    m_canvas.width = 4;
    m_canvas.height = 4;
var tmp = m_canvas.getContext("2d");
    tmp.fillStyle = "rgba(255,255,255,0.8)";
    tmp.fillRect(0, 0, 4, 4);

请查看这个经过修改的JSFiddle

希望对您有所帮助!


啊,这是一个改进,尽管性能仍然比非预渲染的 fiddle 示例差。 - Sir
嗯,你说得对。虽然一个使用 ctx.drawImage(flake.img, flake.x, flake.y);,另一个使用 ctx.fillRect(p.x,p.y,4,4);draw() 函数中,但修改后的代码和他们的示例看起来很相似。这也可能与此有关。 - James Taylor

1
你需要预渲染多次绘制的元素。
例如,假设你有一个景观,在游戏滚动期间在不同位置绘制相同形式的灌木丛。使用内存画布是可以的。
对于你的代码,应该尝试将雪花分成10个大小。创建10个内存画布。然后在随机位置上绘制这些画布。
换句话说,你要复制10个画布1,000次。而不是复制10,000个画布10,000次。

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