Преобразование координат

Это пока черновой вариант. Материал будет дорабатываться в течение семестра.

Преобразование координат

В 3D-графике при работе с 3D-моделями часто приходится осуществлять преобразование координат вершин из одной системы координат в другую.

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

Для преобразования вершин из одной системы координат в другую используют матрицы. Подробнее про матрицы - в курсе линейной алгебры. Здесь будет дан краткий практический обзор.

Для работы с матрицами и векторами на C++ будем использовать математическую библиотеку GLM. Она изначально проектировалась так, чтобы названия типов данных совпадали с типами данных языка GLSL. Также там реализованы многие удобные функции, ранее входившие в спецификацию OpenGL, но убранные оттуда.

Локальная система координат

Изначально вершины 3D-модели заданы в локальной системе координат (local space). Эта система координат выбирается произвольно для каждой 3D-модели. Рекомендуется размещать 3D-модель в начале системы координат. В некоторых графических движках и 3D-редакторах вверх направлена ось Y, в других - ось Z. Это дело вкуса.

Координаты вершин копируются в буфер на видеокарте, как описывалось ранее.

Мировая система координат

Если в графическом приложении имеется несколько 3D-моделей, то они как-то располагаются в пространстве относительно друг друга и образуют 3D-сцену. Удобно ввести единую систему координат для 3D-сцены - мировую систему координат.

Матрицу для преобразования вершины из локальной в мировую систему координат называют матрицей модели (model matrix).

Здесь $v$ - это вектор-столбец в локальной системе координат, $M_{model}$ - матрица модели. Умножение будем делать справа, потому что так принято в языке GLSL.

На практике матрицу модели удобно представлять в виде произведения более простых матриц: матрицы вращения, масштабирования и переноса.

Но, к сожалению, матрицу переноса невозможно задать в виде матрицы 3х3. Поэтому было предложено использовать, так называемые, однородные координаты. Вектор трехмерных координат $(x, y, z)$ дополняют 4й координатой, которая равна 1.

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

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

Действительно:

С помощью библиотеки GLM матрицу переноса можно задать так:

glm::mat4 mat; //начальная матрица (единичная)
mat = glm::translate(mat, glm::vec3(a, b, c)); //начальная матрица умножается на матрицу переноса

Матрица масштабирования будет выглядеть так:

С помощью библиотеки GLM матрицу масштабирования можно задать так:

glm::mat4 mat; //начальная матрица (единичная)
mat = glm::scale(mat, glm::vec3(sx, sy, sz)); //начальная матрица умножается на матрицу масштабирования

Матрица вращения вокруг оси X будет выглядеть так:

С помощью библиотеки GLM матрицу вращения вокруг оси X можно задать так:

glm::mat4 mat; //начальная матрица (единичная)
mat = glm::rotate(mat, theta, glm::vec3(1.0f, 0.0f, 0.0f)); //начальная матрица умножается на матрицу вращения

Функция rotate принимает на вход угол повора и ось вращения.

Пример последовательного применения матрицы вращения и затем переноса:

glm::mat4 mat;
mat = glm::translate(mat, glm::vec3(0.0f, 0.5f, 0.0));
mat = glm::rotate(mat, angle, glm::vec3(0.0f, 0.0f, 1.0f));

В матричной форме это выглядит так:

Система координат виртуальной камеры

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

Вводят систему координат виртуальной камеры. Она устроена следующим образом. Камера располагается в центре. Ось X направлена вправо, ось Y направлена вверх, ось Z направлена назад:

В этой системе координат удобно производить проецирование, т.к. оси XY в экранной системе координат расположены аналогично (вправо и вверх). Но сначала нужно перевести координаты вершин из мировой системы координат в систему координат виртуальной камеры. Матрица для такого перевода называется матрицей вида (view matrix):

В библиотеке GLM есть удобная функция glm::lookAt для построения матрицы вида. Она принимает на вход 3 вектора:

  • вектор eye - соединяет начало мировой системы координат и виртуальную камеру
  • вектор center - соединяет начало мировой системы координат и точку, на которую смотрит камера
  • вектор up - вектор «вверх», обычно берется (0, 0, 1) или (0, 1, 0), смотря какая ось у вас направлена вверх в мировой системе координат

Перспективное проецирование

Далее выполняется операция перспективного проецирования. При перспективной проекции чем дальше расположен объект, тем меньше он выглядит на экране. Это нелинейная операция. Её разбивают на 2 этапа:

  • линейный, подготовительный этап, который выражается матрицей проекции (projection matrix)
  • нелинейный этап (perspective division)

В матрице проекции заложена информация об объёме видимости: какую часть 3D-сцены мы увидим на экране. Объем видимости имеет форму усеченной пирамиды (frustum):

В библиотеке GLM есть 2 функции для построения матрицы проекции. Функция glm::perspective принимает на вход угол обзора по вертикали, соотношение ширины экрана к высоте, расстояния до ближней и дальней стенок пирамиды (плоскостей отсечения):

glm::mat4 projMat = glm::perspective(glm::radians(angle), width / height, znear, zfar);

Функция glm::frustum принимает на вход координаты ближней стенки пирамиды, расстояния до ближней и дальней стенок пирамиды (плоскостей отсечения):

glm::mat4 projMat = glm::frustum(left, right, bottom, top, znear, zfar);

Матрица проекции выглядит так:

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

После умножения на матрицу проекции получаем 4хмерный вектор:

Теперь, чтобы вернуться обратно в 3хмерное пространство, нужно умножить все компоненты векторы на одно и то же число $1/w’$, благодаря свойству однородных координат:

При этом, как видно из матрицы проекции, $w’ = -z$. Получается то самое перспективное деление, нелинейный шаг.

Коэффициенты матрицы проекции подбираются таким образом, чтобы после деления точки объема видимости оказались внутри куба $[-1; 1]^3$. Это называется система нормализованных координат устройства (normalized device coordinates).

Как видно, ось Z поменяла свое направление. Система координат из правой стала левой.

Система координат, в которой находится вектор $(x’, y’, z’, w’)$, называется системой усеченных координат (clip space). В этой системе координат происходит проверка видимости вершин: попадает ли вершина в объем видимости. Условие проверки:

Если координаты вершины не проходят эту проверку, то вершина отбрасывается. Если проходят - дальше производится перспективное деление на $w’$.

Экранная система координат

Далее простыми сдвигом и растяжением происходит переход в экранную систему координат. Начало координат находится в левом нижнем углу. Координаты по осям XY измеряются в пикселях.

Значения по оси Z из диапазона $[-1; 1]$ приводятся к диапазону $[0; 1]$ и превращаются в глубину (depth).

Параметры экранной системы координат задаются функцией glViewport. В ней указываются координаты левого нижнего угла и размеры области вывода относительно графического окна. Например:

glViewport(0, 0, width, height);

Заключение

Полная схема всех преобразований:

Первые 3 операции должны быть выполнены в вершинном шейдере. Шейдер должен записать в переменную gl_Position координаты вершины в системе усеченных координат. Последние 2 этапа фиксированы.

Пример такого шейдера:

#version 330

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

layout(location = 0) in vec3 vertexPosition;

void main()
{
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
}

Матрицы задаются в основной программе:

glm::mat4 modelMatrix = ...;
glm::mat4 viewMatrix = glm::lookAt(...);
glm::mat4 projectionMatrix = glm::perspective(...);

GLint modelMatrixUniformLoc = glGetUniformLocation(programId, "modelMatrix");
glUniformMatrix4fv(modelMatrixUniformLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));

GLint viewMatrixUniformLoc = glGetUniformLocation(programId, "viewMatrix");
glUniformMatrix4fv(viewMatrixUniformLoc, 1, GL_FALSE, glm::value_ptr(viewMatrix));

GLint projectionMatrixUniformLoc = glGetUniformLocation(programId, "projectionMatrix");
glUniformMatrix4fv(projectionMatrixUniformLoc, 1, GL_FALSE, glm::value_ptr(projectionMatrix));

Для оптимизации производительности матрицы можно заранее перемножить в основной программе.