Создание правильного полигона для Box2D

Движок Box2D используется для симуляции физики в двумерном пространстве. Из фигур он поддерживает сферу (круг), бокс (прямоугольник) и выпуклый полигон (с внутренними углами больше 1-10 градусов, иначе будет падение с ошибкой).

Что же делать, если надо нарисовать этот полигон от руки и скормить Box2D, чтоб он не рухнул?

Можно нарисовать составной полигон в виде множества сфер и боксов. Так, например, сделана фигура танка для демки движка NeoAxis Game Engine (этот движок трехмерный, но проблемы с физикой такие же)

Но у меня такой возможности не было, поэтому я скармливал физике предварительно триангулированный полигон, алгоритм решения очень простой:

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

Третий пункт — это просто оптимизация. Делать его необязательно, реализуется простым перебором всех треугольников.

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

И еще один момент: если угол между сторонами полигона равен 180 градусам, можно удалить точку, общую для обеих сторон.

Обработка столкновений (коллизий) камеры с землей в движке Bullet Game Physics

Долго думал как сделать обработку столкновений камеры с землей. Идея, в принципе, простая: пусти луч вниз от камеры «в пол» и проверяй столкновение. Но тут появляются «подводные камни», которые я жуть как не люблю и всегда в таких случаях теряюсь.

Дело в том, что столкновение надо проверять только с определенным объектом, с землей. А ведь на сцене физических объектов еще штук пять, это и машины с колесами, и бонусы, которые вылетают, выплывают или появляются каким-то иным образом на трассе.

В процессе исследования возможностей физического движка (Bullet Game Physics), применяя опыт работы с Box2D, с ходу нашел один способ: через btCollisionObject::setUserPointer(void*). Это такой метод у каждого физического объекта, который позволяет установить ссылку на свои данные. Можно всем объектам назначить имена с помощью этого метода, а затем по имени проверять «Ground» это или не очень. Или структуру хитрую туда воткнуть, в общем полет фантазии не ограничен.

Но затем я нашел способ лучше — через группы коллизий. Каждому физическому объекту вы назначаете группу и битовую маску. Вот как это выглядит (псевдокод):

object a, b;
a.collision_grp = 00000001b;
a.collision_msk = 00000011b;
b.collision_grp = 00000010b;
b.collision_msk = 00000011b;

Физический движок перед непосредственно столкновением объектов делает проверку на совпадение групп и масок коллизий. Как-то так (C++):

bool needsCollision( const object & a, const object & b )
{
    return (a.collision_grp & b.collision_msk) 
        && (a.collision_msk & b.collision_grp);
}

Исходя из этих знаний, проверка столкновения камеры и земли выглядит тривиально, главное правильно расставить битовые маски и группы.

enum CollisionGroup
{
    CG_GROUND = 0x01;
    CG_CAMERA = 0x02;
};
 
enum CollisionMask
{
    CM_GROUND = CG_CAMERA | CG_GROUND,
    CM_CAMERA = CG_GROUND
};
 
btRigidBody * gGroundBody = NULL;
 
// Создадим землю с правильной группой и маской (псевдокод)
void createGround()
{
    gGroundBody = 
        new GroundBody(collisionGroup=CG_GROUND, collisionMask=CM_GROUND);
}
 
bool checkCameraCollision( const btVector3 & p )
{
    // Начальная точка луча (непосредственно камера)
    btVector3 rayFromWorld = p;
    // Направление луча вниз
    btVector3 rayToWorld = btVector3(p.x, p.y - 0.5, p.z);
 
    // Структура для получения рез-та коллизий луча
    btCollisionWorld::ClosestRayResultCallback 
        callback(rayFromWorld, rayToWorld);
 
    // Луч может сталкиваться только с землей
    callback.m_collisionFilterMask = CM_CAMERA;
    // Луч является камерой
    callback.m_collisionFilterGroup = CG_CAMERA;
 
    // Пульнем луч
    world->rayTest(rayFromWorld, rayToWorld, callback);
 
    // Проверим результат
    if (callback.hasHit())
    {
        // Вот тут живет координата пересечения луча с землей
        // если кому надо: callback.m_hitPointWorld
        return true;
    }
    return false;
}

Вот и все. Код включает в себя только базовые вещи, поэтому он не скомпилируется в таком виде, но основная идея понятна.

Лично я проверяю луч не вниз, а от машины, за которой наблюдаем в сторону наблюдателя. Причем наблюдателя во время проверки я ставлю на полметра ниже. Если этот луч пересекается с землей, но камера сдвигается вдоль этого луча по направлению к машине. Вот код из рабочего проекта:

btVector3 CameraManager::fixCameraPos( const btVector3 & cameraPos, 
    const btVector3 & targetPos ) const
{
    btVector3 rayFromWorld = targetPos;
    btVector3 rayToWorld = cameraPos btVector3(0, 0.5, 0);
 
    btCollisionWorld::ClosestRayResultCallback 
        callback(rayFromWorld, rayToWorld);
 
    callback.m_collisionFilterMask = CM_CAMERA;
    callback.m_collisionFilterGroup = CG_GROUND;
 
    btDynamicsWorld * world = 
        Engine::getSingleton().getWorld()->getBulletDynamicsWorld();
 
    world->rayTest(rayFromWorld, rayToWorld, callback);
 
    if (callback.hasHit())
    {
        return btVector3(0, 0.5, 0) + callback.m_hitPointWorld;
    }
 
    return cameraPos;
}

UPD: написал продолжение темы коллизий (ссылка)

P.S. И кстати, огромное спасибо автору русского перевода документации к Box2D. К сожалению, не знаю его имени. В свое время эта документация очень помогла при создании игры «Пинкод. Получи патент первым!».

Блог Евгения Жирнова