Переход на будущий WebGPU означает больше, чем просто переключение графических API. Это также шаг в будущее веб-графики. Но эта миграция окажется лучше при подготовке и понимании — и эта статья поможет вам подготовиться.
Привет всем, меня зовут Дмитрий Иващенко, я инженер-программист в MY.GAMES. В этой статье мы обсудим различия между WebGL и будущим WebGPU, а также расскажем, как подготовить ваш проект к миграции.
Хронология WebGL и WebGPU
Текущее состояние WebGPU и что будет дальше
Концептуальные различия высокого уровня
Инициализация
• WebGL: контекстная модель
• WebGPU: модель устройства.
Программы и конвейеры
• WebGL: программа
• WebGPU: конвейер
Униформа
• Униформа в WebGL 1.
• Униформа в WebGL 2.
• Униформа в WebGPU
Шейдеры
• Язык шейдеров: GLSL против WGSL.
• Сравнение типов данных
• Структуры
• Объявления функций
• Встроенные функции
• Преобразование шейдеров
Конвенционные различия
Текстуры
• Пространство видового экрана
• Пространства обрезки
Советы и рекомендации по WebGPU
• Минимизируйте количество используемых вами конвейеров.
• Создавайте конвейеры заранее
• Используйте RenderBundles.
Краткое содержание
WebGL , как и многие другие веб-технологии, имеет корни, уходящие в далекое прошлое. Чтобы понять динамику и мотивацию перехода к WebGPU, полезно сначала взглянуть на историю разработки WebGL:
В последние годы наблюдается всплеск интереса к новым графическим API, которые предоставляют разработчикам больше контроля и гибкости:
Сегодня WebGPU доступен на нескольких платформах, таких как Windows, Mac и ChromeOS, через браузеры Google Chrome и Microsoft Edge, начиная с версии 113. Поддержка Linux и Android ожидается в ближайшем будущем.
Вот некоторые из движков, которые уже поддерживают (или предлагают экспериментальную поддержку) WebGPU:
Учитывая это, переход на WebGPU или хотя бы подготовка проектов к такому переходу представляется своевременным шагом в ближайшем будущем.
Давайте уменьшим масштаб и взглянем на некоторые концептуальные различия высокого уровня между WebGL и WebGPU, начиная с инициализации.
Приступая к работе с графическими API, одним из первых шагов является инициализация основного объекта для взаимодействия. Этот процесс различается в WebGL и WebGPU, но имеет некоторые особенности для обеих систем.
В WebGL этот объект известен как «контекст», который по сути представляет собой интерфейс для рисования на элементе холста HTML5. Получить этот контекст довольно просто:
const gl = canvas.getContext('webgl');
Контекст WebGL фактически привязан к конкретному холсту. Это означает, что если вам нужно выполнить рендеринг на нескольких холстах, вам понадобится несколько контекстов.
WebGPU представляет новую концепцию под названием «устройство». Это устройство представляет собой абстракцию графического процессора, с которой вы будете взаимодействовать. Процесс инициализации немного сложнее, чем в WebGL, но обеспечивает большую гибкость:
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const context = canvas.getContext('webgpu'); context.configure({ device, format: 'bgra8unorm', });
Одним из преимуществ этой модели является то, что одно устройство может выполнять рендеринг на нескольких холстах или даже ни на одном. Это обеспечивает дополнительную гибкость; например, одно устройство может управлять рендерингом в нескольких окнах или контекстах.
WebGL и WebGPU представляют разные подходы к управлению и организации графического конвейера.
В WebGL основное внимание уделяется программе шейдеров. Программа объединяет вершинные и фрагментные шейдеры, определяя, как должны быть преобразованы вершины и как должен быть окрашен каждый пиксель.
const program = gl.createProgram(); gl.attachShader(program, vertShader); gl.attachShader(program, fragShader); gl.bindAttribLocation(program, 'position', 0); gl.linkProgram(program);
Шаги по созданию программы в WebGL:
Этот процесс обеспечивает гибкое управление графикой, но также может быть сложным и подвержен ошибкам, особенно для больших и сложных проектов.
WebGPU вводит концепцию «конвейера» вместо отдельной программы. Этот конвейер объединяет не только шейдеры, но и другую информацию, которая в WebGL представлена как состояния. Итак, создание конвейера в WebGPU выглядит сложнее:
const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module: shaderModule, entryPoint: 'vertexMain', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }], }, fragment: { module: shaderModule, entryPoint: 'fragmentMain', targets: [{ format, }], }, });
Шаги по созданию конвейера в WebGPU:
В то время как WebGL разделяет каждый аспект рендеринга, WebGPU пытается инкапсулировать больше аспектов в один объект, делая систему более модульной и гибкой. Вместо отдельного управления шейдерами и состояниями рендеринга, как это сделано в WebGL, WebGPU объединяет всё в один объект конвейера. Это делает процесс более предсказуемым и менее подверженным ошибкам:
Униформные переменные предоставляют постоянные данные, доступные всем экземплярам шейдера.
В базовом WebGL у нас есть возможность устанавливать uniform
-переменные непосредственно через вызовы API.
ГЛСЛ :
uniform vec3 u_LightPos; uniform vec3 u_LightDir; uniform vec3 u_LightColor;
JavaScript :
const location = gl.getUniformLocation(p, "u_LightPos"); gl.uniform3fv(location, [100, 300, 500]);
Этот метод прост, но требует нескольких вызовов API для каждой uniform
переменной.
С появлением WebGL 2 у нас появилась возможность группировать uniform
переменные в буферы. Хотя вы по-прежнему можете использовать отдельные шейдеры униформ, лучший вариант — сгруппировать разные униформы в более крупную структуру с помощью буферов униформ. Затем вы сразу отправляете все эти однородные данные в графический процессор, аналогично тому, как вы можете загрузить буфер вершин в WebGL 1. Это имеет несколько преимуществ в производительности, таких как сокращение вызовов API и приближение к тому, как работают современные графические процессоры.
ГЛСЛ :
layout(std140) uniform ub_Params { vec4 u_LightPos; vec4 u_LightDir; vec4 u_LightColor; };
JavaScript :
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
Чтобы связать подмножества большого универсального буфера в WebGL 2, вы можете использовать специальный вызов API, известный bindBufferRange
. В WebGPU есть нечто подобное, называемое динамическими универсальными смещениями буфера, где вы можете передать список смещений при вызове API setBindGroup
.
WebGPU предлагает нам еще лучший метод. В этом контексте отдельные uniform
переменные больше не поддерживаются, и работа ведется исключительно через uniform
буферы.
РГСЛ :
[[block]] struct Params { u_LightPos : vec4<f32>; u_LightColor : vec4<f32>; u_LightDirection : vec4<f32>; }; [[group(0), binding(0)]] var<uniform> ub_Params : Params;
JavaScript :
const buffer = device.createBuffer({ usage: GPUBufferUsage.UNIFORM, size: 8 });
Современные графические процессоры предпочитают, чтобы данные загружались одним большим блоком, а не множеством маленьких. Вместо того, чтобы каждый раз заново создавать и перепривязывать небольшие буферы, рассмотрите возможность создания одного большого буфера и использования разных его частей для разных вызовов отрисовки. Такой подход позволяет значительно повысить производительность.
WebGL более важен: он сбрасывает глобальное состояние при каждом вызове и стремится быть как можно более простым. WebGPU, с другой стороны, стремится быть более объектно-ориентированным и ориентированным на повторное использование ресурсов, что приводит к повышению эффективности.
Переход с WebGL на WebGPU может показаться трудным из-за различий в методах. Однако, начав с перехода на WebGL 2 в качестве промежуточного шага, можно упростить себе жизнь.
Переход с WebGL на WebGPU требует изменений не только в API, но и в шейдерах. Спецификация WGSL создана для того, чтобы сделать этот переход плавным и интуитивно понятным, сохраняя при этом эффективность и производительность современных графических процессоров.
WGSL спроектирован как мост между WebGPU и собственными графическими API. По сравнению с GLSL, WGSL выглядит немного более многословным, но структура остается знакомой.
Вот пример шейдера для текстуры:
ГЛСЛ :
sampler2D myTexture; varying vec2 vTexCoord; void main() { return texture(myTexture, vTexCoord); }
РГСЛ :
[[group(0), binding(0)]] var mySampler: sampler; [[group(0), binding(1)]] var myTexture: texture_2d<f32>; [[stage(fragment)]] fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> { return textureSample(myTexture, mySampler, vTexCoord); }
В таблице ниже показано сравнение базового и матричного типов данных в GLSL и WGSL:
Переход от GLSL к WGSL демонстрирует стремление к более строгой типизации и явному определению размеров данных, что может улучшить читаемость кода и снизить вероятность ошибок.
Синтаксис объявления структур также изменился:
ГЛСЛ:
struct Light { vec3 position; vec4 color; float attenuation; vec3 direction; float innerAngle; float angle; float range; };
РГСЛ:
struct Light { position: vec3<f32>, color: vec4<f32>, attenuation: f32, direction: vec3<f32>, innerAngle: f32, angle: f32, range: f32, };
Введение явного синтаксиса для объявления полей в структурах WGSL подчеркивает стремление к большей ясности и упрощает понимание структур данных в шейдерах.
ГЛСЛ :
float saturate(float x) { return clamp(x, 0.0, 1.0); }
РГСЛ :
fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
Изменение синтаксиса функций в WGSL отражает унификацию подхода к объявлениям и возвращаемым значениям, делая код более последовательным и предсказуемым.
В WGSL многие встроенные функции GLSL были переименованы или заменены. Например:
Переименование встроенных функций в WGSL не только упрощает их имена, но и делает их более интуитивно понятными, что может облегчить процесс перехода для разработчиков, знакомых с другими графическими API.
Тем, кто планирует конвертировать свои проекты из WebGL в WebGPU, важно знать, что существуют инструменты для автоматического конвертирования GLSL в WGSL, например **[Naga](https://github.com/gfx-rs/naga /)** — библиотека Rust для преобразования GLSL в WGSL. Он даже может работать прямо в вашем браузере с помощью WebAssembly.
Вот конечные точки, поддерживаемые Naga:
После миграции вы можете столкнуться с сюрпризом в виде перевернутых изображений. Те, кто когда-либо портировал приложения из OpenGL в Direct3D (или наоборот), уже сталкивались с этой классической проблемой.
В контексте OpenGL и WebGL текстуры обычно загружаются таким образом, что начальный пиксель соответствует левому нижнему углу. Однако на практике многие разработчики загружают изображения, начиная с верхнего левого угла, что приводит к ошибке перевернутого изображения. Тем не менее, эта ошибка может быть компенсирована другими факторами, в конечном итоге нивелирующими проблему.
В отличие от OpenGL, такие системы, как Direct3D и Metal, традиционно используют верхний левый угол в качестве отправной точки для текстур. Учитывая, что многим разработчикам такой подход кажется наиболее интуитивным, создатели WebGPU решили последовать этой практике.
Если ваш код WebGL выбирает пиксели из буфера кадра, будьте готовы к тому, что WebGPU использует другую систему координат. Возможно, вам придется применить простую операцию «y = 1,0 - y», чтобы исправить координаты.
Когда разработчик сталкивается с проблемой, когда объекты обрезаются или исчезают раньше, чем ожидалось, это часто связано с различиями в области глубины. Разница между WebGL и WebGPU заключается в том, как они определяют диапазон глубины клипового пространства. В то время как WebGL использует диапазон от -1 до 1, WebGPU использует диапазон от 0 до 1, аналогично другим графическим API, таким как Direct3D, Metal и Vulkan. Такое решение было принято из-за ряда преимуществ использования диапазона от 0 до 1, выявленных при работе с другими графическими API.
Основная ответственность за преобразование позиций вашей модели в пространство клипа лежит на матрице проекции. Самый простой способ адаптировать ваш код — убедиться, что результаты вашей матрицы проекции находятся в диапазоне от 0 до 1. Для тех, кто использует такие библиотеки, как gl-matrix, есть простое решение: вместо использования функции perspective
вы можете использовать perspectiveZO
; аналогичные функции доступны и для других матричных операций.
if (webGPU) { // Creates a matrix for a symetric perspective-view frustum // using left-handed coordinates mat4.perspectiveZO(out, Math.PI / 4, ...); } else { // Creates a matrix for a symetric perspective-view frustum // based on the default handedness and default near // and far clip planes definition. mat4.perspective(out, Math.PI / 4, …); }
Однако иногда у вас может быть существующая матрица проекции, и вы не можете изменить ее источник. В этом случае, чтобы преобразовать его в диапазон от 0 до 1, вы можете предварительно умножить свою матрицу проекции на другую матрицу, корректирующую диапазон глубины.
Теперь давайте обсудим некоторые советы и рекомендации по работе с WebGPU.
Чем больше конвейеров вы используете, тем больше у вас переключений состояний и тем меньше производительность; это может быть непросто, в зависимости от того, откуда берутся ваши активы.
Создание конвейера и немедленное его использование могут сработать, но это не рекомендуется. Вместо этого создайте функции, которые немедленно возвращаются и начинают работать в другом потоке. Когда вы используете конвейер, очередь выполнения должна дождаться завершения ожидающих создания конвейера. Это может привести к существенным проблемам с производительностью. Чтобы избежать этого, обязательно оставьте некоторое время между созданием конвейера и его первым использованием.
Или, что еще лучше, используйте варианты create*PipelineAsync
! Промис выполняется, когда конвейер готов к использованию, без каких-либо остановок.
device.createComputePipelineAsync({ compute: { module: shaderModule, entryPoint: 'computeMain' } }).then((pipeline) => { const commandEncoder = device.createCommandEncoder(); const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.dispatchWorkgroups(128); passEncoder.end(); device.queue.submit([commandEncoder.finish()]); });
Пакеты рендеринга — это предварительно записанные частичные проходы рендеринга многократного использования. Они могут содержать большинство команд рендеринга (за исключением таких вещей, как настройка области просмотра) и могут быть «воспроизведены» позже как часть фактического прохода рендеринга.
const renderPass = encoder.beginRenderPass(descriptor); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.executeBundles([renderBundle]); renderPass.setPipeline(renderPipeline); renderPass.draw(3); renderPass.end();
Пакеты рендеринга могут выполняться вместе с обычными командами прохода рендеринга. Состояние прохождения рендеринга сбрасывается до значений по умолчанию до и после каждого выполнения пакета. В первую очередь это сделано для уменьшения накладных расходов JavaScript на рисование. Производительность графического процессора остается неизменной независимо от подхода.
Переход на WebGPU означает больше, чем просто переключение графических API. Это также шаг в будущее веб-графики, объединяющий успешные функции и методы различных графических API. Этот переход требует глубокого понимания технических и философских изменений, но преимущества значительны.