Урок · WebGPU · Графіка · JavaScript
📅 Липень 2026 ⏱ ≈ 22 хв 🎯 Початковий – Середній

Вступ до WebGPU: перші кроки

WebGPU — це наступник WebGL: сучасний API для графіки та обчислень, який браузер надає напряму, за зразком нативних API на кшталт Vulkan, Metal і Direct3D 12. Він замінює неявну машину станів WebGL на явні об'єкти: адаптери, пристрої, конвеєри та групи прив'язки. Цей урок проходить через кожне поняття, необхідне для рендерингу першого трикутника та запуску першого обчислювального шейдера.

1. Навіщо WebGPU? Від WebGL до сучасного API

WebGL — це прив'язка JavaScript до OpenGL ES, API, розробленого в епоху неявного глобального стану: прив'язати текстуру, прив'язати буфер, викликати відмальовку — і сподіватися, що ніщо інше в коді не змінило прив'язаний стан у проміжку. WebGPU відкидає цю модель. Він побудований навколо явних, незмінних об'єктів: ви описуєте конвеєр один раз (шейдери, розкладка вершин, змішування, тест глибини) і повторно використовуєте його, замість того щоб щокадру заново викликати десятки функцій встановлення стану.

З такого дизайну випливають три практичні переваги:

Підтримка браузерами: на момент написання WebGPU за замовчуванням доступний у Chrome, Edge і Firefox (останні версії) та за прапорцем у Safari Technology Preview. Завжди перевіряйте підтримку через 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 };
}
Адаптер можна втратити. Пристрій може стати непридатним для використання, якщо драйвер GPU впаде або вкладку згорнули на деяких платформах. Слухайте 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.

Уніформа моделі-огляду-проєкції, 64 байти (матриця f32 4×4): mvp = P · V · M WGSL: @group(0) @binding(0) var<uniform> mvp : mat4x4<f32>; JS: uniformBuffer = device.createBuffer({ size: 64, usage: UNIFORM | COPY_DST }) device.queue.writeBuffer(uniformBuffer, 0, mvpMatrixData)
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()]);
Зчитування результатів: буфери сховища живуть у пам'яті GPU. Щоб переглянути результат на CPU, скопіюйте його в буфер, створений із прапорцями MAP_READ | COPY_DST, через encoder.copyBufferToBuffer(), а потім await buffer.mapAsync(GPUMapMode.READ).

10. Типові пастки та налагодження

Наступні кроки: коли трикутник з'явиться на екрані, спробуйте замінити статичний буфер вершин на матрицю обертання, керовану уніформою, додайте текстуру глибини для 3D- геометрії та поекспериментуйте з буферами storage, щоб передати тисячі інстансів в один виклик відмальовки через @builtin(instance_index).

Часті запитання

Чого я навчуся в цьому уроці?

Почніть роботу з WebGPU: адаптери, пристрої, GPUBuffer, шейдери WGSL, конвеєри рендерингу та обчислювальні проходи. Практичний урок для наступника WebGL.

Які теми розглядаються в цьому уроці?

Цей урок охоплює такі теми: Навіщо WebGPU? Від WebGL до сучасного API, Адаптери та пристрої, Налаштування контексту canvas, WGSL: мова шейдерів WebGPU, Буфери та розкладка вершин, Побудова конвеєра рендерингу, Кодування та надсилання команд, Уніформи та групи прив'язки.

Скільки часу займає цей урок?

Цей урок займає приблизно 22 хв.

Які попередні знання потрібні?

Це урок рівня «Початковий – Середній» — окрема попередня підготовка, крім базового JavaScript, не потрібна.