Около 120 000 частиц на холсте?

У меня есть около 120 000 частиц (каждая частица размером 1 пиксель), которые мне нужны, чтобы найти лучший и самый важный: самый быстрый способ рисования на холсте.

Как бы Вы это сделали?

Прямо сейчас я в основном помещаю свои пиксели в массив, а затем зацикливаю эти частицы, делаю некоторые вычисления x и y и рисую их, используя fillRect. Но частота кадров сейчас составляет 8-9 кадров в секунду.

Есть идеи? Пожалуйста, пример.

Спасибо

ПОСЛЕДНИЕ ОБНОВЛЕНИЯ (мой код)

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

2 ответа

Решение

Перемещение 7,2 миллиона частиц в секунду

Если вы не используете webGL и шейдеры, и вам нужно 120K частиц на кадр при 60 кадрах в секунду, вам нужна пропускная способность 7,2 миллиона точек в секунду. Вам нужна быстрая машина.

Веб-работники многоядерных процессоров

Быстрые решения. На многоядерных машинах веб-работники дают линейное увеличение производительности для каждого аппаратного ядра. Например, на 8 Core i7 вы можете запустить 7 сотрудников, совместно использующих данные через sharedArrayBuffers (позор, что все они превратились в банкомат из-за риска безопасности ЦП, см. MDN sharedArrayBuffer) и получить чуть более чем 7-кратное улучшение производительности. Обратите внимание, что выгоды только от реальных аппаратных ядер, потоки JS имеют тенденцию работать ровно, Работа двух рабочих в одном ядре приводит к общему снижению пропускной способности.

Даже с учетом общих буферов это все еще жизнеспособное решение, если вы контролируете, на каком оборудовании вы работаете.

Сделать фильм.

LOL, но нет, это опция, и нет верхнего предела для количества частиц. Хотя не так интерактивно, как я думаю, вы можете захотеть. Если вы продаете что-то через FX, вы после вау, а не как?

оптимизировать

Легко сказать, трудно сделать. Вам нужно пройтись по коду с тонкой зубной расческой. Помните, что удаление одной строки при работе на полной скорости составляет 7,2 миллиона строк в секунду.

Я перебрал код еще раз. Я не могу проверить это, поэтому он может или не может работать. Но это, чтобы дать вам идеи. Вы могли бы даже рассмотреть использование математики целых чисел. 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 в пиксель 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

Этот и целый ряд других подобных трюков могут дать вам 7,2 миллиона пикселей в секунду, которые вы хотите.

Последнее замечание

Помните, что каждое устройство в наши дни имеет выделенный графический процессор. Лучше всего использовать его, это то, в чем они хороши.

Вычисление этих частиц в шейдере на 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>

Вместо того, чтобы рисовать отдельные пиксели, вы также можете рассмотреть возможность рисования и перемещения нескольких текстур с большим количеством частиц на каждой из них. Это может приблизиться к полному эффекту частиц при лучшей производительности.

Другие вопросы по тегам