Як я налагоджую шейдери GLSL (і зберігаю глузд)

Немає console.log у GLSL. Немає точок зупину. Тільки чорний екран. Ось мої практичні інструменти та робочий процес для налагодження вершинних і фрагментних шейдерів — включно з трюком «колір-як-значення».

Фундаментальна проблема

Коли шейдер GLSL ламається, зазвичай ви не отримуєте повідомлення про помилку. Ви отримуєте чорний екран, або суцільно однорідний колір, або малюнок, який зовсім не схожий на очікуваний. Шейдер виконується на відеокарті — там немає ні точок зупину, ні console.log, ні стеків викликів.

Однак є один вихідний параметр, який можна перевірити: колір пікселя. Майже все в налагодженні GLSL зводиться до кодування підозрілих значень у вигляді кольорів і спостереження за результатом.

Техніка 1: Колір як значення

Найкорисніша техніка. Закодуйте те, що викликає підозру, як колір фрагмента:

// Підозра: чи коректний vNormal?
// Нормалі знаходяться в діапазоні [-1,1] — переведемо в [0,1] для відображення
gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);

// Підозра: чи коректні UV-координати?
gl_FragColor = vec4(vUv, 0.0, 1.0);

// Підозра: чи знаходиться float у потрібному діапазоні?
// Кодуємо як значення зеленого каналу
gl_FragColor = vec4(0.0, clamp(myFloat, 0.0, 1.0), 0.0, 1.0);

Якщо нормалі на сфері спрямовані правильно, ви побачите червоний колір справа, зелений зверху та синій з боку, оберненого до вас. Якщо ви бачите однорідний сірий або пурпуровий колір — нормалі зламані.

Техніка 2: Покрокове коментування

Помилка шейдера рідко знаходиться там, де ви думаєте. Працюйте від кінця до початку: закоментуйте фінальний ефект, додайте базовий колір і поступово розкоментовуйте, поки не виявите момент поломки:

// Крок 1: відомо, що працює — просто білий
gl_FragColor = vec4(1.0);

// Крок 2: додаємо освітлення — все ще працює?
// gl_FragColor = vec4(vec3(diffuse), 1.0);

// Крок 3: додаємо текстуру — все ще працює?
// gl_FragColor = vec4(texture2D(map, vUv).rgb * diffuse, 1.0);

// Крок 4: додаємо ефект Френеля — все ще працює?
// float f = fresnel(vViewDir, vNormal);
// gl_FragColor = vec4(mix(deep, sky, f), 1.0);

Техніка 3: Перевірка помилок компіляції

Three.js виводить помилки компіляції шейдера в консоль, але лише якщо шейдер справді компілюється. Одруківка, яка є валідним синтаксисом GLSL, не викличе помилку — вона дасть неправильний результат. Завжди перевіряйте:

// Примусова перевірка помилки компіляції після зв'язування програми
const gl = renderer.getContext();
const program = material.program?.program;
if (program && !gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error('Shader link failed:', gl.getProgramInfoLog(program));
}

Техніка 4: SpectorJS

SpectorJS — це розширення браузера, яке захоплює кадр WebGL і показує кожен виклик малювання, кожне значення юніформи та кожну текстуру. Це найближчий аналог GPU-дебагера, доступний у браузері.

Я користуюся ним щоразу, коли трюк «колір-як-значення» не дозволяє достатньо швидко звузити коло пошуку. Можливість перевірити точну текстуру, прив'язану до юніформи-семплера, заощадила мені години часу.

Техніка 5: Точність обчислень

На мобільних відеокартах (і деяких вбудованих графічних чипах настільних систем) точність mediump може призводити до несподіваного обрізання значень або втрати значущості. Якщо щось працює на десктопі, але ламається на мобільному, додайте:

// Завжди оголошуйте точність у фрагментних шейдерах
precision highp float;
precision highp int;

Найшвидший цикл налагодження: Додайте gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); (суцільно червоний) у самому кінці вашого фрагментного шейдера. Якщо ви не бачите червоного кольору, шейдер взагалі не використовується — перевіряйте призначення матеріалу, а не код шейдера.