- PVSM.RU - https://www.pvsm.ru -
WebGPU — это один из современных API [1], предназначенных для работы с компьютерной графикой. Среди других подобных API можно отметить Vulkan, DirectX 12 и Metal. То, что в сфере веб-графики появляются подобные решения, даёт пользователям веб-приложений те же возможности, которые есть у пользователей обычных приложений. А именно, это повышение скорости работы программ благодаря использованию видеоускорителей, это сокращение числа проблем, вызываемых графическими драйверами, это появление новых возможностей веб-приложений. Подобные возможности могут опираться как на расширенные функции браузеров, так и на спецификацию [2].
Надо сказать, что сейчас разработка под WebGPU — это занятие не для слабонервных. Это — один из самых сложных графических API, доступных в вебе. Но неудобства, связанные с разработкой, сглаживает то, что применение WebGPU означает рост производительности, и то, что это — стандарт, а значит можно рассчитывать на то, что в будущем он никуда не денется. Обратите внимание на то, что спецификация WebGPU всё ещё находится в разработке. Поэтому то, о чём пойдёт речь ниже, со временем может измениться.
Здесь мы, осваивая возможности WebGPU, займёмся разработкой приложения Hello Triangle на TypeScript.
Вот репозиторий [4], в котором можно найти всё необходимое для начала работы с WebGPU.
Для того чтобы подготовиться к разработке WebGPU-приложений, нужно установить следующее:
<browsername>://flags
(например — chrome://flags
или edge://flags
) и включить флаг unsafe-webgpu
.Затем в любом терминале, да хотя бы во встроенном терминале VS Code, нужно ввести следующие команды:
# Клонировать репозиторий
git clone https://github.com/alaingalvan/webgpu-seed
# Перейти в папку
cd webgpu-seed
# Запустить сборку проекта
npm start
Вот [10], если нужно, материал, в котором можно найти подробности о Node.js, о пакетах и о прочем подобном.
По мере того, как растёт сложность проекта, его файлы может понадобиться организовать так, как обычно организуют файлы движков компьютерных игр [11] или средств рендеринга [12] реального времени. Вот как устроен наш проект:
├─ node_modules/ # Зависимости
│ ├─ gl-matrix # Линейная алгебра
│ └─ ... # Другие зависимости (TypeScript, Webpack, и так далее.)
├─ src/ # Файлы с исходным кодом
│ ├─ renderer.ts # Код рендеринга треугольника
│ └─ main.ts # Главный файл приложения
├─ .gitignore # Файл, задающий правила игнорирования материалов в git-репозитории
├─ package.json # Файл настроек Node.js-проекта
├─ license.md # Лицензия
└─ readme.md # Файл readme
Зависимости:
В приложении, которое мы создадим, нужно выполнить следующие действия:
navigator.gpu
, и если это так — запросить у него GPUAdapter
, затем — запросить GPUDevice
, а после этого получить объект GPUQueue
имеющегося устройства, используемый по умолчанию.GPUSwapchain
для получения GPUTexture
для текущего кадра, а также — для получения любых других прикреплений (наподобие текстур глубины). Создание объектов GPUTextureView
для соответствующих текстур.GPUBuffer
для вершин и индексов, выполнение предварительной компиляции шейдеров в формат SPIR-V и загрузка двоичных данных SPIR-V-шейдеров в виде GPUShaderModule
. Создание конвейера GPURenderPipeline
путём описания каждой стадии графического конвейера. И, наконец, создание GPUCommandEncoder
на основе того, что нужно отрендерить, а затем — создание GPURenderPassEncoder
на основе тех команд рисования, которые мы намереваемся выполнить в текущем проходе рендеринга.GPUCommandEncoder
с помощью метода этого объекта .finish()
и передача его GPUQueue
. Подготовка системы к работе над следующим кадром путём вызова requestAnimationFrame
.
Перейдём к рассмотрению примера. Тут мы будем пользоваться фрагментами кода, полную версию которого можно найти в этом [4] репозитории. Здесь, кроме того, переменные членов классов (вроде this.memberVariable
) объявлены внутри классов без использования ключевого слова .this
. Благодаря этому легче выяснить их типы. Кроме того, это приводит к тому, что примеры, приведённые здесь, могут работать сами по себе.
Для того чтобы получить доступ к API WebGPU, нужно проверить существование объекта gpu
в глобальном объекте navigator
. Вот соответствующий TypeScript-код:
// Начало работы с WebGPU
const entry: GPU = navigator.gpu;
if (!entry) {
throw new Error('WebGPU is not supported on this browser.')
}
Объект GPUAdapter
описывает физические свойства конкретного GPU, такие, как имя, расширения, ограничения устройства.
// Объявляем ссылку на адаптер
let adapter: GPUAdapter = null;
// В асинхронной функции...
// Адаптер физического устройства
adapter = await entry.requestAdapter();
Объект типа GPUDevice
олицетворяет логическое устройство, даёт доступ к основному функционалу API WebGPU и позволяет создавать необходимые структуры данных.
// Объявляем ссылку на устройство
let device: GPUDevice = null;
// В асинхронной функции...
// Логическое устройство
device = await adapter.requestDevice();
Очередь, объект GPUQueue
, позволяет отправлять GPU задания в асинхронном режиме. Сейчас, во время написания этого материала, работать можно только с очередью defaultQueue
конкретного устройства GPUDevice
.
// Объявляем ссылку на очередь
let queue: GPUQueue = null;
// Очередь
queue = device.defaultQueue;
Для того чтобы увидеть на экране то, что мы будем рисовать, нам нужен HTML-элемент canvas
. Кроме того, нужно создать для этого элемента цепочку буферов, представленную объектом GPUSwapChain
.
// Объявляем ссылку на цепочку буферов
let swapchain: GPUSwapchain = null;
const context: GPUCanvasContext = canvas.getContext('gpupresent') as any;
// Создаём цепочку буферов
const swapChainDesc: GPUSwapChainDescriptor = {
device: device,
format: 'bgra8unorm',
usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.COPY_SRC
};
swapchain = context.configureSwapChain(swapChainDesc);
При выполнении различных проходов системы рендеринга нужны выходные текстуры, в которые можно записывать данные. Речь идёт о текстурах глубины для теста глубины или для расчёта теней. Это могут быть прикрепления, реализующие различные аспекты отложенного рендеринга, такие, как применение нормалей и PBR-эффектов, например — реализация формирования отражений и фактур поверхностей.
// Объявляем ссылки на прикрепления
let depthTexture: GPUTexture = null;
let depthTextureView: GPUTextureView = null;
// Создаём вспомогательный механизм для работы с глубиной
const depthTextureDesc: GPUTextureDescriptor = {
size: {
width: canvas.width,
height: canvas.height,
depth: 1
},
arrayLayerCount: 1,
mipLevelCount: 1,
sampleCount: 1,
dimension: '2d',
format: 'depth24plus-stencil8',
usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.COPY_SRC
};
depthTexture = device.createTexture(depthTextureDesc);
depthTextureView = depthTexture.createView();
// Объявляем ссылки текстур для цепочки буферов
let colorTexture: GPUTexture = null;
let colorTextureView: GPUTextureView = null;
colorTexture = swapchain.getCurrentTexture();
colorTextureView = colorTexture.createView();
Буфер — это массив с данными, например, с данными о позициях вершин сетки, с цветовыми данными, с данными об индексах и так далее. При рендеринге треугольников с использованием графического конвейера, формирующего растровое изображение, нужен 1 буфер с данными о вершинах (или большее количество таких буферов, которые обычно называют объектами буфера вершин — Vertex Buffer Object — VBO). Так же нужен 1 буфер индексов, содержащий указатели для буферов вершин тех треугольников, которые планируется вывести (такие буферы называют объектами буфера индекса — Index Buffer Object — IBO).
// Буфер с данными вершин
const positions = new Float32Array([
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0,
0.0, 1.0, 0.0
]);
// Буфер с цветовыми данными вершин
const colors = new Float32Array([
1.0, 0.0, 0.0, // красный
0.0, 1.0, 0.0, // зелёный
0.0, 0.0, 1.0 // синий
]);
// Буфер с индексными данными
const indices = new Uint16Array([ 0, 1, 2 ]);
// Ссылки на буферы
let positionBuffer: GPUBuffer = null;
let colorBuffer: GPUBuffer = null;
let indexBuffer: GPUBuffer = null;
// Вспомогательная функция для создания объектов GPUBuffer из типизированных массивов
let createBuffer = (arr: Float32Array | Uint16Array, usage: number) => {
let desc = { size: arr.byteLength, usage };
let [ buffer, bufferMapped ] = device.createBufferMapped(desc);
const writeArray =
arr instanceof Uint16Array ? new Uint16Array(bufferMapped) : new Float32Array(bufferMapped);
writeArray.set(arr);
buffer.unmap();
return buffer;
};
positionBuffer = createBuffer(positions, GPUBufferUsage.VERTEX);
colorBuffer = createBuffer(colors, GPUBufferUsage.VERTEX);
indexBuffer = createBuffer(indices, GPUBufferUsage.INDEX);
В нашем примере вершинный шейдер описан следующим GLSL-кодом:
#version 450
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inColor;
layout (location = 0) out vec3 outColor;
void main()
{
outColor = inColor;
gl_Position = vec4(inPos.xyz, 1.0);
}
Вот — код фрагментного шейдера, он тоже написан на GLSL:
#version 450
// Варьирование
layout (location = 0) in vec3 inColor;
// Возвращаемый вывод
layout (location = 0) out vec4 outFragColor;
void main()
{
outFragColor = vec4(inColor, 1.0);
}
Учитывая то, что у вас должен быть установлен glslang [16], и то, что пути к соответствующим инструментам находятся в переменной PATH
, для компиляции шейдеров выполните следующие команды в терминале:
glslangValidator -V triangle.vert -o triangle.vert.spv
glslangValidator -V triangle.frag -o triangle.frag.spv
Модули шейдеров — это заранее скомпилированные бинарные файлы шейдеров, которые выполняются на GPU при выполнении конкретного конвейера.
Так как шейдеры должны быть предварительно скомпилированы для использования их в WebGPU, вам понадобится средство для компиляции шейдеров в формат SPIR-V. Я работаю сейчас над новой версией CrossShader [17] — средства, которое позволяет выполнить эту операцию. С этим средством пока не очень удобно работать. Например, ему не помешала бы загрузка шейдеров с использованием Webpack и другие подобные улучшения.
// Объявляем ссылки на модули шейдеров
let vertModule: GPUShaderModule = null;
let fragModule: GPUShaderModule = null;
// Вспомогательная функция для создания модулей GPUShaderModule из SPIR-V-файлов
let loadShader = (shaderPath: string) =>
fetch(new Request(shaderPath), { method: 'GET', mode: 'cors' }).then((res) =>
res.arrayBuffer().then((arr) => new Uint32Array(arr))
);
// В асинхронной функции...
// Обратите внимание на то, что эти двоичные файлы можно включить в JavaScript-код в виде значений переменных.
const vsmDesc: any = { code: await loadShader('triangle.vert.spv') };
vertModule = device.createShaderModule(vsmDesc);
const fsmDesc: any = { code: await loadShader('triangle.frag.spv') };
fragModule = device.createShaderModule(fsmDesc);
Часто нужно передавать данные напрямую в модули шейдеров. Для того чтобы это сделать, нужен uniform-буфер. Для того чтобы создать такой буфер в шейдере, добавьте в его код, до функции main
, следующее:
// Это нужно добавить в файл вершинного шейдера
layout (set = 0, binding = 0) uniform UBO
{
mat4 modelViewProj;
vec4 primaryColor;
vec4 accentColor;
};
// В файле вершинного шейдера замените предпоследнюю строчку на следующую
gl_Position = modelViewProj * vec4(inPos, 1.0);
Затем в JavaScript-коде создадим uniform-буфер, поступив так же, как поступали, создавая вершинные и индексные буферы:
// Uniform-данные
const uniformData = new Float32Array([
// Матрица ModelViewProjection
1.0, 0.0, 0.0, 0.0
0.0, 1.0, 0.0, 0.0
0.0, 0.0, 1.0, 0.0
0.0, 0.0, 0.0, 1.0
// Основной цвет
0.9, 0.1, 0.3, 1.0
// Акцентный цвет
0.8, 0.2, 0.8, 1.0
]);
// Объявление ссылки на буфер
let uniformBuffer: GPUBuffer = null;
uniformBuffer = createBuffer(uniformData, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST);
Для того чтобы лучше справиться с матричными вычислениями, вроде умножения матриц, вам может пригодиться специализированная библиотека — наподобие вышеупомянутой gl-matrix [13].
// Объявление ссылок
let uniformBindGroupLayout: GPUBindGroupLayout = null;
let uniformBindGroup: GPUBindGroup = null;
let layout: GPUPipelineLayout = null;
// Компоновка привязок
uniformBindGroupLayout = device.createBindGroupLayout({
bindings: [{
binding: 0,
visibility: GPUShaderStage.VERTEX,
type: "uniform-buffer"
}]
});
// Группа привязки
uniformBindGroup = device.createBindGroup({
layout: uniformBindGroupLayout,
bindings: [{
binding: 0,
resource: {
buffer: uniformBuffer
}
}]
});
// Компоновка конвейера
layout = device.createPipelineLayout({bindGroupLayouts: [sceneUniformBindGroupLayout]}),
Графический конвейер — это описание всех данных, которые будут переданы в обработку. Он включает в себя следующее:
Ответы на все эти вопросы при разработке WebGL-приложений даются в ходе компоновки конвейера.
// Объявление ссылки на конвейер
let pipeline: GPURenderPipeline = null;
// Графический конвейер
// Входная сборка
const positionAttribDesc: GPUVertexAttributeDescriptor = {
shaderLocation: 0, // [[attribute(0)]]
offset: 0,
format: 'float3'
};
const colorAttribDesc: GPUVertexAttributeDescriptor = {
shaderLocation: 1, // [[attribute(1)]]
offset: 0,
format: 'float3'
};
const positionBufferDesc: GPUVertexBufferLayoutDescriptor = {
attributes: [ positionAttribDesc ],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
const colorBufferDesc: GPUVertexBufferLayoutDescriptor = {
attributes: [ colorAttribDesc ],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
const vertexState: GPUVertexStateDescriptor = {
indexFormat: 'uint16',
vertexBuffers: [ positionBufferDesc, colorBufferDesc ]
};
// Модули шейдеров
const vertexStage = {
module: vertModule,
entryPoint: 'main'
};
const fragmentStage = {
module: fragModule,
entryPoint: 'main'
};
// Дескриптор глубины
const depthStencilState: GPUDepthStencilStateDescriptor = {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus-stencil8'
};
// Дескриптор смешивания цветов
const colorState: GPUColorStateDescriptor = {
format: 'bgra8unorm',
alphaBlend: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
},
colorBlend: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
},
writeMask: GPUColorWrite.ALL
};
// Растеризация
const rasterizationState: GPURasterizationStateDescriptor = {
frontFace: 'cw',
cullMode: 'none'
};
// Uniform-данные
const pipelineLayoutDesc = { bindGroupLayouts: [] };
const layout = device.createPipelineLayout(pipelineLayoutDesc);
const pipelineDesc: GPURenderPipelineDescriptor = {
layout,
vertexStage,
fragmentStage,
primitiveTopology: 'triangle-list',
colorStates: [ colorState ],
depthStencilState,
vertexState,
rasterizationState
};
pipeline = device.createRenderPipeline(pipelineDesc);
Кодировщики команд выполняют кодирование команд рисования, которые планируется выполнить в ходе рендеринга. После завершения кодирования команд в вашем распоряжении окажется буфер команд, который можно передать в очередь.
В этом смысле буфер команд аналогичен коллбэку, который, будучи отправленным в очередь, выполняет функции рисования на GPU.
// Объявление ссылок на кодировщики
let commandEncoder: GPUCommandEncoder = null;
let passEncoder: GPURenderPassEncoder = null;
// Запись команд для отправки GPU
function encodeCommands() {
let colorAttachment: GPURenderPassColorAttachmentDescriptor = {
attachment: colorTextureView,
loadValue: { r: 0, g: 0, b: 0, a: 1 },
storeOp: 'store'
};
const depthAttachment: GPURenderPassDepthStencilAttachmentDescriptor = {
attachment: depthTextureView,
depthLoadValue: 1,
depthStoreOp: 'store',
stencilLoadValue: 'load',
stencilStoreOp: 'store'
};
const renderPassDesc: GPURenderPassDescriptor = {
colorAttachments: [ colorAttachment ],
depthStencilAttachment: depthAttachment
};
commandEncoder = device.createCommandEncoder();
// Кодирование команд рисования
passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setPipeline(pipeline);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setScissorRect(0, 0, canvas.width, canvas.height);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, colorBuffer);
passEncoder.setIndexBuffer(indexBuffer);
passEncoder.drawIndexed(3, 1, 0, 0, 0);
passEncoder.endPass();
queue.submit([ commandEncoder.finish() ]);
}
Рендеринг в WebGPU — это всего лишь обновление любых uniform-данных, которые вы намереваетесь обновить, получение следующего прикрепления из цепочки буферов, отправка закодированных команд на выполнение, и вызов requestAnimationFrame
для того, чтобы снова проделать все эти операции.
let render = () => {
// Получение следующего изображения из цепочки буферов
colorTexture = swapchain.getCurrentTexture();
colorTextureView = colorTexture.createView();
// Запись команд и отправка их в очередь
encodeCommands();
// Обновление элемента canvas
requestAnimationFrame(render);
};
Возможно, писать под WebGPU сложнее, чем под другие графические API, но этот API лучше других соответствует возможностям современных видеокарт. Это, как результат, должно привести не только к тому, что с использованием WebGPU можно будет писать более быстрые приложения, но и к тому, что сама технология WebGPU ещё долго не устареет.
Надо отметить, что здесь не были рассмотрены некоторые продвинутые вопросы по работе с WebGPU. В частности, речь идёт о следующем:
Уважаемые читатели! Планируете ли вы использовать WebGPU в своих проектах?
Автор: ru_vds
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/345003
Ссылки в тексте:
[1] API: https://alain.xyz/blog/comparison-of-modern-graphics-apis
[2] спецификацию: https://gpuweb.github.io/gpuweb/
[3] Image: https://habr.com/ru/company/ruvds/blog/485644/
[4] репозиторий: https://github.com/alaingalvan/webgpu-seed
[5] Microsoft Edge: https://www.microsoftedgeinsider.com/en-us/download
[6] Google Chrome: https://www.google.com/chrome/canary/
[7] Git: https://git-scm.com/
[8] Node.js: https://nodejs.org/en/
[9] VS Code: https://code.visualstudio.com/
[10] Вот: https://alain.xyz/blog/designing-a-web-app
[11] движков компьютерных игр: https://alain.xyz/blog/game-engine-architecture
[12] рендеринга: https://alain.xyz/blog/realtime-renderer-architectures
[13] gl-matrix: https://github.com/toji/gl-matrix
[14] TypeScript: https://github.com/microsoft/typescript
[15] Webpack: https://github.com/webpack/webpack
[16] glslang: https://github.com/KhronosGroup/glslang/releases
[17] CrossShader: https://alain.xyz/libraries/crossshader
[18] Источник: https://habr.com/ru/post/485644/?utm_source=habrahabr&utm_medium=rss&utm_campaign=485644
Нажмите здесь для печати.