Array ( )
Вход:




Главная | OpenGL | GLSL | AI | Сеть | Примеры | Библиотека

OpenGL: Коллизия камеры в мире






Основа этого урока взята из урока "Time-Based Movement" и добавлен функционал урока "коллизия сферы
и полигона". Цель этого урока - показать способ рассчета коллизий камеры и мира (стен).
Если мы окружим нашу камеру воображаемой сферой с радиусом, допустим, "1", то мы сможем
проверять пересечения этой сферы с полигонами, находящимися рядом с ней. Мы не будем делать
никаких предрассчетов, чтобы отбросить дальние треугольники и рассчитывать коллизии только
с близлежащими обьектами, так как у нас будет очень мало треугольников. Но в реальной игре
это было бы необходимо. Вы можете выбрать схемы BSP tree, или octree, в зависимости от
архитектуры вашей сцены/уровня/мира. Если вы не прошли предыдущий урок "коллизия сферы
и полигона", я настоятельно рекоммендую сделать это. Я не буду обьяснять подробности рассчетов,
так как сделал это в прошлом уроке.

Данные нашего мира - координаты составляющих его обьектов - хранятся в файле [a=http://masandilov.ru/coding/sources/World.raw:a]World.raw.
В нём хранятся только данные вершин, по одной на линию. Сам мир я создал в 3D Studio Max,
и экспортировал его в этот простой формат. Так что вам не придется разбираться ещё и в коде,
загружающем нормальные модели. Этой сценки хватит, чтобы было вокруг чего пройтись. У меня
есть текстуры для пола и стен, но я решил не перегружать код. Ещё мы проделаем простенький
трюк, чтобы придать нашей сцене глубину, не окрашивая вершины. Если вы включите в сцене туман
с фильтром GL_EXP2, это придаст сцене ещё лучший вид (если конечно ваша видеокарта поддерживает
туман). Мы же придадим сцене глубину, используя только освещение.



В файл 3dmath.h добавим прототип новой функции:
CVector3 GetCollisionOffset(CVector3 &vNormal, float radius, float distance);


Кроме этого, уберите из этого файла обьявление класса CVector3, так как он есть и в классе камеры.

Файл 3dmath.cpp:
///////////////////////////////// GET COLLISION OFFSET \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: возвращает смещение сферы за плоскость полигона
/////
///////////////////////////////// GET COLLISION OFFSET \\\\\\\\\\\\\\\\*

CVector3 GetCollisionOffset(CVector3 &vNormal, float radius, float distance)
{
    CVector3 vOffset = CVector3(0, 0, 0);

    // Найдя место пересечения, нужно убедится, что сфера не ушла в стену. В нашей
    // программе позиция камеры будет уходить в стену, но мы будет проверять коллизии
    // раньше отрисовки, и смещения не будет заметно визуально. Вопрос в том, как
    // найти направление, в котором нужно "выталкивать" сферу? В нашем обнаружении
    // коллизий мы рассчитывали коллизию по обе стороны полигона. Обычно вам нужно
    // будет заботится только о стороне с вектором нормали и с положительной
    // дистанцией. Если же не нужна обрезка задних сторон и нужно проверять
    // пересечения с обоими сторонами полигона, я покажу, как это делается.
    //
    // Давайте я обьясню происходящие здесь рассчеты. Для начала, у нас есть нормаль
    // плоскости, радиус сферы и дистанция от центра сферы до плоскости. В случае если
    // сфера пересекается с передней стороной полигона, мы просто вычитаем дистанцию
    // из радиуса, а затем умножаем результат на нормаль плоскости. Это спроецирует
    // эту оставшуюся дистанцию вдоль вектора нормали. Например, скажем у нас есть значения:
    //
    //  vNormal = (1, 0, 0)     radius = 5      distance = 3
    //
    // Если мы вычтем дистанцию из радиуса, то получим (5-3=2)
    // Число 2 говорит, что края нашей сферы находятся от плоскости на расстоянии "2".
    // Итак, нам нужно передвинуть сферу назад на 2 единицы. Как же мы узнаем, в каком
    // именно направлении нужно её двигать? Это просто. У нас есть вектор нормали,
    // который сообщает нам направление плоскости.
    // Если мы умножим нормаль на оставшуюся дистанцию, то получим: (2, 0, 0)
    // Этот новый вектор смещения и сообщает нам, в каком направлении и на сколько единиц
    // "выдавливать" сферу. Затем мы вычитаем это смещение из позиции сферы, что даст нам
    // новые координаты, при которых края сферы будут находится точно на плоскости.
    // Вот и всё!
    // Если же сфера пересекается с другой стороны полигона, мы делаем противоположные
    // рассчеты, как показано ниже:

    // Если дистанция больше ноля, мы спереди полигона
    if(distance > 0)
    {
        // Найдем расстояние, на которое сфера углубилась в плоскость, затем
        // найдем вектор направления "выталкивания" сферы
        float distanceOver = radius - distance;
        vOffset = vNormal * distanceOver;
    }
    else // Если же сфера с задней стороны полигона
    {
        // Делаем то же самое, но меняем знаки distance и distanceOver
        float distanceOver = radius + distance;
        vOffset = vNormal * -distanceOver;
    }

    // Есть одна проблема при проверке пересечений с задней стороной полигона:
    // если вы двигаетесь очень быстро и центр вашей сферы прошел сквозь полигон,
    // программа не выпустит вас обратно, просчитывая пересечения с обратной стороны
    // полигона. Лучше убрать блок if / else, но в этом уроке я хотел все-таки показать
    // этот способ.

    // Вернём смещение для "выдавливания" сферы.
    return vOffset;
}


В файле Camera.h добавим хидер:
#include "3dmath.h"

И две новых функции:
// Установка радиуса сферы вокруг камеры
void SetCameraRadius(float radius) {    m_radius = radius;  };

// Принимает список вершин+их количество для определения пересечения с ними
void CheckCameraCollision(CVector3 *pVertices, int numOfVerts);

// И добавим параметр "радиус" классу камеры:
float m_radius;


Немного подредактируем Camera.cpp:

// В функции MoveCamera() дадим игроку возможность "летать" - перемещатся
// по оси Y:
void CCamera::MoveCamera(float speed)
{
    CVector3 vVector = m_vView - m_vPosition;
    vVector = Normalize(vVector);

    m_vPosition.x += vVector.x * speed;
    m_vPosition.y += vVector.y * speed;
    m_vPosition.z += vVector.z * speed;
    m_vView.x += vVector.x * speed;
    m_vView.y += vVector.y * speed;
    m_vView.z += vVector.z * speed;
}



/////////////////////////////////////////////////////////////////////////////////////

///////////////////////////////// CHECK CAMERA COLLISION \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: проверяет полигоны из переданного списка и смещает камеру,
/////   если есть пересечение
/////
///////////////////////////////// CHECK CAMERA COLLISION \\\\\\\\\\\\\\\\*

void CCamera::CheckCameraCollision(CVector3 *pVertices, int numOfVerts)
{
    // Эта функция очень похожа на на SpherePolygonCollision(). Нам нужно немного
    // изменить её, чтобы проверять треугольники из переданного списка. pVertices - это
    // данные мира. Если бы у нас было разделение мира на части, мы передавали бы
    // только вершины, к которым камера расположена достаточно близко. В этой функции
    // мы проходим по всем треугольникам из списка и проверяяем, пересекается ли
    // с ними сфера. Если да, мы на этом не останавливаемся. У нас может быть
    // несколько пересечений, и важно заметить их все. Найдя пересечения, мы вычисляем
    // смещение камеры и "выдавливаем" сферу за полигон, на расстояние найденного
    // смещения.

    // Цикл по всем треугольникам
    for(int i = 0; i < numOfVerts; i += 3)
    {
        // Сохраним текущий треугольник в переменную
        CVector3 vTriangle[3] = { pVertices[i], pVertices[i+1], pVertices[i+2] };

        // 1) ШАГ ОДИН - находим положение сферы

        // Находим нормаль текущего треугольника
        CVector3 vNormal = Normal(vTriangle);

        // Переменная - расстояние сферы от плоскости треугольника
        float distance = 0.0f;

        // Находим положение сферы - сзади, спереди, или пересекается с плоскостью
        int classification = ClassifySphere(m_vPosition, vNormal, vTriangle[0], m_radius, distance);

        // Если сфера пересекает плоскость, продолжаем проверку дальше
        if(classification == INTERSECTS)
        {
            // 2) ШАГ ДВА - находим псевдо точку пересечения

            // Теперь проецируем центр сферы на плоскость треугольника
            CVector3 vOffset = vNormal * distance;

            // Получив смещение от плоскости, просто вычитаем его из центра сферы.
            // "vIntersection" - точка пересечения с плоскостью (или треугольником)
            CVector3 vIntersection = m_vPosition - vOffset;

            // 3) ШАГ ТРИ - Проверяем, находится ли точка пересечения внутри треугольника

            // Сначала проверяем, находится ли точка внутри треугольника, если нет -
            // переходоим к шагу 4, где проверяем на пересечение рёбра треугольника.


            // Мы изменяем одну вещь в параметрах EdgeSphereCollision. Поскольку сфера
            // вокруг нашей камеры большая, нам придется проходить лишнее расстояние,
            // чтобы пройти за угол. Это потому, что угла полигона могут пересечься со
            // сторонами нашей сферы. Так что это будет выглядеть как будто мы можем пройти
            // вперед, угол ведь справа от нас, но будет обрабатыватся пересечение.
            // Чтобы исправить это, просто делим радиус на 2. Помните, это делается только
            // при проверке пересечения рёбер треугольника. Это просто добавит реалистичности
            // при огибании углов. В идеале, если мы будем использовать для коллизий ограничи-
            // вающий куб, цилиндр или эллипс, этой проблемы не возникнет.

            if(InsidePolygon(vIntersection, vTriangle, 3) ||
               EdgeSphereCollision(m_vPosition, vTriangle, 3, m_radius / 2))
            {

                // Если мы здесь - у нас есть пересечение! Чтобы исправить его, все что нам
                // нужно - найти глубину, на которую сфера проникла в полигон, и вытолкнуть
                // её назад на это же значение. GetCollisionOffset() вернет нам нужное
                // смещение, основываясь на нормали, радиусе и дистанции центра сферы
                // от плоскости:
                vOffset = GetCollisionOffset(vNormal, m_radius, distance);

                // Теперь, получив смещение, нужно прибавить его к позиции и вектору взгляда
                // камеры. Это "выдавит" её назад из плоскости. Визуально мы этого
                // не заметим, так как рассччет коллизий происходит до рендера сцены.
                m_vPosition = m_vPosition + vOffset;
                m_vView = m_vView + vOffset;
            }
        }
    }
}




Теперь создадим нашу сцену в файле main.cpp:
//////////////////////////////////////////////////////////////////////////////////
//
// Сначала глобальные переменные вверху файла:

// Файл, содержащий вершины нашего мира
#define FILE_NAME "World.raw"

// Количество вершин, прочитанных из файла
int g_NumberOfVerts = 0;

// Массив 3д точек, хранящих вершины мира
CVector3 *g_vWorld=NULL;



///////////////////////////////// LOAD VERTICES \\\\\\\\\\\\\\\\*
/////
/////   Новая функция: загружает вершины мира из текстового файла
/////
///////////////////////////////// LOAD VERTICES \\\\\\\\\\\\\\\\*

void LoadVertices()
{
    // Эта функция читает вершины из ASCII текстового файла (World.raw).
    // Сначала читаем вершины во временную переменную CVector3 чтобы
    // получить их число. Затем ещё раз читаем вершины, уже сохраняя их в массив.

    // Создаем десткриптор файла и открываем файл с вершинами
    FILE *fp = fopen(FILE_NAME, "r");

    // Убедимся, что файл открылся верно
    if(!fp){
        MessageBox(NULL, "Can't Open File", "Error", MB_OK);
        exit(1);
        }

    // Создаём временную переменную для хранения вершин
    CVector3 vTemp;

    // Получаем число вершин из файла
    while(1)
    {
        // Читаем вершины и получаем результат чтения. Если результат == EOF,
        // завершаем чтение.
        int result = fscanf(fp, "%f %f %fn", &vTemp.x, &vTemp.y, &vTemp.z);

        // Если мы достигли конца файла - завершаем чтение
        if(result == EOF)
            break;

        // Увеличиваем число вершин
        g_NumberOfVerts++;
    }

    // Резервируем память под вершины
    g_vWorld     = new CVector3 [ g_NumberOfVerts ];

    // "перематываем" файл на начало
    rewind(fp);

    // Создаём счетчик для индекса массива g_vWorld[]
    int index = 0;

    // Читаем вершины из файла
    for(int i = 0; i < g_NumberOfVerts; i++)
    {
        // Читаем текущую вершину и увеличиваем индекс
        fscanf(fp, "%f %f %fn", &g_vWorld[ index ].x,
                                 &g_vWorld[ index ].y,
                                 &g_vWorld[ index ].z);

        index++;    // Увеличиваем индекс массива вершин
    }

    // Закрываем файл
    fclose(fp);
}




//////////////////////////////////////////////////////////////////////////////////////////
//
// Изменяем инициализацию (функция Init()):
//

void Init(HWND hWnd)
{
    g_hWnd = hWnd;
    GetClientRect(g_hWnd, &g_rRect);
    InitializeOpenGL(g_rRect.right, g_rRect.bottom);
    g_Camera.PositionCamera(10, 4, 12,   9, 4, 12,   0, 1, 0);

    // В начале нужно установить радиус сферы. Я выбрал 1
    g_Camera.SetCameraRadius(1);

    // Загружаем вершины из файла
    LoadVertices();

    // Сейчас мы включим обрезку задних полигонов. Если вы не знаете, что это
    // такое - это просто значит, что мы не будем отрисовывать обе стороны полигона.
    // Единственная сторона, которая будет отрисовыватся - это сторона с исходящей
    // от неё нормалью. Включая culling, OpenGL будет "обрезать" задние полигоны.
    // Поскольну наш мир похож на "коробку", внутри которой - мы, мы никогда
    // не увидим задней стороны полигонов, так что их отрисовывать нет смысла.
    // Вы можете изменить GL_BACK на GL_FRONT для противоположного эффекта.

    glCullFace(GL_BACK);                // Не отрисовываем задние стороны полигонов
    glEnable(GL_CULL_FACE);             // Включим обрезку

    glClearColor(0.0, 0.0, 0.0, 1);         // Установим цвет бекграунда в черный

    float fogColor[4] = {0.0, 0.0, 0.0, 1.0f};  // Сделаем туман также черным

    glFogi(GL_FOG_MODE, GL_EXP2);       // Режим тумана
    glFogfv(GL_FOG_COLOR, fogColor);    // Цвет тумана
    glFogf(GL_FOG_DENSITY, 0.045f);     // Глубина тумана
    glHint(GL_FOG_HINT, GL_DONT_CARE);  // Способ создания тумана
    glFogf(GL_FOG_START, 0);        // Стартовая глубина
    glFogf(GL_FOG_END, 50.0f);      // Конечная глубина

    glEnable(GL_FOG);       // И включим туман

    ShowCursor(false);
}


/////////////////////////////////////////////////////////////////////////////////////////////
//
// Уберём всё лишнее из обработки клавиш, оставим только ESCAPE:
//

    case WM_KEYDOWN:

        switch(wParam) {
            case VK_ESCAPE:
                PostQuitMessage(0);
                break;
        }
        break;




///////////////////////////////////////////////////////////////////////////////////////////////
//
// И наконец изменим функцию RenderScene():
//
void RenderScene()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();

    // Чтобы рассчитать коллизии камеры, со стороны клиента вызываем только одну
    // функцию. Просто передаём в неё вершины нашего мира, которые мы хотим проверить,
    // и число этих вершин.
    g_Camera.CheckCameraCollision(g_vWorld, g_NumberOfVerts);

    // Позиционируем камеру
    g_Camera.Look();

    // Поскольку вершины нашего мира установлены в правильном порядке, давайте создадим
    // цикл, проходящий через все вершины и передающий их для рендера.

    glBegin(GL_TRIANGLES);

        // Проходим через все вершины и рисуем их
        for(int i = 0; i < g_NumberOfVerts; i += 3)
        {
            glVertex3f(g_vWorld[i].x, g_vWorld[i].y, g_vWorld[i].z);
            glVertex3f(g_vWorld[i+1].x, g_vWorld[i+1].y, g_vWorld[i+1].z);
            glVertex3f(g_vWorld[i+2].x, g_vWorld[i+2].y, g_vWorld[i+2].z);
        }

    glEnd();


    SwapBuffers(g_hDC);
}
 


Кроме этого, удалите из файла следующие функции, так как они уже есть в 3dmath.cpp:
Cross();
Magnitude();
Normalize();






Исходные коды к уроку




Комментарии:

Войдите, чтобы оставить комментарий:












Яндекс.Метрика
 Яндекс цитирования.