Ogre + Bullet: проверка столкновения камеры с землей

Хочу предложить более совершенный алгоритм проверки коллизии камеры с землей, чем опубликованный ранее (ссылка). Теперь все как «у взрослых» (так сделано, например, в Perfect World). Задача: камера не должна проходить сквозь стены.

Оговорим обозначения:

  • cameraPos — позиция камеры
  • targetPos — позиция цели (за которой следит камера)
  • leftTop, rightTop, leftBottom, rightBottom — позиции крайних точек экрана в мировых координатах
  • hitPoint — точка коллизии луча с землей

В общих чертах, алгоритм такой:

  • вычисляем координаты leftTop, rightTop, rightBottom, leftBottom
  • для каждой крайней точки p создаем луч, начало которого в targetPos, а окончание в p
  • для каждого луча проверяем столкновение с землей, назовем точку столкновения hitPoint
  • вычисляем минимальную длину проекции вектора (hitPointtargetPos) на вектор (cameraPostargetPos)
  • новая позиция камеры — это targetPos смещенная в направлении cameraPos на минимальную длину проекции

А теперь выразим это в коде (код писался прямо в блокноте, поэтому не обессудьте)

bool checkCameraCollides( const Ogre::Vector3 & cameraPos, 
    const Ogre::Vector3 & targetPos, Ogre::Vector3 & newCameraPos )
{
    // Сохраняем позицию и направление камеры
    const Ogre::Vector3 lastCameraPos = camera->getPosition();
    const Ogre::Vector3 lastCameraDir = camera->getDirection();
 
    // Установим новую позицию и направление камеры для корректной 
    // работы метода Ogre::Camera::getCameraToViewportRay()
    camera->setPosition(cameraPos);
    camera->setDirection(targetPos - cameraPos);
 
    // Вычисляем крайние точки
    std::vector<Ogre::Vector3> points;
    {
        Ogre::Ray ray;
        // Верхний левый угол
        camera->getCameraToViewportRay(0, 0, &ray);
        points.push_back(ray.getOrigin());
        // Нижний левый угол
        camera->getCameraToViewportRay(0, 1, &ray);
        points.push_back(ray.getOrigin());
        // Верхний правый угол
        camera->getCameraToViewportRay(1, 0, &ray);
        points.push_back(ray.getOrigin());
        // Нижний правый угол
        camera->getCameraToViewportRay(1, 1, &ray);
        points.push_back(ray.getOrigin());
    }
 
    // Вернем камере исходные позицию и направление
    camera->setPosition(lastCameraPos);
    camera->setDirection(lastCameraDir);
 
    // Минимальная длина проекции
    float minProj = 0.0f;
    // Есть столкновение
    bool collideDetected = false;
    // Луч от цели до камеры
    const Ogre::Vector3 targetToCameraDir = 
        (cameraPos - targetPos).normalisedCopy();
 
    for (size_t = 0; i < points.size(); ++i)
    {
        // Начало луча в позиции цели
        const btVector3 from = OgreBtConverter::to(targetPos);
        // Окончание луча крайней точке
        const btVector3 to = OgreBtConverter::to(points[i]);
 
        // Настроим стуктуру для обнаружение столкновения
        btDynamicsWorld::ClosestRayResultCallback rayCallback(from, to);
        // Сталкиваться только с землей
        rayCallback.m_collisionFilterGroup = CG_GROUND;
        rayCallback.m_collisionFilterMask = CM_GROUND;
        // Пускаем луч
        dynamicsWorld->rayTest(from, to, rayCallback);
 
        // Есть контакт
        if (rayCallback.hasHit())
        {
            // Точка столкновения
            const Ogre::Vector3 hitPoint = 
                BtOgreConverter::to(rayCallback.m_hitPointWorld);
 
            // Длина проекции на луч от targetPos до cameraPos
            float proj = (hitPoint - targetPos).dotProduct(targetToCameraDir);
 
            // Сохраним минимальную длину проекции
            if (!collideDetected || minProj > proj)
            {
                minProj = proj;
                collideDetected = true;
            }
        }
    }
 
    // Если столкновение произошло
    if (collideDetected)
    {
        // Новая позиция камеры - это targetPos смещенная в сторону cameraPos на minProj
        newCameraPos = targetPos + targetToCameraDir * minProj;
        return true;
    }
 
    return false;
}

И для «самых маленьких» пример использования:

void updateCameraPosition()
{
    Ogre::Vector3 cameraPos, targetPos, newCameraPos;
    // Вычисляем позицию камеры и цели (решение о том, как это делается, 
    // остается за вами, у меня позиция и цель камеры зависит от режима камеры)
    calculateCameraPosition(cameraPos, targetPos);
    // Проверим столкновение
    if (checkCameraCollides(cameraPos, targetPos, newCameraPos))
    {
        camera->setPosition(newCameraPos);
    }
    else
    {
        camera->setPosition(cameraPos);
    }
    camera->setDirection(targetPos - camera->getPosition());
}
Обработка столкновений (коллизий) камеры с землей в движке 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. К сожалению, не знаю его имени. В свое время эта документация очень помогла при создании игры «Пинкод. Получи патент первым!».

Bullet Game Physics изменение позиции и ориентации физического тела

Не буду долго размусоливать. Приведу сразу код:

void setTransform( btRigidBody * body, const btVector3 & position, const btQuaternion & rotation )
{
    // Матрица транформации
    btTransform transform;
    transform.setIdentity();
 
    // Заполним матрицу
    transform.setOrigin(position);
    transform.setRotation(rotation);
 
    // Применим матрицу к телу
    body->setWorldTransform(transform);
 
    // Важный этап: обнулим линейную и угловую скорости
    body->setLinearVelocity(btVector3(0, 0, 0));
    body->setAngularVelocity(btVector3(0, 0, 0));
}

Для чего требуется обнуление скоростей? Догадайтесь сами.

Используем OgreBullet в своем проекте с помощью CMake

В нашей системе сборки все сторонние библиотеки компилируются вместе с проектом, поэтому стандартное FIND_PACKAGE нам не подходит. Значит, CMakeLists.txt из сборки OgreBullet тоже идет лесом. Поэтому приходится извращаться на свой лад.

CMakeLists.txt

# Название проекта
project( OgreBullet )
 
# Разделяем исходники для двух precompiled headers
file( GLOB_RECURSE OGRE_BULLET_COLLISIONS Collisions/*.cpp )
file( GLOB_RECURSE OGRE_BULLET_DYNAMICS Dynamics/*.cpp )
 
# Папки для поиска #include
include_directories(
    ${GLOBAL_INCLUDE_DIR}/Ogre
    ${GLOBAL_INCLUDE_DIR}/OgreBullet/Collisions
    ${GLOBAL_INCLUDE_DIR}/OgreBullet/Dynamics
    ${GLOBAL_LIB_DIR}/Bullet
    ${GLOBAL_LIB_DIR}/Bullet/BulletConvexDecomposition
    ${GLOBAL_LIB_DIR}/Bullet )
 
# Определяем макрос для использования precompiled headers
macro(use_precompiled_header SRC_LIST HDR_FILE SRC_FILE)
    # Выделяем имя заголовка без пути
    get_filename_component(PCH_HEADER ${HDR_FILE} NAME)
    # Выделям имя заголовка без расширения и пути
    get_filename_component(PCH_BINARY ${HDR_FILE} NAME_WE)
    # Полный путь к созданному pch
    set(PCH_BINARY "${CMAKE_CURRENT_BINARY_DIR}/${PCH_BINARY}.pch")
 
    if (MSVC)
        # Для precompiled.cpp создаем pch, указывая файл (OBJECT_OUTPUTS), который создается в результате комманды 
        set_source_files_properties( ${SRC_FILE} PROPERTIES
            COMPILE_FLAGS "/Yc${PCH_HEADER} /Fp${PCH_BINARY}"
            OBJECT_OUTPUTS "${PCH_BINARY}" )
        # Для каждого исходника в списке, задаем зависимость от precompiled.pch и указываем использовать его параметром "/Yu"
        foreach( SOURCE ${SRC_LIST} )
            set_source_files_properties( ${SOURCE} PROPERTIES 
                COMPILE_FLAGS "/Yu${PCH_HEADER}" 
                OBJECT_DEPENDS "${PCH_BINARY}" )
        endforeach( SOURCE ${SRC_LIST} )
 
    endif(MSVC)
 
endmacro(use_precompiled_header)
 
# Создаем precompiled header для Collisions
use_precompiled_header( OGRE_BULLET_COLLISIONS
    "${GLOBAL_INCLUDE_DIR}/OgreBullet/Collisions/OgreBulletCollisions.h" Collisions/OgreBulletCollisionsPrecompiled.cpp )
 
# Создаем precompiled header для Dynamics
use_precompiled_header( OGRE_BULLET_DYNAMICS
    "${GLOBAL_INCLUDE_DIR}/OgreBullet/Dynamics/OgreBulletDynamics.h" Dynamics/OgreBulletDynamicsPrecompiled.cpp )
 
# Объединим два списка файлов для удобства
set( OGRE_BULLET_SRC
    "${OGRE_BULLET_COLLISIONS}"
    "${OGRE_BULLET_DYNAMICS}" )
 
# Добавляем define для использования precompiled headers библиотекой OgreBullet
add_definitions( -D_PRECOMP )
# Profit!
add_library( ogre_bullet STATIC ${OGRE_BULLET_SRC} )

UPDATE: Окончательный вариант precompiled headers смотрим здесь.

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