Довідник · WGSL · WebGPU

Шпаргалка з шейдерів WGSL

Типи, структури, адресні простори, bind groups, точки входу та вбудовані функції для вершинних, фрагментних і compute-шейдерів WebGPU.

Типи та конструктори

WGSL — статично типізована мова, і, на відміну від GLSL, у ній немає неявних перетворень між числовими типами — 1 (i32) та 1.0 (f32) несумісні без явного приведення типу.

Тип Опис Приклад конструктора
bool Булеве значення var b: bool = true;
i32 32-бітне знакове ціле var n: i32 = -42;
u32 32-бітне беззнакове ціле var u: u32 = 42u;
f32 32-бітне число з плаваючою комою var f: f32 = 1.0;
f16 ext 16-бітне число з плаваючою комою — вимагає розширення f16 var h: f16 = 1.0h;
vec2<T> / vec3<T> / vec4<T> Вектор з 2/3/4 компонентів типу T (f32, i32, u32, bool) let v = vec3<f32>(1.0, 0.5, 0.0);
vec3f / vec3i / vec3u Скорочені псевдоніми для поширених типів векторів let v = vec3f(1.0, 0.5, 0.0);
mat2x2<f32> … mat4x4<f32> Матриця з плаваючою комою, стовпчикова, NxM let m = mat4x4<f32>();
mat4x4f Скорочений псевдонім для mat4x4<f32> var mvp: mat4x4f;
array<T, N> Масив фіксованого розміру; масив змінного розміру — лише як останнє поле структури у storage-буфері var a: array<f32, 4>;
texture_2d<f32> 2D-текстура для семплінгу @group(0) @binding(0) var t: texture_2d<f32>;
sampler Семплер текстури, оголошується окремо від самої текстури @group(0) @binding(1) var s: sampler;
texture_storage_2d<format, access> Storage-текстура для читання/запису у compute-шейдері (без фільтрації) var img: texture_storage_2d<rgba8unorm, write>;

Конструювання векторів і swizzling

Компоненти можна читати через свізли .xyzw або .rgba, як і в GLSL. WGSL також дозволяє «розтягуючий» конструктор з одного скаляра:

WGSL
let col  = vec4f(1.0, 0.5, 0.2, 1.0);
let rgb  = col.rgb;          // vec3f(1.0, 0.5, 0.2)
let half = vec3f(0.5);       // "splat": (0.5, 0.5, 0.5)
let mixed = vec4f(rgb, 1.0); // конструктор конкатенації

// Присвоєння через свізл (запис у кілька компонентів)
var c = vec4f();
c.x = 1.0;
// увага: WGSL НЕ дозволяє c.yz = vec2f(...) у лівій частині '=' —
// присвоюйте компоненти окремо

Структури та масиви

Структури — основний спосіб передавати атрибути вершин, варьінги та uniform-дані. Порядок полів впливає на розкладку буфера — WGSL використовує правила вирівнювання, похідні від std140/std430, узгоджені з layout прив'язок буферів у WGPU.

WGSL Оголошення структур
struct Particle {
  position: vec3f,
  // УВАГА: vec3 в uniform/storage-буфері має вирівнювання як vec4
  // (16 байтів) — додайте поле явно або чергуйте зі скаляром,
  // щоб уникнути несподіванок із розкладкою
  life: f32,
  velocity: vec3f,
  mass: f32,
};

struct Uniforms {
  viewProjection: mat4x4f,
  time: f32,
  deltaTime: f32,
};

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) uv: vec2f,
  @location(1) normal: vec3f,
};
Пастка з вирівнюванням: vec3<f32> усередині uniform- і storage-буферів має вирівнювання, як у vec4<f32> (16 байтів). Чергування vec3f із наступним полем f32 (як вище) пакує їх в один 16-байтовий слот — пропустіть його, і всі наступні поля мовчки зсунуться.

Адресні простори та прив'язки

Кожна змінна, оголошена поза тілом функції, живе у явному адресному просторі, який визначає спосіб прив'язки з JavaScript через GPUBindGroupLayout.

Адресний простір Мутабельність Типове застосування
function читання/запис, локально Типовий простір для var усередині функції
private читання/запис, на кожен виклик Робочий стан на рівні модуля, не спільний
workgroup читання/запис, спільний Спільна пам'ять між викликами в одній compute-групі — використовується для тайлових редукцій
uniform лише читання Невеликі дані, що часто оновлюються (матриці камери, час) — до 64 КБ на прив'язку на більшості пристроїв
storage, read лише читання Великі буфери лише для читання (пули вершин, LUT)
storage, read_write читання/запис Вихідні буфери compute-шейдерів, читання-модифікація-запис стану часток
WGSL Оголошення bind group
// індекс group відповідає setBindGroup(index, ...) у JS
// індекс binding відповідає полю `binding` у GPUBindGroupLayoutDescriptor

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<storage, read> particlesIn: array<Particle>;
@group(0) @binding(2) var<storage, read_write> particlesOut: array<Particle>;
@group(1) @binding(0) var diffuseTex: texture_2d<f32>;
@group(1) @binding(1) var diffuseSampler: sampler;

var<workgroup> tile: array<f32, 64>; // спільна для однієї workgroup

Вбудовані функції

Тригонометричні та експоненційні

Функція Повертає
sin(x), cos(x), tan(x) Покомпонентна тригонометрія
asin(x), acos(x), atan2(y, x) Обернена тригонометрія; зверніть увагу — саме atan2, а не atan(y,x), як у GLSL
pow(x, y), exp(x), log(x) Степінь / логарифм
sqrt(x), inverseSqrt(x) Квадратний корінь; швидкий зворотний корінь (camelCase, на відміну від inversesqrt у GLSL)

Математика та інтерполяція

Функція Повертає
abs(x) Абсолютне значення покомпонентно
floor(x), ceil(x), round(x) Округлення вниз / вгору / до найближчого парного
fract(x) x − floor(x)
x % y Усічений модуль — у WGSL використовується оператор %, а не функція mod()
min(x,y), max(x,y) Покомпонентні мінімум/максимум
clamp(x, lo, hi) Обмеження x у [lo, hi]
mix(a, b, t) Лінійна інтерполяція: a·(1−t) + b·t
step(edge, x) 0, якщо x < edge, інакше 1
smoothstep(lo, hi, x) Кубічна Ерміта: 3t²−2t³ у [lo,hi]
sign(x) −1, 0 або +1
select(f, t, cond) Безрозгалужений вибір: повертає t, якщо cond істинне, інакше f — у WGSL немає тернарного оператора ?:

Вектори та геометрія

Функція Повертає
length(v) Евклідова довжина √(x²+y²+…)
distance(a, b) length(a−b)
dot(a, b) Скалярний добуток
cross(a, b) Векторний добуток для vec3
normalize(v) Одиничний вектор: v / length(v)
reflect(I, N) Відбиття падаючого вектора I відносно нормалі N
refract(I, N, eta) Заломлення за законом Снелла; eta = n₁/n₂
faceForward(N, I, Nref) Повертає N, орієнтовану проти I

Семплінг текстур

Функція Повертає
textureSample(t, s, uv) Фільтрований семпл — лише у fragment-стадії (потребує неявних похідних)
textureSampleLevel(t, s, uv, lod) Семпл на явному рівні mip — можна використовувати у будь-якій стадії, включно з compute
textureLoad(t, coords, lod) Отримання точного тексела за цілочисловими координатами, без фільтрації
textureStore(t, coords, value) Запис тексела у storage-текстуру — лише compute-шейдери
textureDimensions(t) Повертає vec2<u32> розміри текстури на mip 0

Синхронізація (лише compute)

Функція Призначення
workgroupBarrier() Блокує виконання, доки всі виклики у workgroup не досягнуть цієї точки, а записи у workgroup-пам'ять не стануть видимими
storageBarrier() Гарантує видимість записів у storage-буфер для інших викликів у workgroup
atomicAdd(&a, v) / atomicLoad / atomicStore Атомарне читання-модифікація-запис для atomic<i32> або atomic<u32> у storage- чи workgroup-пам'яті

Vertex- і fragment-стадії

На відміну від GLSL, WGSL використовує єдиний shader-модуль із позначеними точками входу (@vertex, @fragment, @compute) замість окремих об'єктів шейдерів. Усі локації атрибутів і варьінгів явні — через @location(n).

WGSL Vertex + fragment в одному модулі
struct Uniforms {
  viewProjection: mat4x4f,
  model: mat4x4f,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;

struct VertexInput {
  @location(0) position: vec3f,
  @location(1) normal: vec3f,
  @location(2) uv: vec2f,
};

struct VertexOutput {
  @builtin(position) clipPos: vec4f,
  @location(0) uv: vec2f,
  @location(1) worldNormal: vec3f,
};

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var out: VertexOutput;
  var pos = in.position;
  pos.y += sin(pos.x * 3.0 + u.time) * 0.1; // просте хвильове зміщення

  let world = u.model * vec4f(pos, 1.0);
  out.clipPos = u.viewProjection * world;
  out.uv = in.uv;
  out.worldNormal = normalize((u.model * vec4f(in.normal, 0.0)).xyz);
  return out;
}

@group(1) @binding(0) var diffuseTex: texture_2d<f32>;
@group(1) @binding(1) var diffuseSampler: sampler;

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
  let albedo = textureSample(diffuseTex, diffuseSampler, in.uv);

  let lightDir = normalize(vec3f(1.0, 2.0, 1.5));
  let diff = max(dot(in.worldNormal, lightDir), 0.0);
  let lit = albedo.rgb * (0.2 + 0.8 * diff);

  return vec4f(lit, albedo.a);
}
Немає gl_Position / gl_FragColor: WGSL використовує @builtin(position) у структурі результату vertex-шейдера та @location(0) для значення, яке повертає fragment-шейдер, замість неявних глобальних змінних GLSL.

Compute-шейдери

Compute-шейдери виконуються у тривимірних сітках workgroups, кожна з яких містить фіксовану 3D-сітку викликів (invocations), оголошену через @workgroup_size(x, y, z). У WebGL цієї стадії немає взагалі — вона ексклюзивна для WGSL/WebGPU і є основою для GPU-систем часток, розв'язувачів рідин та симуляцій N тіл.

WGSL Compute-прохід інтеграції часток
struct Particle {
  position: vec3f,
  life: f32,
  velocity: vec3f,
  mass: f32,
};

struct SimParams {
  deltaTime: f32,
  gravity: f32,
  particleCount: u32,
};

@group(0) @binding(0) var<uniform> params: SimParams;
@group(0) @binding(1) var<storage, read> particlesIn: array<Particle>;
@group(0) @binding(2) var<storage, read_write> particlesOut: array<Particle>;

@compute @workgroup_size(64)
fn cs_main(
  @builtin(global_invocation_id) gid: vec3<u32>,
  @builtin(local_invocation_index) lid: u32
) {
  let i = gid.x;
  if (i >= params.particleCount) {
    return; // захист: кількість workgroup округлюється вгору, можливий перебір
  }

  var p = particlesIn[i];
  p.velocity.y -= params.gravity * params.deltaTime;
  p.position += p.velocity * params.deltaTime;
  p.life -= params.deltaTime;

  particlesOut[i] = p;
}

Розрахунок розміру диспетчеризації у JavaScript

JavaScript
const WORKGROUP_SIZE = 64;
const particleCount = 100_000;
const workgroupCount = Math.ceil(particleCount / WORKGROUP_SIZE);

const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(workgroupCount); // 1D-диспетч; приймає й (x, y, z)
pass.end();
Обмеження розміру workgroup: добуток x * y * z у @workgroup_size не повинен перевищувати maxComputeInvocationsPerWorkgroup (зазвичай 256). 64 — безпечне, переносне типове значення для одновимірних навантажень з частками/масивами.

Налаштування у JavaScript

Мінімальний код для отримання пристрою, створення shader-модуля з рядка WGSL та побудови bind group layout, що відповідає анотаціям @group/@binding вище.

JavaScript Ініціалізація пристрою та пайплайну
if (!navigator.gpu) throw new Error("WebGPU не підтримується");

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext("webgpu");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: "premultiplied" });

const module = device.createShaderModule({ code: wgslSource });

const pipeline = device.createRenderPipeline({
  layout: "auto", // дозволити WebGPU вивести bind group layout із WGSL
  vertex: { module, entryPoint: "vs_main", buffers: [vertexBufferLayout] },
  fragment: {
    module,
    entryPoint: "fs_main",
    targets: [{ format }],
  },
  primitive: { topology: "triangle-list", cullMode: "back" },
});

const computePipeline = device.createComputePipeline({
  layout: "auto",
  compute: { module: computeModule, entryPoint: "cs_main" },
});

Патерни шейдерів

Хеш / шум (без текстур)

WGSL
// Простий 2D-хеш -> псевдовипадковий float у [0,1]
fn hash21(pIn: vec2f) -> f32 {
  var p = fract(pIn * vec2f(127.1, 311.7));
  p += dot(p, p + 19.19);
  return fract(p.x * p.y);
}

// Value noise: плавна інтерполяція між хешованими вузлами ґратки
fn noise(p: vec2f) -> f32 {
  let i = floor(p);
  let f = fract(p);
  let u = f * f * (3.0 - 2.0 * f); // smoothstep
  return mix(
    mix(hash21(i),             hash21(i + vec2f(1.0, 0.0)), u.x),
    mix(hash21(i + vec2f(0.0, 1.0)), hash21(i + vec2f(1.0, 1.0)), u.x), u.y
  );
}

Функції знакової відстані (2D)

WGSL
fn sdCircle(p: vec2f, r: f32) -> f32 { return length(p) - r; }

fn sdBox(p: vec2f, b: vec2f) -> f32 {
  let d = abs(p) - b;
  return length(max(d, vec2f(0.0))) + min(max(d.x, d.y), 0.0);
}

// Плавне булеве об'єднання (k керує радіусом змішування)
fn smin(a: f32, b: f32, k: f32) -> f32 {
  let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

Ping-pong буфери (подвійна буферизація симуляції)

JavaScript
// Storage-буфер не можна безпечно читати й писати в одному й тому ж
// слоті bind group, тож чергуємо два буфери + дві bind group щокадру.
let bufA = device.createBuffer({ size, usage: STORAGE | COPY_DST });
let bufB = device.createBuffer({ size, usage: STORAGE | COPY_DST });
let bindGroupAtoB = makeBindGroup(bufA, bufB); // читати A, писати B
let bindGroupBtoA = makeBindGroup(bufB, bufA); // читати B, писати A
let flip = false;

function frame() {
  const pass = encoder.beginComputePass();
  pass.setPipeline(computePipeline);
  pass.setBindGroup(0, flip ? bindGroupBtoA : bindGroupAtoB);
  pass.dispatchWorkgroups(workgroupCount);
  pass.end();
  flip = !flip;
}

Поширені службові фрагменти WGSL

WGSL
// Перевідображення [a,b] -> [c,d]
fn remap(x: f32, a: f32, b: f32, c: f32, d: f32) -> f32 {
  return c + (d - c) * ((x - a) / (b - a));
}

// Матриця 2D-обертання
fn rot2(a: f32) -> mat2x2f {
  let c = cos(a);
  let s = sin(a);
  return mat2x2f(c, -s, s, c);
}

// Яскравість (сприймана, вагові коефіцієнти Rec. 709)
fn luma(rgb: vec3f) -> f32 {
  return dot(rgb, vec3f(0.2126, 0.7152, 0.0722));
}

// sRGB <-> лінійний простір (наближення через gamma 2.2)
fn toLinear(c: vec3f) -> vec3f { return pow(c, vec3f(2.2)); }
fn toSRGB(c: vec3f)   -> vec3f { return pow(c, vec3f(1.0 / 2.2)); }
Короткі нотатки з міграції GLSL → WGSL: немає неявних перетворень int/float · % замінює mod() · atan2(y,x) замінює atan(y,x) · select() замінює тернарний оператор · функції та змінні потребують явних типів повернення й оголошення (fn f() -> f32, var x: f32) · один модуль може містити vertex-, fragment- і compute-точки входу одночасно.