WebGL проти Canvas 2D: як обрати правильний рендерер для вашої симуляції

Практичний посібник для розробників з вибору між WebGL та Canvas 2D API для симуляцій у браузері — бенчмарки продуктивності, шейдерне програмування, простота використання та чітка система прийняття рішень, вибудувана на досвіді створення понад 1000 симуляцій.

Під час створення браузерної симуляції одне з перших архітектурних рішень стосується рендерингу: Canvas 2D чи WebGL? Відповідь залежить від кількості частинок, частоти оновлення, візуальної складності та того, скільки часу ви готові витратити на шаблонний код. Після створення понад 1000 симуляцій з використанням обох API ми зібрали висновки про те, коли який підхід є правильним інструментом.

Canvas 2D: простота та швидка ітерація

API Canvas 2D — природна відправна точка для будь-якої браузерної симуляції. Він пропонує імперативний API малювання, який тісно відповідає тому, як програмісти природно уявляють собі малювання: перейти до позиції, намалювати фігуру, залити її кольором. Жодного коду шейдерів, жодних буферів, жодного прив'язування. Робочу симуляцію частинок можна вивести на екран менш ніж за 20 рядків:

const ctx = canvas.getContext('2d'); function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = p.color; ctx.fill(); }); requestAnimationFrame(draw); }

Сильні сторони Canvas 2D включають простий, зрозумілий API з чудовою документацією MDN, вбудований рендеринг тексту (важливо для підписів осей і значень у симуляціях), композитинг зображень, обтинання контурів (clipping) та градієнтні заливки, які у WebGL потребують складного коду шейдерів. Прототипування нової симуляції на Canvas 2D займає лише частку часу, необхідного для WebGL.

Фундаментальне обмеження полягає в тому, що Canvas 2D є прив'язаним до CPU. Кожен виклик малювання виконується послідовно в одному потоці процесора. Кожен виклик ctx.arc() є синхронним. GPU простоює. При кількості частинок нижче приблизно 5 000 це рідко має значення — на будь-якому сучасному пристрої ви впевнено отримаєте 60 fps. При 10 000 частинок продуктивність зазвичай падає до 15–30 fps на середньому апаратному забезпеченні. При 50 000 частинок Canvas 2D стає непридатним для будь-якої взаємодії, що претендує на реальний час.

WebGL: сила GPU для шторму з частинок

WebGL надає JavaScript доступ до повного конвеєра GPU. Замість виклику команд малювання одна за одною ви завантажуєте всі геометричні дані в пам'ять GPU один раз (або один раз за кадр), а потім видаєте єдиний виклик малювання, який виконується у масово паралельному режимі одночасно на тисячах шейдерних блоків GPU. Різниця у продуктивності для симуляцій з великою кількістю частинок не є поступовою — вона є трансформаційною.

Реальні виміри з наших симуляцій: симуляція рідини SPH з 50 000 частинок працює з 60 fps у WebGL і лише 4 fps у Canvas 2D на тому самому апаратному забезпеченні. N-body симуляція галактики з 200 000 зірок є інтерактивною у WebGL і повністю непридатною у Canvas 2D. Це не крайні випадки — вони представляють цілі категорії фізичних симуляцій, які просто неможливі без апаратного прискорення GPU.

Мінімальний вершинний шейдер WebGL, що позиціонує та фарбує частинки, виглядає так:

// Vertex shader (GLSL) attribute vec2 a_position; attribute vec4 a_color; uniform vec2 u_resolution; varying vec4 v_color; void main() { vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0; gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); gl_PointSize = 3.0; v_color = a_color; } // Fragment shader (GLSL) precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }

Ціна складності WebGL реальна: приблизно у 5–10 разів більше шаблонного коду порівняно з еквівалентною симуляцією на Canvas 2D. Потрібно створювати й компілювати шейдери, створювати та прив'язувати буфери, завантажувати дані в пам'ять GPU за допомогою типізованих масивів, керувати позиціями атрибутів і обробляти події втрати WebGL-контексту. Кастомні візуальні ефекти, як-от світіння, розмиття руху та візуалізація сил для окремих частинок, потребують нетривіальної роботи з шейдерами GLSL, тоді як у Canvas 2D це вирішується одним присвоєнням властивості.

5 000 Оптимальна межа для Canvas 2D (частинок)
60fps WebGL при 50 тис. частинок
~10× Більше шаблонного коду у WebGL проти Canvas

Система прийняття рішень: що обрати

У MySimulator ми використовуємо чіткий набір критеріїв, розпочинаючи нову симуляцію:

Обирайте Canvas 2D, якщо: кількість частинок або об'єктів залишається нижче 5 000, вам потрібен рендеринг тексту або складні операції з контурами, візуальні ефекти обмежуються простими заливками та обведеннями, ви прототипуєте і важливий час до робочого демо, або симуляцію підтримуватимуть розробники, не знайомі з графічними API.

Обирайте WebGL, якщо: ви очікуєте понад 10 000 одночасних об'єктів, вам потрібні кастомні візуальні ефекти (світіння, шлейфи, кольорові градієнти для окремих частинок у великому масштабі), ви рендерите неперервне поле (рідина, теплова карта, множина Мандельброта), або стабільність частоти кадрів на рівні 60 fps критично важлива для того, щоб фізика виглядала правильно.

У діапазоні 5 000–10 000: протестуйте обидва варіанти. Canvas 2D разом з OffscreenCanvas та Web Workers (перенесення циклу малювання у фоновий потік) можуть суттєво розширити межі — піднімаючи стелю Canvas 2D приблизно до 15 000 частинок на сучасному апаратному забезпеченні, перш ніж WebGL стане необхідним.

Інші варіанти, вартi розгляду: Three.js надає значно вищий рівень абстракції над WebGL, що зменшує обсяг шаблонного коду ціною певного запасу продуктивності. PixiJS створений спеціально для рендерингу 2D-спрайтів і частинок і є чудовим середнім варіантом. WebGPU, вже доступний у Chrome та Firefox за прапорцем (flag), замінює WebGL сучасним API — обчислювальні шейдери у WebGPU можуть виконувати фізику та рендеринг на GPU одночасно, чого WebGL нативно робити не може.

У MySimulator розподіл послідовний: боїди, маятник, подвійний маятник, «хижак-жертва», алгоритми сортування, клітинні автомати — все на Canvas 2D. Гідродинаміка рідини (SPH), N-body-симуляція галактики, феєрверки, океанські хвилі, туманність, аврора, системи частинок з 20 000+ об'єктами — все на WebGL. Точка переходу — приблизно 8 000 частинок для симуляції, якій потрібна фізика на 60 fps.

Практичні поради з міграції

Коли симуляція на Canvas 2D переросла свій рендерер, шлях міграції простіший, якщо симуляцію було написано з розділенням фізики та рендерингу. Якщо ви обчислюєте позиції частинок в одному місці, а малюєте їх в іншому, заміна коду малювання з Canvas 2D на WebGL є прямолінійною. Якщо фізика та рендеринг переплетені, вам доведеться рефакторити обидва одночасно.

Перед міграцією спершу профілюйте. Відкрийте Chrome DevTools, перейдіть на панель Performance і запишіть кілька секунд роботи симуляції. Погляньте на flame-графік: вузьке місце — у функції оновлення фізики (довгі блоки JavaScript) чи у викликах рендерингу (довгі блоки paint і composite)? Якщо вузьке місце — фізика, перехід на WebGL не допоможе — вам потрібно оптимізувати алгоритм оновлення, можливо, використовуючи просторове хешування для запитів сусідства замість парних порівнянь O(n²). Якщо вузьке місце — рендеринг, WebGL значно допоможе.

Для симуляцій на Canvas 2D, де фізика піддається обчисленню на GPU, розгляньте transform feedback — техніку WebGL, за якої вихід вершинного шейдера записується назад у буфер, а не рендериться. Це переносить саму фізичну симуляцію на GPU, залишаючи JavaScript лише оновлювати одну-дві uniform-змінні за кадр. Саме так наші найвимогливіші WebGL-симуляції досягають своєї продуктивності. Обчислювальні шейдери WebGPU роблять цей патерн ще чистішим і є наступним рубежем для браузерних симуляцій із справді екстремальною кількістю частинок.