画布粒子、碰撞和性能

Posted

技术标签:

【中文标题】画布粒子、碰撞和性能【英文标题】:Canvas particles, collisions and performance 【发布时间】:2014-12-28 15:31:45 【问题描述】:

我正在创建一个 Web 应用程序,该应用程序具有交互式背景,粒子四处弹跳。屏幕上始终有大约 200 个圆形粒子,最多大约 800 个粒子。正在为粒子运行的一些碰撞和效果是以下原型。我想知道是否可以通过使用网络工作者进行这些计算来提高性能?

/**
*   Particles
*/

Jarvis.prototype.genForegroundParticles = function(options, count)

    count = count || this.logoParticlesNum;

    for (var i = 0; i < count; i++) 
        this.logoParticles.push(new Particle());
    



Jarvis.prototype.genBackgroundParticles = function(options, count)

    count = count || this.backgroundParticlesNum;

    for (var i = 0; i < count; i++) 
        this.backgroundParticles.push(new Particle(options));
    



Jarvis.prototype.motion = 
    linear : function(particle, pIndex, particles)
        particle.x += particle.vx
        particle.y += particle.vy
    ,
    normalizeVelocity : function(particle, pIndex, particles)

        if (particle.vx - particle.vxInitial > 1) 
            particle.vx -= 0.05;
         else if (particle.vx - particle.vxInitial < -1) 
            particle.vx += 0.05;
        

        if (particle.vy - particle.vyInitial > 1) 
            particle.vy -= 0.05;
         else if (particle.vx - particle.vxInitial < -1) 
            particle.vy += 0.05;
        

    ,
    explode : function(particle, pIndex, particles) 

        if (particle.isBottomOut()) 
            particles.splice(pIndex, 1);
         else 
            particle.x += particle.vx;
            particle.y += particle.vy;
            particle.vy += 0.1;
        

        if (particles.length === 0)
            particles.motion.removeMotion("explode");
            this.allowMenu = true;
               

    


Jarvis.prototype.collision = 
    boundingBox: function(particle, pIndex, particles)

        if (particle.y > (this.HEIGHT - particle.radius) || particle.y < particle.radius) 
            particle.vy *= -1;
        

        if(particle.x > (this.WIDTH - particle.radius) || particle.x < particle.radius) 
            particle.vx *= -1;
        
    ,
    boundingBoxGravity: function(particle, pIndex, particles)
        // TODO: FIX GRAVITY TO WORK PROPERLY IN COMBINATION WITH FX AND MOTION
        if (particle.y > (this.HEIGHT - particle.radius) || particle.y < particle.radius) 
            particle.vy *= -1;
            particle.vy += 5;
         

        if(particle.x > (this.WIDTH - particle.radius) || particle.x < particle.radius) 
            particle.vx *= -1;
            particle.vx += 5;
        

    ,
    infinity: function(particle, pIndex, particles)

        if (particle.x > this.WIDTH)
            particle.x = 0;
        

        if (particle.x < 0)
            particle.x = this.WIDTH;
        

        if (particle.y > this.HEIGHT)
            particle.y = 0;
               

        if (particle.y < 0) 
            particle.y = this.HEIGHT;
        

    


Jarvis.prototype.fx = 
    link : function(particle, pIndex, particles)

        for(var j = pIndex + 1; j < particles.length; j++) 

            var p1 = particle;
            var p2 = particles[j];
            var particleDistance = getDistance(p1, p2);

            if (particleDistance <= this.particleMinLinkDistance) 
                this.backgroundCtx.beginPath();
                this.backgroundCtx.strokeStyle = "rgba("+p1.red+", "+p1.green+", "+p1.blue+","+ (p1.opacity - particleDistance / this.particleMinLinkDistance) +")";
                this.backgroundCtx.moveTo(p1.x, p1.y);
                this.backgroundCtx.lineTo(p2.x, p2.y);
                this.backgroundCtx.stroke();
                this.backgroundCtx.closePath();
            
        
    ,
    shake : function(particle, pIndex, particles)

        if (particle.xInitial - particle.x >= this.shakeAreaThreshold)
            particle.xOper = (randBtwn(this.shakeFactorMin, this.shakeFactorMax) * 2) % (this.WIDTH);
         else if (particle.xInitial - particle.x <= -this.shakeAreaThreshold) 
            particle.xOper = (randBtwn(-this.shakeFactorMax, this.shakeFactorMin) * 2) % (this.WIDTH);
        

        if (particle.yInitial - particle.y >= this.shakeAreaThreshold)
            particle.yOper = (randBtwn(this.shakeFactorMin, this.shakeFactorMax) * 2) % (this.HEIGHT);
         else if (particle.yInitial - particle.y <= -this.shakeAreaThreshold) 
            particle.yOper = (randBtwn(-this.shakeFactorMax, this.shakeFactorMin) * 2) % (this.HEIGHT);
               

        particle.x += particle.xOper;
        particle.y += particle.yOper;

    ,
    radialWave : function(particle, pIndex, particles)

        var distance = getDistance(particle, this.center);

        if (particle.radius >= (this.dim * 0.0085)) 
            particle.radiusOper = -0.02;
         else if (particle.radius <= 1) 
            particle.radiusOper = 0.02;
        

        particle.radius += particle.radiusOper * particle.radius;
    ,
    responsive : function(particle, pIndex, particles)

        var newPosX = (this.logoParticles.logoOffsetX + this.logoParticles.particleRadius) + (this.logoParticles.particleDistance + this.logoParticles.particleRadius) * particle.arrPos.x;
        var newPosY = (this.logoParticles.logoOffsetY + this.logoParticles.particleRadius) + (this.logoParticles.particleDistance + this.logoParticles.particleRadius) * particle.arrPos.y;

        if (particle.xInitial !== newPosX || particle.yInitial !== newPosY)

            particle.xInitial = newPosX;
            particle.yInitial = newPosY;
            particle.x = particle.xInitial;
            particle.y = particle.yInitial;

        

    ,
    motionDetect : function(particle, pIndex, particles)

        var isClose = false;
        var distance = null;

        for (var i = 0; i < this.touches.length; i++) 

            var t = this.touches[i];

            var point = 
                x : t.clientX,
                y : t.clientY
            

            var d = getDistance(point, particle); 

            if (d <= this.blackhole) 
                isClose = true;

                if (d <= distance || distance === null) 
                    distance = d;
                

              

        

        if (isClose)
            if (particle.radius < (this.dim * 0.0085)) 
                particle.radius += 0.25;
            
            if (particle.green >= 0 && particle.blue >= 0) 
                particle.green -= 10;
                particle.blue -= 10;
                       
         else 
            if (particle.radius > particle.initialRadius) 
                particle.radius -= 0.25;
            
            if (particle.green <= 255 && particle.blue <= 255) 
                particle.green += 10;
                particle.blue += 10;
                       
        

    ,
    reverseBlackhole : function(particle, pIndex, particles)

        for (var i = 0; i < this.touches.length; i++) 

            var t = this.touches[i];

            var point = 
                x : t.clientX,
                y : t.clientY
             

            var distance = getDistance(point, particle);

            if (distance <= this.blackhole)

                var diff = getPointsDifference(point, particle);

                particle.vx += -diff.x / distance;
                particle.vy += -diff.y / distance;
            

        
    

此外,如果有人想知道我有 3 个画布层,我将添加粒子渲染功能 以及所有画布层的清除功能

    绘制全屏径向渐变和粒子的背景

    菜单画布

    菜单按钮覆盖选择器(显示哪个菜单处于活动状态等)


Jarvis.prototype.backgroundDraw = function() 

    // particles

    var that = this;

    this.logoParticles.forEach(function(particle, i)

        particle.draw(that.backgroundCtx);

        that.logoParticles.motion.forEach(function(motionType, motionIndex)
            that.motion[motionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
        );
        that.logoParticles.fx.forEach(function(fxType, fxIndex)
            that.fx[fxType].call(that, particle, i, that.logoParticles, "foregroundParticles");
        );
        that.logoParticles.collision.forEach(function(collisionType, collisionIndex)
            that.collision[collisionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
        );
    );

    this.backgroundParticles.forEach(function(particle, i)

        particle.draw(that.backgroundCtx);

        that.backgroundParticles.motion.forEach(function(motionType, motionIndex)
            that.motion[motionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
        );
        that.backgroundParticles.fx.forEach(function(fxType, fxIndex)
            that.fx[fxType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
        );
        that.backgroundParticles.collision.forEach(function(collisionType, collisionIndex)
            that.collision[collisionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
        );
    );



Jarvis.prototype.clearCanvas = function() 

    switch(this.background.type)
        case "radial_gradient":
            this.setBackgroundRadialGradient(this.background.color1, this.background.color2);
            break;
        case "plane_color":
            this.setBackgroundColor(this.background.red, this.background.green, this.background.blue, this.background.opacity);
            break;
        default:
            this.setBackgroundColor(142, 214, 255, 1);
    

    this.foregroundCtx.clearRect(this.clearStartX, this.clearStartY, this.clearDistance, this.clearDistance);
    this.middlegroundCtx.clearRect(this.clearStartX, this.clearStartY, this.clearDistance, this.clearDistance);


Jarvis.prototype.mainLoop = function() 
    this.clearCanvas();
    this.backgroundDraw();
    this.drawMenu();
    window.requestAnimFrame(this.mainLoop.bind(this));

我们将不胜感激任何其他优化技巧。我已经阅读了几篇文章,但我不确定如何进一步优化此代码。

【问题讨论】:

您是否研究过 WebGL 作为优化粒子的平台? 老实说不,我之所以不这样做是因为我不确定笔记本电脑和台式机以外的设备是否支持 webGL 你提到你的粒子系统是“交互式的”。这是否包括用户或代码的影响——如果是的话,如何?或者你的粒子系统是完全自主的——粒子只通过与其他粒子的碰撞做出反应? :-) 我在 mousemove 和 touchmove 上添加了事件侦听器,这会对粒子产生爆炸效果。此外,到目前为止,我会尽快接受最佳答案。 【参考方案1】:

您可以使用FabricJS 画布库。 FabricJS 默认支持交互性,当你创建一个新对象(圆形、矩形等)时,你可以通过鼠标或触摸屏来操作它。

var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect(
    width: 10, height: 20,
    left: 100, top: 100,
    fill: 'yellow',
    angle: 30
);

canvas.add(rect); 

看,我们以面向对象的方式在那里工作。

【讨论】:

不错的选择 :) +1【参考方案2】:

如果您希望加快代码速度,这里有一些微优化:

for(var i = 0, l = bla.length; i &lt; l; i++) ... 而不是 bla.forEach(...) 减少回调使用。内联简单的东西。 由于 SQRT,比较距离很慢。 radius &lt;= distance 慢,radius*radius &lt;= distanceSquared 快。 计算距离是通过计算差异来完成的。您现在执行 2 个函数调用,首先获取距离,然后获取差异。这是一个小的重写:没有函数调用,没有不必要的计算。

reverseBlackhole : function(particle, pIndex, particles) var blackholeSqr = this.blackhole * this.blackhole, touches = this.touches, fnSqrt = Math.sqrt, t, diffX, diffY, dstSqr; for (var i = 0, l = touches.length; i < l; i++) t = touches[i]; diffX = particle.x - t.clientX; diffY = particle.y - t.clientY; distSqr = (diffX * diffX + diffY * diffY); // comparing distance without a SQRT needed if (dstSqr <= blackholeSqr) var dist = Math.sqrt(dstSqr); particle.vx -= diffX / dist; particle.vy -= diffY / dist;

加快绘图速度(或减少绘图过程中的延迟):

将计算与绘图分开 只有在更新计算后才请求重绘

对于整个动画:

this.backgroundParticles.forEach(..): 如果有 200 个粒子,就可以了 200个粒子次(this.backgroundParticles.forEach() 200 个粒子 (that.backgroundParticles.motion.forEach) 200 个粒子 (that.backgroundParticles.fx.forEach) 200 个粒子 (that.backgroundParticles.collision.forEach) this.foregroundparticles.forEach(..) 也是如此 假设我们有 200 个背景和 100 个前景,即 (200*200*3) + (100*100*3) 回调,即 150000 个回调,每个滴答。而且我们实际上还没有计算出任何东西,也没有显示任何东西。 以 60fps 的速度运行它,每秒最多有 900 万次 回调。我想你可以在这里发现问题。 也停止在这些函数调用中传递字符串。

要获得更高的性能,请删除 OOP 内容并使用丑陋的意大利面条代码,只在有意义的地方。

可以通过不对每个粒子进行相互测试来优化碰撞检测。只需查找四叉树。实现起来并不难,而且它的基础知识可以用来提出自定义解决方案。

由于您正在做一些矢量数学,请尝试glmatrix library。优化向量数学:-)

【讨论】:

它呈指数增长。 800x800x3 = 每帧 1920000 次回调,60fps...你算算:-) 既然您将我标记为答案:您到底做了什么改变,它对速度的提高有多大?只是好奇:-)【参考方案3】:

除了切换到使用硬件加速的技术之外,我不知道您可以在此处进行哪些重大改进。

我希望这会有所帮助,尽管如问题中所述的 cmets WebGL 会更快。如果你不知道从哪里开始,这里有个不错的选择:webglacademy

我还是看到了一些小东西:

radialWave : function(particle, pIndex, particles)

        // As you don't use distance here remove this line
        // it's a really greedy calculus that involves square root
        // always avoid if you don't have to use it

        // var distance = getDistance(particle, this.center);

        if (particle.radius >= (this.dim * 0.0085)) 
            particle.radiusOper = -0.02;
         else if (particle.radius <= 1) 
            particle.radiusOper = 0.02;
        

        particle.radius += particle.radiusOper * particle.radius;
    ,

另一件小事:

Jarvis.prototype.backgroundDraw = function() 

    // particles

    var that = this;

    // Declare callbacks outside of forEach calls
    // it will save you a function declaration each time you loop

    // Do this for logo particles
    var logoMotionCallback = function(motionType, motionIndex)
        // Another improvement may be to use a direct function that does not use 'this'
        // and instead pass this with a parameter called currentParticle for example
        // call and apply are known to be pretty heavy -> see if you can avoid this
        that.motion[motionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
    ;

    var logoFxCallback = function(fxType, fxIndex)
        that.fx[fxType].call(that, particle, i, that.logoParticles, "foregroundParticles");
    ;

    var logoCollisionCallback = function(collisionType, collisionIndex)
        that.collision[collisionType].call(that, particle, i, that.logoParticles, "foregroundParticles");
    ;

    this.logoParticles.forEach(function(particle, i)

        particle.draw(that.backgroundCtx);

        that.logoParticles.motion.forEach(motionCallback);
        that.logoParticles.fx.forEach(fxCallback);
        that.logoParticles.collision.forEach(collisionCallback);
    );

    // Now do the same for background particles
    var bgMotionCallback = function(motionType, motionIndex)
            that.motion[motionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
    ;

    var bgFxCallback = function(fxType, fxIndex)
        that.fx[fxType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
    ;

    var bgCollisionCallback = function(collisionType, collisionIndex)
        that.collision[collisionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles");
    ;

    this.backgroundParticles.forEach(function(particle, i)

        particle.draw(that.backgroundCtx);

        that.backgroundParticles.motion.forEach(bgMotionCallback);
        that.backgroundParticles.fx.forEach(bgFxCallback);
        that.backgroundParticles.collision.forEach(bgCollisionCallback);
    );


【讨论】:

【参考方案4】:

我想你可能会发现 webworker 的支持与 WebGL 的支持差不多:

WebGL 支持:http://caniuse.com/#search=webgl WebWorker 支持:http://caniuse.com/#search=webworker

表面上它们可能看起来不同,但实际上并非如此。您将获得的唯一一件事是暂时支持 IE10。 IE11 的市场份额已经超过 IE10,而且差距还会继续扩大。唯一需要注意的是,webgl 支持似乎也是基于更新的显卡驱动程序。

当然,我不知道您的具体需求,所以也许这行不通。

选项

等等什么?屏幕显示 200 项很慢?

对 DoKick 所说的一切 +1 您是否考虑过使用类型化数组:https://developer.mozilla.org/en-US/docs/Web/javascript/Reference/Global_Objects/Float32Array 来拥有一个良好的 API 并使用它,您需要经历一些困难。 我会看看这个例子,因为它做了很多类似的事情:http://threejs.org/examples/#canvas_particles_shapes

在画布领域少做事,在 WebGL 中做一些很酷的事情

许多图书馆都这样做。画布应该可用并且有点酷。 WebGL 通常具有所有很酷的粒子功能。

网络工作者

您可能需要使用延迟库或创建一个系统来确定所有网络工作者何时完成并拥有一个工作线程池。

一些注意事项:

    您无法从主应用程序访问任何内容,并且必须通过事件进行通信 通过网络工作者传递的对象被复制而不是共享 在没有单独脚本的情况下设置网络工作者可能需要一些研究

未经证实的谣言:我听说您可以通过网络工作者消息传递的数据量有限。您应该对此进行测试,因为它似乎直接适用于您的用例。

【讨论】:

这里确认,如果必须将最少的数据量编组到 webworker,然后再编组回 UI 线程,那么 web worker 的性能最好。另外,请记住,webworkers 在线程上运行,如果设备只有一个 CPU(例如移动设备),那么 webworkers 和 UI 必须共享可用的 CPU 时间片。 ;-)

以上是关于画布粒子、碰撞和性能的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D之怎么实现粒子特效的碰撞

Unity使用ParticleSystem粒子系统模拟药水在血管中流动(粒子碰撞)

Unity使用ParticleSystem粒子系统模拟药水在血管中流动(粒子碰撞)

Particles.js基于Canvas画布创建粒子原子颗粒效果

如何修复画布 html5 中的性能滞后?

游戏中实现粒子碰撞,纯java