Перенесення автомата «Гра життя» на GPU відкриває можливість працювати із
сітками на мільйон клітин при 60 FPS. Кожен піксель текстури представляє
одну клітину. Фрагментний шейдер читає текстуру, рахує сусідів і записує
наступний стан — усе паралельно на кожному потоці GPU. Дві текстури
чергуються (ping-pong), тож шейдер читає зі старого стану й записує в
новий.
1Створення ping-pong render targets
import * as THREE from
'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
const SIZE = 1024; // сітка має SIZE × SIZE клітин function makeRT() {
return new THREE.WebGLRenderTarget(SIZE, SIZE, { minFilter:
THREE.NearestFilter, magFilter: THREE.NearestFilter, format:
THREE.RGBAFormat, type: THREE.UnsignedByteType, wrapS:
THREE.RepeatWrapping, // тороїдальне загортання wrapT:
THREE.RepeatWrapping, }); } // Дві цілі — щокадру чергуємо читання/запис
let rtA = makeRT(); let rtB = makeRT(); // Допоміжна функція, щоб міняти
їх місцями function swap() { [rtA, rtB] = [rtB, rtA]; }
NearestFilter має вирішальне значення — ми не повинні
змішувати значення текселів під час семплювання сусідів. Білінійна
фільтрація зіпсувала б двійковий стан клітини.
2Засів початкового стану
// Створюємо текстуру з випадковим шумом (канал R кожного пікселя = жива/мертва)
const data = new Uint8Array(SIZE * SIZE * 4); for (let i = 0; i <
SIZE * SIZE; i++) { const alive = Math.random() < 0.3 ? 255 : 0; //
30 % живих data[i * 4 + 0] = alive; // R = жива data[i * 4 + 3] =
255; // A = непрозора } const seedTex = new THREE.DataTexture(data, SIZE,
SIZE, THREE.RGBAFormat); seedTex.needsUpdate = true; // Малюємо засів
у rtA через простий прохід копіювання const copyMat = new
THREE.MeshBasicMaterial({ map: seedTex }); const quad = new
THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMat); const simCam = new
THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const simScene = new
THREE.Scene(); simScene.add(quad); renderer.setRenderTarget(rtA);
renderer.render(simScene, simCam); renderer.setRenderTarget(null);
function stepSimulation() { // ЧИТАЄМО з rtA, ЗАПИСУЄМО в rtB
simMat.uniforms.u_state.value = rtA.texture;
renderer.setRenderTarget(rtB); renderer.render(simScene, simCam);
renderer.setRenderTarget(null); // Тепер rtB має нове покоління —
міняємо місцями для наступного кадру swap(); } function animate() {
requestAnimationFrame(animate); // Виконуємо кілька кроків симуляції на
один візуальний кадр для швидших візерунків for (let s = 0; s < 2; s++)
stepSimulation(); // Відображаємо поточний стан (rtA після обміну = щойно
записаний rtB) displayMat.uniforms.u_tex.value = rtA.texture;
renderer.render(displayScene, displayCam); }
Завжди читайте з однієї render target і записуйте в іншу. Обмін
вказівниками виконується за O(1) і уникає дорогих копіювань текстур.
5Прохід відображення та взаємодія мишею
// Шейдер відображення: жива=біла, мертва=темно-зелена const displayFrag
= /* glsl */ ` precision highp float; uniform sampler2D u_tex; varying
vec2 vUv; void main() { float v = texture2D(u_tex, vUv).r; vec3 col =
mix(vec3(0.05, 0.07, 0.05), vec3(0.28, 0.93, 0.43), v); gl_FragColor =
vec4(col, 1.0); } `; // Миша: малюємо живі клітини при кліку/перетягуванні
canvas.addEventListener('pointermove', e => { if (e.buttons === 0)
return; const uv = { x: e.offsetX / canvas.width, y: 1 - e.offsetY /
canvas.height }; // Записуємо блок 5×5 живих клітин навколо позиції
миші const px = Math.floor(uv.x * SIZE); const py =
Math.floor(uv.y * SIZE); const tex = rtA.texture; // Для постійного
редагування використовуйте підхід DataTexture + readRenderTargetPixels // або
просто семплюйте стан у цій позиції в шейдері через uniform u_brush
console.info(`Paint at cell (${px}, ${py})`); });
Для інтерактивного малювання передайте позицію пензля як uniform
vec2 у шейдер симуляції та зробіть клітини в межах радіуса
живими — зчитування з GPU на CPU не потрібне.
Часті запитання
Чого я навчуся в цьому уроці?
Запустіть «Гру життя» Конвея повністю на GPU за допомогою ping-pong render targets у WebGL і фрагментного шейдера GLSL із 50 рядків.
Які теми розглядаються в цьому уроці?
Цей урок охоплює такі теми: Створення ping-pong render targets, Засів початкового стану, Написання фрагментного шейдера «Гри життя», Прохід симуляції з повноекранним квадом, Прохід відображення та взаємодія.
Які інструменти й технології використовуються?
Цей урок використовує: WebGL, GLSL, Three.js, Ping-pong-текстури.