WGSL (WebGPU)
Экспресс-тур по WGSL — языку шейдеров WebGPU: типы, векторы, swizzling, атрибуты, точки входа @vertex/@fragment/@compute, структуры и привязки ресурсов.
WGSL (WebGPU Shading Language) — язык шейдеров для нового веб-API WebGPU. Это статически типизированный язык, на котором пишут программы для GPU прямо в браузере. Пройдёмся по всему за 16 минут, целиком в комментариях к коду.
Что такое WGSL и WebGPU
WebGPU — современный браузерный API доступа к GPU, идущий на смену WebGL. Шейдеры для него пишут на WGSL.
// Это однострочный комментарий.
/* А это блочный
комментарий. */
// WGSL компилируется в браузере и выполняется на GPU.
// Два основных вида шейдеров:
// - вершинный (vertex) — считает позиции вершин;
// - фрагментный (fragment) — считает цвет каждого пикселя.
// Плюс вычислительный (compute) — расчёты общего назначения.
Типы
// Скалярные типы:
// i32 — целое со знаком (32 бита)
// u32 — целое без знака (32 бита)
// f32 — число с плавающей точкой (32 бита)
// bool — логическое значение (true / false)
// Векторы (фиксированной длины 2, 3 или 4):
// vec2<f32> — два f32
// vec3<f32> — три f32 (часто это XYZ или RGB)
// vec4<f32> — четыре f32 (XYZW или RGBA)
// Матрицы:
// mat4x4<f32> — матрица 4×4 (типична для трансформаций)
// mat3x3<f32> — матрица 3×3
// Массивы:
// array<f32, 8> — массив из 8 элементов f32
// array<f32> — массив динамической длины (в storage-буфере)
Переменные: let, var, const
// const — константа времени компиляции, вычисляется заранее.
const PI: f32 = 3.14159;
fn demo() {
// let — неизменяемое значение (нельзя присвоить заново).
let x: f32 = 2.0;
// x = 3.0; // ОШИБКА: let нельзя менять
// var — изменяемая переменная.
var counter: i32 = 0;
counter = counter + 1; // OK
// Тип можно не писать — он выводится автоматически.
let y = 5.0; // f32
let n = 10; // i32
}
Векторы и swizzling
Компоненты вектора достают по именам x/y/z/w или r/g/b/a — в любом порядке и количестве.
fn vectors() {
// Конструкторы векторов:
let v = vec3<f32>(1.0, 2.0, 3.0);
let zero = vec3<f32>(0.0); // все компоненты = 0.0
let v4 = vec4<f32>(v, 1.0); // расширяем vec3 до vec4
// Доступ к компонентам:
let a = v.x; // 1.0
let b = v.y; // 2.0
// Swizzling — выбираем несколько компонент сразу:
let xy = v.xy; // vec2<f32>(1.0, 2.0)
let bgr = v.zyx; // переставили порядок
let rgb = v4.rgb; // те же x,y,z, но в "цветовых" именах
// Покомпонентная арифметика:
let sum = v + vec3<f32>(10.0, 20.0, 30.0); // (11, 22, 33)
let scaled = v * 2.0; // (2, 4, 6)
}
Атрибуты входа и выхода
Аннотации с @ связывают данные шейдера с конвейером GPU.
// @location(N) — пользовательский вход/выход по номеру слота.
// @builtin(...) — встроенное значение, которое даёт сам GPU.
// Примеры встроенных значений:
// @builtin(position) — позиция вершины (выход вершинного шейдера)
// @builtin(vertex_index) — индекс текущей вершины (вход)
// @builtin(global_invocation_id) — id потока (в compute-шейдере)
// Пример использования прямо в параметре функции:
// fn vs(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> { ... }
// fn fs(@location(0) color: vec4<f32>) -> @location(0) vec4<f32> { ... }
Функции
// fn имя(параметры) -> ТипВозврата { тело }
// Функция с двумя параметрами f32, возвращает f32:
fn add(a: f32, b: f32) -> f32 {
return a + b;
}
// Без возвращаемого значения стрелку -> не пишут:
fn nothing() {
let tmp = 1.0;
}
// Функции можно вызывать из других функций:
fn use_it() -> f32 {
return add(2.0, 3.0); // 5.0
}
Точки входа: @vertex, @fragment, @compute
Точка входа — функция, которую GPU вызывает напрямую. Её помечают атрибутом стадии.
// Вершинный шейдер: возвращает позицию в clip-пространстве.
@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
// Фрагментный шейдер: возвращает итоговый цвет пикселя.
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0); // красный
}
// Вычислительный шейдер: см. раздел ниже.
Структуры для входа-выхода
Когда полей много, их группируют в struct и навешивают атрибуты на поля.
// Выход вершинного шейдера: позиция + данные для фрагментного.
struct VertexOut {
@builtin(position) pos: vec4<f32>, // обязательная позиция
@location(0) color: vec3<f32>, // передаём цвет дальше
}
@vertex
fn vs(@builtin(vertex_index) i: u32) -> VertexOut {
var out: VertexOut;
out.pos = vec4<f32>(0.0, 0.0, 0.0, 1.0);
out.color = vec3<f32>(0.2, 0.6, 1.0);
return out;
}
// Фрагментный шейдер получает поля по тем же @location.
@fragment
fn fs(in: VertexOut) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
Ресурсы и привязки
Данные снаружи (uniform-буферы, текстуры) подключают через @group и @binding.
// @group(G) @binding(B) задают, откуда GPU берёт ресурс.
// var<uniform> — небольшой буфер общих данных (только чтение).
struct Uniforms {
time: f32,
resolution: vec2<f32>,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
// var<storage> — большой буфер (может быть на чтение/запись).
@group(0) @binding(1) var<storage, read> data: array<f32>;
// Текстура и сэмплер для выборки цветов:
@group(0) @binding(2) var tex: texture_2d<f32>;
@group(0) @binding(3) var samp: sampler;
Встроенные функции
fn builtins() {
// mix(a, b, t) — линейная интерполяция между a и b по t (0..1).
let c = mix(0.0, 10.0, 0.5); // 5.0
// clamp(x, lo, hi) — зажать значение в диапазон [lo, hi].
let k = clamp(1.5, 0.0, 1.0); // 1.0
// normalize(v) — вектор той же длины 1 (единичный).
let dir = normalize(vec3<f32>(3.0, 4.0, 0.0));
// dot(a, b) — скалярное произведение.
let d = dot(vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 0.0)); // 1.0
// Ещё бывают: length, cross, pow, sin, cos, abs, min, max...
}
// textureSample — выборка цвета из текстуры по координатам uv:
// let color = textureSample(tex, samp, uv);
Управление: if/else, for, loop
fn control(n: i32) -> i32 {
// Ветвление:
if (n > 0) {
return 1;
} else if (n < 0) {
return -1;
} else {
// ничего
}
// Цикл for:
var sum: i32 = 0;
for (var i: i32 = 0; i < 10; i = i + 1) {
sum = sum + i;
}
// Бесконечный loop с ручным выходом:
var j: i32 = 0;
loop {
if (j >= 5) { break; }
j = j + 1;
continue;
}
return sum;
}
Вычислительные шейдеры
Compute-шейдеры считают данные пачками потоков (workgroup) — без графики.
// Выходной буфер, в который пишем результат.
@group(0) @binding(0) var<storage, read_write> output: array<f32>;
// @workgroup_size задаёт число потоков в группе (X, Y, Z).
@compute @workgroup_size(64)
fn cs_main(@builtin(global_invocation_id) id: vec3<u32>) {
// id.x — глобальный индекс текущего потока.
let i = id.x;
// Каждый поток обрабатывает свой элемент массива.
output[i] = f32(i) * 2.0;
}
Типичный шейдер: градиент
Соберём всё вместе: треугольник на весь экран с цветным градиентом.
// Данные, которые вершинный шейдер передаёт фрагментному.
struct VSOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
// Вершинный шейдер: рисуем большой треугольник по индексу вершины.
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VSOut {
// Три точки, перекрывающие весь экран:
var p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
let xy = p[vi];
var out: VSOut;
out.pos = vec4<f32>(xy, 0.0, 1.0);
// Переводим координаты из [-1..1] в [0..1] для цвета.
out.uv = xy * 0.5 + vec2<f32>(0.5, 0.5);
return out;
}
// Фрагментный шейдер: цвет зависит от позиции пикселя.
@fragment
fn fs(in: VSOut) -> @location(0) vec4<f32> {
// По горизонтали меняется красный, по вертикали — зелёный.
let color = vec3<f32>(in.uv.x, in.uv.y, 0.5);
return vec4<f32>(color, 1.0);
}