Урок · Усі рівні · ~40 хв
WebGL · Three.js · Spector.js · DevTools · Профілювання GPU

Налагодження 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 надає живу статистику рендерингу — перевіряйте її у своєму налагоджувальному оверлеї:

// Після renderer.render(scene, camera): const i = renderer.info; console.table({ 'Draw calls': i.render.calls, // ключова метрика продуктивності 'Triangles': i.render.triangles, 'Points': i.render.points, 'Lines': i.render.lines, 'Geometries': i.memory.geometries, // об'єкти в пам'яті GPU 'Textures': i.memory.textures, 'Programs': i.programs?.length ?? '–', // скомпільовані шейдери }); // HUD-оверлей (корисний під час розробки) const hud = document.createElement('div'); Object.assign(hud.style, { position:'fixed', bottom:'10px', left:'10px', color:'#4ade80', fontFamily:'monospace', fontSize:'12px', background:'rgba(0,0,0,.5)', padding:'6px 10px', borderRadius:'4px', pointerEvents:'none', zIndex:'9999' }); document.body.appendChild(hud); function updateHUD() { hud.textContent = `DC: ${renderer.info.render.calls} | Tri: ${renderer.info.render.triangles}`; renderer.info.reset(); // скидаємо лічильники кадру }

4Spector.js — захоплення та аналіз кадру

Spector.js — це розширення браузера, яке захоплює повний кадр WebGL і дозволяє переглянути кожен виклик відмалювання, 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.

5Профілювання GPU-таймлайну в Chrome

// Вимірювання часу кадру з боку 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Прийоми зменшення кількості викликів відмалювання

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)); // прибираємо окремі меші