Chcete si naprogramovat letecký simulátor? Směr letu nad krajinou můžete měnit klávesnicí i myší... Vytvoříme několik užitečných tříd, která vám pomohou s matematikou, která stojí za definováním výhledu kamery a pak všechno spojíme do jednoho funkčního celku.
Ahoj, jmenuji se Vic Hollis. Před pár lety jsem se díky NeHe Tutoriálům naučil OpenGL a myslím, že je čas, abych mu to oplatil. Nedávno jsem začal studovat Quaterniony. Abych byl čestný, ještě jim moc nerozumím, alespoň ne tak, jak bych měl. Od té doby, co jsem s nimi začal pracovat, mohu říci, že jejich použití pro 3D rotace a hledání pozice ve scéně může hodně věcí ulehčit. Samozřejmě, že pro tento druh věcí nemusíte používat zrovna Quaterniony, vždy můžete vystačit s obyčejnými maticemi a analytickou geometrií, nic vám nebrání ani vzít ty nejlepší věci z obou. V našem demu se pokusíme vytvořit dvě jednoduché třídy. Jedna bude reprezentovat Quaternion a druhá kameru. Nebudu probírat matematiku stojící za Quaterniony, pro lepší pochopení sice může být důležitá, ale jako programátorovi (a vsadil bych se, že i většině lidí, kteří čtou tento článek) mi jde především o získání výsledků. Vytvoříme výškovou mapu reprezentující terén nebo krajinu, kolem které budeme moci létat. Pomocí třídy kamery a Quaternionu nastavíme výhled na scénu založený na směru letu a rychlosti ve stylu Wing Commandera. Nalezení dalších způsobů, jak létat okolo scény nechávám na vás, ale po přečtení tohoto článku by to už neměl být takový problém.
Quaterniony (Překl.: ze slovníku - čtveřice, čtyřka) jsou na pochopení opravdu těžké. Po měsících neúspěšných pokusů jsem to prostě vzdal, akceptoval jsem je, jako něco, co jednoduše existuje. Jsem si jistý, že alespoň někteří z vás slyšeli o efektu gimbal lock. No, je to něco, co se stane, když začnete aplikovat spoustu rotací najednou, které ovlivňují i následující průchody renderovací funkcí. Quaterniony nám dávají způsob, jak je obejít, a především proto jsou užitečné. Myslím, že už jsem toho dost namluvil, začneme se věnovat kódu.
class glQuaternion// Třída Quaternionu
{
public:
glQuaternion();// Konstruktor
virtual ~glQuaternion();// Destruktor
glQuaternion operator *(glQuaternion q);// Operátor násobení
void CreateFromAxisAngle(GLfloat x, GLfloat y, GLfloat z, GLfloat degrees);// "glRotatef()"
void CreateMatrix(GLfloat *pMatrix);// Vytvoření matice
private:
GLfloat m_w;
GLfloat m_z;
GLfloat m_y;
GLfloat m_x;
};
Začneme funkcí, kterou budeme z Quaternion třídy používat asi nejčastěji. Chová se prakticky úplně stejně jako stará dobrá glRotatef(), všechny parametry jsou také stejné, ale mají trochu jiné pořadí.
void glQuaternion::CreateFromAxisAngle(GLfloat x, GLfloat y, GLfloat z, GLfloat degrees)// "glRotatef()"
{
GLfloat angle = GLfloat((degrees / 180.0f) * PI);// Převedení stupňů na radiány
GLfloat result = (GLfloat)sin(angle / 2.0f);// Sinus polovičního úhlu
m_x = GLfloat(x * result);// X, y, z, w souřadnice Quaternionu
m_y = GLfloat(y * result);
m_z = GLfloat(z * result);
m_w = (GLfloat)cos(angle / 2.0f);
}
Metodu CreateMatrix() budeme také používat hodně často. Vytváří homogenní matici o velikosti 4x4, která může být pomocí glMultMatrix() předána OpenGL. Z toho mimo jiné plyne, že pokud budete používat tuto třídu, nebudete nikdy muset volat přímo funkci glRotatef().
void glQuaternion::CreateMatrix(GLfloat *pMatrix)// Vytvoření OpenGL matice
{
if(!pMatrix)// Nealokovaná paměť?
{
return;
}
// První řádek
pMatrix[ 0] = 1.0f - 2.0f * (m_y * m_y + m_z * m_z);
pMatrix[ 1] = 2.0f * (m_x * m_y + m_z * m_w);
pMatrix[ 2] = 2.0f * (m_x * m_z - m_y * m_w);
pMatrix[ 3] = 0.0f;
// Druhý řádek
pMatrix[ 4] = 2.0f * (m_x * m_y - m_z * m_w);
pMatrix[ 5] = 1.0f - 2.0f * (m_x * m_x + m_z * m_z);
pMatrix[ 6] = 2.0f * (m_z * m_y + m_x * m_w);
pMatrix[ 7] = 0.0f;
// Třetí řádek
pMatrix[ 8] = 2.0f * (m_x * m_z + m_y * m_w);
pMatrix[ 9] = 2.0f * (m_y * m_z - m_x * m_w);
pMatrix[10] = 1.0f - 2.0f * (m_x * m_x + m_y * m_y);
pMatrix[11] = 0.0f;
// Čtvrtý řádek
pMatrix[12] = 0;
pMatrix[13] = 0;
pMatrix[14] = 0;
pMatrix[15] = 1.0f;
// pMatrix[] je nyní homogenní maticí o rozměrech 4x4 použitelná v OpenGL
}
Dále napíšeme operátor násobení. Nebýt jeho, nemohli bychom kombinovat více rotací a celá třída by byla prakticky k ničemu. Pamatujte si, že výsledek součinu Quaternionů není komutativní. To znamená, že Quaternion a * Quaternion b se nerovná Quaternion b * Quaternion a, což vlastně platí i při násobení matic.
glQuaternion glQuaternion::operator *(glQuaternion q)// Operátor násobení
{
glQuaternion r;// Pomocný Quaternion
r.m_w = m_w*q.m_w - m_x*q.m_x - m_y*q.m_y - m_z*q.m_z;// Výpočet :-)
r.m_x = m_w*q.m_x + m_x*q.m_w + m_y*q.m_z - m_z*q.m_y;
r.m_y = m_w*q.m_y + m_y*q.m_w + m_z*q.m_x - m_x*q.m_z;
r.m_z = m_w*q.m_z + m_z*q.m_w + m_x*q.m_y - m_y*q.m_x;
return r;// Vrácení výsledku
}
Třída Quaternionu je hotová, teď zkusíme vytvořit třídu kamery.
class glCamera// Třída kamery
{
public:
GLfloat m_MaxPitchRate;
GLfloat m_MaxHeadingRate;
GLfloat m_HeadingDegrees;
GLfloat m_PitchDegrees;
GLfloat m_MaxForwardVelocity;// Maximální rychlost pohybu
GLfloat m_ForwardVelocity;// Současná rychlost pohybu
glQuaternion m_qHeading;// Quaternion horizontální rotace
glQuaternion m_qPitch;// Quaternion vertikální rotace
glPoint m_Position;// Pozice
glVector m_DirectionVector;// Směrový vektor
void ChangeVelocity(GLfloat vel);// Změna rychlosti
void ChangeHeading(GLfloat degrees);// Změna horizontálního natočení
void ChangePitch(GLfloat degrees);// Změna vertikálního natočení
void SetPerspective(void);// Nastavení výhledu na scénu
glCamera();// Konstruktor
virtual ~glCamera();// Destruktor
};
Začneme funkcí SetPerspective(), která provádí translaci na požadované souřadnice ve scéně. Deklarujeme šestnácti prvkové pole pro matici. Do Pomocného Quaternionu q uložíme součin dvou Quaternionů, které reprezentují rotace na osách x a y a tím nalezneme orientaci ve scéně. Z výsledku extrahujeme matici a aplikujeme ji na OpenGL, čímž natočíme kameru správným směrem. Jako další věc, kterou potřebujeme získat, je směrový vektor založený na orientaci. S jeho pomocí se budeme moci přesunout na pozici ve scéně. Matice založená na m_Pitch obsahuje hodnotu, kterou můžeme použít pro jeho j souřadnici. Za všimnutí stojí zajímavá věc - třetí řádek matice (elementy 8, 9, 10) vždy obsahují translační souřadnice, takže nebudeme muset složitě vypočítávat nový směrový vektor. Pamatujete si, jak jsem psal, že násobení Quaternionů není komutativní? No, teď to s výhodou využijeme pro získání i a k souřadnic vektoru. Při násobení prohodíme m_Heading a m_Pitch, díky čemuž získáme odlišnou matici, ve které jsou koordináty i a k uloženy. Protože jsme pro rotaci použili Quaterniony s jednotkovou délkou, bude i třetí řádek v matici obsahovat normalizovaný vektor. Tento vektor vynásobíme rychlostí pohybu a přičteme ho k pozici. Nakonec zbývá pomocí glTranslatef() přesunout se na ni. Mějte na paměti, že tato funkce modeluje létání ve stylu Wing Commandera. Nebude pracovat jako MS Flight Simulator.
void glCamera::SetPerspective()// Nastavení výhledu na scénu
{
GLfloat Matrix[16];// OpenGL matice
glQuaternion q;// Pomocný Quaternion
// Quaterniony budou reprezentovat rotace na osách
m_qPitch.CreateFromAxisAngle(1.0f, 0.0f, 0.0f, m_PitchDegrees);
m_qHeading.CreateFromAxisAngle(0.0f, 1.0f, 0.0f, m_HeadingDegrees);
q = m_qPitch * m_qHeading;// Kombinování rotací a uložení výsledku do q
q.CreateMatrix(Matrix);
glMultMatrixf(Matrix);// Vynásobí OpenGL matici
// Souřadnice j směrového vektoru
m_qPitch.CreateMatrix(Matrix);
m_DirectionVector.j = Matrix[9];
// Souřadnice i a k směrového vektoru
q = m_qHeading * m_qPitch;
q.CreateMatrix(Matrix);
m_DirectionVector.i = Matrix[8];
m_DirectionVector.k = Matrix[10];
// Zvětšení směrového vektoru pomocí rychlosti
m_DirectionVector *= m_ForwardVelocity;
// Přičtení směrového vektoru k aktuální pozici
m_Position.x += m_DirectionVector.i;
m_Position.y += m_DirectionVector.j;
m_Position.z += m_DirectionVector.k;
glTranslatef(-m_Position.x, -m_Position.y, m_Position.z);// Přesun na novou pozici
}
V inicializačním kódu nahrajeme data výškové mapy a texturu terénu, také nastavíme maximální dovolenou rychlost pohybu, pitch i heading.
int InitGL(GLvoid)
{
// Nastavení OpenGL
glShadeModel(GL_SMOOTH);
glClearColor(0.0f, 0.0f, 0.3f, 0.5f);
glClearDepth(1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
if(!hMap.LoadRawFile("Art/Terrain1.raw", MAP_SIZE * MAP_SIZE))// Výšková mapa
{
MessageBox(NULL, "Failed to load Terrain1.raw.", "Error", MB_OK);
}
if(!hMap.LoadTexture("Art/Dirt1.bmp"))// Textura výškové mapy
{
MessageBox(NULL, "Failed to load terrain texture.", "Error", MB_OK);
}
// Nastavení kamery
Cam.m_MaxForwardVelocity = 5.0f;
Cam.m_MaxPitchRate = 5.0f;
Cam.m_MaxHeadingRate = 5.0f;
Cam.m_PitchDegrees = 0.0f;
Cam.m_HeadingDegrees = 0.0f;
return TRUE;// Inicializace OK
}
Otestujeme stisky kláves. Šipky nahoru a dolů vertikálně naklánějí kameru, jako byste kývali hlavou. Šipky doleva a doprava umožňují horizontální zatáčení a W se S zrychlují/zpomalují pohyb. Třída obsahuje metody pro všechny tyto operace. Jejich kód nebudu uvádět, protože je velmi jednoduché.
void CheckKeys(void)// Ošetření klávesnice
{
if(keys[VK_UP])// Šipka nahoru
{
Cam.ChangePitch(5.0f);// Směr k zemi
}
if(keys[VK_DOWN])// Šipka dolu
{
Cam.ChangePitch(-5.0f);// Směr od země
}
if(keys[VK_LEFT])// Šipka doleva
{
Cam.ChangeHeading(-5.0f);// Otáčení doleva
}
if(keys[VK_RIGHT])// Šipka doprava
{
Cam.ChangeHeading(5.0f);// Otáčení doprava
}
if(keys['W'] == TRUE)// W
{
Cam.ChangeVelocity(0.1f);// Zrychlení pohybu
}
if(keys['S'] == TRUE)// S
{
Cam.ChangeVelocity(-0.1f);// Zpomalení pohybu
}
}
Následující funkce v základu provádí to samé jako CheckKeys() s rozdílem, že se nejedná o klávesnici, ale o myš. Proměnná DeltaMouse bude obsahovat vzdálenost myši relativně od středu okna. Čím rychleji jí uživatel posune, tím bude rozdíl větší a tím rychleji se kamera natočí. Na rozdíl od klávesnice, kde nelze definovat méně nebo více stlačená klávesa, tato funkce neaplikuje vždy konstantní hodnoty.
void CheckMouse(void)// Ošetření myši
{
GLfloat DeltaMouse;// Rozdíl pozice od středu okna
POINT pt;// Pomocný bod
GetCursorPos(&pt);// Grabování aktuální polohy myši
MouseX = pt.x;
MouseY = pt.y;
if(MouseX < CenterX)// Posun doleva
{
DeltaMouse = GLfloat(CenterX - MouseX);
Cam.ChangeHeading(-0.2f * DeltaMouse);
}
else if(MouseX > CenterX)// Posun doprava
{
DeltaMouse = GLfloat(MouseX - CenterX);
Cam.ChangeHeading(0.2f * DeltaMouse);
}
if(MouseY < CenterY)// Posun nahoru
{
DeltaMouse = GLfloat(CenterY - MouseY);
Cam.ChangePitch(-0.2f * DeltaMouse);
}
else if(MouseY > CenterY)// Posun dolů
{
DeltaMouse = GLfloat(MouseY - CenterY);
Cam.ChangePitch(0.2f * DeltaMouse);
}
MouseX = CenterX;// Obnovení pro další průchod
MouseY = CenterY;
SetCursorPos(CenterX, CenterY);// Myš uprostřed okna
}
Na samý závěr jsme si nechali vykreslování. Smažeme obrazovku, resetujeme matici a potom s pomocí třídy natočíme kameru správným směrem. dále změníme měřítko vzdálenosti na osách a vykreslíme výškovou mapu. Jako poslední před ukončením funkce ošetříme zásahy uživatele.
int DrawGLScene(GLvoid)// Vykreslování
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže buffery
glLoadIdentity();// Reset matice
Cam.SetPerspective();// Nastavení výhledu kamery
// Výšková mapa
glScalef(hMap.m_ScaleValue, hMap.m_ScaleValue * HEIGHT_RATIO, hMap.m_ScaleValue);
hMap.DrawHeightMap();
CheckKeys();// Test klávesnice
CheckMouse();// Test myši
return TRUE;// Vše OK
}
Je zde ještě pár věcí, které by se měly o OpenGL a Quaternionech obecně říct. Toto demo by šlo velmi snadno naprogramovat i bez Quaternionů pouze s glRotatef() na otáčení a glGetFloatv() na získání modelview matice pro směrový vektor. Takže proč používat Quaterniony? No, opravdu je nepotřebuejete, alespoň ne na něco podobného, jako je toto demo. Připadá mi, jako by mezi lidmi panoval názor, že aby mohli vytvořit něco ve stylu leteckého simulátoru, potřebují spoustu high tech matematických tříd. OpenGL pro vás samo o sobě uchovává záznam většiny potřebných informací, takže opravdu neexistuje žádný důvod do kódu vkládat extra výpočty pro tento druh operací, navíc většinou zpomalují. Už jsem viděl spousty zdrojových kódů, které prováděly snad všechny druhy šílené vektorové matematiky, stejně tak operace s maticemi a také ty, které nic takového nedělaly. V jedné době jsem věřil, že mi Quaterniony dovolí udělat téměř cokoli, jen jim tak rozumět. Poté, co jsem se konečně naučil, jak pracují, zjistil jsem, že je nikdy na prvním místě nebudu používat. Nemějte mi to za zlé. Neříkám, že jsou k ničemu nebo podobně. Jako každá věc, mají i oni své použití. Jsem si jistý, že nastanou případy, kdy je možná budete chtít použít a teď už můžete. Chci jen poukázat, že je pro létání okolo scény vůbec nepotřebujete, hodit se může i starý dobrý OpenGL kód. Kdybyste nahradili SetPerspective() za kód dole, dostali byste úplně stejný výsledek, ale možná toužíte po efektu gimbal lock, který byl zmíněn dříve. Pro udržení jednoduchosti nebyl v tomto kódu použit. Když kombinujete několik rotací a neresetujete při každém průchodu renderingem matici, gimbal lock často způsobuje bolest hlavy. My jsme jako správní OpenGL programátoři glLoadIdentity() vždy volali.
void glCamera::SetPerspective()// Ukázka alternativního kódu
{
GLfloat Matrix[16];
glRotatef(m_HeadingDegrees, 0.0f, 1.0f, 0.0f);
glRotatef(m_PitchDegrees, 1.0f, 0.0f, 0.0f);
glGetFloatv(GL_MODELVIEW_MATRIX, Matrix);
m_DirectionVector.i = Matrix[8];
m_DirectionVector.k = Matrix[10];
glLoadIdentity();
glRotatef(m_PitchDegrees, 1.0f, 0.0f, 0.0f);
glGetFloatv(GL_MODELVIEW_MATRIX, Matrix);
m_DirectionVector.j = Matrix[9];
glRotatef(m_HeadingDegrees, 0.0f, 1.0f, 0.0f);
m_DirectionVector *= m_ForwardVelocity;// Přidat směru i rychlost
m_Position.x += m_DirectionVector.i;// K pozici přičíst vektor
m_Position.y += m_DirectionVector.j;
m_Position.z += m_DirectionVector.k;
glTranslatef(-m_Position.x, -m_Position.y, m_Position.z);// Přesun na novou pozici
}
Doufám, že tento kód někde s výhodou použijete. Chtěl bych poděkovat DigiBenovi z http://www.gametutorials.com/ za jeho tutoriál na Quaterniony. Odněkud jsem se je přece naučit musel :-)
napsal: Vic Hollis <vichollis (zavináč) comcast.net>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>