GLSL (шейдеры)
GLSL за 16 минут: весь язык шейдеров на одной странице — типы, векторы, swizzling, uniform, встроенные функции, текстуры, фрагментный шейдер.
GLSL (OpenGL Shading Language) — это язык для программ, которые исполняются на видеокарте (GPU) и считают цвет каждого пикселя и положение каждой вершины. На нём пишут шейдеры для игр, графики и эффектов. Синтаксис похож на C. Ниже — весь язык на одной странице: почти без текста, всё в комментариях кода.
Что такое шейдеры
Шейдер — маленькая программа на GPU. Два главных вида: вершинный и фрагментный.
// Шейдер исполняется на видеокарте (GPU), а не на процессоре (CPU).
// GPU считает МНОГО точек ПАРАЛЛЕЛЬНО: тысячи ядер одновременно.
//
// Графический конвейер (упрощённо):
// вершины модели -> ВЕРШИННЫЙ шейдер -> сборка треугольников ->
// растеризация (треугольник -> пиксели) -> ФРАГМЕНТНЫЙ шейдер -> экран
//
// ВЕРШИННЫЙ шейдер (vertex): вызывается для каждой ВЕРШИНЫ,
// его задача — посчитать позицию точки.
// ФРАГМЕНТНЫЙ шейдер (fragment): вызывается для каждого ПИКСЕЛЯ,
// его задача — посчитать ЦВЕТ пикселя.
// Это однострочный комментарий.
/* А это многострочный
комментарий. */
Структура шейдера
Любой шейдер начинается с версии и содержит функцию main.
#version 330 core // версия GLSL (330 = OpenGL 3.3). Всегда первой строкой.
void main() { // точка входа: с неё начинается исполнение
// ... тело шейдера ...
}
// В ВЕРШИННОМ шейдере результат пишут в встроенную переменную gl_Position:
// gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // позиция вершины
//
// Во ФРАГМЕНТНОМ шейдере результат — цвет. В старом стиле это gl_FragColor,
// в современном — своя out-переменная (см. раздел про квалификаторы):
// FragColor = vec4(1.0, 0.0, 0.0, 1.0); // красный цвет (R,G,B,A)
//
// ВАЖНО: точка с запятой ; в конце каждой инструкции обязательна.
Базовые типы
Скаляры, булевы и специальные типы для графики — векторы и матрицы.
float a = 3.14; // число с плавающей точкой. Пиши 1.0, а не 1!
int n = 42; // целое число
bool flag = true; // логический тип: true / false
// Векторы — наборы из 2, 3 или 4 float. Основа всей графики:
vec2 uv = vec2(0.5, 0.5); // 2 компоненты (координаты, UV)
vec3 color = vec3(1.0, 0.0, 0.0); // 3 компоненты (RGB-цвет, позиция XYZ)
vec4 rgba = vec4(1.0, 0.0, 0.0, 1.0); // 4 компоненты (RGBA, однородные коорд.)
// Короткая инициализация: одно число размножится по всем компонентам
vec3 gray = vec3(0.5); // = vec3(0.5, 0.5, 0.5)
// Матрицы (для трансформаций):
mat4 transform; // матрица 4x4 (поворот/масштаб/перенос)
mat3 rot; // матрица 3x3
// Целочисленные и булевы векторы тоже есть: ivec2, bvec3 и т.п.
Векторы и swizzling
Компоненты вектора можно доставать и переставлять по буквам — это swizzling.
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);
// К компонентам обращаются тремя наборами букв (смысл одинаковый):
// .x .y .z .w — позиции/координаты
// .r .g .b .a — цвета (red green blue alpha)
// .s .t .p .q — текстурные координаты
float x = v.x; // 1.0
float red = v.r; // 1.0 (та же компонента, другое имя)
// Swizzling: берём несколько компонент сразу
vec3 rgb = v.xyz; // (1.0, 2.0, 3.0) — отбросили w
vec2 xy = v.xy; // (1.0, 2.0)
// Перестановка (можно в любом порядке и с повторами!)
vec3 bgr = v.bgr; // (3.0, 2.0, 1.0) — перевернули каналы цвета
vec3 xxx = v.xxx; // (1.0, 1.0, 1.0) — размножили одну компоненту
// Swizzling работает и слева от = (запись)
vec4 c = vec4(0.0);
c.rg = vec2(1.0, 0.5); // задали только red и green
Операции с векторами
Арифметика идёт покомпонентно. Плюс есть готовые геометрические функции.
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(4.0, 5.0, 6.0);
vec3 sum = a + b; // (5.0, 7.0, 9.0) — покомпонентно
vec3 mul = a * b; // (4.0, 10.0, 18.0) — НЕ скалярное произведение!
vec3 scl = a * 2.0; // (2.0, 4.0, 6.0) — умножение на число
float d = dot(a, b); // скалярное произведение: 1*4+2*5+3*6 = 32.0
vec3 cr = cross(a, b); // векторное произведение (перпендикуляр)
float len = length(a); // длина вектора: sqrt(1+4+9)
vec3 nrm = normalize(a);// единичный вектор (длина = 1), сохраняет направление
float dst = distance(a, b); // расстояние между точками
Квалификаторы: in, out, uniform
Откуда данные приходят в шейдер и куда уходят — задают квалификаторы.
// in — ВХОД шейдера (данные приходят извне)
// out — ВЫХОД шейдера (данные уходят дальше по конвейеру)
// uniform — общее значение, ОДИНАКОВОЕ для всех вершин/пикселей,
// его задаёт программа на CPU (время, разрешение, мышь...)
in vec3 aPosition; // атрибут вершины (вход вершинного шейдера)
out vec2 vUV; // передаём UV из вершинного во фрагментный
uniform float uTime; // время в секундах — для анимации
uniform vec2 uResolution; // размер экрана в пикселях
// out вершинного шейдера автоматически становится in фрагментного
// (по совпадению имени) и интерполируется между вершинами.
//
// Старый синонимы: varying = переменная in/out между шейдерами
// (varying устарел, в #version 330+ используют in/out).
Встроенные функции
GLSL богат на математику — она работает и со скалярами, и с векторами.
float t = 0.3;
mix(0.0, 10.0, t); // линейная интерполяция: 0 при t=0, 10 при t=1 -> 3.0
clamp(t, 0.0, 1.0); // зажать в диапазон [0, 1]
step(0.5, t); // 0.0 если t<0.5, иначе 1.0 — резкая ступенька
smoothstep(0.0, 1.0, t); // плавный переход 0->1 (мягкий step)
sin(t); cos(t); // синус/косинус (для волн и анимации)
pow(2.0, 3.0); // степень: 8.0
mod(7.0, 3.0); // остаток от деления: 1.0
abs(-5.0); floor(2.7); // модуль; округление вниз -> 2.0
fract(2.7); // дробная часть: 0.7
min(a, b); max(a, b); // минимум / максимум
sqrt(9.0); exp(1.0); // корень; экспонента
// Большинство функций векторизованы: mix(vec3, vec3, t) тоже работает.
Координаты и UV
Чтобы рисовать в пикселях, нужны их координаты. Их нормируют в диапазон 0..1.
// gl_FragCoord — встроенная переменная: позиция текущего пикселя
// в пикселях экрана, отсчёт от левого нижнего угла. .xy — это x,y.
void main() {
// UV — нормированные координаты в диапазоне [0, 1] по обеим осям.
// Делим координату пикселя на размер экрана:
vec2 uv = gl_FragCoord.xy / uResolution.xy;
// uv = (0,0) — левый нижний угол
// uv = (1,1) — правый верхний угол
// Часто центрируют так, чтобы (0,0) было в середине экрана:
vec2 centered = uv - 0.5; // теперь диапазон [-0.5, 0.5]
centered.x *= uResolution.x / uResolution.y; // поправка на пропорции экрана
}
Цвет и текстуры
Картинку (текстуру) читают функцией texture() по UV-координатам.
// sampler2D — тип-ссылка на загруженную 2D-текстуру (картинку).
uniform sampler2D uTexture;
in vec2 vUV; // UV пришли из вершинного шейдера
out vec4 FragColor; // итоговый цвет пикселя
void main() {
// texture(sampler, uv) возвращает vec4 (RGBA) — цвет картинки в точке uv
vec4 texel = texture(uTexture, vUV);
// Цвета — это просто vec, с ними можно делать математику:
vec3 tint = texel.rgb * vec3(1.0, 0.8, 0.8); // лёгкий красный оттенок
FragColor = vec4(tint, texel.a); // собираем обратно RGBA
// Компоненты цвета в диапазоне [0, 1]: 0 — нет, 1 — максимум.
// vec3(1.0) — белый, vec3(0.0) — чёрный.
}
Условия и циклы
Синтаксис как в C. Но на GPU ветвления и циклы стоят дорого.
float x = 0.7;
// Условие if/else — обычное
if (x > 0.5) {
x = 1.0;
} else {
x = 0.0;
}
// Цикл for — обычный
float sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += float(i); // float(i) — приведение int -> float
}
// ОГОВОРКА про GPU:
// 1) Соседние пиксели идут «пачкой» и при if часто считают ОБЕ ветки,
// поэтому тяжёлые ветвления замедляют шейдер.
// 2) Длину цикла стараются делать ИЗВЕСТНОЙ заранее (константой),
// а не зависящей от данных. Где можно — заменяй if на mix/step/clamp.
Матрицы и трансформации
Поворот, масштаб и перенос вершин делают умножением на матрицу 4x4.
#version 330 core
in vec3 aPosition; // позиция вершины из модели
// Матрицы обычно приходят как uniform из программы на CPU:
uniform mat4 uModel; // модель: где объект в мире
uniform mat4 uView; // камера: откуда смотрим
uniform mat4 uProjection; // проекция: перспектива
void main() {
// Умножение матриц НЕ коммутативно: порядок важен!
// Читается справа налево: сначала model, потом view, потом projection.
vec4 worldPos = uModel * vec4(aPosition, 1.0); // vec3 -> vec4 (w=1)
gl_Position = uProjection * uView * worldPos;
// mat4 * vec4 даёт vec4. mat4 * mat4 даёт mat4 (композиция трансформаций).
}
Фрагментный шейдер на практике
Соберём всё вместе: градиент, круг и анимированный эффект.
#version 330 core
out vec4 FragColor;
uniform vec2 uResolution;
uniform float uTime;
void main() {
vec2 uv = gl_FragCoord.xy / uResolution.xy; // UV в [0,1]
// 1) ГРАДИЕНТ: цвет зависит от координаты
vec3 gradient = vec3(uv.x, uv.y, 0.5); // R по X, G по Y
// 2) КРУГ: расстояние от центра экрана
vec2 p = uv - 0.5;
p.x *= uResolution.x / uResolution.y; // поправка пропорций
float dist = length(p); // расстояние до центра
// smoothstep даёт мягкий край круга радиуса 0.3:
float circle = 1.0 - smoothstep(0.29, 0.31, dist);
// 3) АНИМАЦИЯ: пульсация яркости через sin(время)
float pulse = 0.5 + 0.5 * sin(uTime * 2.0); // колеблется в [0, 1]
// Смешиваем фон-градиент и белый круг, домножаем на пульс
vec3 col = mix(gradient, vec3(1.0), circle) * pulse;
FragColor = vec4(col, 1.0); // готовый цвет пикселя
}