Вступ до WebGPU: перші кроки
WebGPU — це наступник WebGL: сучасний API для графіки та обчислень, який браузер надає напряму, за зразком нативних API на кшталт Vulkan, Metal і Direct3D 12. Він замінює неявну машину станів WebGL на явні об'єкти: адаптери, пристрої, конвеєри та групи прив'язки. Цей урок проходить через кожне поняття, необхідне для рендерингу першого трикутника та запуску першого обчислювального шейдера.
1. Навіщо WebGPU? Від WebGL до сучасного API
WebGL — це прив'язка JavaScript до OpenGL ES, API, розробленого в епоху неявного глобального стану: прив'язати текстуру, прив'язати буфер, викликати відмальовку — і сподіватися, що ніщо інше в коді не змінило прив'язаний стан у проміжку. WebGPU відкидає цю модель. Він побудований навколо явних, незмінних об'єктів: ви описуєте конвеєр один раз (шейдери, розкладка вершин, змішування, тест глибини) і повторно використовуєте його, замість того щоб щокадру заново викликати десятки функцій встановлення стану.
З такого дизайну випливають три практичні переваги:
- Менші витрати CPU: кодування команд дешевше, оскільки валідація відбувається один раз — під час створення конвеєра, а не при кожному виклику відмальовки.
- Обчислення "з коробки": WebGPU надає обчислювальні шейдери загального призначення як базову можливість, а не розширення — це корисно для систем частинок, фізики та GPGPU-навантажень безпосередньо в браузері.
- Багатопотокове кодування команд: буфери команд можна будувати поза головним потоком і надсилати пізніше — те, чого неявний одиночний контекст WebGL ніколи не міг безпечно підтримувати.
if (!navigator.gpu) і повертайтеся до
WebGL-рендерера (наприклад, у Three.js) для старіших браузерів.
2. Адаптери та пристрої
WebGPU розділяє поняття «GPU» на два об'єкти. GPUAdapter
представляє фізичний або програмний GPU, доступний браузеру — ви
запитуєте його та перевіряєте його limits і
features. GPUDevice — це логічне з'єднання,
через яке ви фактично створюєте ресурси та надсилаєте роботу.
async function initWebGPU() {
if (!navigator.gpu) {
throw new Error("Цей браузер не підтримує WebGPU.");
}
// Запит фізичного адаптера — можна вказати перевагу продуктивності
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
});
if (!adapter) throw new Error("Не знайдено підходящого GPUAdapter.");
// Запит логічного пристрою — об'єкт, який ви фактично використовуєте
const device = await adapter.requestDevice();
// Виводимо неперехоплені помилки замість тихого збою
device.addEventListener("uncapturederror", (event) => {
console.error("Помилка WebGPU:", event.error.message);
});
return { adapter, device };
}
device.lost
(це Promise) і будьте готові переініціалізувати конвеєр після
відновлення в продакшн-коді.
3. Налаштування контексту canvas
Щоб малювати в <canvas>, запросіть контекст
"webgpu" і налаштуйте його пристроєм та форматом
текстури. На відміну від WebGL, тут потрібно явно вказати формат
пікселів — зазвичай той, який браузер вважає кращим для swap-chain,
заради продуктивності.
const canvas = document.querySelector("canvas");
const context = canvas.getContext("webgpu");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format,
alphaMode: "opaque",
});
Щокадру ви викликатимете
context.getCurrentTexture(), щоб отримати текстуру, в
яку браузер очікує рендеринг, і обгортаєте її в
GPUTextureView для прогону рендерингу.
4. WGSL: мова шейдерів WebGPU
WebGPU не використовує GLSL. Натомість визначено власну мову шейдерів — WGSL (WebGPU Shading Language): статично типізований синтаксис у стилі Rust, розроблений так, щоб чисто відображатися на SPIR-V, MSL та HLSL під капотом. Вершинний і фрагментний етапи можуть жити в одному модулі, розрізняючись атрибутами.
struct VertexOut {
@builtin(position) position : vec4<f32>,
@location(0) color : vec3<f32>,
};
@vertex
fn vs_main(
@location(0) pos : vec2<f32>,
@location(1) color : vec3<f32>
) -> VertexOut {
var out : VertexOut;
out.position = vec4<f32>(pos, 0.0, 1.0);
out.color = color;
return out;
}
@fragment
fn fs_main(in : VertexOut) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
Ключові синтаксичні відмінності від GLSL для тих, хто прийшов
звідти: типи явні (vec2<f32>, а не просто
vec2), @location(n) замінює
layout(location = n), а @builtin(position)
позначає вихід у clip-просторі — еквівалент неявного
gl_Position у GLSL.
5. Буфери та розкладка вершин
Буфери GPU створюються з фіксованим розміром і набором дозволених
прапорців usage. Щоб завантажити дані вершин, ви
створюєте буфер із прапорцями VERTEX | COPY_DST, потім
записуєте в нього дані, а потім описуєте байтову розкладку, щоб
конвеєр знав, як інтерпретувати атрибути кожної вершини.
// Чергування: x, y, r, g, b на вершину (5 float = 20 байтів)
const vertices = new Float32Array([
0.0, 0.5, 1, 0, 0,
-0.5, -0.5, 0, 1, 0,
0.5, -0.5, 0, 0, 1,
]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
const vertexLayout = {
arrayStride: 5 * 4, // 5 float × 4 байти
attributes: [
{ shaderLocation: 0, offset: 0, format: "float32x2" }, // позиція
{ shaderLocation: 1, offset: 2 * 4, format: "float32x3" }, // колір
],
};
gl.bufferData у WebGL, буфери WebGPU — це непрозора
пам'ять GPU. Ви записуєте в них через
queue.writeBuffer() або мапите для прямого доступу з
CPU через mapAsync(), коли буфер створено з прапорцями
MAP_WRITE/MAP_READ.
6. Побудова конвеєра рендерингу
GPURenderPipeline об'єднує модуль шейдера, розкладку
вершин, топологію примітивів і формат кольорової цілі в єдиний
провалідований, незмінний об'єкт. Його компіляція відносно
дорога, тому ви створюєте його один раз і повторно використовуєте
щокадру — ніколи не створюйте конвеєр усередині циклу рендерингу.
const shaderModule = device.createShaderModule({ code: wgslSource });
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vs_main",
buffers: [vertexLayout],
},
fragment: {
module: shaderModule,
entryPoint: "fs_main",
targets: [{ format }],
},
primitive: {
topology: "triangle-list",
cullMode: "back",
},
});
7. Кодування та надсилання команд
Виклики відмальовки в WebGPU не виконуються напряму — вони
записуються в GPUCommandEncoder, обгорнутий
GPURenderPassEncoder, який описує, які текстури
очищати та в які записувати. Готовий буфер команд потім надсилається
в чергу пристрою.
function frame() {
const encoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: textureView,
clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },
loadOp: "clear",
storeOp: "store",
}],
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(3); // 3 вершини, 1 інстанс
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
8. Уніформи та групи прив'язки
У WebGPU немає поняття «розташування уніформи», яке ви встановлюєте
за іменем під час виконання, як у WebGL. Замість цього ресурси
шейдера (уніформ-буфери, текстури, семплери) групуються в
GPUBindGroup, розкладка якого має відповідати парі
@group/@binding, оголошеній у WGSL.
const uniformBuffer = device.createBuffer({
size: 64, // матриця 4x4 з f32 = 16 * 4 байти
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: uniformBuffer } },
],
});
// Усередині прогону рендерингу, перед draw():
pass.setBindGroup(0, bindGroup);
9. Обчислювальні шейдери та обчислювальні проходи
Обчислювальні шейдери виконують довільну паралельну роботу без
растеризації — ідеально для симуляції частинок, інтегрування
фізики або обробки зображень повністю на GPU. Обчислювальний
шейдер оголошує workgroup_size і диспетчеризується по
тривимірній сітці робочих груп.
// WGSL: подвоєння кожного елемента буфера сховища
@group(0) @binding(0) var<storage, read_write> data : array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3<u32>) {
let i = id.x;
if (i >= arrayLength(&data)) { return; }
data[i] = data[i] * 2.0;
}
const computePipeline = device.createComputePipeline({
layout: "auto",
compute: { module: computeShaderModule, entryPoint: "main" },
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, computeBindGroup);
pass.dispatchWorkgroups(Math.ceil(elementCount / 64)); // сітка робочих груп
pass.end();
device.queue.submit([encoder.finish()]);
MAP_READ | COPY_DST,
через encoder.copyBufferToBuffer(), а потім
await buffer.mapAsync(GPUMapMode.READ).
10. Типові пастки та налагодження
-
Забутий
storeOp: "store": якщо встановити"discard", відрендерений кадр відкидається після прогону — на екрані нічого не з'явиться, хоча жодної помилки не буде викинуто. -
Невідповідність розміру буфера:
writeBuffer()тихо провалює валідацію, якщо вихідні дані більші за буфер призначення. Завжди створюйте буфери точно під розмір даних, які плануєте записати. -
Неправильний
arrayStride: крок, що не відповідає розкладці ваших чергованих даних, дає спотворену або невидиму геометрію, а не крах — уважно перевіряйте байтові зсуви. -
Створення конвеєра щокадру: створення конвеєра —
це крок компіляції. Виконання його всередині
frame()замість одного разу при старті різко знижує продуктивність і може спричинити помітні затримки. -
Ігнорування
uncapturederror: помилки валідації WebGPU за замовчуванням не викидаються синхронно — слухайте подію помилки пристрою або обгортайте виклики вdevice.pushErrorScope()/popErrorScope()під час розробки.
storage,
щоб передати тисячі інстансів в один виклик відмальовки через
@builtin(instance_index).
Часті запитання
Чого я навчуся в цьому уроці?
Почніть роботу з WebGPU: адаптери, пристрої, GPUBuffer, шейдери WGSL, конвеєри рендерингу та обчислювальні проходи. Практичний урок для наступника WebGL.
Які теми розглядаються в цьому уроці?
Цей урок охоплює такі теми: Навіщо WebGPU? Від WebGL до сучасного API, Адаптери та пристрої, Налаштування контексту canvas, WGSL: мова шейдерів WebGPU, Буфери та розкладка вершин, Побудова конвеєра рендерингу, Кодування та надсилання команд, Уніформи та групи прив'язки.
Скільки часу займає цей урок?
Цей урок займає приблизно 22 хв.
Які попередні знання потрібні?
Це урок рівня «Початковий – Середній» — окрема попередня підготовка, крім базового JavaScript, не потрібна.