簡體   English   中英

畫布上約有12萬個粒子?

[英]About 120 000 particles on canvas?

我需要約12萬個粒子(每個粒子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);
}

webgl上下文中在着色器中計算那些粒子將提供性能最高的解決方案。 有關示例,請參見例如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> 

除了繪制單個像素,您可能還需要考慮繪制並移動一些紋理,每個紋理上都有很多粒子。 這可能會以更好的性能接近完整的粒子效果。

每秒移動720萬個粒子

不使用webGL和着色器,並且要以60fps的速度每幀120K粒子,則需要每秒720萬點的吞吐量。 您需要一台快速的機器。

Web Worker多核CPU

快速解決方案。 在多核機器上,網絡工作者會為每個硬件核提供線性的性能提升。 例如,在8核i7上,您可以運行7個通過sharedArrayBuffers共享數據的工作者(可惜由於CPU安全風險,它全部關閉了ATM,請參閱MDN sharedArrayBuffer ),並且性能略低於7倍。 請注意,好處僅來自實際的硬件內核,JS線程往往會耗盡資源,在一個內核中運行兩個工作線程會導致總體吞吐量降低。

如果您控制運行的硬件,即使已啟用共享緩沖區,它仍然是可行的解決方案。

拍電影。

大聲笑,但不是它是一種選擇,並且粒子數沒有上限。 雖然不像我想的那樣互動。 如果您通過FX出售商品,那您將大吃一驚,而不是怎么做?

優化

容易說很難。 您需要使用細齒梳仔細檢查代碼。 請記住,如果全速運行,刪除一條線就是每秒刪除720萬條線。

我再看了一次代碼。 我無法對其進行測試,因此它可能會或可能不會起作用。 但它給你的想法。 您甚至可以考慮使用僅整數數學。 JS可以做定點數學。 整數大小是32位,甚至比4K顯示器還大。

第二次優化通過。

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

以上是我所能削減的。 (無法對其進行測試,可能會滿足您的需求,也可能會不滿足您的需求)它可能每秒會給您幾幀。

交錯

另一種解決方案可能適合您,那就是欺騙眼睛。 這會提高幀速率,但不會處理點,並且要求將點隨機分布,否則偽像會非常明顯。

每幀只處理一半的粒子。 每次處理粒子時,都會計算像素索引,設置該像素,然后將像素速度添加到像素索引和粒子位置。

效果是,每個框架只有一半的粒子在力的作用下移動,而另一半則滑過一個框架。

這可能會使幀頻翻倍。 如果粒子非常有條理,並且出現了閃爍的團塊狀偽影,則可以通過在創建時對粒子數組應用隨機混洗來隨機化粒子的分布。 同樣,這需要良好的隨機分布。

下一個片段僅作為示例。 每個粒子都需要將pixelIndex到pixel 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。 最好的選擇是使用它,這是他們擅長的事情。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM