Геометрические свойства нескольких изображений. Перспективные матрицы в графическом API или дьявол прячется в деталях

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

Это не решение проблемы! Давайте разбираться вместе!

Будем реалистами с практическим уклоном и возьмём в качестве подопытного OpenGL версии 3.3. Начиная с этой версии каждый разработчик обязан самостоятельно реализовывать модуль матричных операций. Замечательно, это то, что нам нужно. Проведём декомпозицию нашей с вами нелёгкой задачи и выделим основные моменты. Немного фактов из спецификации OpenGL:

  • Матрицы хранятся по столбцам (column-major);
  • Однородные координаты;
  • Канонический объём отсечения (CVV) в левосторонней системе координат.
Существует два способа хранения матриц: сolumn-major и row-major. На лекциях по линейной алгебре как раз используется схема row-major. По большому счёту представление матриц в памяти не имеет значения, потому что матрицу всегда можно перевести в одного вида представления в другое простым транспонированием. А раз разницы нет, то для всех последующих расчётов мы будем использовать классические row-major матрицы. При программировании OpenGL есть небольшая хитрость, которая позволяет отказаться и от транспонирования матриц при сохранении классических row-major расчётов. В шейдерную программу матрицу нужно передавать как есть, а в шейдере производить умножение не вектора на матрицу, а матрицы на вектор.

Однородные координаты – это не очень хитрая система с рядом простых правил по переводу привычных декартовых координат в однородные координаты и обратно. Однородная координата это матрица-строка размерности . Для того чтобы перевести декартову координату в однородную координату необходимо x , y и z умножить на любое действительное число w (кроме 0). Далее необходимо записать результат в первые три компоненты, а последний компонент будет равен множителю w . Другими словами:
- декартовы координаты
w – действительное число, не равное 0

- однородные координаты

Небольшой трюк: Если w равно единице, то всё что нужно для перевода, это перенести компоненты x , y и z и приписать единицу в последний компонент. То есть получить матрицу-строку:

Несколько слов о нуле в качестве w . С точки зрения однородных координат это вполне допустимо. Однородные координаты позволяют различать точки и вектора. В декартовой же системе координат такое разделение невозможно.

- точка, где (x, y, z ) – декартовы координаты

- вектор, где (x, y, z ) – радиус-вектор

Обратный перевод вершины из однородных координат в декартовы координаты осуществляется следующим образом. Все компоненты матрицы-строки необходимо разделить на последнюю компоненту. Другими словами:

- однородные координаты
- декартовы координаты

Главное что необходимо знать, что все алгоритмы OpenGL по отсечению и растеризации работают в декартовых координатах, но перед этим все преобразования производятся в однородных координатах. Переход от однородных координат в декартовы координаты осуществляется аппаратно.

Канонический объём отсечения или Canonic view volume (CVV) – это одна из мало документированных частей OpenGL. Как видно из рис. 1 CVV – это выровненный по осям куб с центром в начале координат и длиной ребра равной двойке. Всё, что попадает в область CVV подлежит растеризации, всё, что находится вне CVV игнорируется. Всё, что частично выходит за границы CVV, подлежит алгоритмам отсечения. Самое главное что надо знать - система координат CVV левосторонняя!


Рис. 1. Канонический объём отсечения OpenGL (CVV)

Левосторонняя система координат? Как же так, ведь в спецификации к OpenGL 1.0 ясно написано, что используемая система координат правосторонняя? Давайте разбираться.


Рис. 2. Системы координат

Как видно из рис. 2 системы координат различаются лишь направлением оси Z . В OpenGL 1.0 действительно используется правосторонняя пользовательская система координат. Но система координат CVV и пользовательская система координат это две совершенно разные вещи. Более того, начиная с версии 3.3, больше не существует такого понятия как стандартная система координат OpenGL. Как упоминалось ранее, программист сам реализует модуль матричных операций. Формирование матриц вращения, формирование проекционных матриц, поиск обратной матрицы, умножение матриц – это минимальный набор операций, входящих в модуль матричных операций. Возникает два логичных вопроса. Если объём видимости это куб с длиной ребра равной двум, то почему сцена размером в несколько тысяч условных единиц видна на экране? В какой момент происходит перевод пользовательской системы координат в систему координат CVV. Проекционные матрицы – это как раз та сущность, которая занимается решением этих вопросов.

Главная мысль вышеизложенного – разработчик сам волен выбрать тип пользовательской системы координат и должен корректно описать проекционные матрицы. На этом с фактами об OpenGL закончено и подошло время сводить всё воедино.

Одна из наиболее распространённых и сложно постигаемых матриц – это матрица перспективного преобразования. Так как же она связана с CVV и пользовательской системой координат? Почему объекты с увеличением расстояния до наблюдателя становятся меньше? Для того чтобы понять почему объекты уменьшаются с увеличением расстояния, давайте рассмотрим матричные преобразования трёхмерной модели шаг за шагом. Не секрет, что любая трёхмерная модель состоит из конечного списка вершин, которые подвергаются матричным преобразованиям совершенно независимо друг от друга. Для того чтобы определить координату трёхмерной вершины на двухмерном экране монитора необходимо:

  1. Перевести декартову координату в однородную координату;
  2. Умножить однородную координату на модельную матрицу;
  3. Результат умножить на видовую матрицу;
  4. Результат умножить на проекционную матрицу;
  5. Результат перевести из однородных координат в декартовы координаты.
Перевод декартовой координаты в однородную координату обсуждался ранее. Геометрический смысл модельной матрицы заключается в том, чтобы перевести модель из локальной системы координат в глобальную систему координат. Или как говорят, вынести вершины из модельного пространства в мировое пространство. Скажем проще, загруженный из файла трёхмерный объект находится в модельном пространстве, где координаты отсчитываются относительно самого объекта. Далее с помощью модельной матрицы производится позиционирование, масштабирование и поворот модели. В результате все вершины трёхмерной модели получают фактические однородные координаты в трёхмерной сцене. Модельное пространство относительно мирового пространства является локальным. Из модельного пространства координаты выносятся в мировое пространство (из локального в глобальное). Для этого используется модельная матрица.

Теперь переходим к шагу три. Здесь начинает работу видовое пространство. В этом пространстве координаты отсчитываются относительно положения и ориентации наблюдателя так, как если бы он являлся центром мира. Видовое пространство является локальным относительно мирового пространства, поэтому координаты в него надо вносить (а не выносить, как в предыдущем случае). Прямое матричное преобразование выносит координаты из некоторого пространства. Чтобы наоборот внести их в него, надо матричное преобразование инвертировать, поэтому видовое преобразование описывается обратной матрицей. Как же получить эту обратную матрицу? Для начала получим прямую матрицу наблюдателя. Чем характеризуется наблюдатель? Наблюдатель описывается координатой, в которой он находится, и векторами направления обзора. Наблюдатель всегда смотрит в направлении своей локальной оси Z . Наблюдатель может перемещаться по сцене и осуществлять повороты. Во многом это напоминает смысл модельной матрицы. По большому счёту так оно и есть. Однако, для наблюдателя операция масштабирования бессмысленна, поэтому между модельной матрицей наблюдателя и модельной матрицей трёхмерного объекта нельзя ставить знак равенства. Модельная матрица наблюдателя и есть искомая прямая матрица. Инвертировав эту матрицу, мы получаем видовую матрицу. На практике это означает, что все вершины в глобальных однородных координатах получат новые однородные координаты относительно наблюдателя. Соответственно, если наблюдатель видел определённую вершину, то значение однородной координаты z данной вершины в видовом пространстве точно будет положительным числом. Если вершина находилась за наблюдателем, то значение её однородной координаты z в видовом пространстве точно будет отрицательным числом.

Шаг четыре - это самый интересный шаг. Предыдущие шаги были рассмотрены так подробно намеренно, чтобы читатель имел полную картину о всех операндах четвёртого шага. На четвёртом шаге однородные координаты выносятся из видового пространства в пространство CVV. Ещё раз подчеркивается тот факт, что все потенциально видимые вершины будут иметь положительное значение однородной координаты z .

Рассмотрим матрицу вида:

И точку в однородном пространстве наблюдателя:

Произведём умножение однородной координаты на рассматриваемую матрицу:

Переведём получившиеся однородные координаты в декартовы координаты:

Допустим, есть две точки в видовом пространстве с одинаковыми координатами x и y , но разными координатами z . Другими словами одна из точек находится за другой. Из-за перспективного искажения наблюдатель должен увидеть обе точки. Действительно, из формулы видно, что из-за деления на координату z , происходит сжатие к точке начала координат. Чем больше значение z (чем дальше точка от наблюдателя), тем сильнее сжатие. Вот и объяснение эффекту перспективы.

В спецификации OpenGL сказано, что операции по отсечению и растеризации выполняются в декартовых координатах, а процесс перевода однородных координат в декартовы координаты производится автоматически.

Матрица (1) является шаблоном для матрицы перспективой проекции. Как было сказано ранее, задача матрицы проекции заключается в двух моментах: установка пользовательской системы координат (левосторонняя или правосторонняя), перенос объёма видимости наблюдателя в CVV. Выведем перспективную матрицу для левосторонней пользовательской системы координат.

Матрицу проекции можно описать с помощью четырёх параметров (рис. 3):

  • Угол обзора в радианах (fovy );
  • Соотношение сторон (aspect );
  • Расстояние до ближней плоскости отсечения (n );
  • Расстояние до дальней плоскости отсечения (f ).


Рис. 3. Перспективный объём видимости

Рассмотрим проекцию точки в пространстве наблюдателя на переднюю грань отсечения перспективного объёма видимости. Для большей наглядности на рис. 4 изображён вид сбоку. Так же следует учесть, что пользовательская система координат совпадает с системой координат CVV, то есть везде пользуется левосторонняя система координат.


Рис. 4. Проецирование произвольной точки

На основании свойств подобных треугольников справедливы следующие равенства:

Выразим yꞌ и xꞌ:

В принципе, выражений (2) достаточно для получения координат точек проекции. Однако для правильного экранирования трёхмерных объёктов необходимо знать глубину каждого фрагмента. Другими словами необходимо хранить значение компоненты z . Как раз это значение используется при тестах глубины OpenGL. На рис. 3 видно, что значение zꞌ не подходит в качестве глубины фрагмента, потому что все проекции точек умеют одинаковое значение zꞌ . Выход из сложившейся ситуации – использование так называемой псевдоглубины.

Свойства псевдоглубины:

  1. Псевдоглубина рассчитывается на основании значения z ;
  2. Чем ближе к наблюдателю находится точка, тем меньшеe значение имеет псевдоглубина;
  3. У всех точек, лежащих на передней плоскости объёма видимости, значение псевдоглубины равно -1;
  4. У всех точек, лежащих на дальней плоскости отсечения объёма видимости, значение псевдоглубины равно 1;
  5. Все фрагменты, лежащие внутри объёма видимости, имеют значение псевдоглубины в диапазоне [-1 1].
Давайте выведем формулу, по которой будет рассчитываться псевдоглубина. В качестве основы возьмём следующее выражение:

Коэффициенты a и b необходимо вычислить. Для того чтобы это сделать, воспользуемся свойствами псевдоглубины 3 и 4. Получаем систему из двух уравнений с двумя неизвестными:

Произведём сложение обоих частей системы и умножим результат на произведение fn , при этом f и n не могут равняться нулю. Получаем:

Раскроем скобки и перегруппируем слагаемые так, чтобы слева осталась только часть с а , а справа только с b :

Подставим (6) в (5). Преобразуем выражение к простой дроби:

Умножим обе стороны на -2fn , при этом f и n не могут равняться нулю. Приведём подобные, перегруппируем слагаемые и выразим b :

Подставим (7) в (6) и выразим a :

Соответственно компоненты a и b равны:

Теперь подставим полученные коэффициенты в матрицу заготовку (1) и проследим, что будет происходить с координатой z для произвольной точки в однородном пространстве наблюдателя. Подстановка выполняется следующим образом:

Пусть расстояние до передней плоскости отсечения n равно 2, а расстояние до дальней плоскости отсечения f равно 10. Рассмотрим пять точек в однородном пространстве наблюдателя:

Взаимное расположение точки и объёма видимости
Точка Значение Описание
1 1 Точка находится перед передней плоскостью отсечения объёма видимости. Не проходит растеризацию.
2 2 Точка находится на передней грани отсечения объёма видимости. Проходит растеризацию.
3 5 Точка находится между передней гранью отсечения и дальней гранью отсечения объёма видимости. Проходит растеризацию.
4 10 Точка находится на дальней грани отсечения объёма видимости. Проходит растеризацию.
5 20 Точка находится за дальней гранью отсечения объёма видимости. Не проходит растеризацию.

Умножим все точки на матрицу (8), а затем переведём полученные однородные координаты в декартовые координаты . Для этого нам необходимо вычислить значения новых однородных компонент и .
Точка 1:

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

С координатой z разобрались, перейдём к координатам x и y . Как говорилось ранее весь перспективный объём видимости должен умещаться в CVV. Длина ребра CVV равна двум. Соответственно, высоту и ширину перспективного объёма видимости надо сжать до двух условных единиц.

В нашем распоряжении имеется угол fovy и величина aspect . Давайте выразим высоту и ширину, используя эти величины.


Рис. 5. Объём видимости

Из рис. 5 видно, что:

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

На этом вывод матриц закончен.

Пару слов о DirectX - основном конкуренте OpenGL. DirectX отличается от OpenGL только габаритами CVV и его позиционированием. В DirectX CVV - это прямоугольный параллелепипед с длинами по осям x и y равными двойке, а по оси z длина равна единице. Диапазон x и y равен [-1 1], а диапазон z равен . Что касается системы координат CVV, то в DirectX, как и в OpenGL, используется левосторонняя система координат.

Для вывода перспективных матриц для пользовательской правосторонней системы координат необходимо перерисовать рис. 2, рис.3 и рис.4 с учётом нового направления оси Z . Далее расчёты полностью аналогичны, с точностью до знака. Для матриц DirectX свойства псевдоглубины 3 и 4 модифицируются под диапазон .

На этом тему перспективных матриц можно считать закрытой.

Двигатель не двигает корабль.
Корабль остается на месте, а
двигатели двигают вселенную
вокруг него.

Футурама

Это один из самых важных уроков. Вдумчиво прочитайте его хотя бы восемь раз.

Гомогенные координаты

В предыдущих уроках мы предполагали, что вершина расположена по координатам (x, y, z). Давайте-ка добавим еще одну координату – w. Отныне вершины у нас будут по координатам (x, y, z, w)

Вскоре вы поймете, что к чему, но пока примите это как данность:

  • Если w==1, тогда вектор (x,y,z,1) – это позиция в пространстве
  • Если w==0, тогда вектор (x,y,z,0) – это направление.

Запомните это как аксиому без доказательств!!!

И что это нам дает? Ну, для вращения ничего. Если вы вращаете точку или направление, то получите один и тот же результат. Но если вы вращаете перемещение(когда вы двигаете точку в определенном направлении), то все кардинально меняется. А что значит «переместить направление»? Ничего особенного.

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

Матрицы Трансформаций

Введение в матрицы

Если по простому, то матрица, это просто массив чисел с фиксированным количеством строк и столбцов.

Например, матрица 2 на 3 будет выглядеть так:

В 3д графике мы пользуемся почти всегда матрицами 4х4. Это позволяет нам трансформировать наши (x,y,z,w) вершины. Это очень просто – мы умножаем вектор позиции на матрицу трансформации.

Матрица*Вершину = трансформированная вершина

Все не так страшно как выглядит. Укажите пальцем левой руки на a, а пальцем правой руки на x. Это будет ax. Переместите левый палец на следующее число b, а правый палец вниз на следующее число – y. У нас получилось by. Еще раз – cz. И еще раз – dw. Теперь суммируем все получившиеся числа – ax+by +cz +dw . Мы получили наш новый x. Повторите то же самое для каждой строки и вы получите новый вектор (x,y,z,w).

Однако это довольно скучная операция, так что пусть её за нас будет выполнять компьютер.

В С++ при помощи библиотеки GLM:

glm::mat4 myMatrix;


glm::vec4 myVector;



glm :: vec 4 transformedVector = myMatrix * myVector ; // Не забываем про порядок!!! Это архиважно!!!

В GLSL:

mat4 myMatrix;


vec4 myVector;


// заполняем матрицу и вектор нашими значениями… это мы пропускаем


vec 4 transformedVector = myMatrix * myVector ; // точно так же как и в GLM

(Чего-то мне кажется, что вы не скопировали этот кусок кода к себе в проект и не попробовали…ну же, попробуйте, это интересно!)

Матрица перемещений

Матрица перемещения, это, наверное, самая простая матрица из всех. Вот она:


Тут X, Y, Z – это значения, которые мы хотим добавить к нашей позиции вершины.

Итак, если нам нужно переместить вектор (10,10,10,1) на 10 пунктов, по позиции Х, то:
(Попробуйте это сами, ну пожаааалуйста!)

…И у нас получится (20,10,10,1) в гомогенном векторе. Как вы, я надеюсь, помните, 1 значит, что вектор представляет собой позицию, а не направление.

А теперь давайте попробуем таким же образом трансформировать направление (0,0,-1,0):

И в итоге у нас получился тот же вектор (0,0,-1,0).
Как я и говорил, двигать направление не имеет смысла.

Как же нам закодить это?

В С++ при помощи GLM:

#include // после


glm::mat4 myMatrix = glm::translate(10.0f, 0.0f, 0.0f );


glm::vec4 myVector(10.0f, 10.0f, 10.0f, 0.0f );


glm :: vec 4 transformedVector = myMatrix * myVector ; // и какой у нас получится результат?


А в GLSL: В GLSL так редко кто делает. Чаще всего с помощью функции glm::translate(). Сначала создают матрицу в С++, а затем отправляют её в GLSL, и уже там делают лишь одно умножение:

vec4 transformedVector = myMatrix * myVector;

Единичная матрица(Identity Matrix)

Это специальная матрица. Она не делает ничего. Но я упоминаю её, так как важно знать, что умножение A на 1.0 в результате дает А:

glm::mat4 myIdentityMatrix = glm::mat4(1.0f);

Матрица Масштабирования

Матрица масштабирования так же достаточно проста:

Поэтому если вам хочется увеличить вектор(позицию или направление, не важно) в два раза по всем направлениям:

А координата w не поменялась. Если вы спросите: «А что такое масштабирование направления?». Полезно не часто, но иногда полезно.

(заметьте, что масштабирование единичной матрицы с (x,y,z) = (1,1,1))

С++:

// Используйте #include и #include


glm::mat4 myScalingMatrix = glm::scale(2.0f, 2.0f ,2.0f);

Матрица Вращения

А вот эта матрица достаточно сложная. Поэтому я не буду останавливаться на подробностях её внутренней реализации. Если сильно хочется, лучше почитайте (Matrices and Quaternions FAQ)

В С ++:

// Используйте #include и #include


glm::vec3 myRotationAxis(??, ??, ??);


glm::rotate(angle_in_degrees, myRotationAxis);

Совмещенные Трансформации

Теперь мы знаем как вращать, перемещать и масштабировать наши вектора. Хорошо бы узнать, как объединить все это. Это делается просто умножением матриц друг на друга.

TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector;

И снова порядок!!! Сначала нужно изменить размер, потом прокрутить и лишь потом сдвинуть.

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

  • Сделайте шаг вперед(не свалите компьютер со стола) и повернитесь влево
  • Повернитесь влево и сделайте один шаг вперед.

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

Не правильный способ:

  • Перемещаем корабль на (10,0,0). Его центр теперь на 10 по Х от центра.
  • Увеличиваем размер нашего корабля в 2 раза. Каждая координата умножается на 2 относительно центра который далеко… И в итоге у нас получается корабль необходимого размера но по позиции 2*10=20. Что не совсем то, чего мы хотели.

Правильный способ:

  • Увеличиваем размер корабля в 2 раза. Теперь у нас есть большой корабль расположенный по центру.
  • Перемещаем корабль. Размер корабля не изменился и он расположен в нужном месте.
Умножение матрицы на матрицу выполняется почти так же, как и умножение матрицы на вектор. Не будем вдаваться в подробности, а любопытствующие пусть почитают это из специализированных источников. Мы же просто будем полагаться на библиотеку GLM.
ВС++:
glm::mat4 myModelMatrix = myTranslationMatrix * myRotationMatrix * myScaleMatrix;
glm::vec4 myTransformedVector = myModelMatrix * myOriginalVector;
В GLSL:
mat4 transform = mat2 * mat1;
vec4 out_vec = transform * in_vec;

Матрицы Модели, Вида и Проекции

Для иллюстраций предполагаем, что мы уже умеем рисовать в OpenGL любимую 3д модель программы Blender – голову обезьяны Сюзанны.

Матрицы Модели, Вида и Проекции очень удобный метод разделения трансформаций. Если сильно хочется, вы можете не использовать их(мы же не использовали их в уроках 1 и 2). Но я настойчиво рекомендую вам пользоваться ими. Просто почти все 3д библиотеки, игры итд используют их для разделения трансформаций.

Матрица Модели

Данная модель, как и наш любимый треугольничек, задана набором вершин. X, Y, Z координаты заданы относительно центра объекта. Так вот, если вершина расположена по координатам (0,0,0), то она находится в центре всего объекта

Теперь мы имеем возможность двигать нашу модель. Например, потому, что пользователь управляет ей с помощью клавиатуры и мыши. Это сделать очень просто: масштабирование*вращение*перемещение и все. Вы применяете вашу матрицу ко всем вершинам в каждом кадре(в GLSL а не в C++) и все перемещается. Все что не перемещается – расположено в центре «мира».

Вершины находятся в мировом пространстве Черная стрелка на рисунке показывает, как мы переходим из пространства модели, в мировое пространство(Все вершины были заданы относительно центра модели, а стали заданы относительно центра мира)

Эту трансформацию можно отобразить следующей диаграммой:


Матрица Вида

Давайте еще раз прочитаем цитату из футурамы:

«Двигатель не двигает корабль. Корабль остается на месте, а двигатели двигают вселенную вокруг него.»


То же самое можно применить и к фотоаппарату. Если вы хотите сфотографировать гору под каким-нибудь углом, то можно передвинуть фотокамеру…или гору. В реальной жизни это невозможно, но очень легко и удобно в компьютерной графике.

По умолчанию наша камера находится в центре Мировых Координат. Чтобы двигать наш мир нужно создать новую матрицу. К примеру нам нужно переместить нашу камеру на 3 единицы вправо(+Х). Это то же самое, что переместить весь мир на 3 единицы влево(-Х). И пока ваши мозги плавятся, давайте попробуем:


// Используйте #include и #include


glm::mat4 ViewMatrix = glm::translate(-3.0f, 0.0f ,0.0f);

Картинка ниже демонстрирует это: мы переходим от Мирового пространства(все вершины заданы относительно центра мира как мы это делали в предыдущей секции) к пространству камеры(все вершины заданы относительно камеры).

И прежде чем ваша голова совсем взорвется, посмотрите на прекрасную функцию из нашей старой доброй GLM:

glm::mat4 CameraMatrix = glm::LookAt(


cameraPosition , // Позиция камеры в мировых координатах


cameraTarget , // точка на которую мы хотим посмотреть в мировых координатах


upVector // скорее всего glm :: vec 3(0,1,0), а (0,-1,0) будет все показывать вверх ногами, что иногда тоже прикольно.


Вот иллюстрация к вышесказанному:


Но к нашей радости, это еще не все.

Матрица Проекции

Сейчас мы имеем координаты в пространстве камеры. Это значит, что после всех этих трансформаций, вершина которой посчастливилось оказаться в x==0 и y==0 будет отрендерена в центре экрана. Но мы же не можем пользоваться лишь координатами X,Y, чтобы понять куда рисовать вершину: дистанция к камере(Z) должна тоже учитываться! Если у нас есть две вершины, то одна из них будет более ближе к центру экрана чем другая, так как у неё больше координата Z.

Это называется перспективная проекция:


И к большому счастью для нас, матрица 4х4 может представлять собой и перспективные трансформации:

glm::mat4 projectionMatrix = glm::perspective(


FoV , // Горизонтальное Поле Вида в градусах. Или величина приближения . Как будто « линза » на камере . Обычно между 90(суперширокий, как рыбий глаз) и 30(как небольшая подзорная труба)


4.0 f / 3.0 f , // Соотношение сторон. Зависит от размера вашего окна. Например, 4/3 == 800/600 == 1280/960, знакомо, не правда ли?


0.1 f , // Ближнее поле отсечения. Его нужно задавать как можно большим, иначе будут проблемы с точностью.


100.0 f // Дальнее поле отсечения. Нужно держать как можно меньшим.


);

Повторим то что мы сейчас сделали:

Мы ушли от пространства камеры(все вершины заданы в координатах относительно камеры) в гомогенное пространство(все вершины в координатах маленького куба(-1,1). Все что находится в кубе – находится на экране.)

И финальная диаграмма:


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

После умножения на проекционную матрицу у нас выходит следующее:

На предыдущей картинке поле вида превратилось в идеальный куб(с координатами вершин от -1 до 1 по всем осям.), а все объекты деформированы в перспективе. Все голубые объекты которые близко к камере – стали большими, а которые дальше – маленькими. Так же как и в жизни!

Вот какой вид у нас открывается из «объектива»:

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

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

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

На фотографиях, картинах, экране изображения кажутся нам естественными и правильными. Эти изображения называют перспективными. Свойства их таковы, что более удаленные предметы изображаются в меньших масштабах, параллельные прямые в общем случае непараллельны. В итоге геометрия изображения оказывается достаточно сложной, и по готовому изображению сложно определить размер тех или иных частей объекта.

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

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

Первые две системы могут использоваться в многомерных системах координат, но последняя только в двухмерной. Операции являются необратимыми, то есть из двухмерной картинки-проекции невозможно восстановить трехмерное изображение.

В этой матрице элементы a , d , е отвечают за масштабирование, m , n , L - за смещение, p , q , r - за проецирование, s - за комплексное масштабирование, х - за вращение.

Одноточечное проецирование на плоскость z = 0

Суть этого проецирования такова: чем глубже находится предмет, тем больше становится значение z-координаты и знаменателя rz + 1, и, следовательно, тем мельче выглядит предмет на плоскости проекции. Выполним несложные выкладки и поясним их графически:
уравнение x"/F = x/(F + z пр) равносильно: x" = xF/(F + z пр) = x/(1 + z пр /F) = x/(1 + rz пр), где r = 1/F, F - фокус.

Для того, чтобы точки, лежащие на линии, параллельной оси z , не терялись друг за другом, используется одноточечное проецирование на линию (см. матрицу преобразования и рис. 4.2 ); исчезла z-координата, но, поскольку дальние предметы стали более мелкими, чем такие же близкие, у зрителя появляется ощущение глубины. Запомните: это первый способ передачи глубины на плоскости!

Для того, чтобы вращать объекты (или камеру), необходима серьезная математическая база, с помощью которой будут расчитываться координаты всех объектов при выводе на "плоский" экран компьютера. Сразу хочу сказать, что не стоит пугаться, все математические библиотеки уже написаны за нас, мы будем их только использовать. В любом случае, следующий текст пропускать не нужно, независимо от уровня знаний математики.

1. Матрицы, общие понятия

Что такое матрицы? Вспоминаем высшую математику: матрица ¬- это набор чисел с заранее известной размерностью строк и столбцов.

Матрицы можно складывать, умножать на число, перемножать друг с другом и много еще чего интересного, но этот момент мы пропустим, т.к. он достаточно подробно изложен в любом учебнике по высшей математике (учебники можно поискать на google.com). Мы будем пользоваться матрицами как программисты, мы их заполняем и говорим, что с ними делать, все расчеты произведет математическая библиотека Direct3D, поэтому нужно включить в проект заголовочный модуль d3dx9.h (и библиотеку d3dx9.lib).

Наша задача - создать объект, т.е. заполнить матрицу координатами вершин объекта. Каждая вершина - это вектор (X, Y, Z) в трехмерном пространстве. Теперь, чтобы произвести какое-то действие, нужно взять наш объект (то есть матрицу) и умножить на матрицу преобразования, результат этой операции - новый объект, заданный в виде матрицы.

В Direct3D определены и используются три основные матрицы: мировая матрица, матрица вида и матрица проекции. Рассмотрим их подробнее.

Мировая матрица (World Matrix) - позволяет производить вращение, трансформацию и масштабирование объекта, а также наделяет каждый из объектов своей локальной системой координат.

Функции для работы с мировой матрицей:

  • D3DXMatrixRotationX(), D3DXMatrixRotationY(), D3DXMatrixRotationZ() - вращение точки относительно одной из осей;
  • D3DXMatrixTranslation() - перемещение точки в другое положение;
  • D3DXMatrixScale() - масштабирование.

    Матрица вида (View Matrix) - определяет местоположение камеры просмотра сцены и может состоять из любых комбинаций трансляции и вращения.
    D3DXMatrixLookAtLH()и D3DXMatrixLookAtRH() определяет положение камеры и угла просмотра для левостороней и правостороней систем координат соответственно.

    Матрица проекции (Projection Matrix) - создает проекцию 3D сцены на экран монитора. С ее помощью объект трансформируется, начало координат переносится в переднюю часть, а также определяется передняя и задняя плоскости отсечения.

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

    2. Создание объекта

    Создаем новый проект, аналогично первому. Прежде чем продолжить усложнять наш код, разобьем его на части для лучшей читаемости кода. Наш проект логично разделить на три составляющие:
    1. Окно Windows (инициализация окна, сообщения, …)
    2. Инициализация 3D (загрузка координат объектов, удаление ресурсов, …)
    3. Рендер сцены (матрицы, рисование примитивов, …)
    В результате у нас будет 3 файла - window.cpp, init3d.h, render.h с таким содержанием: init3d.h - переносим глобальный переменные и структуры, объявление функций, функции InitDirectX(), InitBufferVertex(), Destroy3D() render.h - переносим функцию RenderScene() все, что осталось, касается главного окна, это будет файл - window.cpp .

    Добавляем заголовочный файл и библиотеку для использования матричных функций

    #include // или C:\DXSDK\Include\d3dx9.h #pragma comment(lib, "d3dx9.lib") //или C:\\DXSDK\\Lib\\d3dx9.lib

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

    #include

    Изменим формат представления вершин:

    #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ/D3DFVF_DIFFUSE) struct CUSTOMVERTEX { FLOAT x, y, z; DWORD color; };

    Будем использовать не преобразованный тип вершин, т.к. преобразования будем делать матрицами.
    Изменяем код функции InitDirectX(). В эту функцию необходимо добавить установку двух режимов отображения.
    Отключаем режим отсечения для того, чтобы при вращении можно было видеть все стороны объекта:

    PDirectDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

    На данный момент мы не пользуемся освещением, а закрашиваем вершины в определенный цвет, поэтому отключаем освещение:

    PDirectDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

    Упростим наше сердце, представив его в виде трех треугольников. Будем использовать локальную систему координат.


    CUSTOMVERTEX stVertex= { { -1.0f, 0.5f, 0.0f, 0x00ff0000 }, { -0.5f, 1.0f, 0.0f, 0x00ff0000 }, { 0.0f, 0.5f, 0.0f, 0x00ff0000 }, { 0.0f, 0.5f, 0.0f, 0x000000ff }, { 0.5f, 1.0f, 0.0f, 0x000000ff }, { 1.0f, 0.5f, 0.0f, 0x000000ff }, { -1.0f, 0.5f, 0.0f, 0x0000ff00 }, { 1.0f, 0.5f, 0.0f, 0x0000ff00 }, { 0.0f, -1.0f, 0.0f, 0x0000ff00 }, };

    3. Создание матриц преобразования

    Напишем в файле render.h функцию SetupMatrix() в которой будут происходить все действия над матрицами.

    Создадим матрицы:

  • D3DXMATRIX MatrixWorld; - мировая матрица
  • D3DXMATRIX MatrixView; - матрица вида
  • D3DXMATRIX MatrixProjection; - матрица проекции
    Установка мировой матрицы

    Для того, чтобы объект вращался, необходимо получить системное время и каждое "мгновение" изменять угол между локальной системой координат и мировой ситемой координат. Вращать будем относительно оси Х, поэтому используем функцию D3DXMatrixRotationX. После расчета мировой матрицы необходимо применить ее значения с помощью функции SetTransform:

    UINT iTime=timeGetTime()%5000; FLOAT fAngle=iTime*(2.0f*D3DX_PI)/5000.0f; D3DXMatrixRotationX(&MatrixWorld, fAngle); pDirectDevice->SetTransform(D3DTS_WORLD, &MatrixWorld); Установка матрицы вида

    Устанавливаем камеру в нужном месте и направляем ее на объект

  • D3DXMatrixLookAtLH(&MatrixView, - результат выполнения функции
  • &D3DXVECTOR3(0.0f, 0.0f, -8.0f), - точка, в которой находится камера
  • &D3DXVECTOR3(0.0f, 0.0f, 0.0f), - точка, в которую мы смотрим
  • &D3DXVECTOR3(0.0f, 1.0f, 0.0f)); - верх объекта

    После расчета необходимо применить полученные значения.

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

    На фотографиях, картинах, экране изображения кажутся нам естественными и правильными. Эти изображения называют перспективными. Свойства их таковы, что более удаленные предметы изображаются в меньших масштабах, параллельные прямые в общем случае непараллельны. В итоге геометрия изображения оказывается достаточно сложной, и по готовому изображению сложно определить размер тех или иных частей объекта.

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

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

    Первые две системы могут использоваться в многомерных системах координат, но последняя только в двухмерной. Операции являются необратимыми, то есть из двухмерной картинки-проекции невозможно восстановить трехмерное изображение.

    Матрица общего перспективного преобразования

    В этой матрице элементы a , d , е отвечают за масштабирование, m , n , L — за смещение, p , q , r — за проецирование, s — за комплексное масштабирование, х — за вращение.