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);
}

Обновления

Dagon 0.35.0

Вышла новая версия движка, в которой добавлена поддержка таймеров (Application.setTimer и событие Timer) и новый модуль dagon.core.sysinfo, предоставляющий базовую информацию об аппаратно-программной платформе, на которой запущено приложение. Исправлен забагованный метод Properties.serialize, реализована поддержка текстур в формате WebP в загрузчике glTF (расширение EXT_texture_webp для текстур), добавлена поддержка Entity.blurMask в режиме прямого рендеринга. Объект NewtonMeshShape в расширении dagon:newton теперь можно создавать с передачей матрицы трансформации, которая применяется ко всем вершинам меша. Также добавлен Dagon Platform — комплект для разработки игр под Dagon на языке GScript3, о нем я расскажу подробнее в одном из следующих постов.

Chillwave Drive 0.2.0

Демка автомобильной физики значительно обновлена: улучшена графика, модель машины заменена на McLaren GT, исправлено множество мелких недочетов в симуляторе, благодаря чему демка теперь адекватно поддерживает заднеприводные авто, а также устранен баг в модели колес, который создавал лишнюю боковую силу в некоторых ситуациях. Я решил развивать Chillwave Drive в качестве полноценной игры, в связи с чем создал соответствующую страничку на сайте GameJolt. Вероятнее всего, это будет симкейд для расслабляющего вождения, в котором можно добавлять собственные машины, настраивать окружение и т.д.

Chillwave Drive

Значительно обновлена демка физики автомобиля на Dagon, которая отныне называется Chillwave Drive. Трение колес теперь моделируется на чистых силах вместо встроенных джоинтов Newton, что сделало симуляцию более точной и стабильной. При движении используется динамическое трение (продольное и поперечное) на основе формул Pacejka ’94, при остановке — статическое, препятствующее боковому скольжению машины на склоне. Улучшено управление с контроллера, также добавлена новая модель машины и шейдер неба с облаками и сменой дня и ночи.

Скачать готовую сборку демки под Windows можно тут.

Симуляция веревок

В 2013-2017 годах я писал собственный физический движок, в котором LCP решается через систему неравенств для скоростей: каждое неравенство вводит в систему ограничение свободы для пары столкнувшихся тел, солвер итеративно решает столкновения путем корректировки скоростей. Это универсальный, но вычислительно недешевый метод, к тому же подверженный эффектам нестабильности. Импульсная физика хороша для движущихся тел, но не очень стабильна в состояниях покоя — тела часто дрожат, и для их «успокаивания» приходится вводить различные уловки и хаки, которые неизбежно вводят в систему ошибки, понижая точность симуляции.

В импульсных движках используется интегрирование Эйлера:

Где Δx — скорость частицы — интегрируется аналогичным образом на основе ускорения:

Таким образом, «физично» подействовать на частицу можно только через скорости и силы. Все ограничения в систему вводятся только через скорости — модифицировать позиции тел вручную нельзя, что делает импульсную физику в играх менее удобной для определенных геймплейных задач. Однако существует альтернативный подход на основе интегрирования Верле:

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

for (size_t i = 0; i < positions.length; i++)
{
    Vector3f currentPos = positions[i];
    positions[i] += (currentPos - oldPositions[i]) + freeFallAcceleration * (dt * dt);
    oldPositions[i] = currentPos;
}

Метод Верле позволяет легко вводить в систему ограничения: чтобы их решить, нужно итеративно скорректировать позиции частиц. Например, в случае с веревкой — то есть, набором последовательно соединенных частиц — расстояние между ними делается таким, каким оно должно быть (то есть, позиции частиц корректируются так, чтобы расстояние между ними не увеличивалось и не уменьшалось):

for (size_t i = 0; i < positions.length - 1; i++)
{
    Vector3f pos1 = positions[i];
    Vector3f pos2 = positions[i + 1];
    
    if (i == 0)
        positions[i] = attachPosition;
    
    Vector3f diffVec = pos1 - pos2;
    float dist = distance(pos1, pos2);
    float difference = 0;
     
    if (dist > 0.0f)
        difference = (nodeDistance - dist) / dist;
    
    Vector3f translation = diffVec * (0.5f * difference) * stiffness;
     
    positions[i] += translation;
    positions[i + 1] -= translation;
}

Аналогичным образом разрешаются столкновения: для частиц вводится объемная оболочка (например, сфера), взаимопроникшие тела перемещаются в кратчайшем направлении так, чтобы их оболочки не пересекались. Алгоритмы проверки столкновений обычно дают необходимую для этого информацию — нормаль и глубину проникновения. Преимущество перед импульсным подходом в том, что для этого не нужно составлять систему для скоростей — вы можете напрямую корректировать позиции и, таким образом, легко реализовать любую модель столкновений.

Физика Верле

Попытка написать для Dagon физику веревки на основе интегрирования Верле увенчалась успехом! Добавил даже поддержку столкновений с боксами. В скором времени выложу эту демку в публичный доступ. Не исключено и добавление веревки в качестве встроенного объекта в движок.

Upd 06.03.2024: исходник демки можно скачать тут.

Физика автомобиля: новое видео

Записал видео обновленной демки с физикой автомобиля на движке Dagon: новая модель трения (формула Пасейки), поддержка звуков (используется движок SoLoud), множество мелких улучшений и багфиксов.

Физика автомобиля

Демка с физикой автомобиля, которую я разработал в начале этого года для Patreon Sponsor Folder, теперь доступна на GitHub в репозитории https://github.com/gecko0307/vehicle-demo — с многочисленными улучшениями, которые я сделал за последние месяцы (в частности, появилась поддержка модели трения колес Pacejka Magic Formula). Пруф того, что на основе связки Dagon + Newton вполне можно написать гоночную игру. Как-нибудь, возможно, напишу подробную статью на эту тему.

Newton Dynamics 4

Классный подарок на Новый год: на днях наконец-то вышел Newton 4. Скомпилировал, потестил — очень понравились производительность и точность в сценах с большим количеством тел, а также примеры с физикой автомобиля. К сожалению, пока нет C-интерфейса, поэтому невозможно написать биндинг для D, но, похоже, работа в этом направлении ведется (разработчики планируют использовать SWIG, либо собственный генератор API для произвольных языков).

Dagon 0.12

Вышла новая версия движка Dagon. Главное нововведение в этом релизе — поддержка моделей в формате glTF (gltf+bin). glTF представляет собой текстовое описание трехмерной сцены на основе JSON, хранящее всю информацию, необходимую для ее отрисовки (граф, материалы, текстуры и т.д.). Главной особенностью glTF является лейаут, оптимизированный по скорости загрузки — для передачи в графические API данные из glTF практически не нужно декодировать или конвертировать, поэтому сцены загружаются очень быстро.

Также значительно улучшен загрузчик текстур в формате DDS. Список поддерживаемых пиксельных форматов расширился (в частности, 32- и 16-битными RGBA с плавающей запятой), появилась поддержка кубических карт и mip-уровней. К примеру, теперь стало возможно загружать кубические карты с предрассчитанными зеркальными лепестками (specular lobes) для разных уровней шероховатости. Декодирование неигровых форматов изображений (PNG, JPEG и др.) в Dagon теперь осуществляется при помощи библиотеки stb_image, что в разы ускорило загрузку ресурсов. Как бесплатный бонус — появилась поддержка формата PSD.

Добавлен новый эффект постобработки Depth of Field (расфокусированность) с реалистичным боке и настройками оптики. Исправлены некоторые баги постобработки и отложенных эффектов — в частности, устранен артефакт «черных точек».

На смену встроенному физическому движку dmech идет привязка к Newton Dynamics, реализованная как расширение dagon:newton (подключается к проекту как отдельная зависимость). Newton предоставляет отличный компромисс между производительностью и стабильностью симуляции, поддерживает все стандартные геометрические тела, а также поддержку физики для ландшафтов и произвольных объектов (автоматически генерирует выпуклые оболочки для мешей).

Dagon теперь использует SDL 2.0.14.

Newton

Экспериментирую с интеграцией Newton Dynamics — лучшего на сегодняшний день свободного физического движка с API для C. Специально для этого написал биндинг bindbc-newton.