Вступ до шейдерів GLSL
Шейдери виконуються на GPU — тисячі крихітних програм, що працюють паралельно, по одній на вершину чи піксель. Саме вони перетворюють трикутники на хром, вогонь, водяні відблиски та інопланетні планети. GLSL (OpenGL Shading Language) — це C-подібна мова, якою ви їх пишете.
1. Конвеєр GPU
Щоб відрендерити трикутник, апаратне забезпечення GPU виконує фіксований конвеєр із програмованими етапами, які ви пишете на GLSL:
У сучасних WebGPU та Vulkan обчислювальні шейдери (compute shaders) додають третій програмований етап, що повністю обходить растеризацію — використовується для фізики, частинок і постобробки.
2. Основи GLSL
GLSL виглядає як C, але розроблена для векторів і матриць. Ключові типи:
float x = 1.0; // для float обов'язково використовуйте десяткову крапку
int n = 3;
bool b = true;
vec2 uv = vec2(0.5, 0.25); // 2D-вектор
vec3 col = vec3(1.0, 0.5, 0.0); // помаранчевий RGB
vec4 pos = vec4(col, 1.0); // w = 1 для позиції
mat3 rotation = mat3(1.0); // одинична матриця 3×3
// Свізлінг: доступ до будь-якої комбінації компонентів
vec3 rgb = col.rgb; // те саме, що й col.xyz
float r = col.r; // те саме, що й col.x або col[0]
vec2 yx = col.yx; // зворотний порядок каналів
Математичні функції діють покомпонентно на вектори:
sin(v), cos(v), length(v),
normalize(v), dot(a,b), cross(a,b),
mix(a,b,t), clamp(x,0.,1.), smoothstep(edge0,edge1,x).
3. Вершинні шейдери
Вершинний шейдер мусить записати у gl_Position — координату
простору відсікання (поділіть на w, щоб отримати NDC; від -1 до +1 по всіх осях). Мінімальний
пропускний вершинний шейдер:
#version 300 es
precision highp float;
in vec3 a_position; // атрибут: позиція вершини з VBO
in vec2 a_uv; // атрибут: текстурна координата
out vec2 v_uv; // varying: інтерполюється до фрагментного шейдера
uniform mat4 u_mvp; // матриця Model-View-Projection (з CPU)
void main() {
v_uv = a_uv;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
Для повноекранних ефектів (у стилі ShaderToy) використовуйте один квад, що вкриває
екран, з a_position у діапазоні [-1, 1] — повністю пропустіть
перспективу та встановіть gl_Position = vec4(a_position, 1.0).
4. Фрагментні шейдери
Фрагментний шейдер виконується один раз на піксель і записує кінцевий колір
у fragColor:
#version 300 es
precision highp float;
in vec2 v_uv; // з вершинного шейдера: UV-координати 0→1
out vec4 fragColor; // вихідний колір (RGBA)
uniform float u_time; // секунди від початку
void main() {
// Анімуємо колірний градієнт у часі
vec3 col = 0.5 + 0.5 * cos(u_time + v_uv.xyx + vec3(0,2,4));
fragColor = vec4(col, 1.0);
}
fragCoord
(позиція пікселя) та uniform-змінні iResolution, iTime.
Перетворіть координати пікселів на UV у діапазоні [-1, 1]:
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
5. Uniform- і varying-змінні
- Uniform — встановлюється один раз із CPU, однакове значення для кожної вершини/фрагмента в одному виклику малювання. Використовується для: часу, матриць, семплерів текстур, позиції миші.
- Attribute / in — дані для кожної вершини, завантажені в буфер. Доступні лише у вершинному шейдері.
- Varying / out → in — вихід із вершинного шейдера, що автоматично інтерполюється по поверхні трикутника та отримується як вхід у фрагментний шейдер. UV-координати — це класичний приклад.
// На боці JavaScript (WebGL 2)
const loc = gl.getUniformLocation(prog, 'u_time');
gl.uniform1f(loc, performance.now() / 1000); // float
const locMVP = gl.getUniformLocation(prog, 'u_mvp');
gl.uniformMatrix4fv(locMVP, false, matrix); // 4×4
6. Малювання фігур за допомогою SDF
Функція знакової відстані (Signed Distance Function, SDF) бере точку та повертає знакову відстань до найближчої поверхні. Якщо < 0, точка перебуває всередині фігури.
// SDF кола: повертає відстань від точки p до кола радіуса r
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
// Плавне згладжене заповнення
float fill(float sdf, float edge) {
return 1.0 - smoothstep(-edge, edge, sdf);
}
void main() {
vec2 uv = (v_uv - 0.5) * 2.0; // перевідображення в -1..1
uv.x *= u_resolution.x / u_resolution.y; // виправлення співвідношення сторін
float d = sdCircle(uv, 0.4);
float c = fill(d, 0.005);
fragColor = vec4(vec3(0.2, 0.8, 1.0) * c, 1.0);
}
SDF чудово комбінуються: min(a, b) = об'єднання,
max(a, b) = перетин, max(a, -b) =
віднімання. Плавне об'єднання: smin(a, b, k) з поліноміальним змішуванням.
7. Процедурний шум
GLSL не має вбудованого випадкового шуму. Класичний значеннєвий шум на основі гешу:
// Простий геш: повертає псевдовипадкове float для цілого зерна
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
// Значеннєвий шум: плавне випадкове скалярне поле
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep
float a = hash(i);
float b = hash(i + vec2(1, 0));
float c = hash(i + vec2(0, 1));
float d = hash(i + vec2(1, 1));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// FBM (фрактальний броунівський рух): багатошаровий шум
float fbm(vec2 p) {
float val = 0.0, amp = 0.5, freq = 1.0;
for (int i = 0; i < 6; ++i) {
val += amp * noise(p * freq);
amp *= 0.5;
freq *= 2.0;
}
return val;
}
8. Запуск шейдерів у WebGL
Мінімальне налаштування повноекранного шейдера WebGL 2 у ~80 рядках JavaScript:
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2');
function compileShader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
throw gl.getShaderInfoLog(s);
return s;
}
const vert = compileShader(gl.VERTEX_SHADER, `#version 300 es
in vec2 a_pos;
out vec2 v_uv;
void main() { v_uv = a_pos * 0.5 + 0.5; gl_Position = vec4(a_pos, 0, 1); }
`);
const frag = compileShader(gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 fragColor;
uniform float u_time;
void main() {
vec3 col = 0.5 + 0.5 * cos(u_time + v_uv.xyx + vec3(0,2,4));
fragColor = vec4(col, 1.0);
}
`);
const prog = gl.createProgram();
gl.attachShader(prog, vert);
gl.attachShader(prog, frag);
gl.linkProgram(prog);
// Повноекранний квад: два трикутники
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
const uTime = gl.getUniformLocation(prog, 'u_time');
function frame(t) {
gl.useProgram(prog);
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(uTime, t / 1000);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
Для складних проєктів використовуйте Three.js
(THREE.RawShaderMaterial або THREE.ShaderMaterial),
який автоматично налаштовує буфери, матричні uniform-змінні та дані
освітлення. Або експериментуйте наживо на ShaderToy — запускайте
фрагментні шейдери в браузері без жодного шаблонного коду.