Урок 3. Матрицы


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



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

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


В предыдущих уроках мы предполагали, что вершина расположена по координатам (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::vec4 transformedVector = myMatrix * myVector; // Не забываем про порядок!!! Это архиважно!!!

В GLSL:
mat4 myMatrix;

vec4 myVector;

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

vec4 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/transform.hpp> // после <glm/glm.hpp>

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

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

glm::vec4 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 <glm/gtc/matrix_transform.hpp> и #include <glm/gtx/transform.hpp>

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


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


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

В С++:
// Используйте #include <glm/gtc/matrix_transform.hpp> и #include <glm/gtx/transform.hpp>

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 <glm/gtc/matrix_transform.hpp> и #include <glm/gtx/transform.hpp>

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

Картинка ниже демонстрирует это: мы переходим от Мирового пространства(все вершины заданы относительно центра мира как мы это делали в предыдущей секции) к пространству камеры(все вершины заданы относительно камеры).
И прежде чем ваша голова совсем взорвется, посмотрите на прекрасную функцию из нашей старой доброй GLM:
glm::mat4 CameraMatrix = glm::LookAt(

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

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

    upVector        // скорее всего glm::vec3(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.0f / 3.0f, // Соотношение сторон. Зависит от размера вашего окна. Например, 4/3 == 800/600 == 1280/960, знакомо, не правда ли?

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

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

);

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


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

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

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

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

И вот, наконец-то у нас отрендереный рисунок!

Совокупная трансформация: матрица МодельВидПроекция
…Это обычное умножение матриц которое вы так любите!
// C++ : вычисление матрицы
glm::mat3 MVPmatrix = projection * view * model; 

// GLSL :применяем координаты
transformed_vertex = MVP * in_vertex;

Все вместе
  •  Шаг первый: создаем МВП матрицу. Это нужно делать отдельно для каждой модели которую вы рендерите на экране.
// Матрица проекции : 45° Угол обзора. 4:3 соотношение, дальность вида : 0.1 единиц <-> 100 единиц
glm::mat4 Projection = glm::perspective(45.0f, 4.0f / 3.0f, 0.1f, 100.0f);
// Матрица камеры
glm::mat4 View = glm::lookAt(
   glm::vec3(4,3,3), // Позиция в  (4,3,3)мировых координат
   glm::vec3(0,0,0), // И смотрит в центр экрана
   glm::vec3(0,1,0)  // Верх камеры смотрит вверх
);
// Матрица модели – единичная матрица. Модель находится в центре мировых координат
glm::mat4 Model = glm::mat4(1.0f);  // Выставляем свое значение для каждой модели!
// НАША МВП : Умножаем все наши три матрицы
glm::mat4 MVP = Projection * View * Model;

  • Шаг второй – передать это все в GLSL
// Получаем хендл МВП чтобы подсунуть в GLSL наш МВП

// Это можно сделать один раз на этапе загрузки

GLuint MatrixID = glGetUniformLocation(programID, "MVP");



// Отправляем нашу матрицу в текущий шейдер,по МВП хендлу

// Это нужно делать каждый раз для каждой новой модели так как у каждой модели своя МВП(ну, по крайней мере, часть М)

 glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);

  • Шаг 3: Используем GLSL чтобы преобразовать вершины
in vec3 vertexPosition_modelspace;

uniform mat4 MVP;

void main(){

    // выходная позиция вершина в экранном пространстве: MVP*положение

    vec4 v = vec4(vertexPosition_modelspace,1); //

    gl_Position = MVP * v;

}
  • Все! Мы имеем тот же самый треугольник что и в уроке 2, все по тем же координатам (0,0,0), но видимый в перспективе из точки (4,3,3) и 45 градусным углом обзора.

В уроке 6 мы изучим как изменять эти значения динамически с помощью клавиатуры и мыши, чтобы создать FPS Камеру…но сначала посмотрим как разукрасить нашу модельку с помощью разных цветов(урок 4) и текстур(урок 5).

Упражнения

  • Поизменяйте параметры glm::perspecitve.
  • Попробуйте использовать ортографическую проекцию вместо перспективной(glm::ortho)
  • Передвиньте матрицу модели, поверните её и измените размер.
  • Сделайте то же, но в другом порядке. Что получается? В каком порядке это лучше всего делать?     

4 комментария:

  1. Лучшее что я читал. Все очень разжевано и понятно. Продолжайте в том же духе!

    ОтветитьУдалить
  2. трансформировать направление (0,0,-1,0)

    Здесь ошибка в результирующей матрице. У Вас единицы по диагонали. А должна быть только одна в третьем ряду

    ОтветитьУдалить