WebGL Transform Feedback — системи частинок на GPU
Transform Feedback захоплює вихідні дані вершинного шейдера назад у буфери GPU
без жодної участі CPU. Це уможливлює симуляції частинок із мільйонами
об'єктів, де фізика, логіка створення та відродження повністю обробляються у
GLSL — CPU лише видає виклики drawArrays.
1Отримання контексту WebGL 2
Transform Feedback — це можливість WebGL 2. Отримайте контекст явно
та підтвердьте його доступність:
Усі сучасні настільні браузери підтримують WebGL 2. На мобільних Safari додав
підтримку WebGL 2 в iOS 15. Перевіряйте caniuse.com/webgl2 для найновішої
матриці покриття.
2Створення ping-pong VAO та буферів
Дві пари буферів зберігають стан частинок. Кожного кадру один читається, а
інший записується. Потім вони міняються місцями.
const N = 500_000; // Розкладка: [x, y, z, vx, vy, vz, age, lifespan] 8
float на частинку const STRIDE = 8; function makeParticleBuffer() {
const data = new Float32Array(N * STRIDE); for (let i = 0; i < N;
i++) { const b = i * STRIDE; data[b] = (Math.random() - 0.5) * 4; // x
data[b+1] = (Math.random() - 0.5) * 4; // y data[b+2] = (Math.random()
- 0.5) * 4; // z data[b+3] = (Math.random() - 0.5) * 0.02; // vx
data[b+4] = Math.random() * 0.02; // vy (дрейф угору) data[b+5] =
(Math.random() - 0.5) * 0.02; // vz data[b+6] = Math.random() * 500;
// вік (зі зміщенням) data[b+7] = 300 + Math.random() * 200; // тривалість життя
} const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_COPY); return buf; }
const bufA = makeParticleBuffer(); const bufB = makeParticleBuffer();
let [readBuf, writeBuf] = [bufA, bufB]; // Один об'єкт transform feedback
на буфер запису const tfA = gl.createTransformFeedback();
const tfB = gl.createTransformFeedback(); function bindTF(tf, buf) {
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); } bindTF(tfA,
bufA); bindTF(tfB, bufB); let [readTF, writeTF] = [tfA, tfB];
3Шейдер оновлення з varying-ами transform
feedback
const updateVS = /* glsl */ `#version 300 es in vec3 a_pos; in vec3
a_vel; in float a_age; in float a_lifespan; out vec3 v_pos; out vec3
v_vel; out float v_age; out float v_lifespan; // LCG-псевдовипадкове,
ініціалізоване даними частинки float rand(float seed) { return fract(sin(seed
* 127.1 + 311.7) * 43758.5453); } void main() { float t = a_age /
a_lifespan; if (a_age >= a_lifespan) { // Відродження в початку координат з
випадковою швидкістю float s = a_age + a_pos.x * 13.0; v_pos = vec3(0.0);
v_vel = vec3(rand(s)*0.04-0.02, rand(s+1.0)*0.04+0.01,
rand(s+2.0)*0.04-0.02); v_age = 0.0; v_lifespan = 300.0 + rand(s+3.0)
* 200.0; } else { // Гравітація + вік v_vel = a_vel + vec3(0.0, -0.00003,
0.0); v_pos = a_pos + v_vel; v_age = a_age + 1.0; v_lifespan =
a_lifespan; } } `; const updateFS = `#version 300 es precision highp
float; void main() {}`; // фрагментний шейдер ніколи не виконується (растеризацію
вимкнено) // Компілюємо та лінкуємо — критично: оголосіть varying-и transform feedback
ПЕРЕД лінкуванням const updateProg = gl.createProgram();
gl.attachShader(updateProg, compileShader(gl, gl.VERTEX_SHADER,
updateVS)); gl.attachShader(updateProg, compileShader(gl,
gl.FRAGMENT_SHADER, updateFS)); gl.transformFeedbackVaryings(
updateProg, ['v_pos', 'v_vel', 'v_age', 'v_lifespan'],
gl.INTERLEAVED_ATTRIBS // єдиний переплетений буфер, а не окремі );
gl.linkProgram(updateProg);
Порядок має значення: викликайте
gl.transformFeedbackVaryings()передgl.linkProgram(). Драйверу потрібно знати розкладку
захоплення під час лінкування.
Логіка відродження у вершинному шейдері (Крок 3) обробляє весь
життєвий цикл частинки на GPU. Зчитування на CPU не потрібні. Щоб підтримати
позицію емітера або сплескове створення,
передайте уніформу:
// JS — оновлюємо уніформу позиції емітера кожного кадру const emitterLoc =
gl.getUniformLocation(updateProg, 'u_emitter'); function updatePass()
{ gl.useProgram(updateProg); gl.uniform3f(emitterLoc, emitter.x,
emitter.y, emitter.z); // ... решта проходу } // GLSL — використовуємо її у
гілці відродження /* glsl */` uniform vec3 u_emitter; // у гілці
відродження: v_pos = u_emitter + vec3(rand(s)*0.1-0.05, 0.0,
rand(s+1.0)*0.1-0.05); ` // Головний цикл function loop() { updatePass();
// фізика на GPU, нуль CPU renderPass(vpMatrix); // відмалювання на GPU
requestAnimationFrame(loop); } requestAnimationFrame(loop);
Transform Feedback — найефективніший шлях для оновлення частинок на боці GPU
у WebGL 2. Для WebGPU натомість використовуйте обчислювальні шейдери — вони
дають більше гнучкості (довільний доступ, атомарні операції, бар'єрна
синхронізація).