LEARN X · ЗА 16 МИН

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);                   // готовый цвет пикселя
}
Поддержать проект