Урок · 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).