В конце 2020 года я с большим энтузиазмом взялся за изучение WebGPU. Для тех, кто не в курсе, поясню: это будущий веб-стандарт низкоуровнего графического API, который позволит браузерным приложениям эффективно задействовать возможности современных видеокарт. Замечательная особенность реализации WebGPU от Mozilla заключается в том, что ее можно использовать в нативных приложениях через C-интерфейс – я, разумеется, сразу занялся созданием собственной привязки WebGPU для D. На сегодняшний день у меня уже практически готов минимальный фреймворк для разработки WebGPU-приложений, исходники которого вы можете найти на GitHub: в текущей стадии он способен загружать и рендерить модели в формате OBJ с тестовой моделью освещения на основе GGX BRDF. Рендер прямой, безо всяких отложенных эффектов, также пока не поддерживается мипмаппинг. Тем не менее, кейс получился вполне достаточный для тестирования основных возможностей API.
Этот фреймворк я писал довольно долго – в основном, из-за того, что wgpu-native жутко нестабилен, от версии к версии в инициализирующие структуры вносится очень много изменений. Часто бывает, что после очередного обновления приложение компилируется, но падает с какой-то экзотической ошибкой – без поллитры не разберешься (в итоге выясняется, что изменилась какая-нибудь константа, или стал обязательным nextInChain в одном из дескрипторов). Особым “удовольствием” было отлаживать шейдеры на WGSL в процессе стандартизации языка: то синитаксис атрибутов изменится, то разделитель полей в структурах… К тому же нестабильность API долгое время не давала мне определиться с архитектурой некоторых компонентов, ведь WebGPU имеет гораздо больше сущностей, чем OpenGL, и к ним нужно правильно подбирать модели данных.
Скажу честно: после OpenGL ко всем этим бинд-группам, очередям и command encoder’ам привыкнуть достаточно сложно. Порой не понимаешь, в какой класс лучше впихнуть очередную головоломную абстракцию наподобие WGPUBindGroupLayout или WGPURenderPassEncoder. Сложность в том, что сущности WebGPU сильно взаимосвязаны – одну не создашь без другой – и нужно заранее знать очень много информации, чтобы правильно проинициализировать конвейер.
Я почти сразу понял, что бинд-группы используются для раздельной передачи в шейдер ресурсов, обновляемых с различной частотой. Я делаю следующим образом:
Группа 0 – покадровые данные (видовая и проекционная матрицы)
Группа 1 – данные, обновляемые каждый проход (общие настройки сцены)
Группа 2 – свойства материала, текстуры
Группа 3 – свойства объекта (модельно-видовая матрица и др.)
Но нужно понимать, что этот лейаут не глобальный – он назначается для каждого пайплайна отдельно (поэтому и были придуманы эти пресловутые WGPUBindGroupLayout’ы). Вдобавок пайплайн в WebGPU неизменяемый – иными словами, если меняется какой-нибудь режим смешивания, то меняется вообще все. Такой подход может сильно обескуражить – за много лет пользования OpenGL его глобальное состояние стало для меня как родное! Тут вы не можете просто изменить конвейер так, как вам нужно – приходится создавать заранее несколько готовых пайплайнов на все случаи жизни и переключаться между ними функцией wgpuRenderPassEncoderSetPipeline. Способ управления пайплайнами сильно зависит от архитектуры вашего приложения, но в общем случае приходится городить достаточно сложный менеджер рендеринга, который создает проходы, задает им пайплайны, обновляет шейдерные ресурсы и подключает их в нужные моменты циклов перебора объектов сцены. Я до сих пор не уверен, что моя реализация этого менеджера годится для создания полноценного движка – надеюсь, что понимание придет в дальнейшем.
Буду ли я портировать Dagon на WebGPU? Отчасти – возможно, но перенести все функции движка с сохранением обратной совместимости, я думаю, нереально. Пока в этом и нет какой-то острой насущной необходимости, но начинать экспериментировать можно уже сейчас: API интересный, непривычный – рано или поздно привыкать все равно придется.