Урок 10. Загрузка и перемещение в трехмерном мире

Этот урок был написан человеком по имени Lionel Brits (Betelgeuse). Урок содержит только те части кода, которые нужно добавить. Но если просто добавите строки кода, описанные ниже, программа не будет работать. Если вы хотите знать, где должна идти каждая строка кода, скачайте исходник и просмотрите его так же, как вы прочитали этот урок.

  Добро пожаловать в непопулярный Урок 10. Сейчас у вас есть вращающийся куб или цепочка звёзд и некоторая базовая ориентация в 3D программировании. Но стойте! Не убегайте сразу же и не начинайте создание Quake IV, пока что. Просто, вращающиеся кубики не доставят вам много удовольствий в десматче J. Вместо этого вам нужен большой, сложный и динамический 3D мир со свободным взглядом во все 6 сторон и с причудливыми эффектами такими, как зеркала, порталы, искривления и, конечно же, с высокой частотой кадров в секунду. В этом уроке представлена базовая «структура» 3D мира и, конечно же, способы перемещения по нему.


Структура данных

До сих пор было легко определять среду 3D мира, используя длинные комбинации чисел, но это становится чрезвычайно нелегко, когда сложность среды начинает возрастать. По этой причине мы должны организовать данные в более мощные структуры. Прежде всего обсудим понятие сектора. Каждый 3Д мир базируется на некотором количестве секторов. Сектор может быть комнатой, кубом или любым другим замкнутым пространством.

 

typedef struct tagSECTOR // Создаём структуру нашего сектора

{

int numtriangles; // Кол-во треугольников в секторе

TRIANGLE* triangle // Ссылка на массив треугольников

} SECTOR; // Обзовём структуру словом SECTOR

  Сектор содержит ряд многоугольников, однако мы будем использовать треугольники, потому что их проще всего запрограммировать.

 

typedef struct tagTRIANGLE // Создаём стр-ру нашего треугольника

{

VERTEX vertex[3]; // Массив трёх вершин

} TRIANGLE; // Обзовём это TRIANGLE

  Треугольник, как и любой многоугольник, определяется вершинами. Вершина содержит реальные для использования OpenGL’ом данные. Мы определяем каждую точку треугольника её расположением в 3D пространстве (x, y, z) и координатами на текстуре (u, v).

 

typedef struct tagVERTEX // Создаём стр-ру нашей вершины

{

float x, y, z; // 3D координаты

float u, v; // Координаты на текстуре

} VERTEX; // Обзовём это VERTEX

 

Загрузка файлов

  Сохранение данных нашего мира внутри программы делает её слишком статичной и скучной. Загрузка миров с диска, тем не менее, даёт больше гибкости в тестировании различных миров, избавляя от перекомпиляции нашей программы. Другое преимущество в том, что пользователь может использовать новые уровни и модифицировать их, не задумываясь о коде нашей программы. Тип файла данных, который мы собираемся использовать будет текстовым. Это сделано для того, чтобы облегчить редактирование мира и уменьшить код программы. Оставим двоичные файлы на дальнейшее рассмотрение.

  Естественный вопрос: как мы извлечем данные из нашего файла? Во-первых, мы создадим новую функцию SetupWorld(). Определим наш файл как filein и откроем его в режиме только чтение. А так же, когда закончим, мы должны не забыть закрыть наш файл. Давайте посмотрим на следующий код:

 

// Декларация выше: char* worldfile = "data\\world.txt";

void SetupWorld() // Установка нашего мира

{

FILE *filein; // Файл для работы

filein = fopen(worldfile, "rt"); // Открываем наш файл

(считываем наши данные)

fclose(filein); // Закрываем наш файл

return; // Возвращаемся назад

}

 

Следующее, чему мы уделим внимание, будет собственно считывание каждой строки текста в переменную. Это можно выполнить очень многими способами. Одна проблема в том, что не все строки содержат значимую информацию. Пустые линии и комментарии не должны быть считаны. Теперь создадим функцию readstr(). Она будет считывать одну значимую строку в инициализированную строку. Вот этот код:

 

void readstr(FILE *f,char *string) // Считать в строку

 

{

do // Начинаем цикл

{

fgets(string, 255, f); // Считываем одну линию

// Проверяем её на условие повт. цикла

} while ((string[0] == '/') || (string[0] == '\n'));

return; // Возврат

}

 

Далее мы должны считать данные сектора. В этом уроке мы будем иметь дело только с одним сектором, но достаточно просто реализовать многосекторный движок. Давайте вернёмся к SetupWorld(). Наша программа должна знать сколько треугольников в секторе. В нашем файле данных мы обозначим количество треугольников следующим образом:

 

NUMPOLLIES n

  Вот код для чтения количества треугольников:

 

int numtriangles; // Кол-во треугольников в секторе

char oneline[255]; // Строка для сохранения данных

readstr(filein,oneline); // Считать одну линию данных

// Считать кол-во треугольников

sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);

 

Остальная часть нашего процесса загрузки мира будет использовать тот же процесс. Далее мы инициализируем наш сектор и считываем в него некоторые данные:

 

// Декларация выше: SECTOR sector1;

char oneline[255]; // Строка для сохранения данных

int numtriangles; // Кол-во треугольников в секторе

float x, y, z, u, v; // 3D и текстурные координаты

// Выделяем память для numtriangles и устанавливаем ссылку

sector1.triangle = new TRIANGLE[numtriangles];

// Определяем кол-во треугольников в Секторе 1

sector1.numtriangles = numtriangles;

// Цикл для всех треугольников

// За каждый шаг цикла — один треугольник в секторе

for (int triloop = 0; triloop < numtriangles; triloop++)

{

// Цикл для всех вершин

// За каждый шаг цикла — одна вершина в треугольнике

for (int vertloop = 0; vertloop < 3; vertloop++) {

readstr(filein,oneline); // Считать строку для работы

// Считать данные в соответствующие переменные вершин

sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);

// Сохранить эти данные

sector1.triangle[triloop].vertex[vertloop].x = x;

// Сектор 1, Треугольник triloop, Вершина vertloop, Значение x = x

sector1.triangle[triloop].vertex[vertloop].y = y;

// Сектор 1, Треугольник triloop, Вершина vertloop, Значение y = y

sector1.triangle[triloop].vertex[vertloop].z = z;

// Сектор 1, Треугольник triloop, Вершина vertloop, Значение z = z

sector1.triangle[triloop].vertex[vertloop].u = u;

// Сектор 1, Треугольник triloop, Вершина vertloop, Значение u = u

sector1.triangle[triloop].vertex[vertloop].v = v;

// Сектор 1, Треугольник triloop, Вершина vertloop, Значение y = v

}

}

  Каждый треугольник в нашем файле данных имеет следующую структуру:

 

X1 Y1 Z1 U1 V1

X2 Y2 Z2 U2 V2

X3 Y3 Z3 U3 V3

 

Отображение миров

  Теперь, когда мы можем загружать наш сектор в память, нам нужно вывести его на экран. Итак, мы уже умеем делать немного простых вращений и проекций, но наша камера всегда находилась в начальном положении (0, 0, 0). Любой хороший 3D движок должен предоставлять пользователю возможность ходить и исследовать мир, так мы и сделаем. Одна из возможностей сделать это  — перемещать камеру и перерисовывать 3D среду относительно её положения. Это медленно выполняется и тяжело запрограммировать. Поэтому мы будем делать так:

 

1)      Вращать и проецировать позицию камеры следуя командам пользователя.

2)      Вращать мир вокруг начала координат противоположно вращению камеры (это даёт иллюзию того, что повернулась камера).

3)      Переместить мир способом, противоположным перемещению камеры (опять-таки, это даёт иллюзию того, что переместилась камера).

  Это красиво и легко реализовывается. Давайте начнём с первого этапа (Вращение и проецирование камеры).

 

if (keys[VK_RIGHT]) // Была ли нажата правая стрелка?

{

yrot -= 1.5f; // Вращать сцену влево

}

 

if (keys[VK_LEFT]) // Была ли нажата левая стрелка?

{

yrot += 1.5f; // Вращать сцену вправо

}

 

if (keys[VK_UP]) // Была ли нажата стрелка вверх?

{

// Переместиться на X-плоскости, базируемой на направлении игрока

xpos -= (float)sin(heading*piover180) * 0.05f;

// Переместиться на Z-плоскости, базируемой на направлении игрока

zpos -= (float)cos(heading*piover180) * 0.05f;

if (walkbiasangle >= 359.0f)// walkbiasangle>=359?

{

walkbiasangle = 0.0f; // Присвоить walkbiasangle 0

}

else // В противном случае

{

// Если walkbiasangle < 359 увеличить его на 10

walkbiasangle+= 10;

}

// Иммитация походки человека

walkbias = (float)sin(walkbiasangle * piover180)/20.0f;

}

 

if (keys[VK_DOWN]) // Была ли нажата стрелка вниз?

{

// Переместиться на X-плоскости, базируемой на направлении игрока

xpos += (float)sin(heading*piover180) * 0.05f;

// Переместиться на Z-плоскости, базируемой на направлении игрока

zpos += (float)cos(heading*piover180) * 0.05f;

if (walkbiasangle <= 1.0f) // walkbiasangle<=1?

{

walkbiasangle = 359.0f; // Присвоить walkbiasangle 359

}

else // В противном случае

{

// Если walkbiasangle >1 уменьшить его на 10

walkbiasangle-= 10;

}

// Иммитация походки человека

walkbias = (float)sin(walkbiasangle * piover180)/20.0f;

}

  Это было довольно просто. Когда нажата стрелка влево или стрелка вправо, переменная вращения yrot увеличивает или уменьшает своё значение. Когда нажата стрелка вверх или стрелка вниз, новое положение камеры высчитывается с использованием синуса и косинуса (требуется немного тригонометрии J). Piover180 это просто коэффициент преобразования для перевода градусов в радианы.

  Далее вы спросите меня: что такое walkbias (дословно: смещение походки)? Это слово, которое я изобрёл J. Оно представляет собой смещение, которое происходит, когда персона идёт (голова смещается вверх и вниз как буй). Это легко устанавливается изменением Y позиции камеры по синусоиде. Я решил использовать это, потому что простое перемещение вперёд и назад выглядит не реально.

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

 

int DrawGLScene(GLvoid) // Нарисовать сцену OpenGL

{

// Очистить сцену и буфер глубины

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glLoadIdentity(); // Сбросить текущую матрицу

 

// Вещ. перем. для временных X, Y, Z, U и V

GLfloat x_m, y_m, z_m, u_m, v_m;

GLfloat xtrans = -xpos; // Проекция игрока на ось X

GLfloat ztrans = -zpos; // Проекция игрока на ось Z

// Для смещения изображения вверх и вниз

GLfloat ytrans = -walkbias-0.25f;

// 360 градусный угол для поворота игрока

GLfloat sceneroty = 360.0f - yrot;

 

int numtriangles; // Количество треугольников

 

glRotatef(lookupdown,1.0f,0,0);// Вращать вверх и вниз

// Вращать в соответствии с направлением взгляда игрока

glRotatef(sceneroty,0,1.0f,0);

// Проецировать сцену относительно игрока

glTranslatef(xtrans, ytrans, ztrans);

// Выбрать текстуру filter

glBindTexture(GL_TEXTURE_2D, texture[filter]);

// Получить кол-во треугольников Сектора 1

numtriangles = sector1.numtriangles;

// Процесс для каждого треугольника

// Цикл по треугольникам

for (int loop_m = 0; loop_m < numtriangles; loop_m++)

{

glBegin(GL_TRIANGLES); // Начинаем рисовать треугольники

// Нормализованный указатель вперёд

glNormal3f( 0.0f, 0.0f, 1.0f);

x_m = sector1.triangle[loop_m].vertex[0].x;// X 1-ой точки

y_m = sector1.triangle[loop_m].vertex[0].y;// Y 1-ой точки

z_m = sector1.triangle[loop_m].vertex[0].z;// Z 1-ой точки

// U текстурная координата

u_m = sector1.triangle[loop_m].vertex[0].u;

// V текстурная координата

v_m = sector1.triangle[loop_m].vertex[0].v;

glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);

// Установить TexCoord и грань

x_m = sector1.triangle[loop_m].vertex[1].x;// X 2-ой точки

y_m = sector1.triangle[loop_m].vertex[1].y;// Y 2-ой точки

z_m = sector1.triangle[loop_m].vertex[1].z;// Z 2-ой точки

// U текстурная координата

u_m = sector1.triangle[loop_m].vertex[1].u;

// V текстурная координата

v_m = sector1.triangle[loop_m].vertex[1].v;

glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);

// Установить TexCoord и грань

x_m = sector1.triangle[loop_m].vertex[2].x;// X 3-ой точки

y_m = sector1.triangle[loop_m].vertex[2].y;// Y 3-ой точки

z_m = sector1.triangle[loop_m].vertex[2].z;// Z 3-ой точки

// U текстурная координата

u_m = sector1.triangle[loop_m].vertex[2].u;

// V текстурная координата

v_m = sector1.triangle[loop_m].vertex[2].v;

glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);

// Установить TexCoord и грань

glEnd(); // Заканчиваем рисовать треугольники

}

return TRUE; // Возвращаемся

}

  И вуаля! Мы только что нарисовали наш первый фрейм. Это не совсем Quake, но эй, мы не совсем Carmack или Abrash. Когда вы запустите программу, можете нажать F, B, PgUp и PgDown и увидеть дополнительные эффекты. PgUp/Down просто наклоняет камеру вверх и вниз (наподобие панорамирования из стороны в сторону). Текстура — это моя обработанная школьная фотография J, если конечно NeHe сохранит ее.

  Теперь, вы наверное задумываетесь чем заняться дальше. Даже не думайте использовать этот код для полнофункционального 3D движка, так как код не был для этого предназначен. Вы наверное захотите более одного сектора в своей игре, особенно, если вы хотите использовать порталы. Вы также захотите использовать многоугольники с более чем тремя вершинами, опять-таки, особенно для движка с порталами. Моя текущая реализация этого кода позволяет загружать несколько секторов и производит удаление невидимых поверхностей (не рисуются многоугольники, не попадающие в камеру). Я напишу по этому поводу урок очень скоро, но это использует много математики, поэтому я собираюсь для начала написать урок по матрицам.

 

© Lionel Brits

 8 января 2002 (c)  Andrew Aseev