Dagon 1.0

Рад сообщить, что наконец-то выпустил Dagon 1.0.0, первый стабильный релиз моего игрового движка! Это в основном исправляющий релиз, который подытоживает огромную работу над Dagon, которую я проделал за последние два года.

Исправлен баг с неправильным сэмплингом BRDF LUT, что ранее приводило к излишней яркости шероховатых поверхностей. Исправлен эффект тумана для точечных источников света.
Улучшена система частиц. Добавлено свойтсво Emitter.fadeInDuration для плавного появления частиц из прозрачности, а также Emitter.gravity для управления гравитацией частиц. Свойство Emitter.initialPositionRandomRadius теперь является вектором и называется Emitter.initialPositionRandomRadii — это радиус-вектор, позволяющий задать разброс случайного появления частиц отдельно по всем трем осям.
Добавлен новый тип событий CustomResize, который используется в тех случаях, когда пользовательский код должен обрабатывать кастомный ресайз вьюпорта (обычно это необходимо, если размер вьюпорта не привязан жестко к размеру окна и вычисляется другим способом). Также менеджер событий теперь обрабатывает SDL_WINDOWEVENT_CLOSE для грациозного закрытия игры при нажатии Alt+F4. Добавлен новый метод Application.isWindowMinimized.
Добавлен новый режим воспроизведения анимации для GLTFBlendedPosePlayMode.OnceAndStop. Он позволяет проиграть анимацию один раз и остановиться на последнем ключевом кадре. Добавлено новое свойство GLTFBlendedPose.timeScale для управления скоростью анимации.
Добавлено свойство Light.angularRadius, которое симулирует солнечный диск угловым радиусом распределения света.
Улучшен шейдер ночного неба StarfieldSkyShader: добавлены свойства sunEnergy (энергия светила), spaceColorZenith (цвет пространства в зените), spaceColorHorizon (цвет пространства на горизонте).
Добавлено новое свойство FirstPersonViewComponent.roll — поворот камеры от первого лица по оси Z для эффекта наклона головы вбок.
В расширении dagon:jolt добавлена поддержка convex cast (JoltPhysicsWorld.shapeCast). Контроллер персонажа JoltCharacterController теперь использует shapeCast в логике приседания (для проверки высоты потолка над головой). Как следствие, персонаж теперь автоматически приседает, чтобы избежать врезания головой в низкие потолки. Добавлен метод onGround, который возвращает true, если персонаж стоит на земле.
Исправлена компиляция расширения dagon:nuklear.

Temporal SSAO

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

На скриншоте ниже используется всего 5 сэмплов на кадр:

Dagon vs Eevee

Рендер в Dagon 2.0 и Blender 5.0 / Eevee. Как говорится, найдите десять отличий 😀

Dagon 2.0 на SDL3. Долгосрочные планы

Я пишу графику на OpenGL много лет (с 2009 года) и считаю, что этот классический API — до сих пор самый очевидный выбор, если вам нужно кроссплатформенно вывести что-то на экран. В сочетании с SDL это еще и очень просто — код приложения со всеми его ключевыми компонентами (окно, графика, ввод) получается на 99% независимый от операционной системы. Для 2D-проектов этой связки более чем достаточно, но в играх с продвинутой 3D-графикой, которая выжимает максимум из видеокарты, теперь, к сожалению, все сложнее.

Я выделяю три главные архитектурные проблемы OpenGL: однопоточность, частая синхронизация CPU и GPU и сложность управления глобальным состоянием конвейера. Новые низкоуровневые API решают все это, особенно последнее, жертвуя удобством написания приложений и вообще входным порогом в профессию графического разработчика (я не представляю, как можно изучить концепции и идиомы Vulkan с полного нуля — я бы рвал на себе волосы, если бы пришлось объяснять начинающим, что такое дескриптор-сеты, PSO и барьеры памяти). Долгое время между OpenGL и Vulkan была совершенно непреодолимая стена концептуальной несовместимости, мешающая портировать игры. OpenGL, между тем, хоть никуда и не делся, развиваться перестал. Под macOS — старая версия, под Windows — довольно серьезная проблема с вертикальной синхронизацией в оконном режиме, приводящая к статтерингу (это полноценно решается только путем работы поверх свопчейна DXGI, что требует нетривиального бойлерплейта в приложении, либо поддержки со стороны видеодрайвера). Ну и до кучи в OpenGL нет поддержки HDR-режима Windows, что некритично, но не круто.

Все это подводит к мысли, что OpenGL, каким бы он комфортным ни был, уже отжил свое. Но и на Vulkan переходить совершенно не хочется. Познакомившись с SDL GPU, я решил попробовать портировать на него некоторые ключевые части Dagon и быстро понял, что этот API — именно то, чего мне и не хватало в последние годы. WebGPU стал разочарованием из-за громоздкости интерфейсов и спорного синтаксиса WGSL, а здесь с этим проблем нет.

Исходники Dagon 2.0 доступны в отдельном репозитории на GitHub: https://github.com/gecko0307/dagon2. На данный момент перенесен dagon.core, реализованы G-буфер, базовый deferred-рендеринг, тонмаппинг и анти-алиасинг. Движок загружает модели OBJ и текстуры в стандартных форматах, поддерживает кубические карты и DDS. Судя по всему, большая часть возможностей Dagon будет перенесена без серьезных изменений, картинка движка останется прежней, но не исключены архитектурные улучшения и CPU-оптимизации. Также изменится шейдерный API — на днях напишу отдельный пост об этом. Переход на Vulkan-бэкенд должен заметно ускорить рендер, а также позволит использовать HDR-свопчейн, если дисплей позволяет выводить в extended linear.

Я не уверен, что процесс портирования будет быстрым, но впечатления от работы пока весьма положительные, код для SDL GPU компактный и читаемый. Самая нетривиальная часть — воркфлоу компиляции шейдеров, но я уже смирился с тем, что от GLSLang в современных условиях никуда не денешься. Есть вероятность, что Dagon 1.0 я выпущу уже в ближайшее время, чтобы полностью сосредоточиться на порте.

Новая техно-демка Dagon

Параллельно с Dagon 0.42 работаю над новой демкой движка, в которой наконец-то будут не бесплатные тестовые сцены из Интернета, а оригинальный контент — хочу сделать фрагмент города со зданиями, в которые можно заходить. Не исключено, что она перерастет в полноценную игру, что-нибудь в духе незабываемых творений Chinese Room.

Стриминг уровней (продолжение)

Несколько лет назад я проектировал для Dagon систему больших миров на основе динамической загрузки/выгрузки ресурсов в ячейках прямоугольной сетки. До практической реализации тогда дело на дошло, но недавно я решил вернуться к этой теме. Итак, встречайте — dagon.openworld.

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

Есть класс OpenWorldManager, который управляет массивом чанков — объектов WorldChunk. Чанки полиморфны; предполагается, что пользователь напишет специализированную версию этого класса под свои нужды. Каждый чанк занимает известную заранее квадратную область в глобальном пространстве мира. Общий размер мира фиксирован и задается при создании OpenWorldManager.

Мир адаптируется к текущей позиции так называемого путешественника (traveler) — это Entity, который представляет игрока. В каждый момент времени активны до 9 чанков вокруг путешественника — окно 3×3. Когда путешественник пересекает границу между чанками, система перемещает окно по карте, активируя новые чанки и деактивируя старые по необходимости.

Активация и деактивация — это виртуальные вызовы, семантика которых полностью определяется реализацией чанка. Типовой сценарий — загрузить/выгрузить статическую геометрию и другие ресурсы, включить/выключить логику динамических объектов. Я предусмотрел небольшой гистерезис на случай, если игрок часто пересекает границу — можно отложить активацию/деактивацию на несколько секунд, до момента, когда текущий чанк перестанет меняться. Таким образом, оверхед на управление системой получается минимальный — весь тяжелый код будет в пользовательских onActivate и onDeactivate.

Для того, чтобы точность float-вычислений не падала по мере отдаления от начала координат, я, как и планировал, реализовал заворачивание позиций. Суть его в том, что при достижении игроком определенного расстояния от центра все объекты мира синхронно переносятся на противоположную сторону координатной плоскости, «из плюса в минус». Сохраняется иллюзия бесконечного пространства при ограниченном координатном охвате. Эта фича полностью опциональная — система лишь сообщает о возможности врапить координаты в те или иные моменты (через виртуальный вызов WorldChunk.onWrap и коллбек onTravelerWrap а делать это фактически или нет, решает пользователь.

Симулятор автомобиля в Jolt

Jolt Physics включает не только готовый контроллер персонажа, но и полнофункциональный raycast vehicle — причем, это не экспериментальная поделка-бонус, как часто бывает в физдвижках, а серьезный такой префаб, который с минимальными усовершенствованиями можно брать для создания симкейда коммерческого уровня. Достаточно сказать, что он безо всякой доработки напильником уже выглядит лучше, чем, скажем, печально известная физика авто в Cyberpunk 2077. А если еще подобрать реалистичные параметры подвески и инерцию шасси, заменить встроенную модель трения шин на свою, то вообще идеально!

На видео — демка 4×4, которую я накидал буквально за день, изучив VehicleController в Jolt:

Пока в Dagon нет соответствующей объектной обертки, но это не проблема, так как можно использовать любые функции joltc напрямую. Исходники и сборку планирую выпустить одновременно с Dagon 0.42, где будет несколько важных улучшений в dagon:jolt и dagon:audio.

Dagon 0.41.0

В новой версии движка дебютирует физический движок Jolt Physics, в виде расширения dagon:jolt. Он предоставляет примерно тот же набор возможностей, что и Newton, при этом он лучше оптимизирован под многоядерные процессоры, а также имеет встроенный контроллер персонажа, который работает более стабильно, чем старый NewtonCharacterController. Newton, однако, в обозримом будущем никуда не денется, оба расширения будут развиваться параллельно. Я планирую также добавить поддержку встроенного симулятора автомобиля Jolt — об экспериментах с ним напишу как-нибудь в отдельном посте.

Исправлен микростаттеринг, связанный с неверной работой главного таймера. Визуально это проявлялось в виде периодических скачков изображения, заметных при движении камеры. Свойство stepFrequency переименовано в updatesPerSecond, в классе Application и в settings.conf.

Добавлены новые цветовые профили вывода: линейный (gl.outputColorProfile = "Linear") и Gamma 2.4 (gl.outputColorProfile = "Gamma24").

Добавлено новое свойство Entity.autoUpdateTransformation. Если его отключить, то Dagon не будет автоматически обновлять матрицы трансформации Entity. Это нужно, главным образом, в компонентах, которые заменяют встроенную трансформацию своей собственной математикой.

Реализована поддержка кинематических тел в dagon:newton. С их помощью можно реализовать, например, движущиеся платформы. Также добавлены новые свойства NewtonRigidBody: bodyType, collisionShape, angularVelocity, acceleration, linearDamping, angularDamping, simulationState, collidable, sleepState, autoSleep, freezeState, gyroscopicTorque и новые методы setMassMatrix, setMassProperties. В контроллере персонажа исправлено застревание в стенах при прыжке.

В dagon:audio добавлены новый метод AudioManager.setPlaySpeed и опциональный параметр громкости в методах SoundComponent.play.

Jolt Physics

Как я уже не раз отмечал, игровая физика — это крайне недооцененный вид софта. Люди много говорят о графике, обыватель в играх вообще ничего, кроме графона, не замечает. А между тем, проверка столкновений и физика — вот что по-настоящему важно. Устаревший визуал и любительский уровень моделей игре можно простить, но проваливание персонажа сквозь пол и стены — нет. Я видел множество инди-проектов, потенциал которых был загублен именно плохой физикой (и, как следствие, неудобным управлением), а не графической или геймплейной составляющими. В мире инди и СПО вообще редко связываются с «физичными» жанрами типа экшнов, гонок, симуляторов и т.д. Свободные игры — это в основном стратегии, песочницы, 2D-аркады, рогалики. Физика довольно устойчиво ассоциируется с AAA и колоссальными бюджетами крупных издателей. Причина, конечно, в том, что она непредсказуема и плохо программируется. Она хороша в «чистом» виде — скажем, в симуляторе бильярда, где простая геометрия и нет вычислительно сложных ситуаций. Но если физика должна быть подчинена игровой логике, а не наоборот, то решения «по учебнику» резко перестают работать, приходится изобретать невиданные хаки и решать дичайшие корнер-кейсы. Это и отличает AAA от любительского геймдева — там коммерческие секреты, ноу-хау, огромный опыт. Я вот давно уже пишу контроллеры персонажа и не понаслышке знаю, как это непросто. А ведь хочется получить этот священный Грааль — чтобы персонаж свободно двигался по карте, не застревал, правильно сталкивался с препятствиями, поднимался по склонам и лестницам. Сегодня это необходимая база для 3D-игр любого жанра, кроме совсем уж простеньких.

Я пробовал писать собственный физдвижок (dmech). Это был очень полезный опыт, но, взвесив все «за» и «против», я все-таки решил использовать то, что сделано профессионалами — я ведь не физик, а графический разработчик. Пусть каждый занимается тем, что лучше всего получается. Я выбрал Newton Game Dynamics, который долгое время был, пожалуй, самым оптимизированным и точным среди всех, что имеют C-интерфейс. Это отличный физический движок, и я очень благодарен Хулио Хересу за этот проект, Newton много лет был прекрасным дополнением к Dagon. Я долго не хотел его менять, но технологии не стоят на месте. Newton 3 заметно устаревает, а Newton 4 — это уже чистый C++ без C-враппера, поэтому настало время переходить на что-то альтернативное. Есть серьезные основания полагать, что новым стандартом индустрии станет Jolt Physics от Guerrilla Games. Jolt используется в их движке Decima, лежащем в основе серий Horizon и Death Stranding — редкий случай, когда технология AAA-уровня сразу переходит в Open Source! Jolt — это реальный конкурент PhysX и Bullet, многие бесплатные и свободные игровые движки уже переходят на него один за другим: Godot, NeoAxis, Dagor. И, что самое замечательное, к Jolt есть полноценный C-враппер, позволяющий использовать его не только в C++.

Итак, что же там такого уникального? Во-первых, отличная оптимизация под SIMD и поддержка многопоточности (правда, это имеет значение лишь при каких-то редко встречающихся высоких CPU-нагрузках). Во-вторых, изначальная ориентация на игры — дизайн и набор фич показывают, что Jolt разработан игроделами для игроделов, для реального продакшна, а не академических публикаций. Движок ощущается, как некий «бесплатный Havok» из закромов энтерпрайза. У него много архитектурных особенностей, но в целом с ним очень приятно работать. Например, очень понравилось, что структуры векторов и кватернионов в Jolt бинарно совместимы с моими родными Vector3f и Quaternionf. Да и сама синхронизация через позиции и кватернионы вместо матриц 4×4 — это мудро, так как матрицы требуют декомпозиции.

Я, пока писал этот пост, уже успел написать для Dagon базовую обертку над Jolt (расширение dagon:jolt), концептуально весьма близкую с dagon:newton. Движки во многом похожи — используется классическая схема с разделением на тела и полиморфные шейпы. Многие функции имеют более удобный API, чем в Newton — например, рейкастинг. В данный момент обертка поддерживает создание статических и динамических тел, шейпов всех видов, включая меши, а также базовый контроллер персонажа, который я планирую довести, как минимум, до уровня NewtonCharacterController. К релизу Dagon 0.41 будут добавлены сочленения.

Контроллер персонажа CharacterVirtual особенно порадовал: я решил сразу попробовать его в деле. Шикарно, что управление контроллером (включая гравитацию и прыжок) осуществляется путем произвольного изменения вектора скорости — можно реализовать любую механику. Контроллер стабильный и точный, встроенный ground-тест работает надежно — основа для игры от первого лица пишется буквально за один вечер.

В Jolt есть поддержка мягких тел и даже встроенный симулятор автомобиля! Очень надеюсь, что когда-нибудь все это тоже получится перенести в dagon:jolt.

Использовать расширение не сложнее, чем dagon:newton, а в чем-то даже проще. Сначала нужно вызвать joltInit для инициализации:

class MyGame: Game
{
    this(uint w, uint h, bool fullscreen, string title, string[] args)
    {
        super(w, h, fullscreen, title, args);

        if (!joltInit())
            exit();

        currentScene = New!MyScene(this);
    }

    ~this()
    {
        joltShutdown();
    }
}

Затем создать JoltPhysicsWorld:

import dagon.ext.jolt;

class MyScene: Scene
{
    JoltPhysicsWorld physicsWorld;

    this(MyGame game)
    {
        super(game);
        this.game = game;
        physicsWorld = New!JoltPhysicsWorld(eventManager, this);
    }
}

Пример создания статического тела для геометрии уровня:

Entity eLevel = addEntity();
eLevel.drawable = aLevel.mesh;
JoltMeshShape levelShape = New!JoltMeshShape(aLevel.mesh, physicsWorld);
JoltRigidBody levelBody = physicsWorld.addStaticBody(eLevel, levelShape);

Динамическое тело:

Entity eCube = addEntity();
Vector3f cubeHalfExtents = Vector3f(0.5f, 0.5f, 0.5f);
eCube.drawable = New!ShapeBox(cubeHalfExtents, assetManager);
JoltBoxShape boxShape = New!JoltBoxShape(cubeHalfExtents, physicsWorld);
auto cubeBody = physicsWorld.addDynamicBody(eCube, boxShape, 10.0f);

В onUpdate сцены, как обычно, шаг интегрирования:

override void onUpdate(Time t)
{
    physicsWorld.update(t);
}

Sponza — обновленная технодемка

Скриншоты демки с моделью Crytek Sponza на основе Dagon 0.40.0 со учетом всех недавних улучшений: тонмаппер AgX, анизотропная фильтрация, CAS, автоэкспозиция, линейный воркфлоу постпроцессинга.

Исходники и сборка для Windows.