Lekcja 48. Rotacja sferyczna
Autor: Tomek 'Wyszo' Wyszomirski
Oryginał: ArcBall Rotation (Terence J. Grant)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson48.zip

Czy nie byłoby wspaniale gdybyśmy mogli obracać swobodnie nasz model za pomocą myszy? Właśnie na tym polega rotacja sferyczna. W tym tutorialu przedstawię moją implementację tej techniki i powody, dla którego warto wykorzystać ją w Twoich projektach.

Moja implementacja klasy ArcBall jest oparta na implementacji Brettona Wadea, która z kolei opiera się na zamieszczonej w "Graphic Gems" autorstwa Kena Shoemake'a. Poprawiłem jednak kilka błędów i nieco zoptymalizowałem kod.

Rotacja sferyczna działa dzięki mapowaniu współrzędnych kliknięcia w okno bezpośrednio w koordynaty wirtualnej kuli, tak jakby znajdowała się ona tuż przed Tobą.

Aby osiągnąć taki efekt, musimy najpierw przeskalować koordynaty myszy z zakresu [0...szerokość), [0...wysokość) do [-1...1], [1...-1] - (zapamiętaj, że odwracamy zwrot osi Y, więc osiągniemy poprawne koordynaty OpenGL). Wygląda to tak:

MousePt.X = ((MousePt.X / ((Width - 1) / 2)) - 1);
MousePt.Y = -((MousePt.Y / ((Height - 1) / 2)) - 1);

Jedynym powodem, dla którego przeskalowujemy współrzędne do zakresu [-1...1] jest fakt, że ułatwi nam to obliczenia matematyczne. Szczęśliwym zbiegiem okoliczności pomoże to również komputerowi w optymalizacjach.

Następnie obliczamy długość wektora i ustalamy czy jest wewnątrz, czy na zewnątrz sfery. Jeśli jest wewnątrz, zwracamy wektor z wewnątrz sfery, w przeciwnym wypadku normalizujemy punkt i zwracamy najbliższy zewnętrzu sfery punkt.

Kiedy już mamy oba wektory, możemy obliczyć wektor prostopadły do wektora początkowego i końcowego, które tworzą kąt będący kwaternionem. Te informacje nam wystarczą do stworzenia macierzy.

System rotacji sferycznej jest inicjowany przy pomocy następującego konstruktora. NewWidth i NewHeight to szerokość i wysokość okna.

ArcBall_t::ArcBall_t(GLfloat NewWidth, GLfloat NewHeight)

Kiedy użytkownik kliknie myszką wewnątrz okna, obliczany jest początkowy wektor w zależności od tego, w którym miejscu nastąpiło kliknięcie.

void ArcBall_t::click(const Point2fT* NewPt)

Kiedy użytkownik przeciąga kursor myszy, wektor końcowy jest modyfikowany przez funkcję draw() i jeśli podaliśmy jako parametr kwaternion, to jest on uzupełniany zgodnie z wynikiem rotacji.

void ArcBall_t::drag(const Point2fT* NewPt, Quat4fT* NewRot)

Jeśli zmieniają się wymiary okna, uaktualniamy system rotacji sferycznej za pomocą funkcji:

void ArcBall_t::setBounds(GLfloat NewWidth, GLfloat NewHeight)

Zmienne potrzebne do użycia systemu rotacji sferycznej we własnym projekcie:

        // ostateczna transformacja
Matrix4fT Transform = {
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f };
Matrix3fT LastRot = {
        1.0f, 0.0f, 0.0f,         // poprzedni obrót
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f };
Matrix3fT ThisRot = {
        1.0f, 0.0f, 0.0f,         // aktualny obrót
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f };
ArcBallT ArcBall(640.0f, 480.0f);         // obiekt systemu rotacji sferycznej
Point2fT MousePt;         // bieżący punkt wskazywany przez mysz
bool isClicked = false;         // czy naciśnięto przycisk myszy?
bool isRClicked = false;         // czy naciśnięto prawy przycisk myszy?
bool isDragging = false;         // czy przeciągamy kursor?

Transform to nasza ostateczna transformacja - nasz obrót i wszelkie ewentualne translacje, jakie możesz chcieć dokonać. LastRot to ostatni obrót, jaki otrzymaliśmy pod koniec przeciągania. ThisRot to obrót podczas przeciągania. Wszystkie inicjalizujemy jako macierze tożsamościowe.

Kiedy klikamy w oknie, mamy tożsamościową macierz obrotu. Kiedy przeciągamy kursor, obliczamy obrót od początkowego punktu kliknięcia do aktualnych współrzędnych myszy. Pomimo, że używamy tych informacji, aby obrócić obiekt na ekranie, ważne jest że nie obracamy naszej wirtualnej kuli.

Teraz zaczynamy używać LastRot i ThisRot. LastRot to wynik wszystkich obrotów aż do teraz, podczas gdy ThisRot to aktualny obrót. Za każdym razem, gdy zaczynamy przeciągać kursor, ThisRot jest modyfikowane przez oryginalny obrót, a potem przez LastRot. Kiedy kończymy przeciąganie myszą, LastRot zostaje przypisana wartość ThisRot.

Gdybyśmy sami nie dodawali wyników obrotów, model zaczynałby obrót od wyjściowej pozycji po każdym kliknięciu. Na przykład: jeśli obrócimy model wokół osi X o 90 stopni, a potem o 45 stopni, chcielibyśmy otrzymać obrót o 135 stopni, a nie o ostatnie 45.

Jeśli chodzi o pozostałe zmienne (oprócz isDragged), wszystko co musisz zrobić, to uaktualniać je w odpowiednim czasie zależnie od twojego systemu. Rotacja sferyczna wymaga podania jej nowych wymiarów okna za każdym razem, kiedy jego rozmiar się zmienia. MousePt jest uaktualniane gdy tylko poruszymy myszą lub gdy przycisk myszy jest wciśnięty. isClicked / isRClicked jest modyfikowane po naciśnięciu przycisku myszy. isClicked jest używane do ustalania kliknięć i przeciągnięć. Użyjemy isRClicked do resetowania wszelkich obrotów (zamiany macierzy na tożsamościową).

Dodatkowy kod aktualizacji w/w zmiennych wyglądałby pod NeHeGL/Windowsem mniej-więcej tak:

void ReshapeGL (int width, int height)
{
    . . .
    ArcBall.setBounds((GLfloat)width, (GLfloat)height);         // aktualizuj wielkość okna dla rotacji sferycznej
}
        // pętla komunikatów Windowsa
LRESULT CALLBACK WindowProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    ...
        // komunikaty myszy dla systemu rotacji sferycznej
    case WM_MOUSEMOVE:
        MousePt.s.X = (GLfloat)LOWORD(lParam);
        MousePt.s.Y = (GLfloat)HIWORD(lParam);
        isClicked = (LOWORD(wParam) & MK_LBUTTON) ? true : false;
        isRClicked = (LOWORD(wParam) & MK_RBUTTON) ? true : false;
        break;
    case WM_LBUTTONUP: isClicked = false; break;
    case WM_RBUTTONUP: isRClicked = false; break;
    case WM_LBUTTONDOWN: isClicked = true; break;
    case WM_RBUTTONDOWN: isRClicked = true; break;
    . . .
}

Kiedy już mamy gotowy kod aktualizacji zmiennych, nadszedł czas umieścić na miejscu logikę kliknięcia. Poniższy kod jest bardzo łatwy do zrozumienia, jeśli wiesz wszystko, co było powiedziane powyżej.

if (isRClicked)
{
        // resetuj obrót    
    Matrix3fSetIdentity(&LastRot);
        // resetuj obrót    
    Matrix3fSetIdentity(&ThisRot);
        // resetuj obrót
    Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);
}
if (!isDragging)         // jeśli nie przeciągamy
{
    if (isClicked)         // pierwsze kliknięcie
    {
        isDragging = true;         // przygotuj się do przeciągania
        LastRot = ThisRot;         // ustaw ostatni statyczny obrót na ostatni dynamiczny
        ArcBall.click(&MousePt);         // aktualizuj wektor początkowy i przygotuj się do przeciągania
    }
}
else
{
    if (isClicked)         // wciąż przycisk jest przyciśnięty, więc trwa przeciąganie
    {
        Quat4fT ThisQuat;
        ArcBall.drag(&MousePt, &ThisQuat);         // aktualizuj wektor końcowy i pobierz wynikowy obrót w postaci kwaternionu
        Matrix3fSetRotationFromQuat4f(&ThisRot, &ThisQuat);         // konwertuj kwaternion do Matrix3fT
        Matrix3fMulMatrix3f(&ThisRot, &LastRot);         // dodaj do tego wynik ostatniego obrotu
        Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);         // ustaw nasz ostateczny obrót
    }
    else
        isDragging = false;         // już nie przeciągamy
}

Ten kod zajmuje się wszystkim za nas. Teraz musimy już tylko ustawić naszą nową macierz transformacji. A to naprawdę proste:

    glPushMatrix();         // przygotuj dynamiczną transformację
    glMultMatrixf(Transform.M);         // dokonaj dynamicznej transformacji
    glBegin(GL_TRIANGLES);         // renderuj model
    . . .
    glEnd();         // koniec renderingu modelu
    glPopMatrix();         // zaniechaj dynamicznej transformacji

Dołączyłem przykład, który ilustruje wszystko powyższe. Nie musisz używać mojej matematyki czy funkcji, prawdę mówiąc polecam przystosować to do Twojego własnego systemu matematycznego, jeśli czujesz się wystarczająco pewnie. Jednak nie jest to konieczne, aby system działał.

Teraz, kiedy już wiesz, jakie to proste, nie powinieneś mieć problemów z dodaniem rotacji sferycznej do swoich projektów.

przykładowy kod znajduje się na dole strony