Введение: зачем нужна линейная алгебра в 3D и операции над векторами.
Это первый, вводный урок по линейной алгебре для разработки 3D-приложений от Александра Паничева — ведущего разработчика логики в UNIGINE. В этом уроке разберемся зачем 3D-разработчикам вообще нужна линейная алгебра, а также рассмотрим основные операции над векторами.
Во втором уроке будут разобраны более сложные темы: углы Эйлера, кватернионы и матрицы.
Почему именно линейная алгебра?
Во-первых, в любом 3D-приложении мы так или иначе сталкиваемся с векторами и вращениями. Vector, Matrix — об этих терминах слышали все. Мы двигаем объекты, поворачиваем их на определенные градусы, вытаскиваем в процессе всякую полезную информацию для дальнейших вычислений… Поэтому умение оперировать с ними быстро и эффективно, минуя лишнюю тригонометрию там, где она не нужна, крайне важно!
Во-вторых, даже после работы в геодезических координатах все сводится к обычному трехмерному евклидову пространству. Таков уж рендер: простой, не кривой. Поэтому знать линал — основа жизни 3D-шника!
А кроме того, у многих знания по математике обрывочные. Надо заполнить пробелы!
Из чего состоит математическая библиотека 3D-движка?
В игровых движках обычно есть 3 группы:
-
Вектора. Может быть точкой, радиус-вектором, направлением (нормаль), линейной скоростью, угловой скоростью, углами в градусах или радианах (углах Эйлера).
Вектор-точка — просто позиция в пространстве. Радиус-вектор означает, что его начало находится в 0 относительно начала координат. Направление — это нормализованный вектор, который можно назвать радиус-вектором фиксированной (единичной) длины. Линейная скорость и угловая скорость отвечают за динамику. А углы Эйлера подробно разберем во втором уроке.
-
Матрицы. Матрица смещения, матрица вращения, матрица масштабирования, матрица трансформации (TRS), проекции (перспективная, ортографическая), система линейных алгебраических уравнений, матрица Якоби и Тензор инерции, матрица гомографии.
Матрица гомографии используется в камерах для правильной проекции с разных углов обзора.
-
Кватернионы. Вращение, выраженное в кватернионах.
Через что можно представить ориентацию объекта?
-
Матрица вращения 3х3 (Matrix 3×3). Чтобы получить поворот, достаточно перемножить матрицу вращения на вектор или на матрицу трансформации. Но возникают проблемы, когда нужно плавно вращать объект из одних углов в другие — простыми способами это не сделать. Кроме того, легко накапливаются ошибки округлений, если много раз перемножать матрицу. В результате объект становится скошенным (ромбообразным).
-
Углы Эйлера (Euler Angles). Состоят из крена (Roll), тангажа (Pitch) и рысканья (Yaw). С этим легко разобраться и легко представить. Интерполяция вокруг одной оси тоже делается легко, но интерполяция по двум осям будет происходить не по кратчайшему пути, а по S-образной кривой. Кроме того, удобно ограничивать вращение сочленений в градусах. Главный недостаток — шарнирный замок (о нем позже).
-
Ось-Вращение (Axis – Angle). Простой метод с простой интерполяцией и удобным ограничением вращения. Однако с помощью него неудобно складывать несколько вращений вместе, чтобы получить единый объект вращения.
-
Кватернионы (Quaternion). Можно представить, как точку на поверхности трисферы единичного радиуса в четырехмерном пространстве. Легко складываются вращения, интерполяция идет по кратчайшему пути. Нет такого недостатка, как шарнирный замок. Просто ограничивать вращение. Однако сложны для прочтения. Кроме того, накапливают ошибку при многоразовом перемножении — нужно периодически нормализовать.
-
Экспоненциальное отображение (Exponential Map). Напоминает Ось-Вращение. Легко складывать вращения. Меньше степеней свободы, поэтому годится только как динамика вращения.
-
6-мерное представление (6D Representation). Зачастую случайно получается в конце вычислений. Часто используется в нейросетях. По сути — две оси: Forward и Up. Используется как основа для создания матрицы 3×3.
Кстати…
Для x нужно взять cos от угла, а для y — sin от угла. Также можно использовать функцию atan2, которая работает в диапазоне от -π до π.
Пару слов про производительность мат. функций
Сложение (plus), вычитание (minus), умножение (mult) и деление (div) занимает примерно одно время. А, например, вычисление квадратного корня (sqrt) в 3,6 медленнее. Самое медленное: аркосинусы (acos), арксинусы (asin), арктангенсы (atan) и округление (round).
Выводы:
-
Тригонометрия очень медленная. Особенно та, что возвращает углы.
-
Взятие квадратного корня (sqrt) по скорости примерно как 6 умножений (6_mult).
-
Вычисление наибольшего элемента (max) и округление (round), внезапно, сильно медленные.
Операции над векторами в 2D
Основные: сложение векторов, вычитание векторов, умножение вектора на скаляр и нормализация вектора. У них внутри простой код с простой нормализацией и взятием длины.
Пример. Есть персонаж, он стоит в начале координат и хочет добежать до дерева. В этом примере можно использовать все 4 базовые операции:
vec2 pos = vec2(2,2); // позиция персонажа vec2 tree = vec2(6,4); // позиция дерева vec2 distance = tree - pos // radius vector vec2 direction = distance.normalize(); // нормализованный unit vector vec2 new_pos = pos + direction * IFps // берем старую позицию и умножаем вектор на скаляр
dot product — скалярное произведение векторов
Это операция над двумя векторами, результатом которой является скаляр:
float dot(vec2 v0, vec2 v1) { v0.x * v1.x + v0.y * v1.y; // 2 умножения, 1 сложение }
-
Равен произведению длин векторов на косинус угла между ними:
dot(a,b) = |a||b|cos(angle_rad)
-
Скалярное произведение > 0, если вектора направлены в одну сторону, 0 — если вектора перпендикулярны и < 0, если направлены противоположно.
-
Является длиной проекции произвольного вектора на нормализованный вектор:
proj_length = dot(a, normal)
-
Скалярное произведение самого на себя является квадратом длины вектора:
dot(a,a) == length2(a)
-
Получить вектор-проекцию можно так:
proj_point = b*dot(a,b)/dot(b,b)
-
dot(a,b) == dot(b,a)
Где еще используется dot?
А как найти перпендикуляр?
В 2D все просто: переставляем (x,y) местами и у какого-нибудь компонента меняем знак. Например, для поворота по часовой стрелке нужно поставить минус у второго компонента, а против часовой — у первого.
А если совместить dot и нахождение перпендикуляра?
Для этого есть операция skew product — косое произведение векторов. Это операция над двумя векторами, результатом которой является псевдоскаляр:
float skew(vec2 v0, vec2 v1) { v0.x * v1.y - v0.y * v1.x; // 2 умножения, 1 вычитание }
В UNIGINE такая операция называется cross.
Где еще используется skew?
Операции над векторами в 3D
Работа с векторами в 3D мало чем отличается от 2D. Можно сказать, что решив задачу в 2D, вы решите ее и в 3D. Так, например, скалярное произведение векторов dot product, о котором речь ниже, одинаково работает в 2D и 3D.
Но в 3D появляется операция cross product — векторное произведение векторов. О нем поговорим в конце главы.
Где еще используется dot в 3D?
В шейдерах. Повсеместно. Например, рассмотрим простейшую модель освещения Ламберта (Lambert, Lambertian Reflectance, или Diffuse Light):
У нас есть модель с набором нормалей и где-то источник света.
Просто вычисляем угол между источником света и нормалью поверхности. Чем меньше угол, тем ярче пиксель.
Вот как выглядит алгоритм:
-
Получаем вектор нормали текущего пикселя — NormalVector.
-
Получаем вектор направления света относительно текущего пикселя — LightVector.
-
Нормализуем векторы.
-
Вычисляем угол между ними — dot.
-
Умножаем конечный цвет на этот коэффициент и коэффициент затухания.
float diffuse = max(dot( LightVector, NormalVector ), 0.0); float attenuation = saturate(1.0 - DistanceToLight / LightRadius); FragColor = color * diffuse * attenuation;
cross product — векторное произведение векторов
cross product появляется в 3D-пространстве. Это операция над двумя векторами, результатом которой является вектор, перпендикулярный исходным двум:
vec3 cross(const vec3 &v0, const vec3 &v1) { vec3 ret; ret.x = v0.y * v1.z - v0.z * v1.y; ret.y = v0.z * v1.x - v0.x * v1.z; ret.z = v0.x * v1.y - v0.y * v1.x; return ret; // 6 умножений, 3 вычитания }
-
Длина результирующего вектора равна площади параллелограмма, образованного исходными векторами.
-
Длина результата — это еще и
|a||b|sin(angle_rad)
-
Перпендикуляр строится по правилу «правой руки».
-
Не коммутативен. То есть:
cross(a,b) != cross(b,a)
Если вам вдруг интересно как потом этот результат можно использовать для вращения тела:
vec3 torque; // крутящий момент quat rotation; // текущее вращение тела // qnew = q0 + 0.5 * w * q0 quat q = (rotation + quat(torque * ifps) * rotation * 0.5f).normalize();
Конечно, задачу с танком можно решить и через:
vec3 rel_pos = inverse(tank_transform_mat4) * vec3_target_position;
Если известна обратная матрица, то такой способ будет примерно равен по скорости комбинации dot(cross).
Но… Не всегда у нас есть обратная матрица на руках. Мы можем быть в процессе изменения направления. Да и не всегда есть матрица как таковая.
Fun fact: Помните задачу нахождения отраженного вектора? Зная ее, можно легко то же самое сделать через dot (еще и быстрее будет работать!):
vec3 new_dir = dir - normal * dir(dir, normal);
dot(cross()) — scalar triple product, смешанное произведение
Скалярное произведение вектора a на векторное произведение векторов b и c
float scalar_triple(const vec3 &a, const vec3 &b, const vec3 &c) { // 9 умножений, 5 сложений return dot(a, cross(b, c)); }
-
Модуль смешанного произведения численно равен объему параллелепипеда, образованного векторами a,b,c.
-
dot(a,cross(b,c)) == dot(cross(a,b),c)
-
Равен детерминанту (определителю) матрицы, составленной из векторов a,b,c. В том числе и по перфу.
-
Аналог skew в мире 3D.
* * *
На этом пока все. Во втором уроке разберем сложные темы: углы Эйлера, кватернионы и матрицы.
ссылка на оригинал статьи https://habr.com/ru/company/unigine/blog/671294/
Добавить комментарий