- PVSM.RU - https://www.pvsm.ru -
В прошлом уроке [1] мы научились рисовать треугольник и работать с шейдерами. В этом уроке мы поговорим о важнейшей части OpenGL, да и вообще компьютерной графики. О матрицах.
Всех заинтересовавшихся, прошу под кат.
КДПВ
Базовые уроки:
Продвинутые уроки:
Всякое:
Двигатели не двигают корабль. Корабль остается на месте, это вселенная движется вокруг корабля.
Футурама
Это самый важный урок из всех. Прочтите его как минимум восемь раз.
До этого времени мы работали только с трехкомпонентными вершинами, такими как (x, y, z). Давайте введем w. Теперь мы будем работать с (x, y, z, w) векторами.
Скоро станет яснее, а пока запомните следующие аксиомы:
Так какая разница? Что же, для вращений ничего не поменялось. Когда вы вращаете точку или направление, Вы получаете один и тот же результат. А вот для перемещения, изменения появились. Что означает «подвинуть направление»? Немного бессмысленно.
Однородныекоординаты позволяют нам работать с обоими случаями векторов, используя одну математическую формулу.
Матрица — это просто набор чисел с заранее известным количеством строк и столбцов. К примеру матрица 2х3 будет выглядеть так:
В трехмерной графике чаще всего используют матрицы 4x4. Они позволяют нам трансформировать наши 4х компонентные вершины. Делается это с помощью умножения матрицы на вершину.
Последовательность умножений важна! Матрица * Вершину = ТрансформированнаяВершина
Не так страшен черт как его малюют. Приложите левый указательный палец на «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 единиц по оси X, мы получим:
… и в результате мы получили преобразованный однородный вектор (20, 10, 10, 1)! Помните, w == 1 означает, что это позиция, а не направление. Если мы попробуем применить эту матрицу к направлению — то ничего не изменится (что хорошо):
Давайте проверим это. Что произойдет с вектором, который прдставляет направление по оси -z: (0, 0, -1, 0)
… и мы получили наше начальное направление. И это абсолютно правильно, поскольку как я и говорил ранее: движение направления смысла не имеет.
Но все таки как же нам производить перемещение в коде?
В С++ с помощью GLM:
#include <glm/gtx/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:
vec4 transformedVector = myMatrix * myVector;
Фактически Вы почти никогда не будете делать этого в GLSL. Чаще всего вы будете производить перемещение в C++ коде с помощью glm::translate и результат отправлять в GLSL.
Эта матрица особенная. Она ничего не делает. Но я упомину ее, поскольку важно понимать, что умножение A на 1.0 дает A.
В С++ с помощью GLM:
glm::mat4 myIdentityMatrix = glm::mat4(1.0f);
Матрица масштабирования тоже довольно проста:
К примеру если вы хотите масштабировать вектор (не важно, позицию или направление) на 2 во всех направлениях:
И вектор W опять не изменился. Вы можете спросить: а какой смысл в «направлении масштабирования»? Что же, чаще всего особого смысла это не несет. Но в некоторых, довольно редких случаях, это может быть полезно.
(Заметьте, что единичная матрица — это частный случай матрицы масштабирования)
В С++ с помощью GLM:
// #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 [3] (довольно популярный ресурс. Скорее всего есть на вашем языке. Хотя я на русском найти не смог). Так же вы можете взглянуть на 17 урок.
В С++ с помощью GLM:
// #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;
!!! ВАЖНО !!! Именно в таком порядке! В НАЧАЛЕ масштабирование, ЗАТЕМ вращение и в КОНЦЕ перемещение. Так работают матрицы!
Использование другого порядка не дадут такой же результат. Попробуйте сами:
Собственно, последовательность, представленная ниже обычно используется игровыми персонажами и другими элементами. В начале их масштабируют, потом вращают (устанавливают направление), затем перемещают. Для примера, дана модель корабля (вращение было убрано для упрощения).
Не правильный порядок:
Правильный порядок:
Умножение матрица-матрица похоже на умножение матрица-вектор, так что я опять опущу некоторые детали и отправлю вас к Matrices and Quaternions FAQ [4] за деталями.
А теперь просто попросим компьютер произвести все умножения:
В C++ с помощью GLM:
glm::mat4 myModelMatrix = myTranslationMatrix * myRotationMatrix * myScaleMatrix;
glm::vec4 myTransformedVector = myModelMatrix * myOriginalVector;
В шейдере GLSL:
mat4 transform = mat2 * mat1;
vec4 out_vec = transform * in_vec;
До конца этого урока мы предположим, что мы знаем, как нарисовать любимую трехмерную модель Blender: обезьяну Suzanne
Матрицы модели, вида и проекции — это инструментарий, который позволяет разделить трансформации. Вы можете не использовать их (по крайней мере мы не использовали их в 1 и 2 уроках). Но вам стоило бы. Так делают все, потому что это самый простой способ.
Эта модель, так же как и наш треугольник описывается набором вершин. X, Y и Z координаты этих вершин объявлены относительно центра объекта. То есть если у вершины координаты (0, 0, 0), то она в центре объекта.
Мы бы хотели иметь возможность двигать эту модель, как минимум по причине пользовательского ввода с клавиатуры и мыши. Расслабьтесь, вы уже знаете, что надо делать: translation * rotation * scale и все. Вы применяете эту матрицу к каждой вершине в каждом кадре (В шейдере GLSL, не в C++!) и все будет движется. А все, что не движется — находится в центре мира.
Ваши вершины теперь расположены в Мировых координатах. Это показывает черная стрелка на следующем изображении: Мы перешли от Координат модели (все вершины определены относительно центра модели) к Мировым координатам (все вершины определены относительно центра мира)
Мы можем подвести все наши действия к следующей диаграмме:
Давайте снова вернемся к футураме:
Двигатели не двигают корабль. Корабль остается на месте, это вселенная движется вокруг корабля.
То же самое применимо и к камерам. Если вы хотете посмотреть на гору под другим углом, вы можете либо подвинуть камеру… либо подвинуть гору. Это не применимо к реальной жизни, зато отлично применимо в компьютерной графике.
По умолчанию ваша камера находится в центре Мировых координат. Для того, что бы подвинуть мир мы введем еще одну матрицу. Давайте представим, что вы хотите сместить камеру на 3 единицы направо (+X). Это эквивалентно сдвигу мира на 3 единицы ВЛЕВО (-X). Пока ваш
// #include <glm/gtc/matrix_transform.hpp> и #include <glm/gtx/transform.hpp>
glm::mat4 ViewMatrix = glm::translate(-3.0f, 0.0f ,0.0f);
Снова, изображение ниже иллюстрирует этот процесс: Мы переходим из Мировых координат (все вершины определены относительно цента мира) к Координатам камеры (все координаты определены относительно камеры).
Пока ваш
glm::mat4 CameraMatrix = glm::lookAt(
cameraPosition, // позиция камеры в мировых координатах
cameraTarget, // куда будет смотреть камера, в мировых координатах
upVector // предположительно glm::vec3(0, 1, 0), но (0, -1, 0) перевернет камеру верх-ногами, что тоже довольно весело.
);
Теперь наша диаграмма выглядит следующим образом:
Но и это еще не все.
Сейчас мы находимся в Координатах камеры. Это означает, что после всех изменений вершины, координаты «x» и «y» которых были равны 0 должны быть отрисованы в центре экрана. Но мы не можем использовать только координаты «x» и «y» для определения позиции обхекта на экране. Ведь координата «z» так же учитывается. Для двух координат с одинаковыми «x» и «y», вершина с большей «z» координатой будет ближе к центру, чем вершина с меньшей «z».
Это называется «перспективной проекцией»:
К счастью матрица 4х4 может отобразить проекцию. (Ну то есть не совсем, но это не важно).
// Генерируем очень сложную для понимания матрицу. Но она также 4х4
glm::mat4 projectionMatrix = glm::perspective(
FoV, // Горизонтальное поле обзора, в градусах: приближение. Читай "линза камеры". [FoV - угловое пространство, видимое глазом при фиксированном взгляде и неподвижной голове. ] Обычно между 90 градусами и 30 градусами.
4.0f / 3.0f, // Соотношение сторон. Зависит от размера окна. Заметьте, что 4 / 3 == 800 / 600 == 1280 / 960. Звучит похоже?
0.1f, // Ближайшая позиция плоскости отсечения. Должна быть как можно больше, иначе получите ошибки округления.
100.0f // Дальняя позиция плоскости отсечения. Должна быть как можно меньше.
);
И в последний раз:
Мы перешли от Координат камеры (все вершины определены относительно камеры) к однородным координатам (все вершины определены в маленьком кубе. Все, что в кубе, находится на экране).
И финальная диаграмма:
Вот другая диаграмма, которая более наглядно показывает преобразования при применении матрицы проекции. До проекции сининие объекты находятся в Координатах камеры и красная фигура показывает сечение камеры: часть сцены, которую камера на самом деле видит.
Умножение всего этого на матрицу проекции дает следующий эффект:
На этом изображении сечение камеры представляет собой идеальный куб (между -1 и 1 по всем осям), а все синие объекты были искажены определенным образом. Также объекты, которые находятся ближе к камере больше, чем объекты находящиеся дальше. Похоже на реальную жизнь!
Давайте посмотрим что видно за сечением камеры:
Вот и наше изображение! Но оно немного слишком квадратное, поэтому применяется еще одна транформация (она применяется автоматически и не требует никаких действий в шейдере), что бы наше изображение было в пору нашему окну:
И вот оно, отрисованное изображение!
… это просто стандартное перемножение матриц, которое вы так полюбили!
// C++ : вычисление матрицы
glm::mat4 MVPmatrix = projection * view * model; // Помните: наоборот !
// GLSL : применение этой матрицы
transformed_vertex = MVP * in_vertex;
// Матрица проекции : 45° ширина обхора, соотношение сторон 4:3, промежуток отображение: 0.1 единиц <-> 100 единиц
glm::mat4 Projection = glm::perspective(glm::radians(45.0f), (float) width / (float)height, 0.1f, 100.0f);
// Или для ортогональной матрицы:
//glm::mat4 Projection = glm::ortho(-10.0f,10.0f,-10.0f,10.0f,0.0f,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) // Верх - сверху (установите на (0, -1, 0), что бы смотреть сверху вниз)
);
// Матрица модели: единичная матрица, поскольку модель будет в центре
glm::mat4 Model = glm::mat4(1.0f);
// Наша ModelViewProjection является умножением 3 матриц
glm::mat4 mvp = Projection * View * Model; // Помните, что матрицы перемножаются в обратном порядке
// Получаем идентификатор для нашей постоянной "MVP"
// Только в процессе инициализации
GLuint MatrixID = glGetUniformLocation(program_id, "MVP");
// Отправляем нашу трансформацию текущему шейдеру в постоянную "MVP"
// Это делается в главном цикле, поскольку для каждой модели своя MVP (по крайней мере M)
glUniformMatrix4fv(mvp_handle, 1, GL_FALSE, &mvp[0][0]);
// Входные данные о вершине. Значение разное при каждом выполнении шейдера.
layout(location = 0) in vec3 vertexPosition_modelspace;
// Значение, остающееся одним для всей модели
uniform mat4 MVP;
void main(){
// Выводим новую позицию вершины: MVP * position
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
}
В шестом уроке Вы научитесь изменять все эти значения динамически с помощью клавиатуры и мыши, но в начале нам надо будет придать нашим моделям немного цвета (Урок 4) и текстур (Урок 5).
Автор: Megaxela
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/opengl/164540
Ссылки в тексте:
[1] прошлом уроке: https://habrahabr.ru/post/306678/
[2] Урок 1. Создание окна: https://habrahabr.ru/post/306650/
[3] Matrices and Quaternions FAQ: http://www.opengl-tutorial.org/assets/faq_quaternions/index.html
[4] Matrices and Quaternions FAQ: http://www.opengl-tutorial.org/assets/faq_quaternions/index.html#Q11
[5] мозг: http://www.braintools.ru
[6] Источник: https://habrahabr.ru/post/306722/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.