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
}
Поддержать проект