Бесплатная реклама

Автор: Павел Загребельный «Kopavel»

Опубликовано: 03.02.2009

Изменено: 03.02.2009

Постоянная ссылка

Комментарии [0]

Анимирование физики внедорожных автомобилей (Spin Tires)


Статья описывает технологию анимации грузовика с пробуксовкой колес на базе физического движка Havok.


Содержание

1. Вступление
2. Havok
3. Spin Tires
   3.1. Анимация грузовика
   3.2. Анимация динамического ландшафта
4. Демонстрационная программа
5. Список литературы

Spin Tires

1. Вступление

Вашему вниманию представляется статья на тему "Анимирование физики внедорожных автомобилей". Данная версия написана специально для конкурса статей по программированию #3 на сайте UralDev.ru.

Изначальное происхождение этой статьи - конкурс Havok Physics Innovation Contest (спонсируемый компанией Intel). Эта статья - переведенная и дополненная версия сопровождающего документа к конкурсной программе Spin Tires. Идея, лежащая в основе программы - анимация грузовика (vehicle) с пробуксовкой колёс.

2. Havok

Havok - мощная система анимации, сделанная как game-specific solutions that work [Havok Physics SDK, 1], и ориентирована на игры, главная цель - работоспособность и производительность. Havok состоит из нескольких модулей, часть из них вы можете вы можете использовать бесплатно. Havok Physics и SDK к нему (всё необходимое для написания собственных программ) доступен на сайте Havok - однако бесплатно его можно использовать лишь в некоммерческих проектах (см. лицензионное соглашение).

Основное назначение Havok Physics - анимация твёрдых тел (rigid bodies). Использовать Havok очень просто и удобно - например, вот код создания коробки размером 1x1x1 и массой 1 кг (код на C++; все типы, структуры, и классы Havok Physics имеют префикс hkp в своём названии):

   hkpRigidBodyCinfo bodyInfo;
   bodyInfo.m_shape = new hkpBoxShape(hkVector4(.5f,.5f,.5f));
   hkpMassProperties massProperties; // параметры массы и инерции
   hkpInertiaTensorComputer::computeShapeVolumeMassProperties(bodyInfo.m_shape, 1.0f/*Масса 1 кг*/ , massProperties);
   // hkpInertiaTensorComputer выполняет расчёт параметров инерции тела (для коробки это тривиальная задача). 
   // Расчитаные таким образом параметры можно впоследствии использовать для создания всех тел с такой же геометрией.

   bodyInfo.m_mass          = massProperties.m_mass;
   bodyInfo.m_centerOfMass  = massProperties.m_centerOfMass;
   bodyInfo.m_inertiaTensor = massProperties.m_inertiaTensor;
   bodyInfo.m_motionType    = hkpMotion::MOTION_SPHERE_INERTIA;

   hkpRigidBody* body = new hkpRigidBody(bodyInfo); 
   bodyInfo.m_shape->removeReference();
   // вместо вызова оператора delete, следует сократить счетчик ссылок.
   // это не удалит данный объект, т.к. уже созданное тело хранит собственную ссылку.
   {указатель на объект физической сцены}->addEntity(body);

Далее - всю работу по освобождению памяти, анимированию, вызову обработчиков событий Havok будет выполнять автоматически. Для сравнения, вот код создания такой же коробки в еще одном известном комплексе анимации физики PhysX (все типы, структуры, и классы PhysX имеют префикс Nx в своём названии):

   NxBoxShapeDesc boxDesc;                       // геометрия
   boxDesc.dimensions = NxVec3(.5f, .5f, .5f);
   NxBodyDesc bodyDesc;                          // описание свойств тела
   bodyDesc.mass = 1.f;                          // масса 1 кг
   NxActorDesc actorDesc;                        // участники анимации в PhysX называются Actor
   actorDesc.shapes.pushBack(&boxDesc);
   actorDesc.body = &bodyDesc;
   {указатель на объект физической сцены}->createActor(actorDesc);
   // в PhysX вместо забав с оператором new и методом removeReference(), создание многих 
   // сущностей выполняется передачей структуры-описания

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

В этом есть смысл, т.к. стандартные операторы new и delete работают очень не экономно в определённых ситуациях - когда не требуется многопоточность и выделение блоков переменной длины - а Havok как раз содержит специализированные механизмы управления памятью.

Стоит отметить, что Havok очень интенсивно использует указатели со счетчиком (reference counter) - т.е. вместо вызова оператора delete для динамически созданного объекта, следует вызвать метод removeReference(), а ядро Havok само позаботиться об освобождении памяти от объектов, счетчик ссылок которых равен нулю. Таким же образом реализованы интерфейсы в DirectX.

Havok можно рассматривать как дополнение к STL, и вот некоторые классы Havok которые пригодились мне:

  • hkArray - высокопроизводительный с точки зрения выделения памяти массив, не является замещением std::vector (не вызывает конструкторы/деструкторы для элементов вектора). Подробности - см. в [Havok Physics SDK, 1].
  • hkStopWatch - удобный счетчик времени.
  • hkAabb - класс ограничивающего куба, хранит минимальное и максимальное значение координат.

Наряду с управлением памятью, предоставляется также реализация специализированных контейнеров и вспомогательных классов. Так же предоставляются функции работы с камерой и устройствами ввода.

Неотъемлемой частью является математическая библиотека. Работать с математической библиотекой Havok неудобно при написании маловажного по времени выполнения кода, т.к. она ориентирована на выполнение операций с максимальной скоростью, используя SIMD-операции.

Например, все вектора - 4-х компонентные, и такой аналог DirectX:

   D3DXVECTOR3 a(0.f, 1.f, 2.f);
   a.x += 1.f;
   D3DXVECTOR3 b = a+a;

будет выглядеть вот так:

   hkVector4 a(0.f, 1.f, 2.f);
   a.add4( hkVector4(0.f, 1.f, 0.f) );
   // в SSE, стоимость четырёх вещественных сложений почти равна стоимости единственного сложения
   hkVector4 summ = a;
   summ.add4(a); // перегрузка операторов добавляет накладные расходы на создание временных объектов
   hkVector4 b = summ;

Важным элементом Havok является собственный визуальный отладчик - отдельная программа, которая отображает вашу физическую сцену. Для подключения отладчика нужно вызвать несколько стандартных функций Havok, создав сервер, а затем запустить собственно отладчик (HavokVisualDebugger). Вот как выглядела отладка Spin Tires (рис. 1):

Анимирование физики внедорожных автомобилей
Рис. 1. Отладка физики

Если вы знакомы с концепцией ООП, то изучить Havok будет очень легко, просто почитайте документацию. Havok специально сделан так, чтобы его было легко изучать и использовать.

3. Spin Tires

3.1. Анимация грузовика

Специальный модуль Havok - Havok Vehicle, содержит всё, что может потребоваться при анимации автомобиля. Он реализует Raycast vehicle. В общем, автомобиль в Havok - всего лишь совокупность тел.

В Havok Physics SDK, приводится общая, полезная информация о параметрах автомобиля в игре, в разделе Havok Physics/Vehicle Dynamics. Далее будут упомянуты следующие параметры (рис. 2):

  • Chassis shape, геометрия корпуса
  • Suspension length, длина подвески, для каждого колеса
  • Suspension strength, упругость подвески, для каждого колеса
  • Wheel hardpoint, точка крепления подвески колеса, для каждого колеса
  • Анимирование физики внедорожных автомобилей
    Рис. 2. Рисунок из Havok Physics SDK Manual

    В отличие от Constraint-driven vehicle, колёса Raycast vehicle не являются отдельными телами - их положение определяется как пересечение луча из hard point в направлении "вниз" автомобиля. См. [Rotations, Handedness, and all that, 2] чтобы понять как по положению тела можно определить направление "вниз", "вверх", "впёрёд", "вправо" и т.д. Взаимодействие колёс осуществляется за счёт Phantom-ов. Phantom - "фантом", объект, который не влияет на другие объекты, а лишь вызывает соответствующие обработчики событий (callback) - например, при столкновении с другим объектом.

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

    Если вы используете Raycast vehicle, вам нужно всего лишь задать все параметры автомобиля (правда, их так много, что это превращается в непростую задачу) - такие как упомянутые выше длину, упругость подвески, геометрию корпуса, и, разумеется, число колёс, число осей, ведущие колёса, поворачивающиеся колёса, число передач, передаточные отношения, и многие другие, вплоть до минимального усилия создания следа от колеса на дороге (tiremarks).

    Однако колёса Raycast vehicle, при взаимодействии с большим количеством тел, ведут себя нестабильно, к тому же мне не требовалась точная модель автомобиля, стабильная даже на большой скорости, поэтому я решил использовать Constraint-driven vehicle.

    Constraint-driven vehicle - это совокупность тел, объединённых соединениями (как, например, дверное соединение, только с большим количеством тел и более сложными типами соединений). Для присоединения колёс (представляемых обыкновенными цилиндрами) используется wheel-constraint. Wheel-constraint - "соединение типа "колесо" - смотрите пример Physics\Api\Constraints\Wheel demo из Havok SDK. У такого соединения можно задавать упругость, длину подвески, и угол поворота колеса. В целом, соединение типа "колесо", как и другие соединения, всего лишь накладывает ограничения на взаимное положению двух тел.

    Настройка сonstraint-driven vehicle выполняется вручную, из общеинженерных соображений - всё что есть у вас а арсенале, это возможность применять к телам импульсы. Например, когда пользователь нажал на газ, к ведущим колёсам применяется угловой имульс - его длина и направление нужно определить по положению колеса, выбранной передаче, текущей скрости, положению педали газа...

    См. исходный документ для технических деталей о реализации коробки передач, торможения, ускорения.

    3.2. Анимация динамического ландшафта

    Требования к ландшафту в контексте Spin Tires:

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

    Ландшафт в Havok представляется таким же телом, как и любая заурядная коробка, но при его создании член структуры-описания устанавливается в:
    m_motionType = hkpMotion::MOTION_FIXED;
    такое тело не анимируется (хотя его положение можно задать вручную). Как можно догадаться, такие тела не соударяются между собой.

    Классы, которыми можно представить ландшафт в Havok:

    Heightfield - карта высот - регулярная сетка, с заданной высотой в каждой точке.

    Плюсы и минусы такого геометрического объекта:
     + минимальное потребление памяти
     + максимально возможное быстродействие
     - невозможность представления рельефа различной частоты
     - шаг сетки должен быть больше радиуса ограничивающей коробки объекта, с которым проверяется 
       столкновение - это связано со спецификой определения пересечений с heightfield в Havok.
    

    Последний минус делает применение heightfield невозможным. Все подробности смотрите в [Havok Physics SDK, 1].

    MOPP - memory optimized partial polytope - список треугольников, завёрнутый в дерево ограничивающих объемов (например, BSP). Список треугольников представляется двумя массивами: массивом вершин (положений вершин) и массивом индексов (как в DirectX). Optimized говорит о том, что это дерево оптимизировано на минимальное потребление памяти.

     + представляется произвольной геометрией - её можно сделать заранее или создать процедурно 
       (или создать процедурно заранее)
     - медленное построение ограничивающего дерева (инициализация). Пожалуй, единственный минус, 
       который и сделал разработку динамического ландшафта нетривиальной задачей.
    

    Как сделан ландшафт в Spin Tires (основные проектные решения):

    Анимирование физики внедорожных автомобилей
    Рис. 3. Ландшафт в Spin Tires

    1. Ландшафт создаётся вручную, в трёхмерном редакторе. Это позволяет проложить по нему дороги - настроив нужным образом текстурные координаты вершин, и раскрасив его несколькими текстурами, используя одну общую маску (по маске определяется, какая текстура должна быть применена к конкретному фрагменту ландшафта). Ландшафт имеет относительно небольшое количество вершин (около 16000) - то есть его треугольники сравнительно велики.

    2. В случае большого ландшафта потребуется значительное количество памяти (как video memory от DirectX, так и system memoty от Havok) - что может привести к значительному замедлению, либо вообще неработоспособности. Поэтому ландшафт разбивается (1) на ячейки фиксированного размера в горизонтальной плоскости. В каждой ячейке, создаётся свой MOPP, вершинный и индексный буферы DirectX. Также, к каждой ячейке приписывается дополнительная информация (список деревьев, расположенных на ней). Далее, по ограничивающей коробке грузовика, определяется список активных ячеек - это выполняется очень просто и быстро, учитывая, что все ячейки - фиксированного размера, и построены они в горизонтальной плоскости (получается двумерная матрица). Только активные ячейки присутствуют в симуляции Havok. Аналагочным обазом выполняется поиск выдимых ячеек (по фрустуму камеры) - невидимые ячейки не рисуются, и вообще никак не влияют на Spin Tires.

    3. Чтобы сделать выемку в заданной точке, соответствующий (2) треугольник разбивается на меньшие треугольники, а затем одна из созданных вершин опускается вниз. Треугольник исходного ландшафта исключается из сцены (3), а новые, меньшие треугольники - добавляются, в виде отдельного MOPP (небольшой независимый участок ландшафта). Так как, обычно, такой MOPP имеет небольшой размер, его построение занимает немного времени (4). Если же исходный треугольник уже разбит, необходимо изменить положение одной из вершин треугольнков, представляющих его разбиение (5).

    4. Чтобы динамически разбить треугольник на меньшие треугольники, можно использовать функцию DirectX D3DXTessellateXXXPatch - однако такое разбиение не исправляет "плохие" (с очень острыми углами) треугольники, к тому же после разбиения необходимо "склеить" (т.е. использовать уже созданные вершины соседних треугольников) вершины на рёбрах - а для этого нужно знать, где найти вершину из n-го угла или из k-го ребра. Поэтому я написал собственную функцию разбиения треугольников.

    5. После выдавливания ландшафта, в соответствующем месте создаётся небольшое тело (с геометрией коробки или сферы) - которое, в отличие от ландшафта, динамическое, и при взаимодействии с колесами создаёт эффект пробуксовки.

    Примечания:

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

    2. На самом деле каждому выдавливанию ландшафта соответствуют несколько треугольников - а именно все инцидентные ближайшей вершине к точке выдавливания. Эти подробности не относятся к анимации физики.

    3. Чтобы исключить треугольник из сцены, необходимо:
      а) исключить его из физической сцены - для этого можно использовать один из классов по управлению MOPP из Havok, который удаляет листья дерева, либо изменить параметры материала конкретного треугольника. Материал в данном случае определяет фильтр столкновений, коэффициент трения, и некоторые другие физические параметры. Фильтр столкновений задаёт, какие тела не будут сталкиваться с заданным треугольником. Именно этот способ используется в Spin Tires: хотя он и более медленный (т.к. листья всё равно сохранаются в MOPP, даже если все треугольники реально исключены), но другой метод давал неверные результаты - возможно, из-за неправильной эксплуатации, либо из-за ошибок в Havok(6).
      б) исключить треугольник из DirectX - для этого необходимо изменить содержимое index buffer-а. Для этого, например, можно скопировать три индекса из конца буфера на место данного треугольника, и сократить число примитивов на 1.

    4. Чтобы избавиться от пиков нагрузки на CPU в момент создания MOPP - используется отдельный поток (thread), который занимается исключительно построением MOPP. На то время, пока MOPP создаётся, в сцене используется обыкновенный список треугольников. А когда создание MOPP завершено, список заменяется на дерево MOPP.

    5. Если требуется изменить положение одной из угловый вершин (или вершин на ребре) - нужно обновить и все соседние разбитые треугольники. Для этого все MOPP разбитых треугольников используют общий массив вершин, но каждый MOPP использует собственный массив индексов. Для того чтобы разбить очередной треугольник, необходимо учитывать разбиение всех его соседей, и вместо того чтобы добавлять копию уже существующей вершины, изменить содержимое массива индексов. Итого, после измененеия какой-либо вершины, нужно обновить несколько MOPP (те, которые используют эту вершину).

    6. У меня не было времени провести достаточно подробный анализ, поэтому просто обратите на это внимание, если будете модфифицировать MOPP классом hkpRemoveTerminalsMoppModifier. Примеры \Physics\Api\Collide\Shapes\Landscape\DestructibleMopp и \Physics\Api\Collide\Shapes\Landscape\BattlefieldMopp из SDK показывают удаление листьев из дерева MOPP - однако не показывают восстановление листьев. Восстановление необходимо для участков ландшафта, которые грузовик уже проехал - такой ландшафт должен восстанановить своё первоначальное состояние. Восстановление листьев производится всё тем же классом hkpRemoveTerminalsMoppModifier - более того, тем же экземпляром этого класса. Подробности см. в [Havok Physics SDK, 1]. Тем не менее, после, казалось бы, правильной процедуры отмены удаления листьев в Spin Tires, часть ландшафта по-прежнему оставалась исключённой. Не меняя логики алгоритма, я реализовал выше упомянутое изменение материалов треугольников - и проблема исчезла.

    Spin Tires
    Рис. 4. Spin Tires

    4. Демонстрационная программа

    Я не использовал готовых решений графического движка, поскольку у меня уже имелись некоторые наработки, к тому же чрезмерное использование сторонних библиотек приводит к разрастанию проекта. Графическое ядро базируется на [Valve Source Shading, 4] - статье, очень понятно описывающей возможную реализацию полу-динамического освещения. Эффекты пост-обработки сделаны по аналогии с примерами из DirectX SDK - однако, на высоких разрешениях их проиводительность неприемлемо низкая.

    К несчастью, используется DXUT - очень плохо написанный, содержащий множество ошибок модуль. В частности, к DXUT нельзя подключить PerfHUD (анализатор загруженности видео карты от NVidia), он неправильно выполняет Device Reset, в некоторых конфигурациях занимает очень много времени при загрузке. В DXUT неоправданно широко используются указатели, очень громоздко и малофункционально реализована камера. GUI, предоставляемый DXUT, на мой взгляд, тоже выглядит очень неэстетично.

    Для созднаия ландшафта, расстановки растительности, прокладки дорог, я использовал 3DSMax. Для задания позиций деревьев и прочей растительности, я расположил всё нужным образом в горизонтальной плоскости над ландшафтом, и написал plugin на MAXScript, который, найдя пересечение луча в направлении "вниз" дерева с ландшафтом, ставил дерево в нужную позицию. Далее, другой plugin экспортировал все данные о деревьях в XML. Пример написания такого plugin поставляется с 3DSMax. Spin Tires, в свою очередь, считав данные о положении деревьев из XML, геометрию самого дерева из X (формат геометрии DirectX), и найдя список видимых деревьев для данного положения камеры, рисовал все деревья используя Hardware Instancing. Пример Hardware Instancing поставляется с DirectX SDK.

    Оказался очень полезен сайт http://www.cgtextures.com, на котором можно скачать отличные текстуры на любой вкус.

    Ссылки для скачивания:

    5. Список литературы

    1. Havok Physics SDK manual, прилагается к SDK, доступен на сайте Havok.
    2. Rotations, Handedness, and all that. Статья из Havok Physics SDK manual, расположенная в Havok Physics /Physics Articles/.
    3. Исходный документ сопровождающий Spin Tires. Это прародитель данной статьи, но на английском языке.
    4. Valve Source Shading. 22 MARCH 2004 / Gary McTaggart, очень полезная информация о графике Half-Life 2.

     


     

    Статья участвовала в конкурсе статей по программированию #3 (2008).