Готовый перевод OpenGL SuperBible (6th Edition) / OpenGL Супер Книга (6-ое издание): Математика для 3D-графики

Внимание! Этот перевод, возможно, ещё не готов, так как модераторы установили для него статус «перевод редактируется»

Глава 4. Математика для 3D-графики
Из этой главы вы узнаете:
• Что такое вектор и чем он интересен
• Что такое матрица и чем она интересен вдвойне
• Как использовать матрицы и векторы для перемещения геометрических фигур
• Соглашения в OpenGL и координатные пространства

Пока, вы научились рисовать только точки, линии и треугольники и написали простые шейдеры, которые передают вершину далее по конвейеру, не изменённой. Но еще не было приведено ни одного примера рендеринга в 3D, что странно для книги о 3D-графике! И так, чтобы превратить группу фигур в целостную сцену, вы должны расположить их относительно друг друга и по отношению к наблюдателю. В этой главе вы начнете передвигать фигуры и объекты в вашей системе координат. Возможность размещать и ориентировать ваши объекты на сцене является ключевым инструментом для любого программиста 3D-графики. Как вы увидите, это действительно удобно - описывать размеры вашего объекта относительно начала координат, а затем перемещать объект в желаемое положение.

Насколько страшна эта глава?
В большинстве книг про программирование 3D-графики, да, подобная глава действительно была бы страшной. Но вы можете расслабиться: наш подход более умеренный.

Одна из основных математических операций, которая будет выполняться вашим шейдером - это преобразование координат, которое сводится к перемножению матриц с векторами и друг другом. Ключ к преобразованиям координат и объектов - два соглашения для матриц, используемых программистами OpenGL. Для вашего ознакомления с этими матрицами, эта глава поразит компромиссом между двумя крайностями в философии компьютерной графики. С одной стороны, мы могли бы предупредить вас, "Пожалуйста, ознакомьтесь с учебником по линейной алгебре, прежде чем читать эту главу." С другой стороны, мы могли бы обманом уверить вас, что вы можете "изучить 3D-графику без всех этих сложных математических формул". Но мы не согласимся ни с одной из сторон.

На самом деле, вы можете прекрасно справиться, не понимая тонкостей математики 3D-графики, точно так же, как вы можете вести свой автомобиль каждый день, не зная вообще ничего об автомобильной механике и двигателе внутреннего сгорания. Но вам лучше знать о вашем автомобиле достаточно, чтобы понимать: время от времени нужно менять масло, регулярно заполнять бензобак и необходимо менять покрышки, когда они стираются. Эти знания делают вас ответственным (и осторожным!) владельцем автомобиля. Если вы хотите быть ответственным и умелым OpenGL программистом, то это относятся и к вам. По крайней мере вам нужно понимать основы, чтобы знать что вы можетe сделать, и какие лучше использовать инструменты для выполнения работы. По мере практики вы ощутите, что математические матрицы и векторы постепенно обретают смысл, и будете развивать более интуитивные (и мощные) навыки, чтобы в полной мере использовать понятия, которые мы представим в этой главе.

Но даже если вы еще не умеете перемножать матрицы в уме, вам нужно знать, что это такое и какую роль они играют в OpenGL. Но прежде, чем вы побежите стряхивать пыль с учебника алгебры (у всех он есть?), мы напомним: в библиотеке sb6 есть компонент, называемый vmath, который содержит несколько полезных классов и функций для представления и манипулирования векторами и матрицами. Они могут быть использованы непосредственно с OpenGL и очень похожи по синтаксису и внешнему виду на GLSL - язык, на котором вы будете писать свои шейдеры. Но хоть вы и не должны совершать матричные и векторные преобразования самостоятельно, будет отлично, если вы будете знать, что это такое и как применять. Видите - вы можете усидеть сразу на двух стульях!

Интенсивный курс математики в 3D-графике
Существует немало книг по математике в 3D-графике, и самые лучшие из тех, что мы нашли, перечислены в Приложении А, "Дальнейшее чтение". Мы не заявляем, что осветим все то, что вам нужно знать. И даже не будем пытаться осветить все то, что вам следует знать. В этой главе мы изучим лишь то, без чего действительно не сможем обойтись. Если вы уже гений математики, сразу же переходите к следующему разделу о стандартных 3D преобразованиях. Теперь вы знаете не только, какой предмет мы сейчас охватим, но и что, к некоторому разочарованию большинства любителей математики, мы не уделили особого внимания их излюбленной теме пространств с однородными координатами. Представьте одно из тех реалити-шоу, где вы должны покинуть искусственное болото полное крокодилов. Сколько математики из области 3D вам нужно знать для выживания? Вот о чём будут два следующих раздела: математические навыки выживания. Крокодилов не волнует, знаете ли вы, что такое однородные координатные пространства или нет.

Вектор, или куда ведет этот путь?
Главная входящая информация в OpenGL - вершина, которая имеет несколько атрибутов, в том числе положение. В основном, это положение в координатном пространстве xyz, которое определяется одним и только одним уникальным XYZ триплетом. Однако, xyz-триплет, может быть представлен как вектор (на самом деле, положение - тоже вектор). Вектор является, пожалуй, самой фундаментальной и ключевой для понимания концепцией, когда дело доходит до операций над 3D-геометрией. Эти три значения (x, y и z) вместе выражают два важных понятия: направление и величину.

Рисунок 4.1 показывает точку в пространстве (взятую произвольно) и стрелку, нарисованную от начала системы координат к этой точке. Точку можно рассматривать как вершину, когда вы составляете треугольник, а стрелку как вектор. В первую очередь, вектор - это просто направление от начала координат к точке в пространстве. Мы используем векторы в OpenGL всё время для представления направленных величин. Например, ось x - вектор (1, 0, 0). Это означает, отложить одну единицу в положительном направлении по оси x, и ноль в направлении по осям y и z. Вектор, также означает направление, куда мы движемся, например, в какую сторону указывает камера или в каком направлении бежать, чтобы уйти от крокодила! Вектор настолько фундаментальное понятие в OpenGL, что в GLSL для него предусмотрены типы разных размеров, которым даны имена, такие как vec3 и vec4 (представляющие соответственно 3- и 4-мерные векторы).

Рисунок 4.1: Точка в пространстве - это и вершина и вектор.

Вторая величина, которую представляет вектор, это модуль. Модуль вектора - это его длина. Для нашего вектора (1, 0, 0) длина вектора равна единице. Вектор с длиной равной одному называется единичным вектором. Если вектор не единичный, а мы хотим изменить его длину до единицы, то мы называем это нормализацией. Процесс нормализации вектора заключается в масштабировании его таким образом, что длина становится равной единице. Такой вектор называется нормализованным. Единичные векторы важны, когда мы хотим указать лишь направление, а не длину. Также, если длины векторов появятся в уравнениях, которые мы будем использовать, они станут намного проще при длинах равных 1! Модуль также может быть важной величиной; например, он может сказать, как далеко мы должны двигаться в данном направлении - как далеко мне нужно убежать от этого крокодила.

Векторы (и матрицы) настолько важные понятия в 3D-графике, что они объекты первого класса в GLSL — язык, в котором вы будете писать ваши шейдеры. Тем не менее, это не так в C++ подобных языках. Чтобы вы могли использовать их в ваших программах на C++, библиотека vmath, которая идёт с исходным кодом этой книгой, содержит классы, которые могут представить векторы и матрицы, которые называются соответственно с аналогами из GLSL: к примеру, vmath::vec3 может представить трёхкомпонентным вектором из чисел с плавающей запятой (x, y, z) и vmath::vec4 может представить вектор из четырёх компонентов чисел с плавающей запятой (x, y, z, w) и так далее. Координата w добавлена, чтобы сделать вектор однородным, но как правило, всегда имеет значение 1.0. Значения x, y, и z в дальнейшем могут быть разделены на w, когда он равен 1.0, по сути оставляет значения xyz прежними. Классы в vmath фактически являются шаблонными классами с определением типов для предоставления общих типов, таких как значения с плавающей запятой одинарной и двойной точностей, и знаковые и без знаковые целочисленные переменные. vmath::vec3 и vmath::vec4 определены просто как следующим образом:

typedef Tvec3<float> vec3;
typedef Tvec4<float> vec4;

Объявлять трёхкомпонентный вектор вот так просто:

sb6::vmath::vec3 vVector;

Если вы добавите “using namespace vmath” в ваш исходный код, вы можете писать

vec3 vVector;

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

vec3 vVertex1(0.0f, 0.0f, 1.0f);
vec4 vVertex2 = vec4(1.0f, 0.0f, 1.0f, 1.0f);
vec4 vVertex3(vVertex1, 1.0f);

Теперь, массив трёхкомпонентных вершин, как для треугольника может быть объявлен как:

vec3 vVerts[] = { vec3(-0.5f, 0.0f, 0.0f),
vec3( 0.5f, 0.0f, 0.0f),
vec3( 0.0f, 0.5f, 0.0f) } ;

Это должно быть знакомым, этот код был представлен в разделе "Рисуем наш первый треугольник" в главе 2. Библиотека vmath также включает много-много математических функций и перегрузки большинства операций её класса, позволяя складывать, вычитать, умножать, транспонировать и так далее векторы и матрицы

Нам нужно быть осторожными, чтобы не пропустить четвёртый компонент W. Большую часть времени, когда вы определяете положение вершин геометрической фигуры, трёхкомпонентные вершины это всё, что вам нужно хранить и отправлять в OpenGL. Для многих направленных векторов, таких как нормаль к поверхности (вектор указывающий перпендикулярно к поверхности, который используется для расчетов освещения) достаточно использовать трёхкомпонентный вектор. Однако, мы скоро погрузимся в мир матриц, и чтобы превратить 3D вершину, вы должны умножить её на матрицу преобразования размером 4 х 4. Правила такое: вы должны умножать четырёхкомпонентный вектор на матрицу 4 x 4, если вы попробуете умножить трёхкомпонентный вектор на матрицу 4 x 4... крокодилы съедят вас! Подробности на счёт этого будут скоро. По сути, если вы собираетесь делать свои собственные матричные операции над векторами, тогда возможно вы будете использовать четырёхкомпонентный вектор, во многих случаях.

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

vmath::vec3 a(1.0f, 2.0f, 3.0f);
vmath::vec3 b(4.0f, 5.0f, 6.0f);
vmath::vec3 c;
c = a + b;
c = a - b;
c += b;
c = -c;

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

Скалярное произведение
Векторы можно складывать, вычитать и масштабировать просто складывая, вычитая или масштабируя их XYZ компоненты. Интересная и полезная операция, впрочем это может быть применимо только для двух векторов, называется скалярное произведение. Скалярное произведение двух (трехкомпонентных) векторов возвращает скаляр (только одно значение), то есть косинус угла между двумя векторами и увеличенное на произведения их длин. Если два вектора имеют единичную длину, возвращаемое значение будет находится между -1.0 и 1.0 и равна косинусу угла между ними. Конечно, чтобы получить угол между векторами, вам надо взять значение обратного косинуса (или арккосинуса). Скалярное произведение широко используется в расчётах рассеивания света при вычислениях освещения и берется между нормалью поверхности и вектором указывающий на источник света. Мы будем углубляться в этот тип шейдерного кода в разделе "Модели освещения" в главе 12. Рисунок 4.2 показывает два вектора, v1 и v2, θ - угол между ними

Рисунок 4.2: Скалярное произведение — косинус угла между двумя векторами

Математически, скалярное произведение двух векторов v1 и v2 считается так:

v1 x v2 = v1.x x v2.x + v1.y x v2.y + v1.z x v2.z

Библиотека vmath имеет несколько полезных функций, которые используют скалярное произведение. Для начинающих, вы можете скалярно умножить векторы с помощью функции wmath::dot или методом векторных классов.

vmath::vec3 a(...);
vmath::vec3 b(...);

float c = a.dot(b);
float d = dot(a, b);

Как упоминалось ранее, скалярное произведение между парой единичных векторов имеет значение между -1.0 и +1.0, что дает косинус угла между ними. Немного выше по уровню функция vmath::angle, вообще-то, возвращает угол в радианах.

float angle(const vmath::vec3& u, const vmath::vec3& v);

Векторное произведение
Еще одна полезная математическая операция между двумя векторами - это векторное произведение. Векторное произведение между двумя векторами - это третий вектор, который перпендикулярен плоскости, в которой лежат первый и второй векторы. Векторное произведение двух векторов v1 и v2 определяются как:

где n это единичный вектор, который перпендикулярен и v1, и v2. Это значит, что если вы нормализуете результат векторного произведения, вы получите нормаль к плоскости. Если оба v1 и v2 имеют длину один, и известно, что они перпендикулярны друг к другу, то вам не нужно будет нормализовать результат, так как он и так будет единицей. Рисунок 4.3: показывает два вектора v1 и v2, v3 - результат их векторного произведения.

Рисунок 4.3: Векторное произведение возвращает перпендикуляр к его параметрам.

Векторное произведение двух трёхмерных v1 и v2 могут быть сосчитаны следующим образом:

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

vec3 a(...);
vec3 b(...);

vec3 c = a.cross(b);
vec3 d = cross(a, b);

В отличие от скалярного произведения, здесь порядок векторов важен. На рисунке 4.3, v3 является результатом векторного произведения v2 и v1. Если бы вы поменяли местами v1 и v2, то результирующий вектор v3 смотрел бы в противоположную сторону. Применение векторного произведения разнообразно, от нахождения нормалей к поверхностям треугольников до построения матриц преобразования.

Длина вектора
Как мы уже обсуждали, векторы имеют направление и модуль. Модуль вектора так же известен, как его длина. Модуль трёхмерного вектора можно найти по следующей формуле:

Квадратный корень суммы квадратов компонентов вектора. Это простая теорема Пифагора, только в двух измерениях - квадрат гипотенузы равен сумме квадратов катетов. Это распространяется на любое количество измерений, библиотека vmath включает функции для таких вычислений.
---------------------------------------
1. Сумма квадратов компонентов вектора - это скалярное произведение вектора с собой.

template <typename T, int len>
static inline T length(const vecN<T,len>& v) { ... }

Отражение и преломление
Распространённые операции в компьютерной графике - вычисление векторов отражения и преломления. Учитывая входящий вектор Rin и нормаль к поверхности N, мы узнаем направление, в котором Rin будет отражаться (Rreflect), и данный показатель преломления η, и в каком направлении Rin будет преломлено. Мы покажем это на рисунке 4.4, с преломленными векторами с Rrefract,η1 по Rrefract,η4 различных значений η.

Рисунок 4.4: Отражение и преломление

Нам интересно вычисление в трёхмерном пространстве (всё-таки эта книга о 3D-графике), хотя на рисунке 4.4 показана система только в двух измерениях. Формула для вычисления Rreflect

Rreflect = Rin – (2N · Rin)N

и формула для вычисления Rrefract с заданным η

Чтобы получить желаемый результат и R и N должны быть единичными векторами (то есть они должны быть нормализованы перед применением). Две vmath функции reflect() и refract() реализовывают эти уравнения.

Матрицы
Матрица это не только голливудская кинотрилогия, но еще и мощный математический инструмент, который значительно упрощает процесс решения уравнений с одной или несколькими переменными, которые имеют сложные взаимосвязи друг с другом. Один часто встречающийся пример, близкий к сердцу графических программистов, преобразование координат. Например, у вас есть точка в пространстве с координатами x, y и z, и вам нужно узнать, где эта точка будет, если если вы ее повернёте на несколько градусов относительно произвольной точки или направления, то вам нужно будет использовать матрицы. Почему? Потому что новая координата x зависит не только от старого x, а еще от других параметров вращения, и от прежних значений y и z. Этот вид зависимости между переменными и решение как раз та проблема, в которой преуспевают матрицы. Любители фильмов "Матрица", кто имеют математический уклон, согласятся, что термин матрица действительно имеет подходящее название.

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

В литературе программирования 3D-графики термины "матрица" и "вектор" важны. Когда вы будете иметь дело с этими величинами, вы также встретите термин "скаляр". Скаляр - это просто число, означающее величину либо количество !ПРОПУСТИЛ!. Матрицы можно перемножать и складывать не только между собой, но еще и с векторами и скалярами. Умножая точку (представленную вектором) на матрицу (преобразования) получаем новую преобразованную точку (другой вектор). Матричные преобразования на самом деле не так уж сложно понять, но могут показаться пугающими сначала. Вы должны познакомится с матричными преобразованиями, потому что эти понятия - основа многих задач в 3D. К счастью, с помощью лёгких для понимания вещей можно делать довольно-таки невероятные вещи с OpenGL. Со временем, и с небольшой практикой, обучаясь, вы освоите этот математический инструмент самостоятельно.

А пока, как ранее с векторами, вы найдете несколько полезных матричных функций и возможностей в библиотеке vmath. Исходный код этой библиотеке доступен в файле vmath.h в папке исходного кода книги. Эта библиотека 3D-математики значительно упрощает многие задачи в этой и грядущих главах. Одна полезная возможность этой библиотеки - недостаток невероятно умного и хорошо оптимизированного кода! Это делает библиотеку переносимой и легкой для понимания. Вы также увидите, что в ней GLSL подобный синтаксис.

В 3D-задач программирования с OpenGL, вы будете использовать матрицы трёх размеров: 2 x 2, 3 x 3, 4 x 4. Библиотека vmath имеет матричные типы данных, которые соответствуют тем, которые определены GLSL, такие как

vmath::mat2 m1;
vmath::mat3 m2;
vmath::mat4 m3;

Как GLSL, матричные классы в vmath определены общие операторы, такие как сложение, вычитание, унарный минус, умножение и деление вместе с конструкторами и операторами сравнения. Опять же, матричные классы в vmath построены с использованием шаблонов и включается в себя определение типов для чисел с плавающей точкой одинарной и двойной точностей и для знакового и без знакового целого матричного типа.

Матричные конструкция и операторы
В OpenGL матрица 4 x 4 представлена не как двумерный массив, а как одномерный с 16 числами с плавающей точкой. По-умолчанию, OpenGL использует основную колонку или первичную строку в расположении матриц. Это означает, что для матрицы 4 x 4 первые четыре элемента расположены в первом столбце матрицы, следующие элементы во втором и так далее. Этот подход отличается от многих математических библиотек, которые используют двумерный массив. Например, OpenGL предпочитает первый из следующего примера:

GLfloat matrix[16]; // Прекрасно дружит с матрицами в OpenGL
GLfloat matrix[4][4]; // Не так удобно для OpenGL программистов

OpenGL может использовать второй вариант, но первый эффективнее представить. Сейчас это станет яснее. Эти 16 элементов представлены в матрице 4 x 4, как показано ниже. Когда в массиве элементы проходят вниз по столбцам один за другим, мы называем это главным порядком по столбцу. В памяти, двумерный массив 4 x 4 (второй вариант в предыдущем коде) разложен по порядку, где строка главная. Такой математический термин, смены порядка, называется транспонирование.

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

static const float A[] =
{
A00, A01, A02, A03, A10, A11, A12, A13,
A20, A21, A22, A23, A30, A31, A32, A33
};

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

static const float A[] =
{
A00, A10, A20, A30, A01, A11, A21, A31,
A20, A21, A22, A23, A30, A31, A32, A33,
};

Настоящая магия заключается в том, что эти 16 значений можно представить особым местом в пространстве и ориентацией трёх осей по отношению к зрителю. Интерпретировать эти числа вовсе не трудно. Каждый из четырёх столбиков представлен четырёхкомпонентным вектором.[2] Чтобы было проще, мы будем обращать наше внимание только на первые три элемента векторов в первых трёх столбцах. Четвёртый столбик-вектор содержит x, y и z значения преобразованной начало системы координат.
-----------------------------------
2. Вообще-то, библиотека vmath имеет внутреннее представление, где матрицы - массивы с собственными векторными классами, каждый вектор содержит столбик матрицы.

Первые три элемента первых трёх столбцов - просто векторы направления, которые представляют ориентацию (здесь векторы используются для направления) x, y и z осей в пространстве. Для большинства целей, эти три вектора всегда под углом 90° относительно друг друга и обычно каждый единичной длины (если вы также не будете применять масштаб или сдвиг). Математический термин этого (для того чтобы впечатлить своих друзей) ортонормированный, когда векторы единичной длины и ортогональны !ЧТО?! Рисунок 4.5 показывает матрицу преобразования 4 x 4 с её подсвеченными компонентами. Заметим, что значения последней строки матрицы равны 0, исключение самый последний, который равен 1.

Рисунок 4.5: Матрица 4 x 4 предоставляющая вращение и перемещение

Верхняя левая подматрица 3 x 3 показанная на рисунке 4.5 предоставляет вращение или ориентацию. Последний столбик матрицы предоставляет перемещение или положение.

Более потрясающая вещь это, если у вас есть матрица 4 x 4, которая содержит местоположение и ориентацию системы координат, и вы умножаете вершины, которые представлены !ПРОПУСК! Это означает, что любое место в пространстве и любая желаемая ориентация может быть определена матрицой 4 x 4, и если вы захотите умножить все вершины объекта на эту матрицу, вы преобразуете весь объект, задав местоположение и ориентацию в пространстве.

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

A · (B · v)
То же самое, что
(A · B) · v

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

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

Рисунок 4.6 показывает последовательность из сначала поворота по оси z, затем перемещение квадрата !ПРОПУСТИЛ! Разница в последней дислокации квадрата, это из-за того, что каждое преобразование осуществляется относительно последней выполненной трансформации. Вверху рисунка 4.6 квадрат поворачивается относительно начального положения. Внизу рисунка 4.6 после того, как квадрат перемещён, осуществляется вращение.

Рисунок 4.6: Моделирование преобразования: вращение затем перемещение, и перемещение затем вращение

Общие сведения о преобразованиях
Если вы действительно думали, что большинство графики в 3D, то на самом деле это не совсем так. Мы используем 3D понятия и термины, чтобы описать то, что-то на подобии, когда 3D данные "сплющенные" на 2D экране компьютера. Мы называем этот процесс сплющивание (squishing) 3D данных в 2D проекцию. Мы ссылаемся на проекцию, когда мы хотим описать тип преобразования (ортогональный или перспективный), что происходит во время обработки вершин, но проекция только один из видов превращений, происходящих в OpenGL. Преобразования также позволяют вращать объекты вокруг; перемещать их; и даже растягивать, сжимать, и деформировать их.

Координатные пространства в OpenGL
Последовательность из одного или более преобразований может быть представлена в виде матрицы, а перемножение матриц позволяет эффективно перемещать вектор из одного координатного пространства в другое. Существует несколько координантых пространств, которые активно используются в OpenGL. Любое число геометрических преобразований может произойти в промежутке, когда вы определяете вершины и когда они появляются на экране, но наиболее распространенными являются моделирующие, видовые и проекционные преобразования. В этом разделе мы мы рассмотрим каждое из координатных пространств, широко используемых в компьютерной 3D-графики (они обобщены в таблице 4.1) и преобразования, используемые для перемещения векторов между ними.

Таблица 4.1. Распространенные координатные пространства, используемые в 3D-графике

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

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

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

Видовое пространство
Важное понятие этой главы - видовые координаты. Они также часто называются координатами камеры или глаз. Видовые координаты отсчитываются относительно положения наблюдателя (отсюда и термины камеры и глаз), вне зависимости от любых преобразований, которые могут произойти; вы можете думать о них как об «абсолютных» координатах. Таким образом, координаты глаз представляют собой виртуальную фиксированную координатную систему, которая используется как общая система отсчёта.

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

Рисунок 4.7: Две перспективы видовых координат

Когда вы рисуете в 3D с помощью OpenGL, вы используете декартову систему координат. При отсутствии каких-либо преобразований, используемая система идентична только что описанной системе координат глаз.

Отсечение и нормированное пространство устройства
Пространство отсечения - это координатное пространство, в котором OpenGL осуществляет отсечение. Когда ваш вершинный шейдер выводит положение вершины через gl_Position, будет считаться, что ее координаты находятся в пространстве отсечения. Это всегда четырёхмерная однородная координата. На выходе из пространства отсечения, все четыре компонента вершины разделены на их компонент w. Очевидно, после этого компонент w станет равным 1.0. Если w не равен 1.0 до этого деления, компоненты x, y и z эффективно масштабируются путем инверсии w. Это позволяет создавать такие эффекты как перспективный ракурс (perspective foreshortening) и проекция (projection). Будет считаться, что результат деления находится в пространстве нормированных координат устройства. Понятно, что, если в результате координата компонента w пространства отсечения будет равна единице, пространство отсечения и NDC-пространство станут идентичными.

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

Рисунок 4.8: Преобразования модели

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

Единичная матрица
Существует несколько типов матриц преобразований, с которыми вы должны познакомиться прежде, чем пытаться их использовать. Первая их них - единичная матрица. Как показано ниже, единичная матрица состоит из нулей, за исключением главной диагонали, состоящей из единиц. Единичная матрица 4 x 4 выглядит так:

Умножение вершины на единичную матрицу эквивалентно умножению на единицу - матрица ничего не меняет.

Объекты, нарисованные с использованием единичной матрицы, не подвергаются преобразованиям; они находятся в исходном положении (последний столбец), а оси x, y и z определены таким же образом, как и в системе координат глаз.

Очевидно, существуют единичные матрицы для любых размеров: 2 x 2, 3 x 3 и т.д., и, как вы уже поняли, они также содержат единицы в главной диагонали. Все единичные матрицы - квадратные. Не бывает не квадратных единичных матриц. Транспонированная матрица для любой единичной выглядит точно также, как и исходная. Вы можете создать единичную матрицу для OpenGL в коде на C++ следующим образом:

// Используя обычный массив:
GLfloat m1[] = { 1.0f, 0.0f, 0.0f, 0.0f, // Столбец X
0.0f, 1.0f, 0.0f, 0.0f, // Столбец Y
0.0f, 0.0f, 1.0f, 0.0f, // Столбец Z
0.0f, 0.0f, 0.0f, 1.0f }; // Столбец W
// Или используя конструктор vmath::mat4:
vmath::mat4 m2( vmath::vec4(1.0f, 0.0f, 0.0f, 0.0f), // Столбец X
vmath::vec4(0.0f, 1.0f, 0.0f, 0.0f), // Столбец Y
vmath::vec4(0.0f, 0.0f, 1.0f, 0.0f), // Столбец Z
vmath::vec4(0.0f, 0.0f, 0.0f, 1.0f) }; // Столбец W

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

vmath::mat2 m2 = vmath::mat2::identity();
vmath::mat3 m3 = vmath::mat3::identity();
vmath::mat4 m4 = vmath::mat4::identity();

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

Матрицы параллельного переноса
Матрица параллельного переноса просто перемещает ваши вершины вдоль одной или более из трёх осей. Рисунок 4.9 показывает, например, параллельный перенос куба вверх по оси y на десять единиц.

Рисунок 4.9: Куб перемещён на десять единиц в положительном направлении по оси y.

Описание матрицы параллельного переноса 4 x 4 выглядит следующим образом:

Здесь tx, ty, и tz представляют перемещение по осям x, y и z соответственно. Изучение строения матрицы параллельного переноса позволяет увидеть одну из причин, почему нам нужно использовать четырёхмерные однородные координаты для представления положений вершин в 3D-графике. Рассмотрим радиус-вектор v, чей w компонент равен 1.0. Умножение на матрицу параллельного переноса, подобную той, что показана выше, дает

Как вы можете увидеть, tx, ty и tz были сложены с соответствующими компонентами вектора v, что в результате дало перемещение. Если бы компонент w вектора v не был равен 1.0, то использование этой матрицы для параллельного переноса привело бы к тому, что tx, ty, and tz были бы промасштабированы на это значение, влияя тем самым на выход преобразования. На практике, радиус-векторы почти всегда кодируются при помощи четырёх компонентов с w (последним компонентой) равной 1.0, в то время как векторы направления кодируются с использованием либо просто трех компонентов, либо четырех с w равной нулю. Таким образом, умножение четырёхкомпонентного вектора направления на матрицу параллельного переноса никак его не меняет.

template <typename T>
static inline Tmat4<T> translate(T x, T y, T z) { ... }
template <typename T>
static inline Tmat4<T> translate(const vecN<T,3>& v) { ... }

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

Здесь, запись Rx(θ) означает вращение вокруг оси x на угол θ. Точно так же, чтобы произвести вращение вокруг оси y или z, мы можем использовать

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

Здесь sψ, sθ и sφ означают синус углов ψ, θ и φ соответственно, а cψ, cθ и cφ - косинус этих углов. Если вам это кажется громоздким расчетом - не беспокойтесь, на помощь вновь придет пара функций из библиотеки vmath:

template <typename T>
static inline Tmat4<T> rotate(T angle_x, T angle_y, T_angle_z);

Вы также можете выполнить вращение вокруг произвольной оси, указав значения x,y и z для данного вектора. Чтобы увидеть ось вращения, вы можете просто нарисовать линию от точки начала координат до точки, представленной триплетом (x, y, z). Библиотека vmath также включает в себя код для создания этой матрицы из представления угл-ось:

template <typename T>
static inline Tmat4<T> rotate(T angle, T x, T y, T z);
template <typename T>
static inline Tmat4<T> rotate(T angle, const vecN<T,3>& axis);

Эти два варианта перегруженной функции vmath::rotate возвращают матрицу вращения, предоставляющую поворот на угол (в градусах) вокруг оси, заданной в первом варианте значениями x, y и z и вектором v - во втором. Здесь мы выполняем вращение вокруг вектора, заданного аргументами x, y и z. Угл поворота, имеющий направление против часовой стрелки и измеряемый в градусах, задан аргументом angle. В простейших случаях, поворот осуществляется вокруг только одной из основных осей (x, y или z) координатной системы.

Следующий код, например, создаёт матрицу вращения, которая поворачивает вершины на 45° вокруг произвольной оси, заданной триплетом (1, 1, 1), как показано на рисунке 4.10.

vmath::mat4 rotation_matrix = vmath::rotate(45.0, 1.0, 1.0, 1.0);

Рисунок 4.10: Куб повернутый вокруг произвольной оси

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

Углы Эйлера
Углы Эйлера - набор трёх углов, которые определяют ориентацию в пространстве. Каждый угол предоставляет поворот вокруг одного из трёх ортогональных векторов, которые определяют наш кадр(например, оси x, y и z). Как вы уже знаете, порядок выполнения матричных преобразований крайне важен, так как выполнение некоторых преобразований (таких, как вращения) в ином порядке даст иной результат. Это связанно со свойством некоммутативности операции умножения матриц. Таким образом, имея набор углов Эйлера, должны ли вы осуществить поворот сначала вокруг оси x, затем y и z, или же вам следует выполнять вращение в противоположном порядке, или даже начать с поворота вокруг y? Что ж, пока вы выполняете операции последовательно, это на самом деле не имеет особого значения.

3. В трехмерном кадре

Представление ориентации в виде набора трёх углов имеет ряд преимуществ. Во-первых, этот тип представления довольно интуитивен, что особенно важно, если вы планируете построить пользовательский интерфейс на основе углов. Еще одно преимущество в том, что довольно просто интерполировать углы, построить матрицу вращения в каждой точке и наблюдать плавную анимацию. Однако, углы Эйлера также имеют серьёзную ловушку - блокировка вращения (gimbal lock).

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

Чтобы избежать этого, как вы можете заметить, наши функции vmath::rotate принимают угол, на который осуществляется поворот, и ось, вокруг которой он осуществляется. Конечно, объединение трех вращений вместе, по одному для каждой из осей х, у и z, позволяет использовать углы Эйлера, если нужно, но намного предпочтительнее для поворотов использовать представление угл-ось или кватернионы для представления преобразований, конвертируя их в матрицах по мере необходимости.

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

Здесь sx, sy и sz представляют собой коэффициенты масштаба по осям x, y и z соответственно. Создание масштабирующей матрицы при помощи библиотеки vmath похоже на создание матриц параллельного переноса и вращения. Есть три функции, которые создадут за вас матрицу:

template <typename T>
static inline Tmat4<T> scale(T x, T y, T z) { ... }
template <typename T>
static inline Tmat4<T> scale(const Tvec3<T>& v) { ... }
template <typename T>
static inline Tmat4<T> scale(T x) { ... }

Первая из них производит масштабирование независимо по каждой оси (x, y и z) на значения, переданные в параметрах x, y и z. Вторая функция делает тоже самое, но использует для представления коэффициентов масштабирования трехкомпонентный вектор вместо трех отдельных параметров. Последняя масштабирует на одинаковое значение, x, по всем трем осям. Масштабирование не обязательно должно быть равномерным, вы можете использовать его как для растягивания, так и для сжатия объектов в разных направлениях. Например, куб размером 10 x 10 x 10 может быть промасштабирован в двух направлениях по осям x и z, как показано на рисунке 4.11.

Figure 4.11: Неравномерное масштабирование куба

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

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

vmath::mat4 translation_matrix = vmath::translate(4.0f, 10.0f, -20.0f);
vmath::mat4 rotation_matrix = vmath::rotate(45.0f,
vmath::vec3(0.0f, 1.0f, 0.0f));
vmath::vec4 input_vertex = vmath::vec4(...);
vmath::vec4 transformed_vertex = translation_matrix *
rotation_matrix *
input_vertex;

Этот код сначала поворачивает модель на 45° вокруг оси y (благодаря rotation_matrix) и потом перемещает ее на 4 единицы по оси x, на 10 единиц по оси y и в отрицательном направлении на 20 единиц по оси z (благодаря translation_matrix). Эти операции ориентируют модель определенным образом и потом позиционируют ее в определенной точке. Читая последовательность преобразований в обратном направлении, можно понять порядок выполнения операций (вращение, потом параллельный перенос). Мы можем переписать этот код следующим образом:

vmath::mat4 translation_matrix = vmath::translate(4.0f, 10.0f, -20.0f);
vmath::mat4 rotation_matrix = vmath::rotate(45.0f,
vmath::vec3(0.0f, 1.0f, 0.0f));
vmath::mat4 composite_matrix = translation_matrix * rotation_matrix;
vmath::vec4 input_vertex = vmath::vec4(...);
vmath::vec4 transformed_vertex = composite_matrix *
input_vertex;

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

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

Кватернионы
Кватернион - четырехмерная величина, похожая в некотором смысле на комплексные числа. Кватернион имеет действительную часть и три мнимые (по аналогии с мнимой единицей в комплексных числах). Так же, как комплексное число имеет мнимую часть i, кватернион имеет три мнимые части: i, j и k. Кватернион q имеет следующее математическое представление:
q = (x + yi + zj + wk)

Мнимые части кватерниона обладают свойствами, похожими на свойства мнимой части комплексного числа. В частности,
i2 = j2 = k2 = ikj = −1

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

Как и в случае с комплексными числами, операция умножения кватернионов некоммутативна. Сложение и вычитание для кватернионов определяется как простое векторное сложение и вычитание, т.е. выполняемое на уровне компонентов. Другие функции, такие как унарное отрицание и magnitude определены также, как и для четырехкомпонентного вектора. Хотя кватернион - четырехкомпонентная сущность, общепринятым является представление кватерниона в виде реальной скалярной части и мнимой трехкомпонентной векторной части. Такое представление часто записывается следующим образом:

Хорошо, отлично - но это же глава не о той самой “страшной математике”, не так ли? Она о компьютерной графике, OpenGL и других прикольных вещах. Поэтому, скажем, где кватернионы становятся действительно полезными. Помните, наши функции вращения принимали угол и ось? Мы можем представить эти две величины в виде кватерниона: угол будет соответствовать реальной части, а ось - векторной - это дает кватернион, представляющий вращение вокруг любой оси.

Последовательность вращений может быть представлена в виде серии кватернионов перемноженных между собой, образуя тем самым единственный результирующий кватернион, который кодирует всю серию преобразований, которые можно применить всего в одном проходе. Хотя можно создать целый набор матриц, которые представляют вращение вокруг различных декартовых осей, и затем перемножить их вместе, данный метод подвержен блокировке вращения. Если вы делаете то же самое с последовательностью кватернионов, блокировка вращения произойти не может. Чтобы подарить вам удовольствие от кодирования, библиотека vmath уже содержит класс vmath::quaterion, который реализует большую часть функций, описанных здесь.

Модельно-видовое преобразование
В простом OpenGL-приложении, наиболее часто используемое преобразование - перемещение модели из пространства модели в видовое пространство для ее отрисовки. В действительности, мы перемещаем модель сначала в мировое пространство (то есть, позиционируем ее относительно начала сцены), а затем оттуда в видовое пространство (​​позиционируя ее относительно наблюдателя). Этот процесс устанавливает точку наблюдения сцены (точку расположения камеры). По умолчанию, точка наблюдения в перспективной проекции находится в начале координат (0,0,0), (смотря / уходя) вниз отрицательным направлением оси z (в монитор или экран). Эта точка наблюдения сдвинута относительно системы координат глаз. Когда точка наблюдения находится в начале координат, как и в перспективной проекции, объекты, нарисованные с положительными значениями z, находятся позади наблюдателя. В ортогональной проекции, тем не менее, предполагается, что наблюдатель находится бесконечно далеко в положительном направлении по оси z и может видеть все, что располагается внутри видимого объема.

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

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

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

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

Lookat-матрица
Если имеется точка наблюдения и объект, на который вы хотите посмотреть, вы, вероятно, захотите разместить в этой точке виртуальную камеру, а затем сориентировать ее в направлении этого объекта. Для того, чтобы правильно сориентировать камеру, вы также должны знать, какое направление указывает вверх. В противном случае, камера могла бы вращаться вокруг своей передней оси, и даже если она технически будет по-прежнему указывать в правильном направлении, это почти наверняка окажется не тем, что вы хотите. Таким образом, имея: исходную точку, интересующую нас точку, и направление, которое мы определяем указывающим вверх - мы задали бы последовательность преобразований, в идеале, объединенные в единственной матрице, которые представят вращение, ориентирующее камеру в правильном направлении, и перенос, перемещающий начало координат в центр камеры. Эта матрица известна как lookat-матрица. Она может быть построена с использованием той математики, которая уже была описана в этой главе ранее.

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

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

Эти три вектора имеют единичную длину и все ортогональны друг другу, и поэтому они образуют набор ортонормированных базисных векторов, представляя наш кадр. Имея эти три вектора, мы можем построить матрицу вращения, которая будет принимать точку в стандартной декартовой системе координат и перемещать ее в систему координат нашей камеры. В следующем математической формуле, e - это положение глаз (или камеры), p - интересующая нас точка, u - восходящий вектор. Итак...

Сначала, построим наш вектор взгляда, f:

Далее, возьмем векторное произведение векторов f и u, чтобы построить боковой вектор s:

Теперь, построим новый восходящий вектор u′ относительно начала отсчета нашей камеры:

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

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

Наконец-то мы получили нашу lookat-матрицу T. Если для вас этот процесс содержит чересчур много шагов, вам повезло. В библиотеке vmath есть функция, которая построит эту матрицу за вас:

template <typename T>
static inline Tmat4<T> lookat(const vecN<T,3>& eye,
const vecN<T,3>& center,
const vecN<T,3>& up) { ... }

Матрица, построенная с помощью функции vmath::lookat, может быть применена к камере для задания ее положения и ориентации. Другими словами, она может быть вашей видовой матрицей.

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

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

Перспективная проекция отображает сцены более реалистично, как в жизни, а не как на чертежах. Отличительной чертой перспективных проекций является ракурс, что делает удаленные объекты кажущимися меньше тех, которые расположены ближе и имеют те же размеры. Линии в трёхмерном пространстве, которые могут быть параллельными, не всегда параллельны для наблюдателя. Рассмотрим, для примера, железную дорогу. Рельсы расположены параллельно, но по мере удаления кажется, что они сходятся в некоторой точки. Преимущество перспективной проекции заключается в том, что вам не нужно выяснить, где сходятся линии или насколько меньше должны быть удаленные объекты. Все, что вам нужно сделать, это определить сцену, используя модельно-видовые преобразования, а потом применить матрицу перспективной проекции. Линейная алгебра наколдует остальное сама. Рисунок 4.12 позволяет сравнить ортогональную и перспективную проекции на двух разных сценах. Как вы можете видеть, при ортогональной проекции, показанной слева, кубики не изменяются в размерах по мере удаления от наблюдателя. В то время, как при перспективной проекции, показанной справа, кубики становятся все меньше и меньше.

Рисунок. 4.12: сравнительный пример ортогональной и перспективной проекций

Ортогональные проекции используются наиболее часто для целей 2D-рисования, когда требуется точное соответствие между пикселями и объектами на чертеже. Вы можете использовать их для схематических макетов, текста или 2D-графиков. Вы также можете использовать ортогональную проекцию для 3D-рендеринга, в случае, если глубина рендеринга ничтожно мала по сравнению с расстоянием от точки наблюдения. Перспективные проекции используются для рендеринга сцен, которые содержат открытые пространства или объекты, которым нужно задать ракурс. По большей части, перспективные проекции типичны для 3D-графики. На самом деле, наблюдение за 3D-объектами в ортогональной проекции может быть несколько тревожным.

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

Для этих целей в библиотеке vmath существует функция vmath::frustum:

static inline mat4 frustum(float left,
float right,
float bottom,
float top,
float n,
float f) { ... }

Другой распространенный метод для построения перспективной матрицы заключается в прямом задании области видимости в виде угла (возможно, в градусах), соотношения сторон (как правило, получаемое путем деления ширины окна на его высоту) и пространственно-видовых положений ближней и дальней плоскостей. Этот метод несколько проще, но задать получается только симметричную пирамиду. Однако, это почти всегда то, что нужно. Для этих целей в библиотеке vmath существует функция vmath::perspective:

static inline mat4 perspective(float fovy /* в углах */,
float aspect,
float n,
float f) { ... }

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

И снова в библиотеке vmath представлена функция для построения подобной матрицы - vmath::ortho:

static inline mat4 ortho(float left,
float right,
float bottom,
float top,
float near,
float far) { ... }

Интерполяция, линии, кривые и сплайны
Интерполяция - это термин, используемый для описания процесса поиска значения, которые лежат между набором известных точек. Рассмотрим уравнение прямой, проходящей через точки А и В:

где P - это любая точка на линии, а D - вектор от A до B:

Поэтому мы можем переписать это уравнение следующим образом

P = A + t (B – A) or
P = (1 – t) A + tB

Легко увидеть, что при t равном нулю, P равна A, а когда t равен единице, P равен A + B – A, т.е. просто B. Такая линия показана на рисунке 4.13.

Рисунок 4.13: Нахождение точки на линии

Если t лежит между 0.0 и 1.0, то Р будет в конечном итоге лежать где-то между А и В. Значения t за пределами этого диапазона будет выталкивать P за концы линии. Вы должны видеть, что путем плавного изменения t, мы можем двигаться точку Р из А в В и обратно. Это называется линейной интерполяцией. Значения A и B (и, следовательно, P) может иметь любое число измерений. Например, они могут быть скалярными значениями; двумерными значениями вроде точек на графике; трехмерными значениями вроде координат в трехмерном пространстве, значениями цветов и т.д.; или величинами даже более высокой размерности вроде матриц, массивов и даже целыми изображениями. Во многих случаях, линейная интерполяция не имеет особого смысла (например, линейная интерполяция между двумя матрицами, как правило, не дает значимого результата), но углы, позиции и координаты обычно можно интерполировать безопасно.

Линейная интерполяция - настолько распространенная операция в графике, что GLSL имеет встроенную функцию mix специально для этой цели:

vec4 mix(vec4 A, vec4 B, float t);

Функция mix имеет несколько версий, которые принимают векторы различной размерности или скаляры во входных параметрах А и В, а также принимают скаляры или соответствующие вектора в параметре t.

Кривые
Если перемещать все вдоль прямой линии между двумя точками - это все, что мы хотим делать, то этой информации будет достаточно. Однако, в реальном мире, объекты движутся по гладким кривым, плавно ускоряясь и замедляясь. Кривая может быть представлена ​​тремя или более контрольными точками. Для большинства кривых, определено более трех контрольных точек, две из которых образуют концевые точки, а остальные задают форму кривой. Рассмотрим простой кривую, показанную на рисунке 4.14.

Рисунок 4.14: простая кривая Безье

Кривая, показанная на рисунке 4.14, имеет три контрольные точки A, B и C. А и С являются концевыми точками кривой, а В определяет форму кривой. Если мы соединим точки А и В одной линией, а точки В и С другой, то мы сможем произвести интерполяцию вдоль двух линий, используя простую линейную интерполяцию, чтобы найти новую пару точек D и E. Теперь, имея эти две точки, мы можем, соединив их с помощью еще одной линии, произвести вдоль нее интерполяцию и найти новые точку P. И пока мы будем изменять наш параметр интерполяции t, точка P будет двигаться по гладкой криволинейной траектории от А до D. Выразив это математически, получим

D = A + t(B – A)
E = B + t(C – B)
P = D + t(E – D)

Сделав подстановку вместо D и E и сократив формулу, мы получим следующее:

P = A + t(B – A) + t((B + (t(C – B))) – (A + t(B – A))))
P = A + t(B – A) + tB + t2(C – B) – tA – t2(B – A)
P = A + t(B – A + B – A) + t2(C – B – B + A)
P = A + 2t(B – A) + t2(C – 2B + A)

Вы должны узнать в этой формуле квадратное уравнение с переменной t. Кривая, которую описывает данное уравнение, известна как квадратичная кривая Безье. На самом деле, в GLSL мы можем реализовать этот функционал очень легко благодаря функции mix, так как все, что мы делаем, является линейной интерполяцией результатов двух предыдущих интерполяций.

vec4 quadratic_bezier(vec4 A, vec4 B, vec4 C, float t)
{
vec4 D = mix(A, B, t); // D = A + t(B - A)
vec4 E = mix(B, C, t); // E = B + t(C - B)
vec4 P = mix(D, E, t); // P = D + t(E - D)
return P;
}

Добавив четвертую контрольную точку, как показано на рисунке 4.15, мы можем увеличить порядок кривой на единицу и получить кубическую кривую Безье.

Рисунок 4.15: кубическая кривая Безье

Теперь у нас есть четыре контрольные точки A, B, C и D. Способ построения кривой аналогичен построению квадратичной кривой Безье. Мы строим первую линию от A до B, вторую от B к C, третью от С до D. Интерполирование вдоль всех из трех линий порождает три новые точки E, F и G. Используя эти три точки, мы формируем еще две линии: одну от E до F, а другую от F до G, интерполирование вдоль которых порождает точки H и I, между которыми мы можем произвести еще одну интерполяцию, чтобы найти наш конечную точку P. Таким образом, наше уравнение будет выглядеть как показано ниже.

E = A + t(B – A)
F = B + t(C – B)
G = C + t(D – C)
H = E + t(F – E)
I = F + t(G – F)
P = H + t(I – H)

Если вам показалось, что уравнения выше выглядят знакомо, вы правы - наши точки E, F и G образуют квадратичную кривую Безье, которую теперь мы используем при интерполяции для нахождения нашей конечной точки P. Если бы мы должны были подставить уравнения для точек E, F и G в уравнения для точек H и I, а те, в свою очередь, подставив в уравнение для P и раскрывая скобки, мы получим кубическое уравнение (“кубическое” означает t3). Опять же, в GLSL мы можем осуществить это просто и эффективно с точки зрения линейной интерполяцией благодаря функции mix:

vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t)
{
vec4 E = mix(A, B, t); // E = A + t(B - A)
vec4 F = mix(B, C, t); // F = B + t(C - B)
vec4 G = mix(C, D, t); // G = C + t(D - C)
vec4 H = mix(E, F, t); // H = E + t(F - E)
vec4 I = mix(F, G, t); // I = F + t(G - F)
vec4 P = mix(H, I, t); // P = H + t(I - H)
return P;
}

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

vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t)
{
vec4 E = mix(A, B, t); // E = A + t(B - A)
vec4 F = mix(B, C, t); // F = B + t(C - B)
vec4 G = mix(C, D, t); // G = C + t(D - C)
return quadratic_bezier(E, F, G, t);
}

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

vec4 quintic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, vec4 E, float t)
{
vec4 F = mix(A, B, t); // F = A + t(B - A)
vec4 G = mix(B, C, t); // G = B + t(C - B)
vec4 H = mix(C, D, t); // H = C + t(D - C)
vec4 I = mix(D, E, t); // I = D + t(E - D)
return cubic_bezier(F, G, H, I, t);
}

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

Сплайны
Сплайн - это фактически длинная кривая состоящая из нескольких небольших кривых (например, Безье). Как минимум, контрольные точки, представляющие концы кривых, разделены между сегментами,[*] и часто одна или более внутренняя контрольная точка одного сегмента является общей или связанной, тем или иным образом, с соседним сегментом. Любое количество кривых может быть соединено вместе этим способом, позволяя формировать сколь угодно длинные линии. Взгляните на кривую, изображенную на рисунке 4.16.

--------
Это то, что (скрепляет / сцепляет) кривые вместе для формирования сплайна. Эти контрольные точки известны как швами (welds), а контрольные точки, находящиеся между ними, иногда называют узлами.

Рисунок 4.16: Кубический сплайн Безье

На рисунке 4.16, кривая определена десятью контрольными точками, от А до J, которые образуют три кубических кривых Безье. Первая определена точками A, B, C и D; вторая образована общей точкой D, а также E, F и G; третья начинается с общей G, за которой идут H, I и J. Этот тип сплайна известен как кубический сплайн Безье, потому что построен из последовательности кубических кривых Безье. Он также называется кубическим B-сплайном - термин, который может быть знаком тем, кто раньше уже читал что-то о графике.

Для интерполяции точки P вдоль сплайна, мы просто разделим его на три части, допуская изменение значения t в диапазоне от 0.0 до 3.0. Между 0.0 и 1.0, мы интерполируем вдоль первой кривой, переходя от А до D. Между 1.0 и 2.0, мы интерполируем вдоль второй кривой, переходя от D до G, а когда t изменяется от 2.0 до 3.0, мы интерполируем вдоль последней кривой между точками G и J.

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

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

vec4 cubic_bspline_10(vec4 CP[10], float t)
{
float f = t * 3.0;
int i = int(floor(f));
float s = fract(t);
if (t <= 0.0)
return CP[0];
if (t >= 1.0)
return CP[9];
vec4 A = CP[i * 3];
vec4 B = CP[i * 3 + 1];
vec4 C = CP[i * 3 + 2];
vec4 D = CP[i * 3 + 3];
return cubic_bezier(A, B, C, D, s);
}

Если мы используем сплайн для определения положения или ориентации объекта, мы должны быть очень осторожны при выборе позиций контрольных точек, чтобы сохранить движение плавным. Частота изменения значения нашей интерполированной точки P (скорость) является дифференциалом уравнения кривой относительно t. Кроме того, частота изменения скорости P (ускорение) является производной второго порядка уравнения сплайна относительно t. Если ускорение не плавное, то P станет внезапно ускоряться или замедляться.

Функция, которая имеет непрерывную первую производную называется C1-непрерывной, и аналогично кривая, которая имеет непрерывную вторую производную называется C2-непрерывной. Сегменты кривой Безье являются C1- и C2-непрерывными, но для того, чтобы убедиться, что мы сохраняем преемственность над швами (welds) сплайна, мы должны убедиться, что каждый сегмент начинается с условиями, при которых предыдущий заканчивается, а именно: в положении, направлении движения и частоте изменения. Итак, частота перемещения в определенном направлении - это просто скорость. Таким образом, вместо того, чтобы назначать произвольные контрольные точки нашему сплайну, мы можем назначить скорость в каждом weld. Если одинаковая скорость кривой в каждом weld используется для вычисления сегментов кривой по обе стороны от этого weld, то мы будем иметь сплайн-функцию, которая является одновременно и C1-, и C2-непрерывной.

Вы все поймете, если еще раз взглянете на рисунок 4.16 - перегибов нет, кривая красиво и плавно проходит через welds (точки D и G). Теперь посмотрим на контрольные точки по обе стороны от welds. Для примера, возьмем точки С и E, которые окружают D. С и E образуют прямую линию, а D находится прямо в середине нее. На самом деле, мы можем назвать отрезок от D до E скоростью в D, или Vd. Имея положение точки D (weld) и скорости кривой Vd в D, точки С и Е могут быть рассчитаны так

Аналогично, если Va является скоростью в A, то B может быть рассчитано так

Таким образом, вы можете видеть, что, имея положения и скорости в welds кубического B-сплайна, мы можем обойтись без других контрольных точек, вычисляя их на лету также, как мы вычисляем каждую контрольную точку. Кубический B-сплайн, определенный подобным образом (в виде набора положений и скоростей weld), известен как кубический эрмитов сплайн (cubic Hermite spline, cspline). Это чрезвычайно полезный инструмент для получения плавной и естественной анимации.

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

Наконец, мы показали, как использовать библиотеку vmath, которая поставляется вместе с этой книгой. Эта библиотека, написанная полностью на переносимом C++, предоставляет вам удобный инструментарий из различных математических и прочих вспомогательных функций, которые могут быть использованы вместе с OpenGL.

Удивительно, но в этой главе мы не рассмотрели ни одной новой для нас OpenGL-функции. Да, эта глава была о математике, и вы могли даже не заметить этого, если считаете, что математика - это только формулы и расчеты. Векторы и матрицы абсолютно необходимы для того, чтобы вы смогли использовать OpenGL для отрисовки трехмерных миров и объектов. Тем не менее, важно отметить, что OpenGL не налагает никаких особых условий на использование математики, как и не содержит никакой реализации математических функций. Даже если вы используете другую математическую библиотеку, или даже написали свою собственную, она вы все равно будет построена на основе тех концепций, что были изложены в этой главе. А теперь двигаемся вперед и начинаем что-то делать!

Внимание! Этот перевод, возможно, ещё не готов.
Его статус: перевод редактируется

Переведено на Нотабеноиде
http://notabenoid.org/book/54048/235164

Переводчики: vk955

Настройки

Готово:

99.46% КП = 1.0