Урок 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)
  • Передвиньте матрицу модели, поверните её и измените размер.
  • Сделайте то же, но в другом порядке. Что получается? В каком порядке это лучше всего делать?     

11 комментариев:

  1. Спасибо) Понятно и доходчиво)

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

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

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

    ОтветитьУдалить
  4. Спасибо тебе автор, добрый ты человек! Отличная подача материала.

    ОтветитьУдалить
  5. отличная статья! 10 из 10! Браво!

    ОтветитьУдалить
    Ответы
    1. Ага, токо это спизджено с opengl-tutorial.org

      Удалить
    2. Слово "переведено" подходит больше, но в этом случае не хватает ссылки на источник))

      Удалить
  6. Спасибо, для питона практика не подходит, но понятнее стало))

    ОтветитьУдалить
  7. Большое спасибо за перевод статьи! Отличный материал, очень доходчивый.

    ОтветитьУдалить
  8. Я не мог поверить, что существует настоящий онлайн-кредитор, который может быть таким добрым и честным, как Бенджамин Ли, который предоставил мне ссуду в 2 миллиона евро для выполнения моего проекта, который так долго ждал своего исполнения, но с С помощью офицера Бенджамина все было легко для меня. Я скажу вам связаться с кредитным офицером Бенджамином Ли по адресу 247officedept@gmail.com

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