Типи та конструктори
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 також дозволяє
«розтягуючий» конструктор з одного скаляра:
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.
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-шейдерів, читання-модифікація-запис стану часток |
// індекс 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).
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 тіл.
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
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();
x * y * z у @workgroup_size не повинен
перевищувати maxComputeInvocationsPerWorkgroup
(зазвичай 256). 64 — безпечне, переносне типове значення для
одновимірних навантажень з частками/масивами.
Налаштування у JavaScript
Мінімальний код для отримання пристрою, створення shader-модуля
з рядка WGSL та побудови bind group layout, що відповідає
анотаціям @group/@binding вище.
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" },
});
Патерни шейдерів
Хеш / шум (без текстур)
// Простий 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)
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 буфери (подвійна буферизація симуляції)
// 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
// Перевідображення [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)); }
% замінює
mod() · atan2(y,x) замінює
atan(y,x) · select() замінює тернарний
оператор · функції та змінні потребують явних типів повернення
й оголошення (fn f() -> f32,
var x: f32) · один модуль може містити vertex-,
fragment- і compute-точки входу одночасно.