Nebylo by skvělé otáčet modelem pomocí myši jednoduchým drag & drop? S ArcBall rotacemi je to možné. Moje implementace je založená na myšlenkách Brettona Wadea a Kena Shoemakea. Kód také obsahuje funkci pro rendering toroidu - kompletně i s normálami.
ArcBall funguje tak, že mapuje okenní souřadnice kliknutí přímo do souřadnic ArcBallu. Zmenší poměr souřadnic myši z rozsahu [0..šířka, 0..výška] na rozsah [-1..1, 1..-1]. Pamatujte si, že aby v OpenGL dosáhl korektního výsledku, musí převrátit znamínko y souřadnice. Vzorec vypadá takto:
MousePt.X = ((MousePt.X / ((Width - 1) / 2)) - 1);
MousePt.Y = -((MousePt.Y / ((Height - 1) / 2)) - 1);
Jediný důvod, proč jsme měnili měřítko souřadnic je, abychom zjednodušili matematiku, nicméně šťastnou shodou okolností to dovoluje kompilátoru kód trochu optimalizovat. Dále vypočítáme délku vektoru a určíme, jestli se nachází nebo nenachází uvnitř koule. Pokud ano, vrátíme vektor z jejího vnitřku, jinak normalizujeme bod a vrátíme nejbližší pozici k vnějšku koule. Poté, co máme oba vektory, získáme vektor současně kolmý na počáteční i koncový vektor, čímž dostaneme quaternion. S tímto v rukách máme dost informací na vygenerování rotační matice.
Konstruktoru třídy ArcBall budeme předávat rozměry okna.
ArcBall_t::ArcBall_t(GLfloat NewWidth, GLfloat NewHeight)
Když uživatel klikne myší, vypočítáme počáteční vektor podle toho, kam kliknul.
void ArcBall_t::click(const Point2fT* NewPt)
Když táhne myší (drag), aktualizujeme koncový vektor pomocí metody drag() a pokud je poskytnut i výstupní quaternion, aktualizujeme ho pomocí výsledné rotace.
void ArcBall_t::drag(const Point2fT* NewPt, Quat4fT* NewRot)
Při změně velikosti okna jednoduše aktualizujeme i rozměry ArcBallu.
void ArcBall_t::setBounds(GLfloat NewWidth, GLfloat NewHeight)
V projektu budeme potřebovat i několik dalších proměnných. Transformation je finální transformace, která určuje rotaci, ale také posunutí. LastRot představuje poslední zaznamenanou rotaci od konce dragu a ThisRot určuje rotaci v době táhnutí myší. Všechny tři na začátku inicializujeme na matici identity.
Při kliknutí se začíná z identického stavu rotace a když následně táhneme, rotace se počítá od pozice kliknutí až po bod táhnutí. I když na otáčení objektů ve scéně používáme tuto implementaci, je důležité poznamenat, že nerotujeme samotný ArcBall. S rostoucími (přírůstkovými) rotacemi se musíme vypořádat sami. To je úkol LastRot a ThisRot. LastRot si můžeme představit jako všechny rotace až do teď a ThisRot jako aktuální rotace. Vždy, když začne rotace, ThisRot se modifikuje pomocí originální rotace a potom se aktualizuje jako výsledek součinu s LastRot (a také se upraví konečná transformace). Po skončení dragu přiřadíme do LastRot hodnoty z ThisRot. Kdybychom neakumulovali rotace samotné, model by vypadal, jako by se při každém kliknutí přilepil na začátek souřadnic. Například při rotaci okolo osy x o 90 stupňů a potom o 45 stupňů, chceme získat 135 namísto posledních 45.
Matrix4fT Transform =// Finální transformace
{
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 =// Minulá rotace
{
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
Matrix3fT ThisRot =// Současná rotace
{
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
Co se týče zbytku proměnných (kromě isDragged), všechno, co s nimi musíme udělat, je vždy je ve správný čas aktualizovat. ArcBall potřebuje, aby se jeho hranice při každé změně velikosti okna resetovaly. MousePt se aktualizuje při pohybu myší nebo stisknutí tlačítka a isClicked/isRClicked při stlačení levého/pravého tlačítka myši. Levé tlačítko slouží pro dragging a pravé pro resetování všech rotací do výchozího identity stavu.
ArcBallT ArcBall(640.0f, 480.0f);// Instance ArcBallu
Point2fT MousePt;// Pozice myši
bool isClicked = false;// Kliknuto myší?
bool isRClicked = false;// Kliknuto pravým tlačítkem myši?
bool isDragging = false;// Táhnuto myší?
Aktualizace proměnných vypadají takto:
// Konec ReshapeGL()
ArcBall.setBounds((GLfloat)width, (GLfloat)height);// Nastaví hranice pro ArcBall
// Funkce WindowProc() - ošetření zpráv myši
case WM_MOUSEMOVE:// Pohyb
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:// Uvolnění levého tlačítka
isClicked = false;
break;
case WM_RBUTTONUP:// Uvolnění pravého tlačítka
isRClicked = false;
break;
case WM_LBUTTONDOWN:// Kliknutí levým tlačítkem
isClicked = true;
break;
case WM_RBUTTONDOWN:// Kliknutí pravým tlačítkem
isRClicked = true;
break;
Máme-li toto všechno, je na čase vypořádat se s klikací logikou.
void Update(DWORD milliseconds)// Aktualizace scény
{
if (g_keys->keyDown [VK_ESCAPE] == TRUE)// Stisk ESC
{
TerminateApplication(g_window);// Ukončení programu
}
if (g_keys->keyDown [VK_F1] == TRUE)// Stisk F1
{
ToggleFullscreen(g_window);// Přepnutí do fullscreenu
}
if (isRClicked)// Kliknutí pravým tlačítkem - reset všech rotací
{
Matrix3fSetIdentity(&LastRot);
Matrix3fSetIdentity(&ThisRot);
Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);
}
if (!isDragging)// Netáhne se myší?
{
if (isClicked)// Kliknutí?
{
isDragging = true;// Příprava na dragging
LastRot = ThisRot;// Nastavení minulé statické rotace na tuto
ArcBall.click(&MousePt);// Aktualizace startovního vektoru a příprava na dragging
}
}
else// Už se táhne
{
if (isClicked)// Je ještě stisknuto tlačítko?
{
Quat4fT ThisQuat;
ArcBall.drag(&MousePt, &ThisQuat);// Aktualizace koncového vektoru a získání rotace jako quaternionu
Matrix3fSetRotationFromQuat4f(&ThisRot, &ThisQuat);// Konvertování quaternionu na Matrix3fT
Matrix3fMulMatrix3f(&ThisRot, &LastRot);// Akumulace minulé rotace do této
Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);// Nastavení koncové transformační rotace na tuto
}
else// Už není stisknuto
{
isDragging = false;// Konec draggingu
}
}
}
Teď už jenom potřebujeme aplikovat transformaci na naše modely a jsme hotovi.
void Draw(void)// Vykreslování
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smazání bufferů
glLoadIdentity();// Reset matice
glTranslatef(-1.5f, 0.0f, -6.0f);// Translace doleva a do hloubky
glPushMatrix();// Uložení matice
glMultMatrixf(Transform.M);// Aplikování transformace
glColor3f(0.75f, 0.75f, 1.0f);// Barva
Torus(0.30f, 1.00f);// Vykreslení toroidu (speciální funkce)
glPopMatrix();// Obnovení původní matice
glLoadIdentity();// Reset matice
glTranslatef(1.5f, 0.0f, -6.0f);// Translace doprava a do hloubky
glPushMatrix();// Uložení matice
glMultMatrixf(Transform.M);// Aplikování transformace
glColor3f(1.0f, 0.75f, 0.75f);// Barva
gluSphere(quadratic,1.3f,20,20);// Vykreslení koule
glPopMatrix();// Obnovení matice
glFlush();// Flushnutí renderovací pipeline
}
Přidal jsem i ukázku kompletního kódu, který toto všechno demonstruje. Nemusíte používat moji matematiku a funkce stojící na pozadí, naopak, pokud si věříte, doporučuji vytvořit si vlastní. Nicméně i s mými vzorci a výpočty by všechno mělo bez problémů fungovat.
napsal: Terence J. Grant <tjgrant (zavináč) tatewake.com>
do slovenštiny přeložil: Pavel Hradský - PcMaster <pcmaster (zavináč) stonline.sk>
do češtiny přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>