LEARN X · ЗА 16 МИН

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