Deferred Texturing для ландшафта

Еще одной новинкой в Dagon 0.14 станет довольно интересная и нетипичная техника, которую я еще не встречал в готовых реализациях — Deferred Texturing (отложенное текстурирование) для ландшафта.

Когда у меня в движке появилась поддержка ландшафтов, я не стал особо мудрить и сделал для них простейший шейдер, к которому подключаются до четырех текстур, причем для них пришлось сделать специальные свойства у материала, что выглядело не очень эстетично с точки зрения дизайна API. Также этот шейдер не поддерживал текстуры шероховатости и металличности, что делало его неполноценным по сравнению с остальными компонентами рендера. Техника Deferred Texturing, описанная Натаном Ридом, практически идеально вписалась в мой пайплайн и полностью решает эти две старые проблемы.

Основная идея заключается в том, чтобы вынести процесс наложения каждой текстуры в отдельный проход. Чтобы не рендерить сетку ландшафта по нескольку раз, мы запекаем геометрические данные в G-буфер, как в обычном отложенном рендере — отличие в том, что нам при этом нужно сохранять только интерполированные вершинные нормали и текстурные координаты ландшафта.

Полученные буферы вместе с PBR-текстурами (карта нормалей, base color, roughness, metallic) скармливаем текстурирующему проходу, который рисует экранный квад и записывает значения уже в обычный G-буфер. Для вычисления финальных нормалей используются значения из буфера нормалей ландшафта (плюс глубина, которую я сохраняю как альфа-канал буфера нормалей — она нужна для реконструирования eyespace-позиции пикселя). PBR-текстуры сэмплируются UV-координатами из буфера. Чтобы экранный квад не перезаписывал значения, уже сохраненные в G-буфере, нужно отсекать пиксель по маске, которую я храню в Z-канале буфера текстурных координат — 0 означает, что в данной точке нет ландшафта, 1 — есть.

Текстурирующий проход срабатывает несколько раз подряд — по количеству слоев текстур, которые я храню в специальном многослойном материале, каждый слой которого сам является отдельным материалом. Проход рисует в режиме альфа-смешивания, альфа-канал для прохода хранится в слое как отдельная текстура. Что самое интересное, прозрачность можно анимировать/клиппить и таким образом создать эффект плавного перехода одной текстуры в другую — например, постепенного покрытия ландшафта снегом. Возможности открываются самые широкие!

Новая статья на Medium

Я написал статью «Almighty Alias» на Medium о моем любимом ключевом слове alias, основное назначение которого — определение псевдонимов к типам и объектам. Но на самом деле alias способен на большее, особенно в связке с шаблонами.

ImGui

В следующей версии Dagon появится экспериментальная интеграция популярного UI-тулкита ImGui на основе биндинга bindbc-imgui — очень интересный инструмент, я только начал его изучать, но уже многое нравится. Удивил большой выбор готовых виджетов.

Nuklear при этом никуда из движка не денется, оба тулкита будут сосуществовать в виде двух отдельных расширений.

Новости по проектам

Долго не писал в блог — не потому, что не о чем, а просто в последнее время психическое состояние не располагало к публичности. Ушел в себя, в рефлексию и размышления. Не могу отрицать, что энтузиазм по хобби-проектам у меня уже не тот, что раньше. Стало сложно сохранять прежний настрой, и вообще очень многие вещи в жизни отошли на второй план. Но я не бросаю D, не бросаю работу над движком — просто в данный момент пишу код небольшими итерациями, насколько хватает кратковременных состояний потока.

Из нового: практически доделал рефакторинг системы загрузки текстур в Dagon (ветка texture). Текстуры из DDS теперь загружаются напрямую, без создания промежуточных объектов SuperImage. Также 2D-текстуры и кубические карты объединены в один класс Texture, и работать с ними стало проще — например, загрузка карты окружения на стороне пользователя теперь выглядит одинаково как для кубической карты из DDS, так и для равнопромежуточной карты HDR. А еще появилась поддержка формата сжатия ASTC.

Обновил и выложил на GitHub свой старый программный растеризатор MiniGL. Попутно внес несколько улучшений — например, теперь конвейер поддерживает «шейдеры»: можно задать функции D, которые выполняются при обработке вершины и пикселя. Я не знаю, кому и для чего эта штука может пригодиться в 2022 году, но писать ее было весело, и код получился довольно наглядный и компактный — меньше 1000 строк, так что это как минимум хороший пример использования dlib.

И, наконец, около месяца у меня ушло на доработку одного интересного инструмента, не связанного с D, о котором я напишу подробнее в одном из следующих постов.

Texture Tools Exporter

Отличный бесплатный конвертер текстур от NVIDIA, о котором я узнал почему-то только недавно. Поддерживает системы сжатия BC1 (S3TC/DXT1), BC2 (S3TC/DXT3), BC3 (S3TC/DXT5), BC4/5 (RGTC), BC6/7 (BPTC), ASTC, а также несжатые 8-битные целочисленные форматы и 16- и 32-битные с плавающей запятой. Можно экспортировать как 2D текстуры, так и кубические карты. Список поддерживаемых контейнеров также внушителен: DDS, KTX2, OpenEXR, HDR и все обычные — PNG, JPEG, BMP, TGA, PPM и др.

Очень пригодится для тестов, когда буду переписывать загрузчик текстур в Dagon. Как-нибудь в ближайшее время хочу написать большую статью о сжатии текстур с описанием всех форматов.

Обращение к пользователям dlib, Dagon и других моих публичных проектов в связи с происходящими событиями

Я, Тимур Гафаров, создатель и мэйнтейнер dlib, Dagon, bindbc-wgpu, bindbc-newton, bindbc-soloud и др., в настоящее время живу в России без возможности в обозримом будущем покинуть эту страну.

В целях личной безопасности, не буду сейчас комментировать саму ситуацию и делать заявления, которые могут быть расценены как политические. Скажу только, что при работе над свободным ПО между людьми не должно быть границ, политика не должна мешать людям во всем мире делиться знаниями, создавать нечто новое, решать интеллектуальные задачи.

Тем не менее, реальность такова, что разработчики из России в любой момент могут оказаться отрезанными от мирового сообщества СПО. Это серьезно повлияет на проекты, в которых они принимают участие. Я искренне надеюсь, что этого не произойдет. В противном случае я, к моему огромному сожалению, не смогу управлять разработкой dlib, Dagon и других моих пакетов в реестре Dub. В отсутствие сопровождения эти пакеты будут постепенно терять актуальность. Также у меня пока нет уверенности, что я смогу получать финансирование через Patreon и PayPal.

До ухудшения ситуации, пока у меня есть доступ к GitHub и реестру пакетов, я продолжаю сопровождение dlib и Dagon, в настоящее время уделяя этому минимум своего свободного времени. Я на неопределенный срок приостанавливаю разработку dlib 2.0 и новых версий Dagon из-за негативного психологического фона и неясной картины будущего. В случае катастрофического развития событий я не обещаю, что смогу посвящать проектам даже минимальные время и силы.

В связи с этим, я с сегодняшнего дня не рекомендую использовать dlib, Dagon и другие мои пакеты в качестве критических зависимостей в важных программных продуктах. Кто хочет взять активную разработку на себя, создать форк — буду только рад. Пожалуйста, не делайте pull request’ы с большим количеством коммитов, существенными изменениями и новыми фичами, которые мне придется читать и проверять — у меня для этого может физически не оказаться возможности. Если у меня сохранится доступ к GitHub, я, скорее всего, увижу ваш форк и сам сделаю слияние, как только смогу.

P.S. Я в срочном порядке выпустил Dagon 0.13.0.

P.P.S. Облачная папка с примерами Dagon (Sponsor Folder), которая была ранее доступна только для подписчиков на Patreon, теперь открыта для всех.

dlib 1.0

Спустя 10 лет разработки моя библиотека общего назначения для геймдева наконец-то стабилизировалась — итог этой работы отражает первый мажорный релиз проекта, dlib 1.0.0.

Из наиболее важных нововведений могу отметить ускорение загрузки изображений (от 2 до 10 раз в зависимости от формата и веса файла) — оптимизация заключается в том, чтобы декодировать из буфера в памяти, а не напрямую из файлового потока. Добавлена валидация при создании POSIX-потоков, улучшен модуль dlib.math.interpolation.hermite — добавлена функция вычисления производной для сплайна Эрмита. Исправлено несколько важных багов в математическом и геометрическом пакетах.

В связи с этим знаковым релизом, а также тем, что в этом году dlib как публичному OpenSource-проекту исполняется ровно 10 лет, хочу немного рассказать об истории этой разработки.

dlib фактически начался как порт разнородных исходников с C++ на D. В 2010-11 годах, когда я познакомился с D, у меня был свой небольшой игровой движок на C++ (Phantom3D), а также библиотека общего назначения Sparx, и я решил портировать их на D2 — язык в те годы как раз стабилизировался. Начал, естественно, с векторной алгебры — так появился dlib.math, старейший и наиболее законченный из пакетов dlib. Сам 3D-движок, конечно, претерпел немало трансформаций и, в итоге, от старого кода почти ничего не осталось — мой следующий движок DGL был написан почти с нуля, хотя и начинался как полный порт Phantom3D и сначала носил то же название.

Sparx я поначалу использовал в D в виде динамически слинкованной библиотеки, через Derelict. Но это было не очень удобно, поэтому следующим этапом стало переписывание Sparx на D, в результате чего родился dlib.image — Sparx использовался в основном как загрузчик ресурсов (изображений PNG, JPEG, JPEG2000, TGA, BMP, DDS, моделей OBJ и MD5). Сейчас, правда, старого кода из Sparx в библиотеке очень мало — но, например, отдельные участки кода dlib.image, некоторые математические и геометрические функции напрямую унаследованы оттуда.

Сама идея создать такую библиотеку появилась в результате осмысления чужих открытых проектов, в особенности Давида Анри (чудо, что его сайт до сих пор существует!). Изучая чей-нибудь код, я часто замечал, что многие функции и классы можно сделать обобщенными и независимыми, что позволяет переиспользовать их в самых разных проектах — наверное, около половины кода любой игры можно вынести в библиотеку общего назначения. Это, в первую очередь, код, который не зависит от графического API и логики движка — линейная алгебра, вычислительная геометрия, работа с файловой системой, многопоточностью, взаимодействие с ОС, сетью и т.д. Главный принцип — скрыть платформозависимый код под абстрактным интерфейсом, так, чтобы приложению не приходилось взаимодействовать с операционной системой напрямую. Это делает код приложения на удивление простым.

Но вернемся к проектам Давида Анри. Меня очень впечатлили его загрузчики PNG, TGA и BMP — они очень сильно повлияли на мои собственные реализации декодеров этих форматов. Декодер JPEG — отдельная история, я написал его полностью самостоятельно, не заглядывая в готовые реализации — только читая спеки и техническую литературу (фактически, из заимствований в нем только ДКТ-преобразователь — хардкорная fixed-point математика, написанная темными магами). Модули для работы с векторами и матрицами — тоже отчасти влияние Анри, а именно его библиотеки Mathlib. Мои реализации, конечно, за 10 лет стали намного лучше — в dlib все алгебраические объекты обобщенные, одно и то же описание используется для векторов и квадратных матриц всех стандартных размерностей (2, 3 и 4). Выше 4 я поддерживать не стал, так как в компьютерной графике они практически не используются. Также у меня есть оптимизированные функции инвертирования и декомпозиции матриц, свизлинг векторов, огромное множество функций-фабрик и различных операторов — практически все, что может понадобиться для любых вычислений, связанных с трехмерными моделями.

Точная дата юбилея dlib — 28 сентября, так как именно в этот день я создал репозиторий на Google Code (я тогда еще использовал SVN, а не Git). Самый интересный этап в жизни проекта начался в 2013, когда разработка была перенесена на GitHub. Появился модуль dlib.core, к проекту примкнули новые разработчики: Martin Сejp провел огромную работу по реализации потоков ввода-вывода и абстрактной файловой системы (dlib.core.stream, dlib.filesystem), Eugene Wissner написал сетевой пакет (dlib.network) и аллокаторы памяти (dlib.memory), Nick Papanastasiou написал модуль комбинаторики, Вадим Лопатин (к слову, автор знаменитой читалки Cool Reader) помог улучшить декодер PNG, Роман Чистоходов и Андрей Пенечко внесли множество исправляющих патчей. Отдельное спасибо Роману за улучшенную поддержку BMP и TGA, а также ценные советы.

В 2015 году я начал рефакторинг, связанный с поддержкой ручного управления памятью. Эта грандиозная работа была полностью завершена в 2019 году. Динамическая память — это отдельная большая история, далеко выходящая за рамки D, я мог бы написать целую книгу на эту тему. Если теория вычислений разработана математиками и инженерами очень глубоко и досконально, то «теории памяти», в общем-то, до сих пор нет. Все, что мы имеем научного в этой сфере — это теория типов и ее высшее достижение, система типов Хиндли-Милнера. К сожалению, нет какого-то общего, формального подхода к управлению динамической памятью — сколько языков, столько и практик. Полагаю, что необходимо выделять парадигмы памяти, подобно тому, как существуют парадигмы программирования в целом.

В dlib я реализовал парадигму единственного владельца (single ownership), которая очень хорошо ложится на базовое ООП. Каждый объект может «владеть» другими объектами. У любого объекта бывает только один владелец, либо нет владельца вовсе (у корневых объектов). Когда удаляется владелец, удаляются и все объекты, которыми он владеет. Такой подход позволяет полностью избавиться от сборщика мусора и сделать высвобождение памяти детерминированным. Объектами в этом контексте может быть все, что угодно — парадигма не зависит от логики приложения — но, естественно, речь идет о данных, которые создаются один раз и надолго (в предельном случае — о неизменяемых наборах данных, которые создаются при старте приложения и удаляются при завершении работы). Эта модель не очень хорошо работает с алгоритмами, где нужно создавать и уничтожать объекты многократно, в цикле — но на то есть аллокация в стеке. Именно подход к работе с динамической памятью я считаю одним из главных достижений dlib — эту фичу, я думаю, можно и нужно нести в мир, который, кажется, сдался без боя сборщикам мусора.

Впрочем, рефакторинг, связанный со сменой парадигмы памяти, не привел к тому, что dlib стала @nogc-библиотекой (то есть, формально, по контракту гарантирующей отсутствие вызовов сборщика). Этой целью пришлось пожертвовать для сохранения обратной совместимости API. Но у меня есть план вернуться к этой проблеме в следующей версии, dlib 2.0 — расскажу о своих идеях в одном из будущих постов.

В 2016 году появился пакет dlib.audio, который может служить бэкендом для звуковых движков, плееров, DAW и т.д. Пока реализованы только базовые функции, но пакет будет развиваться. Также появился пакет dlib.network, который содержит независимую от Phobos поддержку сокетов и функции, связанные с вебом. В 2021 году работа над основной функциональностью библиотеки была завершена.

dlib — это, наверное, самый грандиозный мой проект за всю жизнь. Начавшись как маленький набор модулей «на все случаи жизни» для экспериментов с OpenGL, библиотека превратилась в один из самых популярных пакетов в реестре Dub. Сейчас количество загрузок dlib составляет 800-1000 ежемесячно. На основе dlib пишутся игровые движки, инструменты анализа изображений, GUI-тулкиты, даже синтезаторы и библиотеки генетических алгоритмов. О такой широкой области применения я, честно говоря, изначально даже не думал! К сожалению, D все еще остается довольно нишевым языком, и востребованность библиотек на нем ограничена востребованностью самого языка — но я счастлив уже тем, что сделал для сообщества что-то полезное. Накопление алгоритмов, достижений математической и инженерной мысли в одном месте, в удобочитаемом виде — я считаю, дело нужное. И, в конечном счете, это был интересный путь.

Как ускорить загрузку изображений

Совет пользователям dlib. Не декодируйте изображение напрямую из файлового потока, это слишком медленно. Вместо этого рекомендую загрузить файл в память целиком, создать поток массива (ArrayStream) и уже его передавать в функцию-декодер:

InputStream input = openForInput("image.jpg");
ubyte[] data = New!(ubyte[])(input.size);
input.fillArray(data);
ArrayStream arrStrm = New!ArrayStream(data);
SuperImage img = loadJPEG(arrStrm);
Delete(arrStrm);
Delete(data);
input.close();

Для JPEG, например, это дает ускорение в 5-10 раз, в зависимости от размера картинки.

Новые статьи

Я написал две статьи на Medium (на английском):

Game UI with Nuklear — о тулкитах немедленного режима и о том, как использовать Nuklear в приложениях на основе Dagon. Я задумал цикл статей на эту тему, так как Nuklear поддерживает очень много всякого интересного, и на нем можно делать достаточно сложные вещи.

Metaprogramming with Alias Sequences — вводная статья о последовательностях псевдонимов. Это довольно мощный инструмент метапрограммирования в D, хотя и редко используемый.

Несколько новостей

  • Готовлю к выпуску dlib 1.0. Бета-релиз, скорее всего, состоится уже в январе — осталось довести покрытие до 50% (поставил такую цель несколько лет назад) и провести аудит некоторых модулей
  • Выложил все свои старые игры на этот сервер, ссылки в разделе Игры обновлены.