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

Данные нашего мира — координаты составляющих его объектов — хранятся в файле 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().

 

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