Основа этого урока взята из урока «Time-Based Movement» и добавлен функционал урока «коллизия сферы и полигона». Цель этого урока — показать способ расчета коллизий камеры и мира (стен).
Если мы окружим нашу камеру воображаемой сферой с радиусом, допустим, «1», то мы сможем проверять пересечения этой сферы с полигонами, находящимися рядом с ней. Мы не будем делать никаких предрасчетов, чтобы отбросить дальние треугольники и рассчитывать коллизии только с близлежащими объектами, так как у нас будет очень мало треугольников. Но в реальной игре это было бы необходимо. Вы можете выбрать схемы BSP tree, или octree, в зависимости от архитектуры вашей сцены/уровня/мира. Если вы не прошли предыдущий урок «коллизия сферы и полигона», я настоятельно рекомендую сделать это. Я не буду объяснять подробности расчетов, так как сделал это в прошлом уроке.
Данные нашего мира — координаты составляющих его объектов — хранятся в файле World.raw.
В нём хранятся только данные вершин, по одной на линию. Сам мир я создал в 3D Studio Max, и экспортировал его в этот простой формат. Так что вам не придется разбираться ещё и в коде, загружающем нормальные модели. Этой сценки хватит, чтобы было вокруг чего пройтись. У меня есть текстуры для пола и стен, но я решил не перегружать код. Ещё мы проделаем простенький трюк, чтобы придать нашей сцене глубину, не окрашивая вершины. Если вы включите в сцене туман с фильтром GL_EXP2, это придаст сцене ещё лучший вид (если конечно ваша видеокарта поддерживает туман). Мы же придадим сцене глубину, используя только освещение.
В файл 3dmath.h добавим прототип новой функции:
Кроме этого, уберите из этого файла объявление класса CVector3, так как он есть и в классе камеры.
Файл 3dmath.cpp:
/////
///// Новая функция: возвращает смещение сферы за плоскость полигона
/////
///////////////////////////////// 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 добавим хидер:
И две новых функции:
void SetCameraRadius(float radius) { m_radius = radius; };// Принимает список вершин+их количество для определения пересечения с ними
void CheckCameraCollision(CVector3 *pVertices, int numOfVerts);// И добавим параметр «радиус» классу камеры:
float m_radius;
Немного подредактируем Camera.cpp:
// по оси 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().