【问题标题】:About 120 000 particles on canvas?画布上大约有 120 000 个粒子?
【发布时间】:2018-01-19 20:24:31
【问题描述】:

我有大约 120 000 个粒子(每个粒子大小为 1px),我需要找到最好的和最重要的:在画布上绘制的最快方法。

你会怎么做?

现在我基本上是将像素放入一个数组中,然后循环这些粒子,进行一些 x 和 y 计算并使用 fillRect 将它们绘制出来。但是现在的帧率是 8-9 fps。

有什么想法吗?请举例。

谢谢

最新更新(我的代码)

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

【问题讨论】:

  • 如果不想使用 3D 上下文,可以先调用context.getImageData(),然后操作返回的图像数组中的像素,最后使用context.putImageData() 将它们放回去
  • @le_m 嗯,好的。你能看看我更新的代码并举个例子吗?我没有真正关注
  • 你可以将速度翻倍,第二个循环中的数学有点草率。平方数 (homeDX * homeDX)Math.pow(homeDX) 快 你可以避免所有三角函数 atan2, sin.cos。你有var homeAngle = Math.atan2(homeDY,homeDX); 然后homeForce * Math.cos(homeAngle) 和y。删除homeAngle,homeForce,并将homeForce * Math.cos(homeAngle) 替换为0.2 * homeDX,对于y 0.2 * homeDY,它的作用完全相同,减少了2 个变量和3 个触发调用。与cursorAngle 类似,在同一循环中绘制像素作为计算可以节省时间。用于像素的 Uint32Array
  • @Blindman67 感谢您指出这一点。我确实更新了我的本地版本,是的,它节省了一些调用,并且像以前一样工作。但是,fps 仍然太低,我想我需要找到 le_m 建议的方法。请参阅我对他的帖子的评论。
  • @Blindman67 是的,我知道,有很多要点...请查看我的代码,看看您是否可以看到我需要更改的任何内容以获得我期望的结果在运动?粒子在 le_m 建议下以大约 30 fps 的速度运行,但它们的运动不再像以前那样了..?

标签: javascript html canvas html5-canvas particles


【解决方案1】:

每秒移动 720 万个粒子

不使用 webGL 和着色器,您希望每帧 120K 粒子 60fps 你需要每秒 720 万点的吞吐量。你需要一台快速的机器。

Web Worker 多核 CPU

快速解决方案。在多核机器上,网络工作者为每个硬件核心提供线性性能提升。例如,在 8 核 i7 上,您可以运行 7 个工作人员通过 sharedArrayBuffers 共享数据(遗憾的是,由于 CPU 安全风险,它全部关闭了 ATM,请参阅MDN sharedArrayBuffer)并获得略低于 7 倍的性能提升。注意好处仅来自实际的硬件内核,JS 线程往往会耗尽,在一个内核中运行两个 worker 会导致整体吞吐量下降。

即使关闭共享缓冲区,如果您可以控制运行的硬件,它仍然是一个可行的解决方案。

制作一部电影。

LOL 但不是,它是一个选项,并且粒子数没有上限。虽然不像我想的那样互动。如果您通过 FX 销售商品,您是在追求惊喜,而不是如何?

优化

说起来容易做起来难。您需要用细齿梳检查代码。请记住,如果全速运行,则删除一条线每秒会删除 720 万条线。

我又检查了一遍代码。我无法对其进行测试,因此它可能会或可能不会起作用。但它给你的想法。您甚至可以考虑仅使用整数数学。 JS 可以做定点数学。整数大小比 4K 显示器所需的多 32 位。

第二次优化。

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

以上是我可以减少的数量。 (无法对其进行测试,因此可能会或可能不会满足您的需要)它可能会每秒多给您几帧。

交错

另一种解决方案可能适合您,那就是欺骗眼睛。这会增加帧速率,但不会增加处理的点,并且需要随机分布点,否则伪影会非常明显。

每一帧你只处理一半的粒子。每次处理粒子时,都会计算像素索引,设置该像素,然后将像素速度添加到像素索引和粒子位置。

效果是每一帧只有一半的粒子受力移动,另一半滑行一帧..

这可能会使帧速率加倍。如果您的粒子非常有条理,并且您获得了聚集闪烁类型的伪影,则可以通过在创建时对粒子阵列应用随机洗牌来随机化粒子的分布。这同样需要良好的随机分布。

下一个sn-p只是一个例子。每个粒子都需要将pixelIndex 保存到像素data32 数组中。请注意,第一帧必须是完整帧才能设置所有索引等。

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

更少的颗粒

接缝很明显,但经常被视为可行的解决方案。更少的粒子并不意味着更少的视觉元素/像素。

如果您将粒子数减少 8 并在设置时创建一个大的偏移索引缓冲区。这些缓冲区保存与像素行为非常匹配的动画像素移动。

这可能非常有效,并给人一种每个像素实际上是独立的错觉。但工作是在预处理和设置偏移动画。

例如

   // 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

这个技巧和其他各种类似技巧可以为您提供所需的每秒 720 万像素。

最后一点。

请记住,如今的每台设备都有专用的 GPU。你最好用它,这种东西是他们擅长的。

【讨论】:

  • 天啊,好帖子。谢谢你。我会通读这个,然后尝试一下,可能会带着一个问题回来=)谢谢!
  • 好的。请在此处查看我的原始版本,那个很慢,但移动是正确的:https://jsfiddle.net/7fxwrkgw/ 然后我有基于您的代码的快速版本,但是移动关闭的地方,我不知道为什么?如果您可以比较,请:https://jsfiddle.net/L8vtLqhu/1/
  • @nickelman 好的,我错过了ratio 及其价值。您是否知道您正在以设备显示分辨率的两倍进行渲染。设备被配置为可以处理各种外围设备的处理负载。您渲染到的画布比屏幕大 4 倍,您渲染的像素仅覆盖实际物理像素的四分之一。将ratio 设置为 1,您将立即从 8fps 变为 30fps。
  • 您是否更新了任何代码?好的,所以比率应该设置为 1,但那如何与视网膜一起工作呢?这就是我这样做的原因……你看到“我的慢版”和“快版”在动画上的区别了吗?我不明白为什么两者之间存在差异?
  • 好的,所以将 ratio 设置为 1 确实可以提高 fps。但仍然存在 2 个问题:1)我如何以视网膜分辨率获得它? 2)您对为什么动画/运动(基于鼠标光标)在快速版本(我使用您的技术)之间有所不同有任何想法吗?既是运动不一样,又是渲染中存在某种“像素间隙”?
【解决方案2】:

webgl 上下文的着色器中计算这些粒子将提供最高性能的解决方案。见 e。 G。以https://www.shadertoy.com/view/MdtGDX 为例。

如果您希望继续使用 2d 上下文,您可以通过在屏幕外执行此操作来加快渲染粒子:

  1. 调用context.getImageData()获取图像数据数组
  2. 通过操作数据数组绘制像素
  3. context.putImageData()放回数据数组

一个简化的例子:

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>

除了绘制单个像素之外,您可能还需要考虑绘制和移动一些纹理,每个纹理上都有很多粒子。这可能会以更好的性能接近完整的粒子效果。

【讨论】:

  • 例如,谢谢。我想我已经接近了,但是只要我移动鼠标,一切都会瞬间消失......关于为什么的任何想法?我的代码已更新。
  • @nickelman 第一个猜测:你需要舍入4 * (particle.x + particle.y * (this.width*this.ratio))
  • 是的,它越来越近了 =) 但是现在粒子真的在大移动,每个粒子都有一些 rgb 不同的颜色......我已经用圆形更新了我的代码,请看一下。关于我为什么得到这个结果的任何想法?
  • 我的代码又更新了。它运行平稳,但我现在看到两个问题:1)光标力对我来说越来越不清楚,粒子正在移动,就像存在某种角度问题一样,使得粒子在靠近时变得像漩涡一样。 2) 近距离看,文字中出现了像“1px gap”一样的渲染,基本上是空像素。。运行我的代码你会看到,就像你看到的“力圈”?跨度>
  • @nickelman 啊,您需要在 for 循环中分别对 x 和 y 分量进行四舍五入,即const particle = particles[i]; const x = Math.round(particle.x); const y = Math.round(particle.y); const index = 4 * (x + y * width);
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多