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

Текстура волос может иметь альфа-канал, но вместо альфа-смешивания используется упрощенная прозрачность на основе дизеринга Байера.
В рамках Dagon 2.0 скопилось множество улучшений для dlib, и данный релиз подытоживает эту работу.
Добавлен модуль dlib.math.base — замена std.math, которая заменяет некоторые часто используемые функции быстрыми интринсиками LLVM при компиляции LDC (подробнее об этом тут). Ускорены следующие элементарные функции: fabs, sqrt, sin, cos, tan, ceil, floor, round, trunc, rint, nearbyint, pow, exp, exp2, log, log2, log10, fmax, fmin, fma, copysign. В среднем они вдвое быстрее, чем в Phobos. Единственный нюанс, который следует помнить — лучше не импортировать оба модуля одновременно, иначе будет конфликт, и вам придется использовать квалифицированные имена при вызовах.
Добавлена встроенная SIMD-оптимизация для перемножения матриц 4×4 (Matrix4x4f). Это ускоряет оператор в 3-4 раза. В связи с этим удален модуль dlib.math.sse как устаревший, неподдерживаемый и, как выяснилось, нерабочий под x86_64.
Хэш-таблица FlatHashMap из Dagon 2.0 добавлена в библиотеку как dlib.container.hashmap. Реализация xxHash64, соответственно, — как dlib.hash.xxhash64 в новом пакете dlib.hash. Напомню, что это очень быстрая реализация ассоциативных массивов без использования сборщика мусора, которая в десятки раз эффективнее, чем dlib.container.dict и в 1,5-2 раза — чем встроенные ассоциативные массивы D.
Полностью переработан dlib.random — пакет теперь работает на основе перемешанного конгруэнтного генератора (PCG), реализованного для 32- и 64-битных чисел. Это современный эффективный генератор псевдослучайных чисел, который превосходит как Вихрь Мерсенна (std.random), так и линейный конгруэнтный генератор (rand в стандартной библиотеке C). Добавлены новые функции choice (случайный выбор из заданного множества вариантов) and rollDice (случайное целое число от 0 до заданного порога). Функция seed, генерирующая зерно для инициализации генератора из системной энтропии, теперь использует функцию смешивания fmix64 из MurmurHash3 вместо mix Боба Дженкинса.
Стандартная библиотека использует классический Вихрь Мерсенна (MT19937). Недавно я, вдохновившись эффективностью PCG для шейдерных вычислений, добавил соответствующий модуль в dlib, заменив им rand в dlib.random. Оказалось, что моя функция random для выборки равномерного распределения в диапазоне 0..1 аж в 4 раза быстрее, чем std.random.uniform!
Бенчмарк для 10000000 вызовов:
std.random.uniform: 77 ms, 38 μs, and 3 hnsecs total
dlib.random.random: 19 ms, 599 μs, and 3 hnsecs total
Стандартное отклонение у обеих функций примерно одинаковое:
std.random.uniform: 0.544σ
dlib.random.random: 0.413σ
Наконец-то дошли руки написать теоретическую статью о модели освещения, которую я использую в движке, со всеми формулами и ссылками на научную литературу. Это GGX/Trowbridge-Reitz, на которую опирается освещение в Unreal Engine 4 и многих других движках — физически обоснованная модель зеркально отраженного света для изотропных шероховатых поверхностей. Ее многие сейчас используют, одни и те же уравнения можно встретить в самых разных движках, но мало кто задумывается, откуда они взялись.
Поскольку в тексте много математики, я оформил статью в виде PDF — можно скачать файл или читать при помощи встроенного в браузер просмотрщика.
Шучу, конечно. Но специализированная хэш-таблица действительно может быть заметно быстрее встроенных ассоциативных массивов 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 и уменьшает тем самым энергопотребление и нагрев, что важно на ноутбуках;Я терпеть не могу форкать чужие OpenSource-проекты, но иногда все же приходится это делать. Вокруг некоторых замечательных вещей почему-то не складывается сообщество, и если автор забрасывает проект, то форк — единственный выход. Всем хотелось бы, чтобы поддержкой нужного софта занимался кто-нибудь другой, какие-то специалисты с опытом, но в какой-то момент понимаешь, что ты вообще-то сам один из них и должен что-то брать на себя.
Так получилось с SoLoud, моим любимым свободным звуковым движком для игр. Игровое аудио преследует странное проклятие — ни одна библиотека не становится стандартом в достаточной степени, чтобы ее развивали всем миром. Так было с OpenAL, так было с популярным когда-то российским проектом Squall, и теперь вот SoLoud тоже оказался заброшен. Конечно, есть альтернативы — например, те же SDL Audio и SDL_Mixer, но в них из коробки почти ничего нет, все функции нужно реализовывать самому. А в SoLoud есть отличный многоканальный 3D-микшер и поддержка всех популярных аудиоформатов, плюс фильтры и множество полезных настроек. Он работает поверх самых разных системных API и других аудиодвижков, включая те же SDL, OpenAL, MiniAudio и много чего еще. Это единственный в своем роде комбайн, который заслуживает звания «свободного FMOD», и отказываться от такой удобной штуки как-то совсем не с руки.
Беда в том, что переход игрового движка на SDL3 ломает все, что в нем зависело от SDL2. В SoLoud так и не появился официальный бэкенд поверх SDL3, и это стало очередной серьезной проблемой для Dagon 2. Конечно, можно просто выставить в настройках альтернативный бэкенд (под Windows самый очевидный выбор WASAPI, под Linux — что заведется), но мне стало досадно, что нельзя жить по-старому — ведь SDL все-таки штука надежная, работающая практически везде со стопроцентной гарантией. Поэтому я решил сделать свою версию SoLoud со включенным PR #402, добавляющим поддержку SDL3.
Попутно пришлось сделать новый CMakeLists.txt, потому что старый работал через пень-колоду, а официальным генератором сборки проекта вообще является какой-то неведомый форк Premake под названием GENie 🤯 Но это было только началом квеста: оказалось, что PR кривой, с недоделанной загрузкой функций SDL и неправильной инициализацией, из-за которой бэкенд тупо не работал. Пришлось продираться сквозь баги — как в той песне группы PR-Mex, «что ж их так много, ей-богу?»
Я также решил поэкспериментировать и добавил собственный бэкенд поверх DirectSound — ностальгия! И он на удивление хорошо заработал. Статус всех бэкендов на сегодняшний день такой:
На практике, по моему опыту, самые надежные бэкенды — SDL и MiniAudio. В целях сохранения обратной совместимости ABI я не стал менять константы существующих бэкендов и добавил новые в конец: SDL3 идет под номером 17, DirectSound — 18.
Мой форк доступен тут: https://github.com/gecko0307/soloud, патчи с исправлениями приветствуются. Я пока не уверен, буду ли добавлять новые функции — смысл форка состоит пока только в базовом сопровождении.