简体   繁体   English

帆布颗粒,碰撞和性能

[英]Canvas particles, collisions and performance

I'm creating a web app which has an interactive background with particles bouncing around. 我正在创建一个Web应用程序,它具有交互式背景,粒子在弹跳。 At all times there are about 200 circular particles on the screen and at most around 800 particles. 在任何时候屏幕上都有大约200个圆形颗粒,最多约800个颗粒。 Some of the collisions and effects that are being run for the particles are the following prototypes. 正在为粒子运行的一些碰撞和效果是以下原型。 I wonder if I could improve the performance by using web workers to do these calculations? 我想知道我是否可以通过使用网络工作人员来进行这些计算来提高性能?

/**
*   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;
            }

        }
    }
}

Furthermore in case anyone wonders I have 3 canvas layers & I'll add the particles rendering function and the clear function for all canvas layers 此外,万一有人想知道我有3个画布层,我将为所有画布层添加粒子渲染功能和清晰功能

  1. Background which draws a full screen radial gradient & particles 绘制全屏径向渐变和粒子的背景

  2. Menu canvas 菜单画布

  3. Menu button overlay selectors (show which menu is active etc) 菜单按钮覆盖选择器(显示哪个菜单处于活动状态等)


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

Any other optimization tips will be greatly appreciated. 任何其他优化技巧将不胜感激。 I've read a couple of articles but I'm not sure how to optimize this code further. 我已经阅读了几篇文章,但我不确定如何进一步优化此代码。

You can use FabricJS Canvas Library. 您可以使用FabricJS Canvas Library。 FabricJS by default supports interactivity, when you create a new object (circle, rectangle and etc) you can manipulate it by mouse or touchscreen. 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); 

See, we work there in object oriented way. 看,我们以面向对象的方式在那里工作。

I don't know what major improvement you can do here except switching to a technology that uses hardware acceleration. 除了切换到使用硬件加速的技术之外,我不知道你可以做些什么重大改进。

I hope this helps a bit, though as stated in question's comments WebGL would be really faster. 我希望这会有所帮助,但正如问题所述,WebGL会更快。 If you don't know where to start, here is a good one: webglacademy 如果你不知道从哪里开始,这里是一个很好的: webglacademy

Still I saw some little thingies: 我还是看到了一些小东西:

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;
    },

Another little thingy: 另一件小事:

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

}

If you are looking to speed up code, here are some micro-optimizations: 如果您希望加速代码,这里有一些微优化:

  • for(var i = 0, l = bla.length; i < l; i++) { ... } instead of bla.forEach(...) for(var i = 0, l = bla.length; i < l; i++) { ... }而不是bla.forEach(...)
  • reduce callback usage. 减少回调使用量。 Inline simple stuff. 内联简单的东西。
  • comparing against distance is slow because of the SQRT. 由于SQRT,与距离的比较是缓慢的。 radius <= distance is slow, radius*radius <= distanceSquared is fast. radius <= distance慢, radius*radius <= distanceSquared很快。
  • calculating the distance is done by calculating the difference. 通过计算差异来计算距离。 You now do 2 function calls, first to get the distance, then to get the difference. 你现在做2个函数调用,首先得到距离,然后得到差异。 here's a small rewrite: No function calls, No unnecessary calculations. 这是一个小的重写:没有函数调用,没有不必要的计算。

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

To speed up drawing (or make it lag less during drawing): 为了加快绘图速度(或在绘图过程中减少绘图):

  • Separate your calculations from your drawing 将计算与绘图分开
  • Only request a redraw after you have updated your calculations 只有在更新计算后才请求重绘

And for the whole animation: 而对于整个动画:

  • this.backgroundParticles.forEach(..) : in case of 200 particles, this will do this.backgroundParticles.forEach(..) :在200个粒子的情况下,这样做
    • 200 particles times ( this.backgroundParticles.forEach( ) 200个粒子次数( this.backgroundParticles.forEach(
      • 200 particles ( that.backgroundParticles.motion.forEach ) 200个粒子( that.backgroundParticles.motion.forEach
      • 200 particles ( that.backgroundParticles.fx.forEach ) 200个粒子( that.backgroundParticles.fx.forEach
      • 200 particles ( that.backgroundParticles.collision.forEach ) 200个粒子( that.backgroundParticles.collision.forEach
  • same goes for this.foregroundparticles.forEach(..) 同样适用于this.foregroundparticles.forEach(..)
  • let's say we have 200 background and 100 foreground, that is (200*200*3) + (100*100*3) callbacks, which is 150000 callbacks, per tick. 假设我们有200个背景和100个前景,即(200 * 200 * 3)+(100 * 100 * 3)回调,即每个刻度150000个回调。 And we haven't actually calculated a single thing yet, haven't displayed anything either. 我们实际上还没有计算过任何东西,也没有显示任何东西。
  • Run it at 60fps and you are up to 9million callbacks a second. 以60fps的速度运行, 每秒钟可以回收900万次回调。 I think you can spot a problem here. 我想你可以在这里发现一个问题。
  • Stop passing strings around in those function calls too. 停止在这些函数调用中传递字符串。

To get this more performance, remove the OOP stuff and go for ugly spaghetti code, only where it makes sense. 为了获得更高的性能,删除OOP的东西,并只在有意义的地方寻找丑陋的意大利面条代码。

Collision detection can be optimized by not testing every particle against each other. 可以通过不对每个粒子彼此进行测试来优化碰撞检测。 Just look up quadtrees. 只需查看四叉树。 Not that hard to implement, and the basics of it can be used to come up with a custom solution. 并不难实现,并且可以使用它的基础来提出自定义解决方案。

Since you are doing quite some vector math, try out the glmatrix library . 由于你正在做一些矢量数学,试试glmatrix库 Optimized vector math :-) 优化的矢量数学:-)

I think you might find that webworker support is about equal to WebGL support: 我想您可能会发现webworker支持与WebGL支持相同:

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

On the surface they may appear to be different, but they aren't really. 从表面上看,它们看起来可能不同,但实际上并非如此。 The only thing you'll gain is IE10 support temporarily. 你唯一能获得的是暂时的IE10支持。 IE11 has already surpassed IE10 in market share and the divide will continue to grow. IE11的市场份额已经超过IE10,并且这一鸿沟将继续增长。 The only thing to note is that webgl support also seems to be based on updated graphics card drivers. 唯一需要注意的是webgl支持似乎也基于更新的显卡驱动程序。

Of course, I don't know your specific needs so maybe this doesn't work. 当然,我不知道你的具体需求,所以这可能不起作用。

Options 选项

Wait what? 等什么? 200 items on screen is slow? 屏幕上有200个项目很慢?

Do less in canvas land and do the cool stuff in WebGL 在画布领域做得少,在WebGL中做很酷的事情

Many libraries do this. 许多图书馆都这样做。 Canvas should be usable and just a little cool. 画布应该可以使用,只是有点酷。 WebGL generally has all the cool particle features. WebGL通常具有所有很酷的粒子特征。

WebWorkers WebWorkers

You'll likely need to use a deferred library or create a system that figures out when all the webworkers are done and have a pool of worker threads. 您可能需要使用延迟库或创建一个系统来确定所有Web工作者何时完成并拥有工作线程池。

Some caveats: 一些警告:

  1. You can't access anything from your main application, and must communicate through events 您无法从主应用程序访问任何内容,并且必须通过事件进行通信
  2. Objects passed through a webworker are copied not shared 通过webworker传递的对象不会被共享复制
  3. Setting up webworkers without a separate script can require some research 在没有单独脚本的情况下设置Web工​​作者可能需要一些研究

Unconfirmed Rumor: I've heard that there's a limited amount of data you can pass through the web worker messages. 未经证实的谣言:我听说您可以通过网络工作人员消息传递的数据量有限。 You should test this since it seems directly applicable to your use case. 您应该对此进行测试,因为它似乎直接适用于您的用例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM