Размещение обектов сцены в OpenGL

OpenGL game world object placement

Проект

Исходники проекта

Текстуры

Описание

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

Если отобразить несколько таких обектов в рамках одной сцены, то их начала координат совпадут с началом координат сцены. Все объекты будут расположены в одном месте. В случае системы NDC (Native Device Coordinates) — в центре сцены.

Кроме того, при загрузке модели в память и формировании объекта VAO (Vertex Array Object), учитываются именно локальные координаты вершин без привязки к какому-либо месту на отображаемой сцене. Вместе с тем флаг GL_STATIC_DRAW говорит о том, что координаты и другие параметры вершин объекта не будут менятся в рамках данного массива. Но никто не проектирует статичные модели с учетом их положения на сцене.

Для того чтобы разместить объект в нужном месте сцены, используют соответствующие преобразования координат вершин в коде vertex шейдера. В случае трехмерного пространства все преобразования осуществляются путем умножения вектора с координатами вершины на матрицу размера 4х4 слева.

Вектор, содержащий координаты вершины, имеет вид: v = \begin{pmatrix} x \\ y \\ z \\ 1.0 \end{pmatrix}. Последний компонент существует для корректности операции умножения на матрицу и, кроме того, участвует в работе z-буфера.

Масштабирование

Пусть вектор v соответствует одной из вершин объекта.

v = \begin{pmatrix} x \\ y \\ z \\ 1.0 \end{pmatrix}

Масштабирование осуществляется путем регулирования расстояния от вершины до начала координат: v_{1} = \begin{pmatrix} dx \ dy \ dz \ 1.0 \end{pmatrix} , где d — показатель, определяющий во сколько раз результирующий обект больше (меньше) оригинального.

Получить такой вектор можно путем произведения исходного вектора на матрицу:

v_{1} = \begin{pmatrix} d & 0 & 0 & 0 \\ 0 & d & 0 & 0 \\ 0 & 0 & d & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \times \begin{pmatrix} x \\ y \\ z \\ 1.0 \end{pmatrix} = \begin{pmatrix} dx \\ dy \\ dz \\ 1.0 \end{pmatrix}

Перемещение

Чтобы переместить объект в пространстве, достаточно к векторам вершин объекта прибавить вектор перемещения. Пусть вектор t соответствует перемещению, а вектор v соответствует вершине, тогда результирующий вектор можно получить следующим образом:

v_{1} = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \times \begin{pmatrix} x \\ y \\ z \\ 1.0 \end{pmatrix} = \begin{pmatrix} x + t_x \\ y + t_y \\ z + t_z \\ 1.0 \end{pmatrix},

где вектор перемещения:

t = \begin{pmatrix} t_x \\ t_y \\ t_z \end{pmatrix} \\

Следует отметить, что все преоброзования можно производить последовательно, умножая результат предыдущего преобразования на матрицу (слева), соответствующую текущему преобразованию:

v_{1} = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \times \begin{pmatrix} d & 0 & 0 & 0 \\ 0 & d & 0 & 0 \\ 0 & 0 & d & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \times \begin{pmatrix} x \\ y \\ z \\ 1.0 \end{pmatrix} = \begin{pmatrix} dx + t_x \\ dy + t_y \\ dz + t_z \\ 1.0 \end{pmatrix},

В силу ассоциативности операций матричного умножения, выражение для v_1 можно представить так:

v_{1} = \begin{pmatrix} d & 0 & 0 & t_x \\ 0 & d & 0 & t_y \\ 0 & 0 & d & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \times \begin{pmatrix} x \\ y \\ z \\ 1.0 \end{pmatrix} = \begin{pmatrix} dx + t_x \\ dy + t_y \\ dz + t_z \\ 1.0 \end{pmatrix},

Вращение

Рассмотрим случай поворота объекта вокруг оси z. В системе NDC (Normalized Device Coordinates) ось z соответствует направлению «взгляда» камеры.

Матрица поворота вектора на угол \alpha будет иметь следующий вид:

M_{rot_z} = \begin{pmatrix} \cos \alpha & -\sin \alpha & 0 & 0 \\ \sin \alpha & \cos \alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

Так как поворот объекта осуществляется вокруг оси z в системе координат сцены, целесообразно произвести поворот до параллельного переноса. В таком случае объект повренется вокруг своей оси. В противном случае поворот будет осуществлятся вокруг оси, проходящей через центр сцены.

Матрицы поворота вокруг других осей будут выглядеть аналогично:

M_{rot_x} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos \alpha & -\sin \alpha & 0 \\ 0 & \sin \alpha & \cos \alpha & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \\ M_{rot_y} = \begin{pmatrix} \cos \alpha & 0 & -\sin \alpha & 0 \\ 0 & 1 & 0 & 0 \\ \sin \alpha & 0 & \cos \alpha & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

Объединение всех преобразований в одном выражении

Так как масштабирование и поворот следует выполнять первыми, получаем следующую формулу для преобразования вершин объекта:

v_1 = M_{trans} \times M_{rot} \times M_{scale} \times v, где \\ \\ M_{trans} = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}, M_{rot} = M_{rot_z}, M_{scale} = \begin{pmatrix} d & 0 & 0 & 0 \\ 0 & d & 0 & 0 \\ 0 & 0 & d & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

Исходный код

Здесь я опишу только ту часть проекта, которая отвечает за преобразования координат. Так как в качестве объекта сцены выбран прямоугольник, то для описания всех его параметров, кроме координат вершин в VAO, выбрана структура:

typedef struct {
	float ms;
	float tr[3];
	float angle;
	unsigned int tex;
	float trans[16];
} MyBox;

Здесь ms — параметр, определяющий масштаб, tr — вектор перемещения, angle — угол поворота, tex — идентификатор текстуры, trans — матрица, равная произведению матриц переноса, масштабирования и поворота.

Следующие методы позволяют сформировать матрицу преобразования координат вершин, выделить память для структуры, описывающей обект сцены и заполнить её данными, удалить обект из памяти:

void makeBoxTrans(MyBox* box) {
	float trans[] = {
		box->ms*cos(box->angle), -box->ms*sin(box->angle), 0.0, box->tr[0],
		box->ms*sin(box->angle), box->ms*cos(box->angle), 0.0, box->tr[1],
		0.0, 0.0, box->ms, box->tr[2],
		0.0, 0.0, 0.0, 1.0 };
		memcpy(box->trans, trans, sizeof(trans));
};

MyBox* makeBox(float ms, float tx, float ty, float tz, float angle, unsigned int tex) {
	MyBox* box = (MyBox*)malloc(sizeof(MyBox));
	box->ms = ms;
	box->tr[0] = tx;
	box->tr[1] = ty;
	box->tr[2] = tz;
	box->tex = tex;
	box->angle = angle;
	makeBoxTrans(box);
	return box;
};

void freeMyBox(MyBox* box) {
	free(box);
};

Следующий фрагмент описывает процесс создания набора «коробок», в котором параметры мастабирования, перемещения и поворота — случайные величины:

	MyBox* boxes[BOX_COUNT];
	for(int i=0; i<BOX_COUNT-1; i++) {
		float ms = (float)(rand() % 100) / 500.0 + 0.1;
		float tx = (float)(rand() % 150) / 100.0 - 0.75;
		float ty = (float)(rand() % 150) / 100.0 - 0.75;
		float tz = (float)(rand() % 150) / 100.0 - 0.75;
		float angle = (float)(rand() % 360) / 360 * 6.28;
		boxes[i] = makeBox(ms, tx, ty, tz, angle, tex_box);
	};

	boxes[BOX_COUNT-1] = makeBox(1.0, 0.0, 0.0, 0.1, 0.0, tex_back);

Последний элемет в массиве boxes — это фон сцены, тоже представляющий из себя коробку, но с другой текстурой и занимающую всё видимое пространство по осям x и y.

Чтобы корректно отобразить объекты сцены, необходимо передать матрицу, описывающую преобразования вершин в vertex шейдер. Предположим, что в vertex шейдере переменная с именем trans должна содержат такую матрицу, тогда получим идентификатор расположения этой переменной следующим образом:

unsigned int uniform_trans = glGetUniformLocation(shaderProgram, "trans");

Теперь в цикле рисования сцены достаточно добавить следующий код:

		for(int i=0; i<BOX_COUNT; i++) {
			glUseProgram(shaderProgram); /* Активируем шейдерную программу */
			glUniformMatrix4fv(uniform_trans, 1, GL_FALSE, boxes[i]->trans);

			glActiveTexture(GL_TEXTURE0);
			glBindTexture(GL_TEXTURE_2D, boxes[i]->tex);

			glBindVertexArray(VAO); /* Активируем массив с данными о вершинах */
			glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); /* Отображаем треугольник */
		};

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

Система OpenGL воспринимет матрицы так, как если бы номер столбца указывался первым. Следовательно, в коде шейдера необходимо поменять порядок умножения, либо указать третим аргументом метода glUniformMatrix4fv флаг GL_TRUE.

Код vertex шейдера:

#version 300 es

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 tCoords;

out vec3 pos;
out vec2 tcoords;

uniform mat4 trans;

void main() {
  pos = aPos;
  tcoords = tCoords;
  gl_Position = vec4(aPos, 1.0f) * trans;
}

Выделенная строка содержит описание глобальной переменной, содержащей данные матрицы преобразования.

41

Добавить комментарий

Ваш e-mail не будет опубликован.