Урок №7. Трансформации в OpenGL

  Дмитрий Бушуев  | 

  Обновл. 25 Апр 2020  | 

 691

 ǀ   1 

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

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

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

Векторы

Векторы — это объекты, задающие направления. Основными характеристиками вектора являются направление и его длина. Вы можете представить себе векторы, как направления на карте сокровищ: «идите влево 10 шагов, теперь идите на север 3 шага и затем направо 5 шагов»; здесь «влево» — это направление, а «10 шагов» — это длина вектора. Таким образом, направления для карты сокровищ содержат 3 вектора. Векторы могут иметь любую размерность, но мы обычно имеем дело с размерностями от 2 до 4. Если вектор имеет 2 измерения, то он представляет собой направление на плоскости (вспомните школьные графики), а когда он имеет 3 измерения, то уже может задавать любое направление в трёхмерном пространстве.

Ниже изображены 3 вектора, каждый из которых представлен парой чисел вида (X,Y) и изображен на 2D-графике в виде стрелки. Поскольку для простоты восприятия векторов их изображают на плоскости (в 2D-пространстве, а не в 3D), то вы можете рассматривать 2D-векторы как 3D-векторы, у которых третья координата Z равна 0. Ещё одна особенность заключается в том, что, так как векторы представляют собой направления, перенос вектора в другую точку не изменяет его величины. На приведенном ниже графике мы видим, что векторы v и w равны, хотя точки, откуда они берут своё начало, различны:

При описании векторов математики обычно предпочитают обозначать их буквой с небольшой горизонтальной чёрточкой сверху, например, v. А также, при использовании векторов в формулах, их обычно записывают в следующем виде:

Поскольку векторы задаются как направления на точки (или другие объекты) в пространстве, иногда бывает трудно наглядно изобразить местоположение данных точек. В таком случае можно поступить следующим образом: просто представьте себе вектор, с началом в точке (0,0,0), указывающим в направлении на некоторую заданную точку. Такой вектор принято называть радиус-вектором (или ещё «вектором позиции»). При этом никто нам не мешает, например, взять другой вектор, начало которого расположено в точке с координатами, отличными от (0,0,0), перенести его (не меняя направления) в точку (0,0,0) и затем сказать: «Этот вектор, например, (3,5), с началом в точке (0,0), указывает на ту точку на графике с координатами (3,5). Таким образом, используя векторы, мы можем описывать направления и положения в 2D и 3D пространстве.

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

Скалярные векторные операции


Скаляр (от лат. «scalaris» = ступенчатый) — это самое обычное математическое число, которое имеет только своё численное значение. При сложении/вычитании/умножении или делении вектора на скаляр мы просто складываем/вычитаем/умножаем или делим каждый элемент вектора на этот скаляр. Для сложения это будет выглядеть так:


Противоположный вектор

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


Сложение и вычитание векторов


Сложение двух векторов определяется как покомпонентное сложение, то есть каждая компонента одного вектора добавляется к соответствующей компоненте другого вектора (первая — к первой, вторая — ко второй, третья — к третьей):

Наглядно можно это себе представить следующим образом (правило треугольника): пусть у нас даны вектор v = (4,2) и вектор k = (1,2), мы прикладываем начало вектора k к концу вектора v. А дальше просто соединяем начало вектора v с концом вектора k. В результате этого мы получаем третий вектор v + k с координатами (5,4), который является суммой векторов v и k:

Так же, как и со стандартным сложением и вычитанием, векторное вычитание — это то же самое, что и сложение с противоположным вторым вектором:

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


Длина вектора

Чтобы получить длину/величину вектора, необходимо воспользоваться теоремой Пифагора, которую вы, возможно, помните из школьных уроков математики. Ведь если изобразить вектор на координатной плоскости, то можно заметить прямоугольный треугольник, у которого длина одной стороны равна значению X-компоненты, а длина другой стороны — Y-компоненты вектора:

А поскольку длина двух сторон — (X,Y), и мы хотим знать длину наклоненной стороны, то можно вычислить её, используя теорему Пифагора:

Где символом ||v|| обозначается длина вектора v. Это легко трансформировать в 3D, просто добавив z2 в уравнение.

Итак, в нашем случае, длина вектора с координатами (4,2) равна:

Существует особый тип вектора, который мы называем единичным вектором. Единичный вектор имеет одно дополнительное свойство — его длина составляет ровно 1. Мы можем получить единичный вектор из любого вектора, разделив каждую координату заданного вектора на его длину:

Описываемый процесс называется нормализацией вектора. Единичные векторы обозначаются буквой с «домиком на голове», и с ними, как правило, легче работать, особенно в те моменты, когда нас интересуют только их направление (замечу, что направление вектора не меняется, если мы изменяем длину вектора).

Умножение двух векторов


А вот с умножением двух векторов дела обстоят несколько хуже, т.к. для обычного умножения векторов не существует наглядного представления. Но при этом есть два конкретных случая, которые мы могли бы использовать для операции умножения, заданной на векторах: первый случай — это скалярное (точечное) произведение векторов, обозначаемое как v · k, а второй — это векторное произведение векторов, обозначаемое как v × k.

Скалярное умножение

Скалярное произведение двух векторов равно произведению их длин, умноженное на косинус угла между ними. Если это звучит запутанно, то взгляните на его формулу:

где θ (тета) — это угол между векторами. Почему это произведение так важно? Ну, представьте себе, что если v и k — это единичные векторы, то их длина будет равна 1. А значит, исходная формула сокращается до вида:

И теперь скалярное произведение зависит только от угла между векторами. Благодаря этому, легко определить, являются ли два вектора параллельными (находятся под углом 0 или 180 градусов) или ортогональными (находятся под углом 90 градусов) друг к другу. Возможно, вы помните, что функция косинус (или cos) равна 0, когда угол θ равен 90 градусам, и равна 1 или -1, когда угол θ равен 0 или 180 градусам, соответственно.

На случай, если вы хотите узнать больше о функциях sin или cos, я бы предложил вам посмотреть видеоролики Академии Хана об основах тригонометрии.

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

Итак, как же вычисляется скалярное произведение векторов? Скалярное произведение векторов — это покомпонентное умножение, при котором мы складываем результаты умножения. Рассмотрим пример с двумя единичными векторами (вы сами можете проверить, что их длины равны 1):

А дальше, для вычисления значения угла (в градусах) между данными единичными векторами мы используем обратную к косинусу функцию cos-1 (которая называется «арккосинус»), в результате чего мы получаем значение 143,1 градуса. Вот мы и вычислили угол между этими двумя векторами. Как видите, это не так уж и сложно. К слову сказать, скалярное произведение является очень полезным инструментом, особенно, когда мы дойдём до расчётов освещения.

Векторное умножение

Векторное умножение определено только для 3D-пространства, в качестве входных данных принимает два непараллельных вектора и создаёт третий вектор, ортогональный обоим входным векторам. Если оба входных вектора также ортогональны друг другу, то в результате их векторного произведения мы получим 3 взаимно-ортогональных вектора; это будет полезно в последующих уроках. На нижеследующем рисунке показано, как это выглядит в 3D-пространстве:

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

Как видите, это действительно кажется бессмысленным. Однако, если вы просто выполните эти действия, то получите ещё один вектор, ортогональный вашим входным векторам.

Матрицы

Теперь, когда мы обсудили почти всё, что касается векторов, пришло время войти в матрицу! Матрица — это прямоугольный массив чисел, символов и/или математических выражений. Каждое отдельно взятое число в матрице называется элементом матрицы. Пример матрицы 2×3 показан ниже:

Обозначение матрицы очень похоже на обозначение вектора. В общем случае оно имеет следующий вид — (i, j), где индексы i — это количество строк в матрице, а j — количество столбцов. Поэтому приведенная выше матрица называется матрицей размера 2×3 (3 столбца и 2 строки). Заметьте, это является противоположностью тому, к чему вы привыкли, обозначая на координатной плоскости точки записью (X,Y), где Х — это координата вдоль горизонтальной оси (можно сказать, вдоль строки), а Y — координата вдоль вертикальной оси (можно сказать, вдоль столбца).

Возвращаясь к изображению нашей матрицы, выберем в ней какой-нибудь элемент, например, 4 — про него можно сказать, что он стоит на пересечении 2 строки и 1 столбца, т.е. имеет индексы (2,1).

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

Сложение и вычитание матриц


Сложение и вычитание между двумя матрицами производится поэлементно. Таким образом, применяются те же самые общие правила, которые мы знаем для обычных чисел, но выполненные на элементах обеих матриц, одинаковой размерности. Это означает, что матрица размера 3×2 и матрица размера 2×3 (или матрица 3×3 и матрица 4×4) не могут быть сложены или вычтены между собой. Давайте посмотрим, как работает сложение матриц на двух квадратных матрицах размера 2×2:

Те же правила применяются и для вычитания матриц:

Умножение матрицы на скаляр

Матрично-скалярное произведение выполняется путём умножения каждого элемента матрицы на заданный скаляр. Нижеследующий пример иллюстрирует данную операцию:

Можно сказать, что скаляр масштабирует все элементы матрицы по своему значению. В предыдущем примере все элементы были масштабированы на 2.

Умножение матриц

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

   Умножать две матрицы можно только в том случае, если число столбцов в левой матрице равно числу строк в правой матрице.

   Матричное умножение не является коммутативным, то есть A·B≠B·A.

Давайте начнём с примера умножения двух квадратных матриц размера 2×2:

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

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

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

В результате получается матрица, имеющая размеры (n,m), где n — равно числу строк левой матрицы, а m — равно столбцам правой матрицы.

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

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

Как вы можете видеть, умножение матрицы на матрицу — довольно громоздкий процесс, при котором легко допустить ошибку (именно поэтому, обычно, данную работу мы оставляем на откуп компьютерам), и начинает доставлять ещё больше проблем, когда используются матрицы больших размерностей. Если вам интересно узнать ещё больше о некоторых математических свойствах матриц, то я рекомендую взглянуть на эти видеоролики Академии Хана о матрицах.

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

Матрично-векторное умножение

До сих пор в примерах по программированию мы, по большей части, имели дело с векторами. Использовали их для представления позиций, цветов и даже текстурных координат. Давайте пройдём немного дальше по кроличьей норе, и я скажу вам, что вектор — это матрица размера Nx1, где N — это число компонентов вектора (также его ещё называют «N-мерный вектор»). Если вдуматься, то в этом понятии кроется большой смысл. Векторы — это, как и матрицы, массивы чисел, но только с 1 столбцом. Итак, каким образом нам может помочь данная информация? Ну, если у нас есть матрица размера MxN, то мы можем умножить эту матрицу на наш вектор размера Nx1, так как столбцы матрицы равны числу строк вектора. Таким образом, операция умножения матрицы является допустимой.

Но почему нас волнует, можем ли мы умножать матрицы с векторами? Так уж получилось, что есть много интересных 2D/3D преобразований, которые мы можем поместить внутри матрицы, и умножение такой матрицы на вектор приведет к преобразованию (трансформированию) того вектора. В случае, если вы всё ещё в небольшом замешательстве от новой информации, давайте начнём с рассмотрения нескольких примеров, и вы скоро поймёте, о чём идёт речь.

Единичная матрица

В OpenGL мы работаем с матрицами преобразований размером 4×4 по нескольким причинам, и одна из этих причин заключается в том, что большинство векторов имеют размер равный 4. Самая простая матрица преобразования, которую мы можем придумать — это единичная матрица. Единичная матрица — это квадратная матрица размера NxN, у которой элементы, стоящие на диагонали от верхнего-левого угла до нижнего-правого, равны 1 (такую диагональ принято называть главной диагональю), а остальные — равны 0. Как вы увидите, эта матрица преобразования оставляет вектор совершенно нетронутым:

Это становится очевидным из правил умножения: первый элемент результирующей матрицы получается путём суммирования произведения каждого отдельно взятого элемента первой строки матрицы, на каждый отдельно взятый элемент вектора. Поскольку каждый из элементов строки равен 0, кроме первого, то мы получаем: 1⋅1+0⋅2+0⋅3+0⋅4=1 и то же самое относится к остальным 3-м элементам вектора.

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

Масштабирование

При масштабировании вектора мы увеличиваем длину стрелки на некоторую заданную величину, сохраняя неизменным её направление. Поскольку мы работаем либо в двух, либо в трёх измерениях, то можем определить масштабирование в виде вектора из 2-х или 3-х переменных масштабирования, каждая из которых масштабирует (уменьшает или увеличивает) отдельно взятую ось (X, Y или Z).

Попробуем масштабировать вектор v = (3,2). Мы будем масштабировать вектор вдоль оси X на 0,5, таким образом, делая его в два раза уже; и мы будем масштабировать вектор на 2 вдоль оси Y, делая его вдвое выше. Давайте посмотрим, как выглядит результат масштабирования (вектор s) вектора v на (0.5, 2):

Имейте в виду, что OpenGL обычно работает в трёхмерном пространстве, поэтому для этого 2D-случая мы могли бы установить масштаб оси Z на 1, оставив его нетронутым. Операция масштабирования, которую мы только что выполнили, представляет собой пример неоднородного (англ. «non-uniform») масштаба, поскольку коэффициент масштабирования для каждой из осей был разным. Если бы по всем осям он был один и тот же, то его можно было бы назвать однородным (англ. «uniform») масштабом.

Давайте построим матрицу преобразования, которая выполнит для нас операцию масштабирования. Из раздела с описанием единичной матрицы мы узнали, что каждый из диагональных элементов умножается на соответствующий ему элемент вектора. А что, если мы изменим число 1 в единичной матрице на число 3? В этом случае, мы будем умножать каждый из элементов вектора на число 3 и, таким образом, однородно масштабируем вектор на 3. Если мы представим масштабирующие переменные как набор (S1, S2, S3), то можно определить матрицу масштабирования для любого вектора (x, y, z) следующим образом:

Обратите внимание, что мы сохраняем 4-й элемент масштабирования равный 1. Компонента w, как мы увидим позже, используется для других целей.

Трансляция вектора

Трансляция (от лат. «translatio» — перенос, перемещение) вектора выполняется очень просто: мы берём первый вектор (который собираемся переносить), к его концу добавляем вектор перемещения (вдоль которого будем выполнять перемещение) и получаем третий вектор, но с другой позицией. Мы уже обсуждали векторное сложение, так что это не должно быть чем-то новым для вас.

Как и в случае с масштабированием, в матрице 4×4 есть несколько местоположений, которые мы можем использовать для выполнения определённых операций, и для трансляции — это 3 верхних значения 4-го столбца. Если мы представим вектор перемещения как (Tx, Ty, Tz), то сможем определить матрицу перемещения следующим образом:

Данный пример работает потому, что все транслируемые значения умножаются на w-столбец вектора перемещения и добавляются к значениям исходного вектора (помните правила умножения матриц). Это было бы невозможно сделать с матрицей 3×3.

Примечание об однородных координатах: Компонента w выбранного нами вектора также именуется однородной координатой. Чтобы получить трёхмерный вектор из однородного вектора, мы делим координаты x, y и z на его w-координату. Обычно мы этого не замечаем, так как компонента w большую часть времени равна 1.0. Использование однородных координат имеет ряд преимуществ: они позволяют нам делать матричные трансляции на 3D-векторах (без компоненты w мы не можем транслировать векторы), и в следующем уроке мы будем использовать значение w для создания 3D-перспективы. Кроме того, вектор, у которого однородная координата равна 0, называется вектором направления, так как вектор с координатой w=0 не может быть транслирован.

С помощью матрицы перемещения мы можем переносить объекты в любом из 3-х осевых направлений (x, y, z), что делает её очень полезной матрицей преобразования для нашего инструментария преобразования.

Поворот/Вращение

Последние несколько преобразований относительно легко можно было понять и представить в 2D или 3D пространстве, но вращения, в этом плане, выглядят несколько сложнее. Если вы хотите точно знать, как строятся матрицы вращения, то я рекомендую вам посмотреть видео по линейной алгебре в Академии Хана.

Сначала давайте определим, чем фактически является вращение вектора. Поворот в 2D или 3D пространстве представлен углом поворота. Угол может задаваться как в градусах, так и в радианах, при этом вся окружность составляет 360 градусов или 2*Pi радиан. Я предпочитаю объяснять вращения, используя градусную меру, поскольку для нас она более привычная.

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

угол в градусах = угол в радианах * (180/Pi)
угол в радианах = угол в градусах * (Pi/180)

Где число Pi равно (округлено) 3.14159265359.

Выполняя оборот на половину окружности, мы поворачиваемся на 360/2 = 180 градусов, а поворачиваясь на 1/5 вправо, мы выполняем поворот на 360/5 = 72 градуса вправо. Ниже показан пример для базисного 2D вектора, где вектор v повернут на 72 градуса вправо (или по часовой стрелке от вектора k):

Вращения в 3D задаются с помощью угла поворота и оси вращения. Задаваемый угол будет вращать объект вдоль заданной оси вращения. Попробуйте визуализировать это, поворачивая на несколько градусов голову, постоянно смотря вдоль одной из осей вращения. Например, при вращении 2D векторов в трёхмерном мире мы устанавливаем ось вращения на ось Z (попробуйте наглядно это представить и изобразить).

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

Ниже описаны матрицы вращения для каждой из единичных осей в трёхмерном пространстве, где символ θ (тета) — это угол поворота:

Вращение вокруг оси X:

Вращение вокруг оси Y:

Вращение вокруг оси Z:

Используя матрицы поворота, мы можем преобразовывать наши радиус-векторы вокруг одной из трёх единичных осей. Вращение вокруг произвольной 3D-оси представляет собой объединение 3-х отдельных поворотов: сначала вращаясь вокруг оси X, затем Y и затем Z, например. Однако так мы быстро попадём на проблему, называемую проблемой складывания рамок. Мы не будем вдаваться в её детали, но лучшим решением избежать этого, будет выполнение вращения вокруг произвольной единичной оси, например, (0.662, 0.2, 0.722) (обратите внимание, что это единичный вектор), вместо сочетания матриц вращения. Такая (довольно большая и странная) матрица, где (Rx, Ry, Rz) — это произвольная ось вращения, показана ниже:

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

Комбинирование матриц

Истинная сила использования матриц преобразований заключается в том, что, благодаря умножению матрицы на матрицу, мы можем объединить несколько преобразований в одной матрице. Давайте посмотрим, сможем ли мы создать матрицу, объединяющую несколько преобразований. Предположим, что у нас есть вектор (x, y, z), и мы хотим сначала масштабировать его на 2, а затем транслировать его на (1, 2, 3). Для выполнения этих шагов нам потребуются две матрицы: перемещения и масштабирования. Результирующая матрица преобразования будет выглядеть следующим образом:

Обратите внимание, что при умножении матриц мы сначала выполняем перемещение, а затем масштабирование. Умножение матриц не является коммутативным, что означает, что их порядок следования очень важен. При умножении матриц крайняя правая матрица сначала умножается на вектор, обратите на это внимание. При объединении матриц, рекомендуется сначала выполнять операции масштабирования, затем вращения и, наконец, перемещения, иначе они могут (отрицательно) повлиять друг на друга. Например, если вы сначала сделаете перемещение, а затем масштабирование, то вектор перемещения также будет масштабирован!

Применение последней матрицы преобразования к нашему вектору приводит к следующему вектору:

Отлично! Вектор сначала масштабируется на два, а затем транслируется на (1, 2, 3).

На практике

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

Библиотека GLM

GLM (от англ. «OpenGL Mathematics») является библиотекой, состоящей только из заголовочных файлов. Всё, что нам нужно сделать — это подключить соответствующие заголовочные файлы, и… всё; никакой возни с настройками линкера и компилятора не требуется. GLM можно скачать с оф. сайта. Скопируйте корневой каталог заголовочных файлов в свою папку includes и приступайте к работе.

Большинство функций GLM, которые нам потребуются, находятся в 3-х заголовочных файлах, которые мы подключим следующим образом:

Давайте применим наши знания о преобразованиях и посмотрим, сможем ли мы транслировать вектор (1,0,0) на (1,1,0) (обратите внимание, что мы определяем его как glm::vec4 с однородной координатой, заданной значением 1.0):

Сначала мы определяем вектор с именем vec, используя встроенный в GLM класс векторов. Далее мы определяем переменную матрицы mat4 и явно инициализируем её в виде единичной матрицы, присваивая диагональным элементам значение 1.0; если мы этого не сделаем, то получим нулевую матрицу (все элементы равны 0), и все последующие матричные операции в результате будут давать нулевую матрицу.

Следующим шагом является создание матрицы преобразования путём передачи нашей единичной матрицы в функцию glm::translate() вместе с вектором перемещения (данная матрица затем умножается на матрицу перемещения и возвращается результирующая матрица).

Затем мы умножаем наш вектор на матрицу преобразования и выводим результат. Результирующий вектор должен быть (1+1,0+1,0+0), то есть (2,1,0). Этот фрагмент кода выводит значение 210, так что матрица перемещения выполнила свою работу.

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

Сначала мы масштабируем ящик на 0,5 по каждой оси, а затем поворачиваем контейнер на 90 градусов вокруг оси Z. GLM ожидает получить углы в радианах, поэтому мы преобразуем градусы в радианы, используя функцию glm::radians(). Обратите внимание, что текстурированный прямоугольник находится на плоскости XY, поэтому мы будем выполнять вращение вокруг оси Z. Поскольку мы передаём матрицу каждой из GLM-функций, то GLM автоматически умножает матрицы вместе, в результате чего получается матрица преобразования, которая объединяет все трансформации.

Возникает вопрос: «Как матрица преобразования попадёт в шейдеры?». Мы уже упоминали ранее, что язык GLSL также имеет тип mat4. Поэтому мы адаптируем вершинный шейдер для принятия uniform-переменной mat4 и умножим радиус-вектор на uniform-переменную матрицы:

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

Мы добавили uniform-переменную и перемножили радиус-вектор с матрицей преобразования, прежде чем передать его в gl_Position. Теперь наш ящик должен быть вдвое меньше и повёрнут на 90 градусов (наклонен влево). Однако нам всё ещё нужно передать шейдеру матрицу преобразования:

Сначала мы запрашиваем местоположение (location) uniform-переменной, а затем отправляем матричные данные шейдерам, используя функцию glUniform() с Matrix4fv в качестве её постфикса:

   Аргумент №1: Местонахождение uniform-переменной.

   Аргумент №2: Сообщаем OpenGL, сколько матриц мы хотели бы отправить, в нашем случае, это 1 матрица.

   Аргумент №3: Здесь спрашивают нас, хотим ли мы транспонировать нашу матрицу (т.е. поменять местами столбцы и строки). Разработчики OpenGL часто используют определённую внутреннюю компоновку матрицы, упорядочивая матрицу по столбцам, данная компоновка задаётся по умолчанию для матриц в GLM, поэтому нет необходимости транспонировать матрицы; мы можем оставить значение GL_FALSE.

   Аргумент №4: Фактические данные матрицы, но GLM хранит данные своих матриц таким образом, что они не всегда соответствуют ожиданиям OpenGL, поэтому мы сначала преобразуем данные с помощью встроенной GLM-функции value_ptr().

Мы создали матрицу преобразования, объявили uniform-переменную в вершинном шейдере и отправили матрицу в шейдеры, где мы преобразуем наши вершинные координаты. Результат должен выглядеть примерно следующим образом:

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

Имейте в виду, что в предыдущем случае мы могли бы объявить матрицу преобразования где угодно, но теперь мы должны создавать её каждую итерацию, чтобы постоянно обновлять вращение. Это означает, что мы должны заново создавать матрицу преобразования в каждой итерации цикла рендеринга. Обычно при рендеринге сцен мы имеем несколько матриц преобразования, которые воссоздаются с новыми значениями каждого кадра.

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

Если вы всё сделали правильно, то должны получить следующую анимацию:

И вот оно! Транслированный контейнер, который вращается с течением времени, и всё это делается одной матрицей преобразования! Теперь вы можете понять, почему матрицы являются такой мощной конструкцией в графической стране. Мы можем определить бесконечное количество преобразований и объединить их все в единую матрицу, которую будем повторно использовать так часто, как нам захочется. Использование подобных преобразований в вершинном шейдере экономит наши усилия по повторному определению данных вершин, а также экономит некоторое время обработки, так как нам не нужно постоянно пересылать наши данные (что довольно медленно); всё, что нам нужно сделать — это обновить uniform-переменную трансформации.

Если вы не получили нужный результат или застряли где-то ещё, взгляните на исходный код и обновленный класс шейдеров.

Исходный код — Урок №7. Трансформации в OpenGL

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

Дополнительные ресурсы

   Essence of Linear Algebra: отличная серия видеоуроков Гранта Сандерсона об основах математики преобразований и линейной алгебре.

Упражнения

Задание №1

Используя для ящика вышеописанные преобразования, попробуйте поменять местами порядок выполнения операций преобразований: сначала — поворот, а затем — трансляция. Посмотрите, что произойдёт, и попытайтесь понять, почему это происходит.

Ответ №1

Задание №2

Попробуйте нарисовать второй ящик с ещё одним вызовом функции glDrawElements(), но поместите его в другую позицию, используя только преобразования. Убедитесь, что второй ящик расположен в левом верхнем углу окна, и вместо того, чтобы вращаться, масштабируйте его с течением времени (здесь полезно использовать функцию sin; обратите внимание, что использование sin приведёт к инвертированию объекта, как только будет использована отрицательная шкала).

Ответ №2

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (8 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 1

  1. Аватар Арбузик❤❤❤:

    Особенно веселая картинка получается если связать угол поворота с функцией тангенса 😀

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *