简体   繁体   English

画布上约有12万个粒子?

[英]About 120 000 particles on canvas?

I have about 120 000 particles (each particle 1px size) that I need to find the best and most important: fastest way to draw to my canvas. 我需要约12万个粒子(每个粒子1px大小),以找到最好和最重要的:最快的绘制到画布上的方法。

How would you do that? 你会怎么做?

Right now I'm basically getting my pixels into an Array, and then I loop over these particles, do some x and y calculations and draw them out using fillRect. 现在,我基本上是将像素放入一个数组中,然后遍历这些粒子,进行一些x和y计算,并使用fillRect绘制它们。 But the framerate is like 8-9 fps right now. 但是现在的帧速率约为8-9 fps。

Any ideas? 有任何想法吗? Please example. 请举个例子。

Thank you 谢谢

LATEST UPDATE (my code) 最新更新(我的代码)

function init(){

    window.addEventListener("mousemove", onMouseMove);

    let mouseX, mouseY, ratio = 2;

    const canvas = document.getElementById("textCanvas");
    const context = canvas.getContext("2d");
    canvas.width = window.innerWidth * ratio;
    canvas.height = window.innerHeight * ratio;

    canvas.style.width = window.innerWidth + "px";
    canvas.style.height = window.innerHeight + "px";

    context.imageSmoothingEnabled = false;
    context.fillStyle = `rgba(255,255,255,1)`;
    context.setTransform(ratio, 0, 0, ratio, 0, 0);

    const width = canvas.width;
    const height = canvas.height;

    context.font = "normal normal normal 232px EB Garamond";
    context.fillText("howdy", 0, 160);

    var pixels = context.getImageData(0, 0, width, height).data;
    var data32 = new Uint32Array(pixels.buffer);

    const particles = new Array();

    for(var i = 0; i < data32.length; i++) {

        if (data32[i] & 0xffff0000) {
            particles.push({
                x: (i % width),
                y: ((i / width)|0),
                ox: (i % width),
                oy: ((i / width)|0),
                xVelocity: 0,
                yVelocity: 0,
                a: pixels[i*4 + 3] / 255
            });
        }
    }

    /*const particles = Array.from({length: 120000}, () => [
        Math.round(Math.random() * (width - 1)),
        Math.round(Math.random() * (height - 1))
    ]);*/

    function onMouseMove(e){
        mouseX = parseInt((e.clientX-canvas.offsetLeft) * ratio);
        mouseY = parseInt((e.clientY-canvas.offsetTop) * ratio);
    }

    function frame(timestamp) {

        context.clearRect(0, 0, width, height);
        const imageData = context.getImageData(0, 0, width, height);
        const data = imageData.data;
        for (let i = 0; i < particles.length; i++) {
            const particle = particles[i];
            const index = 4 * Math.round((particle.x + particle.y * width));

            data[index + 0] = 0;
            data[index + 1] = 0;
            data[index + 2] = 0;
            data[index + 3] = 255;
        }
        context.putImageData(imageData, 0, 0);

        for (let i = 0; i < particles.length; i++) {
            const p = particles[i];

            var homeDX = p.ox - p.x;
            var homeDY = p.oy - p.y;

            var cursorForce = 0;
            var cursorAngle = 0;

            if(mouseX && mouseX > 0){
                var cursorDX = p.ox - mouseX;
                var cursorDY = p.oy - mouseY;
                var cursorDistanceSquared = (cursorDX * cursorDX + cursorDY * cursorDY);
                cursorForce = Math.min(10/cursorDistanceSquared,10);

                cursorAngle = -Math.atan2(cursorDY, cursorDX);
            }else{
                cursorForce = 0;
                cursorAngle = 0;
            }

            p.xVelocity += 0.2 * homeDX + cursorForce * Math.cos(cursorAngle);
            p.yVelocity += 0.2 * homeDY + cursorForce * Math.sin(cursorAngle);

            p.xVelocity *= 0.55;
            p.yVelocity *= 0.55;

            p.x += p.xVelocity;
            p.y += p.yVelocity;
        }
        requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
}

Computing those particles within a shader on a webgl context will provide the most performant solution. webgl上下文中在着色器中计算那些粒子将提供性能最高的解决方案。 See eg https://www.shadertoy.com/view/MdtGDX for an example. 有关示例,请参见例如https://www.shadertoy.com/view/MdtGDX

If you prefer to continue using a 2d context, you could speed up rendering particles by doing so off-screen: 如果您希望继续使用2d上下文,则可以通过在屏幕外进行操作来加快渲染粒子的速度:

  1. Get the image data array by calling context.getImageData() 通过调用context.getImageData()获得图像数据数组。
  2. Draw pixels by manipulating the data array 通过操纵数据数组绘制像素
  3. Put the data array back with context.putImageData() 使用context.putImageData()将数据数组放回

A simplified example: 一个简化的例子:

 const output = document.getElementById("output"); const canvas = document.getElementById("canvas"); const context = canvas.getContext("2d"); const width = canvas.width; const height = canvas.height; const particles = Array.from({length: 120000}, () => [ Math.round(Math.random() * (width - 1)), Math.round(Math.random() * (height - 1)) ]); let previous = 0; function frame(timestamp) { // Print frames per second: const delta = timestamp - previous; previous = timestamp; output.textContent = `${(1000 / delta).toFixed(1)} fps`; // Draw particles: context.clearRect(0, 0, width, height); const imageData = context.getImageData(0, 0, width, height); const data = imageData.data; for (let i = 0; i < particles.length; i++) { const particle = particles[i]; const index = 4 * (particle[0] + particle[1] * width); data[index + 0] = 0; data[index + 1] = 0; data[index + 2] = 0; data[index + 3] = 255; } context.putImageData(imageData, 0, 0); // Move particles randomly: for (let i = 0; i < particles.length; i++) { const particle = particles[i]; particle[0] = Math.max(0, Math.min(width - 1, Math.round(particle[0] + Math.random() * 2 - 1))); particle[1] = Math.max(0, Math.min(height - 1, Math.round(particle[1] + Math.random() * 2 - 1))); } requestAnimationFrame(frame); } requestAnimationFrame(frame); 
 <canvas id="canvas" width="500" height="500"></canvas> <output id="output"></output> 

Instead of drawing individual pixels, you might also want to consider drawing and moving a few textures with a lot of particles on each of them. 除了绘制单个像素,您可能还需要考虑绘制并移动一些纹理,每个纹理上都有很多粒子。 This might come close to a full particle effect at better performance. 这可能会以更好的性能接近完整的粒子效果。

Moving 7.2Million particles a second 每秒移动720万个粒子

Not using webGL and shaders and you want 120K particles per frame at 60fps you need a throughput of 7.2million points per second. 不使用webGL和着色器,并且要以60fps的速度每帧120K粒子,则需要每秒720万点的吞吐量。 You need a fast machine. 您需要一台快速的机器。

Web workers multi-core CPUs Web Worker多核CPU

Quick solutions. 快速解决方案。 On multi core machines web workers give linear performance increase for each hardware core. 在多核机器上,网络工作者会为每个硬件核提供线性的性能提升。 Eg On a 8 Core i7 you can run 7 workers sharing data via sharedArrayBuffers (shame that its all turned of ATM due to CPU security risk see MDN sharedArrayBuffer ) and get slightly lower than 7 times performance improvement. 例如,在8核i7上,您可以运行7个通过sharedArrayBuffers共享数据的工作者(可惜由于CPU安全风险,它全部关闭了ATM,请参阅MDN sharedArrayBuffer ),并且性能略低于7倍。 Note benifits are only from actual hardware cores, JS threads tend to run flat out, Running two workers in one core results in an overall decreased throughput. 请注意,好处仅来自实际的硬件内核,JS线程往往会耗尽资源,在一个内核中运行两个工作线程会导致总体吞吐量降低。

Even with shared buffers turned of it is still a viable solution if you are in control of what hardware you run on. 如果您控制运行的硬件,即使已启用共享缓冲区,它仍然是可行的解决方案。

Make a movie. 拍电影。

LOL but no it is an option, and there is no upper limit to particle count. 大声笑,但不是它是一种选择,并且粒子数没有上限。 Though not as interactive as I think you may want. 虽然不像我想的那样互动。 If you are selling something via the FX you are after a wow, not a how? 如果您通过FX出售商品,那您将大吃一惊,而不是怎么做?

Optimize 优化

Easy to say hard to do. 容易说很难。 You need to go over the code with a fine tooth comb. 您需要使用细齿梳仔细检查代码。 Remember that removing a single line if running at full speed is 7.2million lines removed per second. 请记住,如果全速运行,删除一条线就是每秒删除720万条线。

I have gone over the code one more time. 我再看了一次代码。 I can not test it so it may or may not work. 我无法对其进行测试,因此它可能会或可能不会起作用。 But its to give you ideas. 但它给你的想法。 You could even consider using integer only math. 您甚至可以考虑使用仅整数数学。 JS can do fixed point math. JS可以做定点数学。 The integer size is 32bits way more than you need for even a 4K display. 整数大小是32位,甚至比4K显示器还大。

Second optimization pass. 第二次优化通过。

// call this just once outside the animation loop.
const imageData = this.context.getImageData(0, 0, this.width * this.ratio, this.height * this.ratio);
// create a 32bit buffer
const data32 = new Uint32Array(imageData.data.buffer);
const pixel = 0xFF000000; // pixel to fill
const width = imageData.width;


// inside render loop
data32.fill(0); // clear the pixel buffer

// this line may be a problem I have no idea what it does. I would
// hope its only passing a reference and not creating a copy 
var particles = this.particleTexts[0].getParticles();

var cDX,cDY,mx,my,p,cDistSqr,cForce,i;
mx = this.mouseX | 0; // may not need the floor bitwize or 0
my = this.mouseY | 0; // if mouse coords already integers

if(mX > 0){  // do mouse test outside the loop. Need loop duplication
             // But at 60fps thats 7.2million less if statements
    for (let i = 0; i < particles.length; i++) {
        var p = particles[i];
        p.xVelocity += 0.2 * (p.ox - p.x);
        p.yVelocity += 0.2 * (p.oy - p.y);
        p.xVelocity *= 0.55;
        p.yVelocity *= 0.55;
        data32[((p.x += p.xVelocity) | 0) + ((p.y += p.yVelocity) | 0) * width] = pixel;
    }
}else{
    for (let i = 0; i < particles.length; i++) {
        var p = particles[i];
        cDX = p.x - mx;
        cDY = p.y - my;
        cDist = Math.sqrt(cDistSqr = cDX*cDX + cDY*cDY + 1);
        cForce = 1000 / (cDistSqr * cDist)
        p.xVelocity += cForce * cDx +  0.2 * (p.ox - p.x);
        p.yVelocity += cForce * cDY +  0.2 * (p.oy - p.y);
        p.xVelocity *= 0.55;
        p.yVelocity *= 0.55;
        data32[((p.x += p.xVelocity) | 0) + ((p.y += p.yVelocity) | 0) * width] = pixel;

    }
}
// put pixel onto the display.
this.context.putImageData(imageData, 0, 0);

Above is about as much as I can cut it down. 以上是我所能削减的。 (Cant test it so may or may not suit your need) It may give you a few more frames a second. (无法对其进行测试,可能会满足您的需求,也可能会不满足您的需求)它可能每秒会给您几帧。

Interleaving 交错

Another solution may suit you and that is to trick the eye. 另一种解决方案可能适合您,那就是欺骗眼睛。 This increases frame rate but not points processed and requires that the points be randomly distributed or artifacts will be very noticeable. 这会提高帧速率,但不会处理点,并且要求将点随机分布,否则伪像会非常明显。

Each frame you only process half the particles. 每帧只处理一半的粒子。 Each time you process a particle you calculate the pixel index, set that pixel and then add the pixel velocity to the pixel index and particle position. 每次处理粒子时,都会计算像素索引,设置该像素,然后将像素速度添加到像素索引和粒子位置。

The effect is that each frame only half the particles are moved under force and the other half coast for a frame.. 效果是,每个框架只有一半的粒子在力的作用下移动,而另一半则滑过一个框架。

This may double the frame rate. 这可能会使帧频翻倍。 If your particles are very organised and you get clumping flickering type artifacts, you can randomize the distribution of particles by applying a random shuffle to the particle array on creation. 如果粒子非常有条理,并且出现了闪烁的团块状伪影,则可以通过在创建时对粒子数组应用随机混洗来随机化粒子的分布。 Again this need good random distribution. 同样,这需要良好的随机分布。

The next snippet is just as an example. 下一个片段仅作为示例。 Each particle needs to hold the pixelIndex into the pixel data32 array. 每个粒子都需要将pixelIndex到pixel data32数组中。 Note that the very first frame needs to be a full frame to setup all indexes etc. 请注意,第一帧必须是完整帧才能设置所有索引等。

    const interleave = 2; // example only setup for 2 frames
                          // but can be extended to 3 or 4

    // create frameCount outside loop
    frameCount += 1;

    // do half of all particals
    for (let i = frameCount % frameCount  ; i < particles.length; i += interleave ) {
        var p = particles[i];
        cDX = p.x - mx;
        cDY = p.y - my;
        cDist = Math.sqrt(cDistSqr = cDX*cDX + cDY*cDY + 1);
        cForce = 1000 / (cDistSqr * cDist)
        p.xVelocity += cForce * cDx +  0.2 * (p.ox - p.x);
        p.yVelocity += cForce * cDY +  0.2 * (p.oy - p.y);
        p.xVelocity *= 0.55;
        p.yVelocity *= 0.55;

        // add pixel index to particle's property 
        p.pixelIndex = ((p.x += p.xVelocity) | 0) + ((p.y += p.yVelocity) | 0) * width;
        // write this frames pixel
        data32[p.pixelIndex] = pixel;

        // speculate the pixel index position in the next frame. This need to be as simple as possible.
        p.pixelIndex += (p.xVelocity | 0) + (p.yVelocity | 0) * width;

        p.x += p.xVelocity;  // as the next frame this particle is coasting
        p.y += p.yVelocity;  // set its position now
     }

     // do every other particle. Just gets the pixel index and sets it
     // this needs to remain as simple as possible.
     for (let i = (frameCount + 1) % frameCount  ; i < particles.length; i += interleave)
         data32[particles[i].pixelIndex] = pixel;
     }

Less particles 颗粒少

Seams obvious, but is often over looked as a viable solution. 接缝明显,但通常被视为可行的解决方案。 Less particles does not mean less visual elements/pixels. 更少的粒子并不意味着更少的视觉元素/像素。

If you reduce the particle count by 8 and at setup create a large buffer of offset indexes. 如果将粒子数减少8,并在设置时创建一个较大的偏移索引缓冲区。 These buffers hold animated pixel movements that closely match the behavior of pixels. 这些缓冲区保存与像素行为紧密匹配的动画像素运动。

This can be very effective and give the illusion that each pixels is in fact independent. 这可能非常有效,并给人一种幻想,即每个像素实际上是独立的。 But the work is in the pre processing and setting up the offset animations. 但是工作是在预处理和设置偏移动画中。

eg 例如

   // for each particle after updating position
   // get index of pixel

   p.pixelIndex = (p.x | 0 + p.y | 0) * width;
   // add pixel
   data32[p.pixelIndex] = pixel;

   // now you get 8 more pixels for the price of one particle 
   var ind = p.offsetArrayIndex; 
   //  offsetArray is an array of pixel offsets both negative and positive
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   data32[p.pixelIndex + offsetArray[ind++]]  = pixel;
   // offset array arranged as sets of 8, each set of 8 is a frame in 
   // looping pre calculated offset animation
   // offset array length is 65536 or any bit mask able size.
   p.offsetArrayIndex = ind & 0xFFFF ; // ind now points at first pixel of next
                                       // set of eight pixels

This and an assortment of other similar tricks can give you the 7.2million pixels per second you want. 这以及其他类似的技巧可以为您提供每秒720万像素的像素。

Last note. 最后一点。

Remember every device these days has a dedicated GPU. 请记住,这些天的每台设备都有专用的GPU。 Your best bet is to use it, this type of thing is what they are good at. 最好的选择是使用它,这是他们擅长的事情。

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

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