GLSL-шейдери з нуля — пишемо свій перший фрагментний шейдер

Кожна яскрава візуалізація на цьому сайті — океанські хвилі, візерунки реакції-дифузії, сяйво полярного сяйва — виконується всередині фрагментного шейдера GLSL на GPU. Ось як вони працюють, крок за кроком.

Що таке GLSL-шейдер?

Шейдер — це невелика програма, яка виконується паралельно на GPU — один раз на піксель (фрагментний шейдер) або один раз на вершину (вершинний шейдер). GLSL (OpenGL Shading Language) — це схожа на C мова, якою їх пишуть. У WebGL і Three.js потрібні обидва типи, але саме у фрагментному шейдері відбувається основна візуальна магія.

GPU виконує один і той самий фрагментний шейдер для кожного пікселя одночасно. Полотно 1920×1080 запускає шейдер приблизно 2 мільйони разів у кожному кадрі, і все це паралельно — саме тому шейдери такі швидкі.

Мінімальний фрагментний шейдер

Єдиний обов'язковий вихід — gl_FragColor — це vec4 зі значеннями (red, green, blue, alpha), кожне в діапазоні 0.0–1.0.

// Суцільний червоний — "hello world" шейдерів precision mediump float; void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // RGB = червоний, A = непрозорий }

Використання координат: вбудований gl_FragCoord

Уніформ gl_FragCoord.xy дає позицію пікселя у віконних пікселях. Нормалізуйте її, щоб отримати позицію в [0,1], і використайте як кольоровий градієнт:

precision mediump float; uniform vec2 u_resolution; // ширина, висота — передається з JS void main() { vec2 uv = gl_FragCoord.xy / u_resolution; // нормалізація до [0,1]² gl_FragColor = vec4(uv.x, uv.y, 0.5, 1.0); // червоний зростає зліва направо, зелений — знизу вгору }

Це відтворює градієнт від червоного до жовтого зліва направо та від зеленого до блакитного знизу вгору — класичний вступний результат GLSL.

Основні вбудовані функції

Функція Що вона робить Типове застосування
sin(x) / cos(x) Тригонометрична хвиля, період 2π Хвилі, коливання, обертання
fract(x) Дробова частина x — тобто x за модулем 1.0 Повторювані плитки, смуги, сітки
length(v) Евклідова відстань вектора від початку координат Радіальні градієнти, кола, поля відстаней
smoothstep(a, b, x) S-подібна крива від 0→1, коли x йде від a→b Згладжені краї, віньєтки
mix(a, b, t) Лінійна інтерполяція: a*(1-t) + b*t Змішування кольорів, переходи
abs(x) Абсолютне значення Симетрія, згортання простору
floor(x) Округлення вниз до найближчого цілого Отримання ID клітинки/сітки
clamp(x, lo, hi) Обмеження x до [lo, hi] Безпечні значення кольору, глибина
dot(a, b) Скалярний добуток двох векторів Освітлення, проєкції, дифузне затінення
normalize(v) Одиничний вектор того ж напрямку Нормалі, напрямки

Малюємо коло за допомогою SDF

Поле знакової відстані (Signed Distance Field, SDF) — це функція, що повертає від'ємні значення всередині фігури та додатні — зовні. Використайте length + smoothstep, щоб отримати чітке згладжене коло:

precision mediump float; uniform vec2 u_resolution; void main() { vec2 uv = (gl_FragCoord.xy / u_resolution) * 2.0 - 1.0; // [-1, 1]² uv.x *= u_resolution.x / u_resolution.y; // корекція співвідношення сторін float d = length(uv); // відстань від центру float circle = smoothstep(0.5, 0.49, d); // 1 всередині, 0 зовні gl_FragColor = vec4(vec3(circle), 1.0); }

Шум: секретний інгредієнт

У GLSL немає функції випадковості, але багато шейдерів імітують її хеш-функцією, а потім згладжують у шум у стилі Перліна. Ось компактний псевдовипадковий хеш, який використовується по всьому сайту:

// Псевдовипадкове число з вектора-насіння vec2 (0–1) float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 19.19); return fract(p.x * p.y); } // Значеннєвий шум: білінійна інтерполяція 4 хеш-значень float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); // крива smoothstep return mix(mix(hash(i), hash(i + vec2(1,0)), u.x), mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); }

Накладіть кілька октав шуму зі зростаючою частотою й спадною амплітудою, щоб отримати фрактальний броунівський рух (fBm) — турбулентну, хмароподібну текстуру, яку використовують у симуляціях океану та атмосфери.

Використання уніформів: передача даних із JavaScript

Уніформи — це значення лише для читання, що передаються з CPU (JavaScript) у шейдер щокадру. У Three.js вони оголошуються в об'єкті uniforms, а в GLSL — з ключовим словом uniform:

// Сторона JavaScript (Three.js) const material = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } }, vertexShader: `...`, fragmentShader: ` precision mediump float; uniform float u_time; uniform vec2 u_resolution; void main() { vec2 uv = gl_FragCoord.xy / u_resolution; float wave = sin(uv.x * 10.0 + u_time) * 0.5 + 0.5; gl_FragColor = vec4(wave, uv.y, 1.0 - wave, 1.0); } ` }); // У циклі анімації: material.uniforms.u_time.value += 0.016; // просуваємо час у кожному кадрі

Де на цьому сайті зустрічаються шейдери

Фрагментний шейдер змішує глибокі й мілководні відтінки синього на основі уніформа карти висот. Ефект Френеля по краю додається обчисленням dot(normal, viewDir).
4-октавний шум fBm керує густиною хмар. Порогова функція smoothstep створює різку об'ємну межу між чистим небом і хмарою.
Симуляція Грея-Скотта записує у ціль рендеру з плаваючою комою на кожному кроці. Фрагментний шейдер відображає концентрації речовин u/v у колір через таблицю палітри.
Інтенсивність обчислюється аналітично для кожного пікселя — без текстур, лише математика. Смугастий візерунок — це чистий результат функції суперпозиції хвиль GLSL.
Шарований шум зі змінною в часі фазою та спотворенням високого співвідношення сторін створює мерехтіння, схоже на завісу. Повністю процедурно — без файлів текстур.
Форми стоячих хвильових мод обчислюються для кожного пікселя. Поріг біля нульового переходу підсвічується, щоб показати вузлові лінії, де осідає пісок.

Налагодження GLSL

У GLSL немає console.log. Стандартна техніка — візуалізувати свої дані як колір:

Порада щодо продуктивності: на мобільних пристроях розгалуження (оператори if) усередині шейдерів можуть бути значно повільнішими за арифметику, оскільки на деяких GPU обидві гілки можуть виконуватися паралельно. Надавайте перевагу mix(a, b, step(...)) і clamp замість умовних операторів, де тільки можливо.

Наступні кроки

Коли ви впевнено почуватиметеся з фрагментними шейдерами, досліджуйте далі: