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.

Dagon 0.40.0

Этот релиз ознаменовался серьезным рефакторингом постпроцессинга в движке: внесено множество оптимизаций и новых возможностей, а также появилась поддержка пользовательских фильтров (PostProcRenderer.addFilterPass). Возвращена поддержка автоэкспозиции, которая когда-то уже была реализована, но впоследствии удалена из-за архитектурных изменений в движке. Управляется параметрами hdr.autoexposure, hdr.keyValue, hdr.exposureAdaptationSpeed в render.conf. Добавлены эффекты виньетирования (vignette.enabled, vignette.strength, vignette.size, vignette.roundness, vignette.feathering) и film grain (filmGrain.enabled, filmGrain.colored). Реализован высококачественный фильтр повышения резкости с алгоритмом на основе FidelityFX CAS (sharpening.enabled, sharpening.strength). Добавлены новый тонмаппер Lottes (hdr.tonemapper: "Lottes"), новые параметры фильтра Depth of Field (dof.circleOfConfusion, dof.pentagonBokeh, dof.pentagonBokehFeather), улучшен фильтр шумоподавления SSAO — добавлена поддержка взвешивания на основе глубины, что устраняет гало-артефакты на близких расстояниях. Фильтр цветокоррекции теперь поддерживает изменение яркости, контраста и насыщенности (cc.brightness, cc.contrast, cc.saturation). Также можно напрямую задать 4×4 матрицу цветокоррекции (cc.colorMatrix). Если используется LUT, эти параметры игнорируются.

Улучшен воркфлоу таблиц цветокоррекции (LUT). Добавлено новое свойство TextureAsset.lutFormat, посредством которого загрузчик текстур понимает, что нужно конвертировать двумерную таблицу в 3D-текстуру. Поддерживаются два формата — LUTFormat.Hald и LUTFormat.GPUImage. Свойство TextureAsset.loadAs3D удалено, вместо него теперь надо задавать TextureAsset.lutFormat = LUTFormat.Hald;. Дефолтная таблица формата GPUImage (загружаемая опцией lut.file) теперь автоматически конвертируется в 3D-текстуру. Если вы загружаете GPUImage LUT вручную, то можно ее не конвертировать, но отныне рекомендуется делать это для более эффективного сэмплинга в шейдере.

Добавлен опциональный вывод в честный sRGB вместо обычного Gamma 2.2. Теперь можно задать цветовой профиль вывода при помощи опции gl.outputColorProfile в settings.conf. Возможные значения (строковые): "Gamma22", "sRGB".

Реализована поддержка глобальных определений макропроцессора GLSL (функция globalShaderDefine в dagon.graphics.shader). Чтобы их использовать в шейдерах, нужно добавить виртуальный инклюд #include <dagon>.

Добавлена поддержка сохранения в DDS кубических карт, 3D-текстур и RGTC-текстур. Появилась новая функция downloadTexture для выгрузки текстур любого формата из видеопамяти.

Появился новый тип событий EventType.KeyboardLayoutChange и соответствующий метод-обработчик onKeyboardLayoutChange. Это событие возникает, когда пользователь переключает раскладку клавиатуры.

Повышение резкости

Добавил в Dagon фильтр повышения резкости на основе FidelityFX CAS. Его можно включить свойством sharpening.enabled в render.conf, свойство sharpening.strength управляет силой эффекта. Вкупе с анизотропной фильтрацией это значительно повышает качество рендера. На скриншоте разница особенно заметна на золотых узорах гобеленов и текстуре каменной кладки.

Dagon 0.39.0

Новая версия движка. Добавил возможность загружать кастомные курсоры и заменять ими системные курсоры в приложении (методы Application.loadCursor, Application.replaceCursor). Появилась поддержка многомониторных конфигураций — теперь можно задать индекс монитора для создания игрового окна (опция window.display в settings.conf). Добавлены новые свойства класса Application: displayCount, displayIndex, displayWidth, displayHeight, desktopWidth, desktopHeight, refreshRate, framebufferFormat.

В модуль dagon.graphics.shape добавлены новые геометрические тела: ShapeCapsule (капсула), ShapeTorus (тор).

Появилась Поддержка uniform-массивов — шаблонный класс ShaderParameterArray и метод Shader.createParameterArray. Методы Shader.setParameter, Shader.setParameterRef, Shader.setParameterCallback, Shader.setParameterSubroutine, Shader.getParameterValue помечены как deprecated. Рекомендуется работать с объектами параметров напрямую.

Добавлена поддержка несжатых текстур RGB8 и RGBA8 в экспортер DDS, а также несжатых RGB8 в загрузчик DDS. Реализована перегрузка функции loadImageViaSDLImage, которая возвращает SDL_Surface*.

Dagon 0.38.0

Очередной релиз. Главное нововведение — это модуль dagon.core.dxt, быстрый компрессор текстур в DXT1/DXT5 (D-порт библиотеки RygsDXTc, о котором писал ранее). Текстуру теперь можно сжать после загрузки при помощи свойства TextureAsset.compress. Также реализована функция dagon.resource.dds.saveDDS для сохранения сжатых текстур в DDS. Добавлено новое свойство TextureAsset.assetManager. Добавлена поддержка дополнительных форматов SDL_Image в структуре ImageFileFormat, а именно GIF, QOI, PNM, XCF, XPM, PCX, LBM.

Добавлены новые опции settings.conf для глобального управления анизотропной фильтрацией текстур: gl.anisotropicFiltering и gl.defaultTextureAnisotropy. По умолчанию анизотропная фильтрация отключена.

Добавлен новый оператор тональной компрессии Uchimura.

Реализована обработка SEH-исключений под Windows с трассировкой стека в отладочных сборках (dagon.core.crashhandler).

Добавлена поддержка API вибрации (Haptic API) SDL (GameInputDevice.haptic), который, впрочем, на практике является экзотикой — далеко не все устройства с ним работают. Для большинства контроллеров в SDL поддерживается только Rumble API.

Добавлена функция dagon.core.dialogs.showMessage для графического вывода сообщений.

Если в системе не найдена библиотека Wintab, движок теперь логирует предупреждение вместо ошибки.

В расширении dagon:audio исправлены сигнатуры некоторых функций SoLoud, добавлена перегрузка метода AudioManager.createSound для создания звуков из буферов в памяти.

Исправлены некоторые важные баги в различных компонентах движка, а также ошибки компиляции под x86.

window.hiDPI теперь включен в дефолтном settings.conf.

RygsDXTc и сжатие текстур на лету

Очередная часть Марлезонского балета с компрессией текстур. Я долго думал, делать или не делать в движке сжатие в DXT, и ответ пришел в виде проекта RygsDXTc, быстрого компрессора DXT1/DXT5. Причем написанного не кем-нибудь, а самим Фабианом Гизеном aka ryg из легендарного Farbrausch!

Залипнув на вечер, я портировал на D эту тысячу строк зубодробительного C-кода, под завязку набитого черной магией целочисленной арифметики, и вот результат — в Dagon теперь можно простым переключением свойства отправлять текстуры в видеопамять в сжатом виде:

aTexture = addTextureAsset("data/input.png");
aTexture.compress = true;
aTexture.generateMipmaps = true;

При включенном generateMipmaps перед сжатием будут сгенерированы мип-уровни. Полноценно поддерживаются NPOT-текстуры. При этом оверхед на все про все ничтожный — libktx при транскодировании из Basis Universal и то срабатывает дольше.

Если задать перед загрузкой aTexture.persistent = true, то ассет сохранит исходный TextureBuffer в памяти, и вы сможете сохранить сжатую текстуру в DDS:

auto outputStrm = game.vfs.stdfs.openForOutput("out.dds");
saveDDS(outputStrm, &aTexture.buffer);
Delete(outputStrm);

Ловим необработанные исключения под Windows

Обработка исключений — это всегда скучно и многословно, никому не хочется загромождать свой код вездесущими try/catch. Кроме того, в D обработка исключений на уровне языка не работает с SEH-исключениями под x86_64, что сильно усложняет отлов багов, связанных с доступом по невалидному указателю — приложение просто падает молча, что не соответствует принципу «fail loudly». Но оказалось, что есть относительно простой способ решения этой проблемы.

(далее…)

Dagon 0.37.0

Вышла новая версия движка. Основное нововведение — поддержка объемных конусных источников света, о которых я ранее писал тут. В conf-файлах появилась поддержка комментариев (строки, начинающиеся с //) и булевых значений (true/false). Добавлен простой кинематический движок проверки столкновений (dagon.collision.collision), который лучше всего подходит для 3D-платформеров и квестов. Добавлен новый модуль dagon.ui.axes — отрисовка осей для 3D-манипуляторов. В данный момент реализован только один класс осей, TranslationAxes. Исправлена ошибка конфликта сэмплеров под AMD для некоторых шейдеров. Добавлена перегрузка метода Application.takeScreenshot для снятия скриншота в текстуру. Dagon теперь использует dlib 1.4.1.

Всемогущий alias

Русский перевод моей статьи «Almighty Alias». Оригинал опубликован на Medium.

alias — мое любимое ключевое слово в D! Не перестаю удивляться тому, сколько разных вещей оно делает. В D почти ко всему можно объявить псевдоним, и есть фичи, которые работают исключительно при помощи alias.

(далее…)