Опыт изучения OpenGL — Часть 7 — Пространственные преобразования. Перспективная проекция.

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

Литература

  • Jason McKesson — Learning Modern 3D Graphics Programming.
  • Jason Gregory — Game Engine Architecture (2nd Edition).

Пространства в 3d-играх

World Space

В трехмерных программах используется несколько координатных пространств. Одно из них называется мир (world space). В мире находятся объекты (в компьютерной игре это — персонажи, окружение и пр.) и камера. И камера, и объекты могут перемещаться в мировом пространстве.

Model Space

Объекты создаются на основе моделей. Модель — это трехмерный чертеж объекта — mesh — полигональная сетка, которая состоит из вершин (vertices). Например, треугольник, который я рисовал в прошлой заметке, состоял из трех вершин. Чертеж создается в системе координат, которая называется пространством модели (model space). Как правило центр этой системы координат находится где-то в центре чертежа, т. е. модели.

Camera (View) Space

Камера смотрит на происходящее в мире. У нее есть позиция и направление взгляда, которые определяют ее собственное пространство координат — camera space.

Про camera, world и model spaces читайте также в [Gregory, 4.3.9. Coordinate Spaces].

Screen (Window) Space

Изображение на экране — это то, как камера видит мир. Экран у нас плоский, соответственно можно говорить о плоском координатном пространстве экрана (screen space). Изображение на экране представляет собой проекцию трехмерного мира на плоскость. Есть два вида проекций: перспективная и ортогональная. Ортогональная используется в программах типа CAD (Computer-Aided Design). В компьютерных играх используется перспективная проекция. Она же имеет место в человеческом зрении.

Однородные (гомогенные) координаты

Здесь мне придется сделать отступление и объяснить понятие однородные координаты. Я о них знаю немого. К трем привычным для нас координатам XYZ добавляется четвертая координата W (полученный набор из четырех координат называется однородными координатами). Причем в пространствах world, model и camera для любой точки координате W присваивается значение 1. Зачем?

Первый плюс — в том, что благодаря координате W теперь преобразование переноса (translation) может быть выражено при помощи умножения на матрицу (рис. 1). Имея три координаты, при помощи умножения на матрицу можно выразить преобразования поворота, масштабирования и сдвига, но нельзя выразить преобразование переноса. А хотелось бы иметь универсальный способ для осуществления любых преобразований, и однородные координаты предоставляют такую возможность.

Рис. 1 — Преобразование переноса в однородных координатах.

Второй плюс — четвертая координата позволяет свести преобразование перспективной проекции к двум операциям: умножение на матрицу (т. н. матрица перспективной проекции) и деление получившегося вектора на его четвертую координату W (perspective division). Подробности о перспективной проекции читайте ниже.

Clip Space и Normalized Device Coordinates Space

Если предыдущие четыре пространства довольно понятны и очевидны, то следующие два специфичны для графического API типа OpenGL, и объяснить их суть не так просто. Clip space — это то, что получается после умножения координат в camera space на матрицу перспективной проекции. Normalized Device Coordinates Space (NDC) — это то, что получается из clip space после деления на координату W (эта операция называется perspective division; о координате W говорилось выше). Точки, у которых в NDC space все три координаты (xyz) попадают в диапазон -1…+1 имеют шанс попасть на экран, все остальные отбрасываются. Более подробно про clip space и NDC space читайте в [McKesson, Introduction. Rasterization Overview].

Итого имеем пять пространств (систем координат):

model -> world -> camera -> clip -> NDC -> screen

Стрелками обозначены преобразования, которые производятся над вершинами моделей. Чтобы нарисовать объект на экране, его вершины нужно перенести из пространства модели в пространство экрана, пройдя при этом через несколько других пространств. Далее мы рассмотрим все эти преобразования по-отдельности.

Perspective Projection: Camera -> Screen

Преобразование перспективной проекции проецирует трехмерное пространство на двумерную плоскость, которая называется плоскостью проекции (projection plane). При этом близкие предметы становятся большими, а далекие — маленькими. Традиционно в компьютерной графике ось Z трехмерного пространства направлена вглубь экрана, а плоскость проекции параллельна плоскости XY и пересекает ось Z в точке z=1. Чтобы спроецировать точку трехмерного пространства на плоскость, надо провести из этой точки линию в начало координат, проекцией будет пересечение этой линии с плоскостью проекции (рис. 2).

На экране нельзя отобразить все трехмерное пространство, только некую его область, которая имеет вид усеченной пирамиды (truncated pyramid или frustum). Усеченная пирамида задается
1) углом обзора (view angle)
2) двумя плоскостями: near plane (z = zNear) и far plane (z = zFar)
3) соотношением длины и ширины (aspect ratio)

Рис. 2 — Перспективная проекция. Усеченная пирамида (frustum).

Теперь вычислим матрицу перспективной проекции. Для простоты возьмем угол обзора 90 градусов и соотношение сторон 1:1, как на рисунке 2. Вычисление матрицы показано на рис. 3. Из рисунка видно, что для того, чтобы спроецировать точку с координатами XY на плоскость проекции, надо разделить эти координаты XY на координату Z. Это деление производится автоматически (т. е. ее не нужно специально программировать) на этапе преобразования координат из clip space в NDC space. Только вот деление производится на координату W, поэтому надо сделать так, чтобы координата W стала равна координате Z, и поэтому в 4-й строке (W) и 3-ем столбце (Z) матрицы перспективной проекции (рис. 3) вы видите число 1. Что касается координат XY, то они остаются теми же самыми, поэтому в первых двух строках матрицы по диагонали у нас единицы.

С 1-й, 2-й и 4-й строками матрицы перспективной проекции мы разобрались. Осталась 3-я строка, которая отвечает за преобразование координаты Z. Проблема тут такова: диапазон [zNear, zFar] после умножения на матрицу перспективной проекции и деления на Z должен перейти в диапазон [-1, +1]. Можно составить два уравнения (как показано на рис. 3) и найти неизвестные элементы матрицы.

Рис. 3 — Матрица перспективной проекции. Для простоты угол обзора (view angle) принят равным 90 градусов, а соотношение сторон (aspect ratio) принято равным 1.

Итак, мы нашли матрицу перспективной проекции для случая угла обзора 90 градусов и для соотношения сторон 1:1. Теперь посмотрим, как влияет на нее угол обзора (рис. 4). Угол обзора — это угол нашей усеченной пирамиды в плоскости XZ. Он определяет насколько широкую панораму мы видим на экране. Крайние точки, лежащие на лучах этого угла превращаются в точки с X=+-1 в NDC space. Исходя из этого можно составить уравнение для элементов 1-й строки матрицы ПП (рис. 4).

Рис. 4 — Влияние угла обзора на матрицу перспективной проекции. x_clip = f(x_cam, viewAngle).

Аналогично дело обстоит с влиянием соотношения сторон (width:height) — aspect ratio. Оно влияет на элементы 2-й строки матрицы ПП (рис. 5).

Рис. 5 — Влияние соотношения сторон на матрицу перспективной проекции. y_clip = f(y_cam, viewAngle, aspectRatio).

Ну и наконец, запишем окончательное выражение для общего случая для матрицы перспективной проекции с учетом угла обзора и соотношения сторон (рис. 6).

Рис. 6 — Окончательное выражение для матрицы перспективной проекции.

Depth test

И последнее, о чем надо рассказать в контексте перспективной проекции. Предметы расположенные ближе к наблюдателю, кажутся ему больше тех, которые расположены дальше. А еще предметы, расположенные ближе, заслоняют предметы, расположенные дальше. И вот, чтобы реализовать данное явление, в компьютерной графике применяется технология под названием depth test. В прошлых заметка я упоминал о том, что есть несколько буферов памяти, в которых хранится информация об изображении — самый понятный из них — color buffer, который хранит цвета всех пикселей на экране. Но есть еще один буфер — буфер глубины (depth buffer). В него записывается Z-координата в NDC space для объектов (точнее — их пикселей, еще точнее — фрагментов (фрагмент — это то, что потенциально может стать пикселем)), ближайших к наблюдателю. Вот как это делается для нашего случая, когда ось Z направлена вглубь экрана (рис. 7): если фрагмент имеет координату Z меньшую, чем текущее значение в depth buffer, то эта координата записывается в depth buffer, а цвет фрагмента записывается в color buffer. Если фрагмент имеет координату Z большую, чем текущее значение в depth buffer, то этот фрагмент отбрасывается (discard).

Рис. 7 — Depth test

Однако описанное поведение depth test’а не является единственно возможным. Например, возможна такая конфигурация, когда ось Z смотрит не вглубь экрана, а направлена на наблюдателя — тогда логика depth test инвертируется. Короче, depth test надо настроить, и это у меня делается в функции opengl::SetupDepthTest в файле OpenGLUtility.h:

/*
Sets up the depth test.
Parameters:
[zNear] - near z value in window space coordinates. Must be between 0 and [zFar].
[zFar] - far z value in window space coordinates. Must be between [zNear] and 1.
[testFunction] - one of the following: GL_LESS, GL_EQUAL, GL_LEQUAL, GL_GREATER, GL_NOTEQUAL, GL_GEQUAL, GL_ALWAYS. When the test is True the incoming fragment is written.
Default values of parameters are arranged in assumption that Z-axis in camera space goes from the viewer deep into the screen.
*/

inline void SetupDepthTest(
    GLclampd zNear = 0.0f,
    GLclampd zFar = 1.0f,
    COMPARISON_FUNCTION testFunction = COMPARISON_FUNCTION::LEQUAL)
{
    glClearDepth(zFar);        // задать значение, которое записывается во все ячейки depth buffer при его очистке функцией glClear
    glDepthRange(zNear, zFar); // задать диапазон глубины в window space, в который будут транслироваться значения из depth buffer
    glEnable(GL_DEPTH_TEST);   // разрешить depth test
    glDepthMask(GL_TRUE);      // разрешить запись в depth buffer
    glDepthFunc(static_cast<GLenum>(testFunction)); // задать логику depth test
}

Здесь то, какой фрагмент пройдет depth test, а какой не пройдет и будет отброшен, определяется функцией glDepthFunc. По-умолчанию значение параметра testFunction = LEQUAL, поэтому тест проходит тот фрагмент, у которого Z меньше или равно текущему значению в depth buffer. Про остальные функции написано в комментариях в коде. Подробнее о depth test читайте в [McKesson, Chapter 5. Objects in Depth. Overlap and Depth Buffering.]

На этом пока всё. В следующих заметках обсудим преобразования model->world->camera. Основное внимание будет уделено преобразованию поворота. Также рассмотрим общий вопрос смены базиса и вопрос правых и левых систем координат (right or left handedness).

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *