Добавил анизотропную BRDF Kajiya-Kay для рендеринга волос:

Текстура волос может иметь альфа-канал, но вместо альфа-смешивания используется упрощенная прозрачность на основе дизеринга Байера.
Шучу, конечно. Но специализированная хэш-таблица действительно может быть заметно быстрее встроенных ассоциативных массивов D. На днях начал писать менеджер ассетов для Dagon 2.0 и решил заодно заменить dlib.container.dict для хранения ссылок на ассеты чем-то поэффективнее. Результатом стал класс FlatHashMap — таблица, которая хранит все элементы в одном непрерывном блоке памяти, что значительно улучшает производительность за счет кэш-дружественности. Кроме того, такая таблица работает в среднем за постоянное время, что выгоднейшим образом отличает ее от префиксного дерева (на котором основан dlib.container.dict) — скорость префиксного дерева пропорциональна длине ключей, а flat hash map работает с хэшами фиксированного размера.
В качестве хэш-функции я взял быстрый xxHash64, который можно использовать в реальном времени. Моя версия в Dagon — это частичный D-порт xxhash-clean, минимальной референсной реализации xxHash. Результаты бенчмарка следующие: вставки у FlatHashMap получились аж в 36 раз быстрее, чем у dlib.container.dict, и в 1,7 раз быстрее, чем у ассоциативных массивов. Поиск — в 15 раз быстрее, чем у dlib.container.dict, и в 1,6 раз быстрее, чем у ассоциативных массивов. Эффективен и opApply — это обычный линейный обход буфера безо всякой рекурсии и сложной логики.
Долгое время в Dagon поддерживался лишь один алгоритм антиалиасинга — FXAA. Его главные достоинства — почти нулевой оверхед и довольно приличное качество (лучше хоть какой-то AA, чем вообще никакого). Но есть ситуации, в которых результат FXAA оставляет желать лучшего — например, длинные прямые края под маленькими углами относительно горизонтали. Также FXAA часто приписывают замыливание текстур, но это, скорее всего, миф, возникший из-за каких-то некорректных реализаций алгоритма в определенных играх — нормальный FXAA 3.11 ничего не мылит.
Сегодня многие движки используют TAA, но в Dagon я его пока добавлять не хочу по ряду причин. Все темпоральные техники основаны на идее накопления случайных сэмплов в условиях кадровой когерентности — это нормально работает, только когда камера неподвижна или движется очень медленно. При резких движениях камеры историю приходится сбрасывать, и визуально это проявляется в том, что эффект то ослабевает, то усиливается. Для сглаживания, имхо, это выглядит слишком неестественно — и так уже приходится терпеть темпоральные артефакты всяких SSLR и т.п. Плюс TAA сглаживает всю картинку, а не только края — а это то, от чего индустрия, по идее, пыталась уйти. В общем, даже качественные реализации TAA, например в Unreal, выглядят на мой вкус так себе.
Окончательным компромиссом между стареньким FXAA и современным, но не очень привлекательным TAA стал алгоритм SMAA — субпиксельное морфологическое сглаживание, разработанное в начале 2010-х по заказу AMD. Работает на современных видеокартах очень быстро — на глаз я не заметил никакого замедления. SMAA сглаживает только края, так как алгоритм основан на распознавании краев. Минусом по сравнению с FXAA является повышенное потребление памяти — добавляются два дополнительных буфера.
Чтобы включить SMAA в Dagon 2.0, нужно указать в render.conf
antialiasing: "SMAA";
Либо просто включить высокий профиль качества (profile: 1). FXAA тоже никуда не девается и включен по умолчанию для низкого профиля качества (profile: 0).
antialiasing: "FXAA";
На днях провел самую нетривиальную сессию отладки за всю историю Dagon: по-тихому сломалась загрузка одной из текстур в сцене с лапшичной — 16-битный grayscale PNG, карта высот под parallax mapping. Я уже сталкивался с багом обработки 16-битных изображений в SDL_Image, но пофиксил эту проблему только для RGB — SDL_Image выдает формат SDL_PIXELFORMAT_RGB48, можно перехватывать такие кейсы и конвертировать в 8 бит на канал вручную вместо вызова SDL_ConvertSurface. Но в случае с grayscale чудит уже сам декодер, выдавая почему-то формат SDL_PIXELFORMAT_INDEX8, тогда как grayscale — это не индексированный режим, а отдельный пиксельный формат, поддерживающий разную битовую глубину. Я так и не понял, что с этим делать, и решил для загрузки PNG адаптировать мой старый декодер из dlib.
Сказано — сделано: я создал dagon.resource.png, заменил SuperImage на TextureBuffer и выпилил поддержку APNG, которая в играх все равно не нужна. Но после этого внезапно сломалась загрузка сцены Sponza — для всех текстур декодер просто выдавал мусор в чанках. Я долго ломал голову, искал ошибку в алгоритме разбора, пока не додадался на всякий случай проверить одну из текстур hex-редактором. Оказалось, что это вообще не PNG, а JPEG! SDL_Image такие картинки переваривал без проблем, поэтому я даже не замечал. Поэтому, чтобы вернуть старое поведение, пришлось городить детектор формата на основе сигнатур. В итоге, впрочем, загрузчик текстур стал работать даже чуть быстрее, чем прежний вариант.
Потрясающая киберпанковая лапшичная от Марины Белов + несколько других моделек в том же стиле.




Видео:
Разработка новой версии движка идет гораздо быстрее, чем я успеваю писать о ней. В августе я уже отчитывался о порте dagon:imgui и dagon:audio, на днях я также портировал dagon:physfs, dagon:video, dagon:security и dagon:server. Перенесен менеджер ввода InputManager, биндинг Wintab и модуль ввода с графического планшета, реализована поддержка компонентов для Entity, поддержка HiDPI, вычислительных шейдеров и ресэмплинга текстур на GPU.

Кроме того, я занимался оптимизацией некоторых частей рендера и решил ряд застарелых проблем производительности:
Cadencer) теперь использует высокоточный наносекундный таймер. Реализован новый планировщик кадров, который сильно снижает нагрузку на CPU и уменьшает тем самым энергопотребление и нагрев, что важно на ноутбуках;Рендер Dagon 2 еще не завершен, но я решил уже сейчас наскоком закрыть одну из самых нестандартных задач всего проекта — написать бэкенд ImGui для SDL GPU. Что интересно, эта работа вскрыла очередную проблему с фрейм-таймингом, на которую я раньше не обращал внимания.

ImGui — потрясающе хорошо спроектированная библиотека. Она рисует интерфейсы при помощи абстрактного низкоуровневого воркфлоу, путем формирования списка примитивов. Чтобы фактически вывести графику ImGui на экран, нужно написать код, который в реальном времени передает эти данные в графический API. Это не самая тривиальная задача, но, как ни парадоксально, для SDL GPU она решается даже несколько проще, чем для OpenGL. Меня вообще этот API зацепил именно тем, что он избавляет от неудобных и довольно запутанных идиом OpenGL, не порождая при этом кучу новых, как это делают другие. На SDL многие распространенные конструкции выглядят в разы короче, чем на OpenGL или Direct3D (и я уж молчу про чистый Vulkan). В общем, оказалось, что ImGui отлично дружит с SDL GPU.
Перебор примитивов, полученных через igGetDrawData, и их трансляция в вызовы отрисовки — довольно простой процесс. Управление вершинным и индексным буферами под это дело уже сложнее: особенность в том, что предельное количество входящих примитивов ImGui заранее не известно, поэтому буферы должны быть динамическими. Точнее, нужно их динамически пересоздавать с запасом, если текущего объема не хватает. Когда пишешь такое, ощущение как от системной разработки — кажется, будто создаешь какой-то свой X11 😅
Но мало написать графический бэкенд, нужна еще прослойка, обрабатывающая ввод из SDL, иначе интерфейс не будет реагировать на события мыши и нажатия клавиш. Ее тоже пришлось написать самостоятельно, чтобы не трогать внутренности cimgui.dll — в Dagon 1.0 я полагался на встроенный в библиотеку бэкенд под SDL2. Фактически эта задача сводится к переводу событий и клавиатурных кодов SDL в формат ImGui, а также небольшому хаку с функциями TextInput для того, чтобы правильно работал ввод текста:
if (io.WantTextInput)
{
if (!SDL_TextInputActive(application.window))
SDL_StartTextInput(application.window);
}
else
{
if (SDL_TextInputActive(application.window))
SDL_StopTextInput(application.window);
}
Наконец, последнее, что я сделал — слегка изменил логику рендера, а именно вынес получение текстуры свопчейна из PresentPass в Renderer, в начало кадра (это нужно, чтобы можно было рендерить в задний буфер несколькими проходами подряд). Не поверите, но это единственное, что пришлось изменить в ядре движка, чтобы добавить поддержку ImGui! Все остальное — исключительно надстройки.
Я было обрадовался, но потом заметил инпут-лаг (причем в Dagon 1.x он тоже есть, хотя и не такой сильный). Когда перетаскиваешь окошко ImGui, видно, что курсор его опережает, хотя, по идее, не должен, если движение синхронизировано со вводом. Это напрямую связано с VSync. Непосредственно в игре задержка ввода не так заметна, как в GUI, но она есть, и в некоторых случаях из-за этого возникает что-то сродни морской болезни. Вокруг этой темы сломано немало копий, холивары о том, зло ли VSync, бытуют еще чуть ли не с 90-х. Иногда, при конфликте с оконным менеджером ОС, включение VSync может привести к еще более неприятному артефакту в виде статтеринга, так что я все же склоняюсь к мысли, что классический VSync больше мешает, чем помогает.
Для решения этой проблемы в современных API придумали Mailbox. Это разновидность тройной буферизации, которая позволяет выводить кадры с минимальной задержкой на синхронизацию с монитором. На моей системе этот режим показывает себя на удивление хорошо, стабилизируя тайминг кадра так, что реакция на ввод срабатывает молниеносно. Естественно, я его добавил в Dagon 2. Он включается опцией vsync: 2 в settings.conf.
В предыдущем посте я уже писал о том, почему glTF нельзя использовать в качестве полноценного формата игровых ассетов, и вот альтернатива, которую я специально проектирую для Dagon 2.0 — Dagon Asset Format, сокращенно DAF.
DAF хранит, в первую очередь, вершинные буферы для прямой передачи в VRAM. Формат бинарный, архитектурно соответствующий API Dagon 2.0 и расширяемый: в нем могут быть объявлены любые дополнительные структуры данных и даже динамические свойства, не ломая обратную совместимость.
Основные задачи формата:
Спецификация DAF находится здесь.
В Dagon 1.0 я очень много времени потратил на поддержку этого монструозного формата моделей, и спустя пять лет работы с ним у меня не осталось ничего, кроме разочарования. Главный вывод: glTF — не для игровых ассетов (т.е. карт и отдельных переиспользуемых моделей). Это формат, предназначенный исключительно для просмотра, причем в основном в браузере, на WebGL, при помощи вьюверов типа Sketchfab. В этом он свою задачу выполняет, хотя и тоже неидеально. Но то, что его все начали использовать как формат обмена данными между программами моделирования и игровыми движками — это фундаментальная ошибка, которую я и сам по наивности допустил. Ниже мой личный список недостатков glTF, актуальных, прежде всего, в контексте игрового рендеринга.
В общем, для Dagon 2.0 я обязательно буду пилить собственный формат ассетов, учитывая весь этот опыт. Поддержка glTF сохранится благодаря библиотеке Assimp, но формат больше не будет позиционироваться как основной.