Ćwiczenia11

Celem tych zajęć jest przećwiczenie wykorzystania kwaternionów do obsługi obrotów.

Opis projektu

Projekt rysuje ramie robota, naszym zadaniem jest dodanie obsługi jego stawów za pomocą myszki i klawiatury opcjonalnie gamepada.

W tej chwili rysowany obiekt nie wygląda jak ramie robota, naprawimy to w następnych zadaniach

Materiały i graf sceny

Import

modele które importujemy w trakcie tych zajęć mają rozszerzenie fbx są to dużo bardziej skomplikowane pliki od dotychczasowych obj, mogą na przykład one zawierać więcej niż jeden mesh lokalne macierze transformacji i informację o materiałach.

Graf sceny

Sceny w większych projektach potrafią być skomplikowane. Obiekty są zwykle rozmieszczone hierarchicznie. Przykładowo bohater w grze posiada ręce, które poruszają się razem z nim (z reguły) jednak mogą same z siebie się poruszać, gdy postać atakuje. W ręku tej postaci może znajdować się miecz lub inna broń i ta broń będzie poruszać zawsze gdy bohater się się będzie przemieszczać, ale też gdy się zamachnie. W poprzednich projektach przykładem takich zależności była planeta i krążący wokół niej księżyc. Takich zależności może być więcej i pamiętanie wszystkich poprzednich interakcji staje się kłopotliwe. Rozwiązaniem, które pozwala wprowadzić taką hierarchię jest graf sceny. Graf sceny jest drzewem (jako struktura danych), w którym każdy węzeł jest jakimś obiektem w grze oraz zawiera informację o lokalnej transformacji względem obiektu nadrzędnego-ojca. To rozwiązanie pozwala myśleć tylko o lokalnych transformacjach, a globalne odtworzyć przechodząc w górę grafu.

W naszym projekcie graf sceny jest zrealizowany w formie tablicy. To znaczy w projekcie znajduje się tablica robot_arm. Odpowiada ona za graf dla robota. Tablice zawierają węzły - struktury Core::Node:

	struct Node {
		std::vector<RenderContext> renderContexts;
		std::string name;
		glm::mat4 localTransformation;
		glm::mat4 cacheTransformation;
		glm::mat4 initialTransformation;
		int parent;
	};

W pojedynczym węźle znajduje się RenderContext, z których składa się to ramie, macierze transformacji lokalnej localTransformation i indeks węzła nadrzędnego (oraz dodatkowe macierze initialTransformation zawiera początkową transformację i cacheTransformation możemy wykorzystać do pośrednich obliczeń). Na przykład dla robot_arm[3] jego ojcem będzie robot_arm[robot_arm[3].parent], jeżeli atrybut parent jest równy -1 to znaczy, że dotarliśmy do korzenia, czyli obiektu który już nie ma nadrzędnego.

Zadanie 1

W tej chwili wszystkie elementy, na które składa się ramie są rysowane w początku układu współrzędnych. Wynika to z tego, że funkcjarenderRecursive jest niekompletna. Dopisz obliczanie macierzy transformacji. Najpierw przypisz do niej macierz transformacji z obecnego węzła node, następnie w pętli domnóż od lewej macierz transformacji nadrzędnego węzła, nadrzędnego nadrzędnego węzła i tak aż do korzenia. Po wykonaniu powinna wyglądać następująco:

aaa

Zadanie 1b*

Zoptymalizuj tą aplikację korzystając z programowania dynamicznego. Wykorzystaj cacheTransformation do przechowywania pośrednich wyników.

Zadanie 2

W dalszej części będziemy poruszać ramieniem poprzez rotacje. Musimy wiedzieć, gdzie znajdują się w grafie obiekty, które nas interesują. Aby się tego dowiedzieć wypisz w pętli indeksy i nazwy wszystkich obiektów znajdujących się w std::vector<Core::Node> robotArm. Zanotuj sobie gdzieś wyniki dla dalszych zadań.

Materiały*

Do tej pory ręcznie wybieraliśmy program, którym rysowaliśmy dla każdego obiektu ręcznie. wiązało się to z tym, że trzeba było pamiętać jakie parametry należy przesłać dla danego obiektu. Rozwiązaniem jest przechowywanie informacji o stosowanych shaderach i przesyłanych parametrach w osobnej strukturze nazywanej materiałem. W projekcie w klasie RenderContext jest wskaźnik na zmienną typu Material, której zadaniem jest przechowywanie tych informacji.

Zadanie 3*

Zmodyfikuj plik robot_arm.fbx w blenderze, zmień kolor segmentów na zielony lub niebieski, zapisz. Następnie odczytaj w projekcie.

Zadanie 3b**

Nałóż teksturę na ten obiekt w blenderze. zmodyfikuj kod aplikacji tak, żeby ładowała ścieżkę do tekstury.

Kwaterniony

Kwaterniony są rozszerzeniem liczb zespolonych, można je zapisać w postaci $$q=a+bi+cj+dk,$$ gdzie $a,b,c,d\in \mathbb{R}$ oraz $i,j,k$ spełniają następujące własności:

$i^2=j^2=k^2=-1,$

$ij=-ji=k,$

$ki=-ik=j,$

$jk=-kj=i.$

Możemy wykorzystać jednostkowe kwaterniony (jednostkowy to taki, który spełnia równanie $a^2+b^2+c^2+d^2=1$) do opisu rotacji w 3D. Dowolną rotację można opisać poprzez oś i kąt obrotu. Niech $\theta$ będzie kątem obrotu a $(u_x,u_y,u_z)$ jego osią, kwaternion odpowiadający tej rotacji jest opisany wzorem $q=\cos\tfrac{\theta}{2}+(u_x i+u_y j+u_z k) \sin\tfrac{\theta}{2}$. Zapisujemy punkt $p=(x,y,z)$ jako kwaternion pod postacią $r = 0 + xi + yj + zk$. Rotacja jest realizowana za pomocą sprzężenia : $$r’=prp^{-1}.$$ Kwaterniony posiadają kilka zalet w porównaniu do notacji macierzowej. W porównaniu z kątami eulera pozwalają uniknąć gimbal locka. Ponadto, w przeciwieństwie do macierzy, możemy łatwo interpolować rotacje opisane za pomocą kwaternionów korzystając z sferycznej interpolacji linowej, w skrócie slerp. Dzięki temu kwaterniony są dużo lepszym rozwiązaniem do opisywania orientacji obiektów.

Kwaterniony w glm

W bibliotece glm mamy zaimplementowaną obsługę kwaternionów. Możemy je utworzyć na różne sposóby, wśród nich są

glm::quat MyQuaternion;
//bezpośrednio
MyQuaternion = glm::quat(w,x,y,z); 
//z kątów Eulera
vec3 glm::EulerAngles(90, 45, 0);
MyQuaternion = glm::quat(EulerAngles);
//poprzez podanie kąta i osi rotacji (oś musi być jednostkowa)
MyQuaternion = glm::angleAxis(degrees(RotationAngle), RotationAxis);

My będziemy korzystać raczej z ostatniej opcji. Bedziemy jednak kwaterniony przekształcać do macierzy zapomocą glm::toMat4, aby pozostać w jednorodnym opisie transformacji.

Następne dwa zadania będziemy wykonywać wewnątrz processInput za obecnymi tam instrukcjami.

Zadanie 4

Zaimplementuj poruszanie pierwszym stawem (o nazwie shoulder) przy użyciu myszki i za pomocą obrotów kwaternionowytch.

Zadanie będziemy wykonywać wewnątrz processInput za obecnymi instrukcjami.

  1. Oblicz różnicę pomiędzy pozycją myszki w kolejnych wywołaniach processInput. Wykorzystaj zmienne oldX, oldY, deltaX, deltaY.
    • Utwórz zmienne x, y typu float.
    • Odbierz pozycję kursora za pomocą instrukcji glfwGetCursorPos(window, &x, &y).
    • Jeżeli oldX, oldY jest większe od zera to oblicz różnicę w pozycji w X i Y i przypisz do odpowiednich zmiennych deltaX, deltaY (w ten sposób nie będziemy mieli problemu, że oldX i oldY nie jest zainicjalizowane na pocztku)
    • nadpisujemy oldX i oldY nowymi pozycjami.
  2. Oblicz kwaterniony rotacji w osiach X i Y jako kąt obrotu weź odpowiednio deltaX i deltaY przemnożone przez 0.01. Skorzystaj z funkcji glm::angleAxis.
  3. Domnóż kwaterniony do shoulderOrientation, która będzie odpowiadać za orientację pierwszego stawu robota.
  4. Przypisz do lokalnej transformacji węzła shoulder transformację początkową przemnożoną przez rotację otrzymaną z shoulderOrientation. Powinno to wyglądać tak: robotArm[N].localTransformation = robotArm[N].initialTransformation * glm::toMat4(shoulderOrientation);, przy czym N to indeks węzła shoulder.

Atrybut initialTransformation nie ustawiany prawidłowo. Wewnątrz funkcji loadRecusive za linią

nodes[index].localTransformation = Core::mat4_cast(node->mTransformation);

dodaj

nodes[index].initialTransformation = Core::mat4_cast(node->mTransformation);

Zadanie 4b*

Zrób rotację osi Z na przyciskach 1 i 2.

Zadanie 5

Zrób obroty drugiego stawu na przyciskach IJKL. Zadanie jest analogiczne do zadania 4. Jeżeli zostanie wciśnięty któryś z przycisków przypisz domnóż do elbowOrientation odpowiedni kwaterion uzyskany przez glm::angleAxis, jako kąt obrotu weź angleSpeed. Następnie ustaw węzeł elbow w tej orientacji analogicznie jak zrobiliśmy to w zadaniu 4 dla shoulder.

Zadanie 5b**

Zastąp wejście myszki i klawiatury wejściem z gałek i spustów gamepada

Interpolacja kwaternionów

W następnych zadaniach wykorzystamy interpolację kwaternionów do wykonania prostych animacji obrotów. Wykorzystamy do tego funkcję slerp, która przyjmuje dwa kwaterniony i parametr t od 0 do 1. dla 0 zwróci pierwszy kwaternion, dla 1 zwróci drugi. dla wartości pośrednich zwróci liniową interpolację pomiędzy nimi. W następujących zadaniach wykorzystamy jej własność do wykonania animacji aby to zrobić należy czas, w którym animacja ma się odbywać przekształcić do wartości 0-1, następnie wykorzystać ją do interpolacji 2 kwaternionów.

Zadanie 6

W tym zadaniu chcemy sprawić, żeby szczypce chwytaka się zamykały i otwierały.

W projekcie znajduje się funkcja runAnimationContionus jej zadaniem jest wykonywanie zapętlonej animacji. Dlatego parametr t jest obliczany funkcją okresową, w naszym przypadku jest to przeskalowany sinus. Szczypce chwytaka są pod nazwami gripper r i gripper l. Będę się w dalczym tekście odnosić do nich jako pierwsze i drugie ramie.

Dla obu ramion weź orientację początkową kwaternion opisujący brak rotacji auto start = glm::angleAxis(0.f, glm::vec3(0, 0, 1)). Jako wartości końcowe dla pierwszego szczypca weź obrót o 0.8 radianów w osi X, czyli(1,0,0), dla drugiego o -0.8 radianów w tej samej osi. Oblicz interpolowaną orientację obu ramion z użyciem funkcji glm::slerp, jako pierwszy argument wpisz orientację początkową, jako drugi orientację końcową odpowiedniego ramienia i jako trzeci zmienną t. Następnie ustaw gripper r i gripper l w tych orientacjach, tak jak to było zrobione w zadaniu 4 i 5

Zadanie 6b*

Zamiast funkcji sinus wykorzystaj inne funkcje okresowe na przykład fmod albo funkcję trójkątną.

Zadanie 7

W tym zadaniu zrobimy, żeby na wciśnięcie klaiwsza 0 robot wykonywał płynną animację z jednego ustawienia do drugiego. W tej chwili, wciśnięcie tego przycisku odbiera nam kontrolę nad sterowaniem na kilka sekund, ponieważ nie zaimplementowaliśmy animacji. Do tego będzie służyć funkcja runAnimation.

Wewnątrz tej funkcji, wewnątrz ifa zaimplementuj płynną animację dla stawów shoulder i elbow.

Wybierz dwa dowolne jednostkowe kwaterniony jako początkową orientację ramienia i łokcia oraz dwa inne dowolne dla końcowej orientacji. Następnie oblicz wartości pośrednie za pomocą glm::slerp i parametru t. Tak jak w poprzednim zadaniu ustaw stawy shoulder i elbow w interpolowanych orientacjach

Zadanie 7b*

Zamiast korzystać z parametru t wypróbuj kilka z easing functions. Możesz przykładowe znaleźć pod linkiem https://easings.net/