Цей туторіал трішки не про OpenGL, але про дуже загальну проблему - як представити повороти?

В третьому туторіалі - Матриці, ми вивчили як матриці здатні повертати точку навколо певної осі. Хоча матриці є акуратним способом для перетворення вершин, сама робота з матрицями досить складна - отримання осі обертання з фінальної матриці досить складна задача.

Ми подивимось на два основних способи представлення поворотів - кути Ейлера та кватерніони. А головне, ми пояснимо, чому Ви повинні віддавати перевагу кватерніонам.

Передмова: обертання проти орієнтації

Читаючи статтю про повороти та орієнтацію, Ви можете плутати ці поняття. В цьому туторіалі:

  • Орієнтація - це стан - “Об’єкт має наступну орієнтацію…”
  • Поворот - це операція - “Виконаємо поворот об’єкта…”

Тобто, коли Ви виконуєте поворот Ви змінюєте орієнтацію. Обидва вирази можуть позначати одне й теж, що може трішки заплутувати. Отже, почнемо…

Кути Ейлера

Кути Ейлера - це найпростіший спосіб уявити орієнтацію. Ви просто зберігаєте значення поворотів навколо осі X, Y та Z. Це дуже проста концепція для розуміння. Ви можете використовувати vec3, що б їх зберегти.

vec3 EulerAngles( RotationAroundXInRadians, RotationAroundYInRadians, RotationAroundZInRadians);

Ці три повороти виконуються по черзі, зазвичай в наступному порядку - спочатку Y, потім Z і в кінці X (але це не обов’язково). Використання іншого порядку призведе до інших результатів.

Одне з найпростіших використань кутів Ейлера - встановлення орієнтації персонажа. Зазвичай, ігровий персонах не крутиться по осям X та Z, тільки вертикально. Тому, це дуже легко написати, розуміти та підтримувати float direction, а не вектор з трьох орієнтацій.

Інший гарний приклад використання кутів Ейлера - FPS камера - у Вас є один кут для напрямку (Y) і один для “вгору-вниз” (X). Дивіться приклад тут common/controls.cpp

Нажаль, коли все стає складнішим, кути Ейлера погано справляються з роботою. Наприклад:

  • Плавне переміщення між 2 орієнтаціями дуже складний. Якщо просто наївно робити інтерполяцію кутів по осями X,Y та Z, результат може бути дуже дивним.
  • Виконання декількох поворотів одночасно досить складне та неточне - Вам потрібно розрахувати кінцеву матрицю повороту та вгадати кути з неї.
  • Одна з відомих проблем - “Блокування обертання” - “Gimbal Lock” - який може блокувати Ваші повороти або просто перевернуть Вашу модель.
  • Різні кути будуть давати однакові повороти (наприклад, -180° та 180°).
  • І, як було сказано вище, зазвичай правильний порядок поворотів - YZX, але можуть бути бібліотеки, які використовують інший порядок і у Вас будуть проблеми.
  • Деякі операцію дуже складні. Наприклад, поворот на N градусів в обраному напрямку.

Кватерніони допомагають вирішити всі ці проблеми.

Кватерніони

Кватерніон це масив з 4 чисел, [x y z w] які мають наступні значення:

// RotationAngle в радіанах, так як sin/cos приймає радіани
x = RotationAxis.x * sin(RotationAngle / 2)
y = RotationAxis.y * sin(RotationAngle / 2)
z = RotationAxis.z * sin(RotationAngle / 2)
w = cos(RotationAngle / 2)

RotationAxis це, як говорить ім’я, осі, навколо яких будуть повороти.

RotationAngle це кут, на який буде поворот.

Отже, по суті, кватерніони зберігають вісь обертання та кут повороту таким чином, що полегшує комбінування обертань.

Читання кватерніонів

Цей формат точно менш інтуїтивний, ніж кути Ейлера, але все ще можна здогадатись - компонента xyz приблизно відповідає осям повороту, а w - це косинус кута повороту, поділений на 2. Наприклад, уявімо, що ви бачите таке значення - [ 0.7 0 0 0.7 ]. x=0.7, точно більше, ніх y та z, отже поворот відбувається в основному навколо осі X. 2*acos(0.7) = 1.59 радіан, це власне кут повороту, приблизно рівний 90°.

Аналогічно, [0 0 0 1] (w=1) означає, що кут рівний 2*acos(1) = 0, отже це “одиничний кватерніон”, який нічого не повертає.

Базові операції

Знання математики, що стоїть за кватерніонами, рідко буває корисним - їх представлення настільки не інтуітивне, що Ви будете зазвичай покладатись на допоміжні функції, які зроблять всю математику. Якщо ж Вам цікаво, подивіться сторінку з математичними книгами Корисні інструменти та посилання

Як створити кватерніон в с++?

// не забудьте про #include <glm/gtc/quaternion.hpp> and <glm/gtx/quaternion.hpp>

// Створити одиничний кватерніон (нема повороту)
quat MyQuaternion;

// Пряма передача всіх чотирьох компонентів
// Навряд чи Ви це будете використовувати
MyQuaternion = quat(w,x,y,z); 

// Конвертування з кутів Ейлера (в радіанах?) в кватерніон
vec3 EulerAngles(90, 45, 0);
MyQuaternion = quat(EulerAngles);

// Перетворення з осей-кутів
// В GLM кути мають бути в градусах, отже, конвертуємо
MyQuaternion = gtx::quaternion::angleAxis(degrees(RotationAngle), RotationAxis);

Як створити кватерніон в GLSL ?

Ніяк. Конвертуйте Ваш кватерніон в матрицю повороту і використовуйте в Матриці Моделі. Ваші вершини будуть крутитись як і раніше, за допомогою MVP матриці.

В деяких випадках, Вам може дійсно захотітись використовувати кватерніони в GLSL, наприклад, для скелетної анімації на GPU. В GLSL немає спеціального типу для кватерніонів, але Ви можете використовувати vec4 і виконувати всю математику самостійно.

Як конвертувати кватерніон в матрицю ?

mat4 RotationMatrix = quaternion::toMat4(quaternion);

І тепер можете використовувати для отримання матриці Моделі як і раніше:

mat4 RotationMatrix = quaternion::toMat4(quaternion);
...
mat4 ModelMatrix = TranslationMatrix * RotationMatrix * ScaleMatrix;
// Тепер Ви можете використовувати ModelMatrix для отримання MVP матриці

Отже, що обрати: кути Ейлера чи кватерніони?

Вибір між кутами Ейлера та кватерніонами є складним. Кути Ейлера є інтуітивно зрозумілими для дизайнерів, якщо ви пишете якийсь 3D редактор - використовуйте їх. А кватерніони зручні для програмістів і досить швидкі, отже їх варто використовувати в ядрі 3D рушія.

Загальний консенсус саме такий - кватерніони всередині, кути Ейлера назовні, в інтерфейсі користувача.

Ви зможете впоратись з всім, що вам потрібно (як мінімум, це буде не складно) і Ви все ще зможете використовувати кути Ейлера для сутностей, які потребують цього (як було сказано вище - камера, гуманоїди і подібне) і робити це з невеликими перетвореннями.

Інші ресурси

Шпаргалки

Як дізнатись, що два кватерніони подібні?

Для векторів скалярний добуток дає косинус кута між ними. Якщо воно рівне 1, вони в одному напрямку.

З кватерніонами те ж саме:

float matching = quaternion::dot(q1, q2);
if ( abs(matching-1.0) < 0.001 ){
    // q1 і q2 подібні
}

Ви також можете отримати кут між q1 та q2, використавши арккосинус для скалярного добутку

Як повернути точку?

Робіть наступне:

rotated_point = orientation_quaternion *  point;

… але якщо Ви хочете обчислити матрицю моделі, можливо краще конвертувати в матрицю.

Зверніть увагу, що центр обертання завжди знаходиться в центрі координат. Якщо ж Ви хочете обертати навколо іншої точки:

rotated_point = origin + (orientation_quaternion * (point-origin));

Як зробити інтерполяцію між двома кватерніонами?

Це називається SLERP - Spherical Linear intERPolation - сферична лінійна інтерполяція. З GLM ви можете зробити це за допомогою функції mix:

glm::quat interpolatedquat = quaternion::mix(quat1, quat2, 0.5f); // чи інший множник в діапазоні 0..1

Як накопичити два повороти?

Це легко! Просто перемножте два кватерніони. Порядок такий самий, як і для матриць - зворотній:

quat combined_rotation = second_rotation * first_rotation;

Як знайти поворот між двома векторами?

(іншими словами - знайти кватерніон, який потрібний для того, що б так повернути v1, що б він став v2)

Ідея дуже проста:

  • Кут між цими двома векторами дуже легко знайти - скалярний добуток є косинусом цього кута.
  • Потрібні осі теж легко знайти - це векторний добуток цих векторів.

Наступний код робить саме це, але ще враховує декілька специфічний випадків:

quat RotationBetweenVectors(vec3 start, vec3 dest){
	start = normalize(start);
	dest = normalize(dest);

	float cosTheta = dot(start, dest);
	vec3 rotationAxis;

	if (cosTheta < -1 + 0.001f){
		// Спеціальний випадок - вектори направлені в протилежному напрямку:
		// тут немає ідеальної осі для повороту
		// отже, оберемо будь-яку, яка перпендикулярна до початкової
		rotationAxis = cross(vec3(0.0f, 0.0f, 1.0f), start);
		if (gtx::norm::length2(rotationAxis) < 0.01 ) // Навдача, вони паралельні, спробуємо ще раз!
			rotationAxis = cross(vec3(1.0f, 0.0f, 0.0f), start);

		rotationAxis = normalize(rotationAxis);
		return gtx::quaternion::angleAxis(glm::radians(180.0f), rotationAxis);
	}

	rotationAxis = cross(start, dest);

	float s = sqrt( (1+cosTheta)*2 );
	float invs = 1 / s;

	return quat(
		s * 0.5f, 
		rotationAxis.x * invs,
		rotationAxis.y * invs,
		rotationAxis.z * invs
	);

}

(Ви можете знайти цю функцію тут common/quaternion_utils.cpp)

Мені потрібен аналог функції gluLookAt. Як мені зорієнтувати об’єкт в напрямку точки?

Використовуйте RotationBetweenVectors !

// Знайти поворот між передньою частиною об'єкта (ми допускаємо, що вісь Z направлена до нас
// але це залежить від Вашої моделі) і бажаним напрямком
quat rot1 = RotationBetweenVectors(vec3(0.0f, 0.0f, 1.0f), direction);

Now, you might also want to force your object to be upright:

// Заново розрахуйте desiredUp так що воно буде перпендикулярно до напрямку
// Ви можете пропустити цю частину, якщо Ви дійсно бажаєте змусити desiredUp
vec3 right = cross(direction, desiredUp);
desiredUp = cross(right, direction);

// Тому що через перший поворот, напрямок "вгору" може бути "зіпсованим"
// Знайдемо поворот між "вгору" об'єкта, який повертається і бажаного "вгору"
vec3 newUp = rot1 * vec3(0.0f, 1.0f, 0.0f);
quat rot2 = RotationBetweenVectors(newUp, desiredUp);

Тепер об’єднаємо їх:

quat targetOrientation = rot2 * rot1; // пам'ятаєте, зворотній порядок.

Будьте обережні, “напрямок” це напрямок, а не бажана позиція! Але її легко розрахувати - targetPos - currentPos.

Як тільки ви отримали цільову орієнтацію, ми можете захотіти інтерполювати орієнтацію між startOrientation і targetOrientation.

(Ви можете знайти цю функцію в common/quaternion_utils.cpp)

Як використовувати LookAt, але обмежити поворот певною швидкістю?

Основна ідея - використовувати SLERP ( = use glm::mix ), але погратись з значенням інтерполяції, що б кут не був більшим, ніж бажане значення:

float mixFactor = maxAllowedAngle / angleBetweenQuaternions;
quat result = glm::gtc::quaternion::mix(q1, q2, mixFactor);

Ось практично вся реалізація, яка працює з більшістю спеціальних випадків. Зверніть увагу, що тут немає функції mix() для оптимізації.

quat RotateTowards(quat q1, quat q2, float maxAngle){

	if( maxAngle < 0.001f ){
		// Не потрібно робити поворот, що б не було ділення на нуль.
		return q1;
	}

	float cosTheta = dot(q1, q2);

	// q1 і q2 дуже подібні
	// Форсуємо q2 для впевненості
	if(cosTheta > 0.9999f){
		return q2;
	}

	// Не будемо обирати "довгий шлях" через всю сферу
	if (cosTheta < 0){
	    q1 = q1*-1.0f;
	    cosTheta *= -1.0f;
	}

	float angle = acos(cosTheta);

	// Якщо тут всього 2&deg; різниці, а ми дозволяємо 5&deg;,
	// тоді ми нічого не робимо.
	if (angle < maxAngle){
		return q2;
	}

	float fT = maxAngle / angle;
	angle = maxAngle;

	quat res = (sin((1.0f - fT) * angle) * q1 + sin(fT * angle) * q2) / sin(angle);
	res = normalize(res);
	return res;

}

Використовувати наступним чином:

CurrentOrientation = RotateTowards(CurrentOrientation, TargetOrientation, 3.14f * deltaTime );

(Ви можете знайти цю функцію в common/quaternion_utils.cpp)

Як…

Якщо Ви не можете чогось зрозуміти, напишіть нам лист, і ми додамо Ваше питання до списку!