LEARN X · ЗА 16 МИН
HLSL (шейдеры)
HLSL за 16 минут: типы, векторы и swizzling, семантики, вершинный и пиксельный шейдеры, cbuffer, текстуры, матрицы — весь язык в комментариях кода.
HLSL (High-Level Shading Language) — это язык программирования шейдеров для GPU в DirectX и Unity. Шейдеры — это маленькие программы, которые выполняются на видеокарте для каждой вершины и каждого пикселя. Ниже — весь язык на одной странице через закомментированный код. Запускать примеры удобно в Unity (.shader/.hlsl), в RenderDoc/ShaderToy-подобных средах или прямо в DirectX-пайплайне.
Что такое HLSL и шейдеры
Шейдер — это код, который GPU гоняет параллельно для миллионов пикселей.
// HLSL = High-Level Shading Language. Си-подобный синтаксис.
// Где применяется:
// * DirectX (Direct3D 9..12) — родная среда HLSL.
// * Unity — шейдеры пишутся на HLSL (внутри ShaderLab).
// * Unreal — частично (HLSL-вставки в материалах).
// Два главных типа шейдеров в графическом конвейере:
// 1) Вершинный шейдер (vertex shader) — выполняется ДЛЯ КАЖДОЙ ВЕРШИНЫ.
// Задача: перевести 3D-координаты модели в координаты экрана.
// 2) Пиксельный шейдер (pixel/fragment shader) — ДЛЯ КАЖДОГО ПИКСЕЛЯ.
// Задача: вычислить итоговый ЦВЕТ пикселя.
// GPU выполняет шейдер ПАРАЛЛЕЛЬНО для тысяч элементов сразу,
// поэтому циклы и ветвления стоят дорого — старайся писать просто.
// Однострочный комментарий // ...
/* и многострочный
тоже работает */
Базовые типы
// Скалярные типы:
float a = 3.14; // вещественное число (основной рабочий тип в шейдерах)
int n = 42; // целое
uint u = 7; // беззнаковое целое
bool flag = true; // логический тип
half h = 1.5; // половинная точность (16 бит) — быстрее на мобилках
double d = 2.0; // двойная точность (редко, медленно на GPU)
// Векторные типы — упакованные наборы float (главная фишка HLSL):
float2 uv = float2(0.5, 0.5); // 2 компонента: x, y
float3 pos = float3(1.0, 2.0, 3.0); // 3 компонента: x, y, z
float4 col = float4(1.0, 0.0, 0.0, 1.0); // 4 компонента: x, y, z, w (RGBA)
// Бывают и int2/int3/int4, bool2 и т.д.
// Матрицы:
float4x4 M; // 4x4 — основная матрица трансформаций
float3x3 N; // 3x3 — обычно для нормалей
// Запись floatRxC: R строк, C столбцов.
// Конструктор вектора может «разворачивать» меньшие векторы:
float3 rgb = float3(0.2, 0.4, 0.8);
float4 rgba = float4(rgb, 1.0); // склеили float3 + float = float4
Векторы и swizzling
Swizzling — обращение к компонентам вектора по буквам, в любом порядке.
float4 v = float4(1.0, 2.0, 3.0, 4.0);
// Доступ к компонентам по позиции: x, y, z, w
float x = v.x; // 1.0
float y = v.y; // 2.0
// Те же компоненты как цвет: r, g, b, a (синонимы x,y,z,w)
float red = v.r; // 1.0 — удобно, когда вектор это цвет
// Swizzle — выбираем несколько компонентов сразу:
float3 xyz = v.xyz; // float3(1, 2, 3)
float2 xy = v.xy; // float2(1, 2)
float3 rgb = v.rgb; // float3(1, 2, 3) — то же самое в «цветовой» нотации
// Перестановки (порядок любой):
float3 bgr = v.zyx; // float3(3, 2, 1) — развернули
float4 same = v.xxxx; // float4(1, 1, 1, 1) — размножили один компонент
float2 wz = v.wz; // float2(4, 3)
// Swizzle работает и слева (запись):
float4 c = float4(0, 0, 0, 0);
c.rgb = float3(1, 0.5, 0.2); // записали сразу 3 компонента
c.a = 1.0;
Операции с векторами
float3 a = float3(1.0, 2.0, 3.0);
float3 b = float3(4.0, 5.0, 6.0);
// Арифметика ПОКОМПОНЕНТНАЯ (по каждому элементу отдельно):
float3 s = a + b; // (5, 7, 9)
float3 d = a - b; // (-3, -3, -3)
float3 m = a * b; // (4, 10, 18) — НЕ скалярное произведение!
float3 q = a / b; // (0.25, 0.4, 0.5)
// Со скаляром — применяется к каждому компоненту:
float3 t = a * 2.0; // (2, 4, 6)
// Встроенные векторные функции:
float dp = dot(a, b); // скалярное произведение: 1*4+2*5+3*6 = 32
float3 cr = cross(a, b); // векторное произведение (перпендикуляр), float3
float len = length(a); // длина вектора = sqrt(1+4+9) ≈ 3.742
float3 nrm = normalize(a); // единичный вектор (длина 1), сохраняет направление
float dist = distance(a, b); // расстояние между точками = length(a - b)
float r = reflect(a, nrm); // отражение вектора относительно нормали
Семантики
Семантика — метка после двоеточия, говорит GPU, что означает поле.
// Семантики связывают данные с конвейером. Пишутся через двоеточие.
// ВХОДНЫЕ семантики вершинного шейдера (что приходит из меша):
// POSITION — позиция вершины в пространстве модели
// NORMAL — нормаль вершины
// TEXCOORD0 — текстурные координаты (UV), бывают TEXCOORD0..TEXCOORD7
// COLOR — цвет вершины
// СИСТЕМНЫЕ семантики (SV_ = System Value, заполняет сам GPU):
// SV_POSITION — позиция в координатах экрана (выход вершинного шейдера)
// SV_Target — итоговый цвет пикселя (выход пиксельного шейдера)
// SV_VertexID — порядковый номер вершины
// SV_Depth — глубина пикселя
// Пример объявления поля с семантикой:
struct Example {
float4 vertex : POSITION; // вход: позиция вершины
float2 uv : TEXCOORD0; // вход: UV-координаты
float4 pos : SV_POSITION; // выход: экранная позиция
};
Структуры ввода-вывода
// Данные между стадиями конвейера передают через struct с семантиками.
// Вход вершинного шейдера — то, что лежит в вершинах меша:
struct VertexInput {
float4 position : POSITION; // позиция вершины (модельные координаты)
float3 normal : NORMAL; // нормаль
float2 uv : TEXCOORD0; // текстурные координаты
};
// Выход вершинного -> вход пиксельного (интерполируется по треугольнику):
struct VertexOutput {
float4 position : SV_POSITION; // ОБЯЗАТЕЛЬНО: экранная позиция
float2 uv : TEXCOORD0; // UV пробрасываем дальше в пиксельный шейдер
float3 worldNormal : TEXCOORD1; // можно нести любые свои данные
};
// GPU автоматически интерполирует значения VertexOutput между вершинами
// треугольника, прежде чем передать их в пиксельный шейдер.
Вершинный и пиксельный шейдеры
// Вершинный шейдер: принимает VertexInput, возвращает VertexOutput.
VertexOutput Vert(VertexInput input)
{
VertexOutput o;
// Переводим позицию модели в экранные координаты (через матрицу, см. ниже).
o.position = mul(MVP, input.position);
o.uv = input.uv; // пробрасываем UV дальше
o.worldNormal = input.normal;
return o;
}
// Пиксельный шейдер: принимает интерполированный VertexOutput,
// возвращает цвет в семантике SV_Target.
float4 Frag(VertexOutput input) : SV_Target
{
// Простейший вариант — вернуть UV как цвет (зелёно-красный градиент):
return float4(input.uv, 0.0, 1.0);
}
// В чистом DirectX функции связывают с ПРОФИЛЯМИ компиляции:
// vs_5_0 — vertex shader, shader model 5.0
// ps_5_0 — pixel shader, shader model 5.0
// В Unity это задаётся прагмами: #pragma vertex Vert / #pragma fragment Frag
Константные буферы (cbuffer)
// cbuffer — блок данных, одинаковых для всех вершин/пикселей одного вызова
// (uniform-данные). Их выставляет CPU перед отрисовкой.
cbuffer PerFrame {
float4x4 MVP; // матрица Model-View-Projection
float time; // текущее время (для анимаций)
float3 lightDir; // направление света
};
cbuffer PerMaterial {
float4 baseColor; // базовый цвет материала
float roughness; // шероховатость
};
// В старом синтаксисе то же делают через ключевое слово uniform:
uniform float4 _Color; // в Unity-шейдерах свойства приходят так
// Внутри шейдера к этим полям обращаются как к обычным переменным:
// float4 c = baseColor * time;
Встроенные функции
float t = 0.3;
float3 A = float3(0,0,0);
float3 B = float3(1,1,1);
// Интерполяция и ограничение:
float3 mix = lerp(A, B, t); // линейная интерполяция A->B по t (t=0.3)
float sat = saturate(2.5); // зажать в [0,1] -> 1.0 (как clamp(x,0,1))
float cl = clamp(5.0, 0, 3); // зажать в [0,3] -> 3.0
float st = step(0.5, t); // 0, если t<0.5; иначе 1 (здесь 0)
float sm = smoothstep(0, 1, t); // плавный переход 0->1 (S-кривая)
// Математика (все работают и покомпонентно для векторов):
float p = pow(2.0, 10.0); // степень: 1024
float sq = sqrt(16.0); // корень: 4
float ab = abs(-3.5); // модуль: 3.5
float fr = frac(3.75); // дробная часть: 0.75
float fl = floor(3.75); // вниз: 3
float md = fmod(7.0, 3.0); // остаток: 1
float mn = min(2.0, 5.0); // 2
float mx = max(2.0, 5.0); // 5
// Тригонометрия (часто для волн и анимаций):
float si = sin(time);
float co = cos(time);
Текстуры и сэмплеры
Текстура — картинка в памяти GPU; сэмплер задаёт, как из неё читать.
// Современный синтаксис (DirectX 10+/Unity):
Texture2D _MainTex; // сама текстура (картинка)
SamplerState _MainSampler; // правила выборки: фильтрация, wrap/clamp
float4 SampleColor(float2 uv)
{
// Sample: читаем цвет текстуры по UV-координатам (0..1).
float4 c = _MainTex.Sample(_MainSampler, uv);
return c;
}
// Старый синтаксис (DirectX 9 / часть Unity-шейдеров):
// sampler2D _MainTex;
// float4 c = tex2D(_MainTex, uv);
// Другие виды текстур:
// Texture3D — объёмная (3D) текстура
// TextureCube — кубическая (скайбоксы, отражения), Sample по float3
// Texture2DArray — массив 2D-текстур
Условия и циклы
float v = 0.7;
// Ветвление как в Си:
if (v > 0.5) {
v = 1.0;
} else if (v > 0.2) {
v = 0.5;
} else {
v = 0.0;
}
// Тернарный оператор:
float r = (v > 0.5) ? 1.0 : 0.0;
// Цикл for (на GPU дорог — лучше фиксированное число итераций):
float sum = 0.0;
for (int i = 0; i < 4; i++) {
sum += float(i) * 0.1;
}
// while тоже есть, но на GPU крайне нежелателен:
int k = 0;
while (k < 3) { k++; }
// Подсказки компилятору, как разворачивать цикл:
[unroll] for (int j = 0; j < 8; j++) { /* развернуть в линейный код */ }
[loop] for (int q = 0; q < 8; q++) { /* оставить настоящим циклом */ }
Матрицы и трансформации
// Матрицы трансформируют точки: модель -> мир -> камера -> экран.
float4x4 modelMatrix; // модель -> мир
float4x4 viewMatrix; // мир -> пространство камеры
float4x4 projectionMatrix; // камера -> экран (перспектива)
// mul — умножение матриц/векторов. ВАЖЕН порядок аргументов!
float4 localPos = float4(1.0, 0.0, 0.0, 1.0); // w=1 для точки
float4 worldPos = mul(modelMatrix, localPos); // в мировые координаты
// Цепочка трансформаций до экрана:
float4x4 MVP = mul(projectionMatrix, mul(viewMatrix, modelMatrix));
float4 clipPos = mul(MVP, localPos); // итоговая позиция для SV_POSITION
// Трансформация нормали (только поворот -> матрица 3x3, w отбрасываем):
float3x3 normalMatrix = (float3x3)modelMatrix;
float3 worldNormal = normalize(mul(normalMatrix, float3(0, 1, 0)));
// Транспонирование и единичная матрица:
float4x4 Mt = transpose(MVP);
Пиксельный шейдер на практике
Соберём всё вместе: анимированный градиент с эффектом.
// Полный мини-шейдер: рисует переливающийся градиент по UV.
cbuffer PerFrame {
float4x4 MVP;
float time; // время в секундах для анимации
};
struct VIn { float4 pos : POSITION; float2 uv : TEXCOORD0; };
struct VOut { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
// Вершинный шейдер: позиция -> экран, UV пробрасываем.
VOut Vert(VIn i) {
VOut o;
o.pos = mul(MVP, i.pos);
o.uv = i.uv;
return o;
}
// Пиксельный шейдер: цвет = функция от UV и времени.
float4 Frag(VOut i) : SV_Target {
float2 uv = i.uv; // координаты пикселя 0..1
// Базовый градиент: по горизонтали красный, по вертикали зелёный.
float3 grad = float3(uv.x, uv.y, 0.5);
// Добавим бегущую волну синусом по времени для «дыхания» яркости.
float wave = 0.5 + 0.5 * sin(time + uv.x * 6.2831);
grad *= wave; // модулируем яркость
// Мягкая виньетка: затемняем края (расстояние от центра).
float vign = 1.0 - smoothstep(0.4, 0.75, distance(uv, float2(0.5, 0.5)));
grad *= vign;
return float4(saturate(grad), 1.0); // зажали в [0,1], альфа = 1
}