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:
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.
- Oblicz różnicę pomiędzy pozycją myszki w kolejnych wywołaniach
processInput
. Wykorzystaj zmienneoldX
,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 zmiennychdeltaX
,deltaY
(w ten sposób nie będziemy mieli problemu, żeoldX
ioldY
nie jest zainicjalizowane na pocztku) - nadpisujemy
oldX
ioldY
nowymi pozycjami.
- Utwórz zmienne
- Oblicz kwaterniony rotacji w osiach
X
iY
jako kąt obrotu weź odpowiedniodeltaX
ideltaY
przemnożone przez 0.01. Skorzystaj z funkcjiglm::angleAxis
. - Domnóż kwaterniony do
shoulderOrientation
, która będzie odpowiadać za orientację pierwszego stawu robota. - Przypisz do lokalnej transformacji węzła
shoulder
transformację początkową przemnożoną przez rotację otrzymaną zshoulderOrientation
. Powinno to wyglądać tak:robotArm[N].localTransformation = robotArm[N].initialTransformation * glm::toMat4(shoulderOrientation);
, przy czymN
to indeks węzłashoulder
.
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/