🟩 GLSL · WebGL · Програмування для GPU
📅 Березень 2026 ⏱ ≈ 12 хв читання 🟢 Початковий–середній · Останнє оновлення: 23 червня 2026 р.

Вступ до шейдерів GLSL

Шейдери виконуються на GPU — тисячі крихітних програм, що працюють паралельно, по одній на вершину чи піксель. Саме вони перетворюють трикутники на хром, вогонь, водяні відблиски та інопланетні планети. GLSL (OpenGL Shading Language) — це C-подібна мова, якою ви їх пишете.

1. Конвеєр GPU

Щоб відрендерити трикутник, апаратне забезпечення GPU виконує фіксований конвеєр із програмованими етапами, які ви пишете на GLSL:

CPU Завантаження даних вершин Позиції, нормалі, UV записуються в буфер GPU (VBO)
ШЕЙДЕР Вершинний шейдер Виконується один раз на вершину — перетворює позицію в простір відсікання
Фіксований Растеризація GPU заповнює пікселі трикутника, інтерполює varying-змінні між вершинами
ШЕЙДЕР Фрагментний (піксельний) шейдер Виконується один раз на піксель — видає кінцевий колір RGBA
Фіксований Об'єднання виводу / змішування Тест глибини, альфа-змішування, запис у буфер кадру

У сучасних 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);
}
Сумісність із ShaderToy: ShaderToy використовує fragCoord (позиція пікселя) та uniform-змінні iResolution, iTime. Перетворіть координати пікселів на UV у діапазоні [-1, 1]: vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

5. Uniform- і varying-змінні

// На боці 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;
}
Шум Сімплекса / шум Перліна: більше на основі градієнтів, менше візуальних артефактів, вирівняних за осями. Для продакшену Inigo Quilez (iquilezles.org) має GLSL-реалізації градієнтного шуму, Вороного (клітинного) та FBM зі спотворенням області — основу більшості ландшафтів ShaderToy.

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 — запускайте фрагментні шейдери в браузері без жодного шаблонного коду.