Налагодження WebGL — DevTools, Spector.js та профілювання
Помилки WebGL відомі своєю «мовчазністю» — чорний екран не дає жодних
підказок. Цей посібник охоплює систематичне налагодження: опитування
gl.getError(), запит інформації про рендерер, захоплення
кадрів за допомогою Spector.js, профілювання GPU-таймлайнів та зменшення
кількості викликів відмалювання задля продуктивності у продакшені.
1gl.getError() та помилки компіляції
шейдерів
WebGL ставить помилки в чергу — викликайте gl.getError()
після кожного підозрілого виклику під час розробки:
function glCheck(gl, label = '') { const err = gl.getError(); if (err
!== gl.NO_ERROR) { const names = { [gl.INVALID_ENUM]: 'INVALID_ENUM',
[gl.INVALID_VALUE]: 'INVALID_VALUE', [gl.INVALID_OPERATION]:
'INVALID_OPERATION', [gl.OUT_OF_MEMORY]: 'OUT_OF_MEMORY', }; throw new
Error(`GL error at "${label}": ${names[err] ?? err}`); } } //
Використання: gl.bindBuffer(gl.ARRAY_BUFFER, buf); glCheck(gl, 'bindBuffer');
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); glCheck(gl,
'bufferData'); // Компіляція шейдера — завжди перевіряйте інформаційний лог
function compileShader(gl, type, src) { const s =
gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(s)); // <-- номери рядків + помилки
GLSL throw new Error('Shader compile failed'); } return s; } //
Лінкування програми gl.linkProgram(prog); if (!gl.getProgramParameter(prog,
gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(prog)); //
інформація про невідповідність attribute / varying throw new Error('Program link
failed'); }
Обгортайте виклики gl.getError() у прапорець режиму
налагодження — виклик щокадру примушує синхронізацію CPU/GPU і різко
знижує продуктивність. Видаліть їх або обмежте умовою
if (DEBUG) перед випуском.
2WEBGL_debug_renderer_info
Це розширення надає фактичний рядок моделі GPU, корисний для
виявлення помилок, специфічних для драйвера:
const canvas = document.querySelector('canvas'); const gl =
canvas.getContext('webgl2') ?? canvas.getContext('webgl'); const
dbgInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (dbgInfo) {
const vendor = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL); const
renderer = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL);
console.log('GPU vendor: ', vendor); // напр. "NVIDIA Corporation"
console.log('GPU renderer:', renderer); // напр. "NVIDIA GeForce RTX
4090/PCIe/SSE2" } else { console.warn('WEBGL_debug_renderer_info not
available (some Firefox configs)'); } // У Three.js: const info =
renderer.extensions.get('WEBGL_debug_renderer_info'); // renderer тут
— це THREE.WebGLRenderer if (info) { const r =
renderer.getContext().getParameter(info.UNMASKED_RENDERER_WEBGL);
console.log('Three.js GPU:', r); }
Режим протидії відстеженню у Firefox може повертати загальні рядки
("Mozilla" / "Mozilla"), щоб запобігти зніманню «відбитка» GPU. Це
функція приватності, а не помилка.
3Three.js renderer.info
renderer.info надає живу статистику рендерингу —
перевіряйте її у своєму налагоджувальному оверлеї:
Перейдіть на свою сторінку, тоді натисніть іконку Spector на панелі
інструментів
Натисніть Record, щоб захопити наступний кадр
Перегляньте кожен виклик відмалювання: вершинні буфери,
uniform-змінні, активні текстури, стан глибини
Для програмної інтеграції завантажте Spector.js як модуль:
// Лише для розробки — приберіть із продакшен-збірок import * as
SPECTOR from
'https://cdn.jsdelivr.net/npm/spectorjs/dist/spector.bundle.js'; const
spector = new SPECTOR.Spector(); // Захоплюємо наступні N кадрів і
відкриваємо переглядач результатів spector.displayUI(); // плаваюча кнопка UI поверх
полотна // або spector.startCapture(canvas, 1); // захопити 1 кадр
програмно spector.onCapture.add(result => {
console.log('Spector result JSON:', JSON.stringify(result, null, 2));
});
Шукайте у Spector надлишкові зміни стану — повторні
виклики bindBuffer чи useProgram з тим самим
об'єктом є поширеним джерелом зайвих витрат CPU.
Запишіть профіль на 3–5 секунд, поки працює ваша сцена
Шукайте довгі блоки на доріжці GPU (обмеження
швидкістю заповнення) проти довгих блоків JS
(обмеження з боку CPU)
// Вимірювання часу кадру з боку CPU за допомогою Performance API let t0 =
performance.now(); renderer.render(scene, camera); // Примітка: це НЕ
включає час GPU — виклики WebGL повертаються миттєво. // Для справжнього
вимірювання часу GPU використовуйте EXT_disjoint_timer_query: const ext =
gl.getExtension('EXT_disjoint_timer_query_webgl2'); if (ext) { const
query = gl.createQuery(); gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
renderer.render(scene, camera); gl.endQuery(ext.TIME_ELAPSED_EXT); //
Опитуємо результат у майбутньому кадрі (без блокування) setTimeout(() => { const
available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); if
(available) { const ns = gl.getQueryParameter(query, gl.QUERY_RESULT);
console.log('GPU frame time:', (ns / 1e6).toFixed(2), 'ms'); } },
100); }
6Прийоми зменшення кількості викликів відмалювання
InstancedMesh — замінює N викликів відмалювання
одним. Використовуйте для повторюваної геометрії (дерева, каміння,
будівлі).
Злиття геометрії —
BufferGeometryUtils.mergeGeometries([...]) запікає
кілька статичних мешів в один буфер.
Відсікання за пірамідою видимості — Three.js відсікає
за замовчуванням. Переконайтеся, що об'єкти мають коректну
geometry.boundingSphere (викликайте
computeBoundingSphere() після зміни вершин).
LOD — підставляйте меші з меншою кількістю полігонів
на відстані за допомогою THREE.LOD.
Атлас текстур — упакуйте кілька малих текстур в один
великий атлас, щоб уникнути перемикань матеріалів.
import { BufferGeometryUtils } from
'three/examples/jsm/utils/BufferGeometryUtils.js'; // Зливаємо 1000 дерев
в єдиний виклик відмалювання const merged =
BufferGeometryUtils.mergeGeometries( trees.map(t => { const g =
t.geometry.clone(); g.applyMatrix4(t.matrixWorld); // запікаємо світову
трансформацію return g; }) ); scene.add(new THREE.Mesh(merged, treeMat));
trees.forEach(t => scene.remove(t)); // прибираємо окремі меші