Az OpenGL (Open Graphics Library) egy platformfüggetlen API (Application Programming Interface), amellyel a grafikus hardvert szabványos felületen keresztül lehet elérni. Az OpenGL használatával valós idejű 2D és 3D grafikát lehet létrehozni. OpenGL-t széles körben alkalmaznak, például CAD alkalmazásokban, virtuális valóság megjelenítésénél, tudományos vizualizációkban, és a videó játékokban.
Az 1981-ben alapított Silicon Graphics (SGI) 3D számítógépes grafikához gyártott hardvert, és softvert. Az általuk gyártott grafikus hardvereket az IrisGL-t (Integrated Raster Imaging System Graphical Library) nevű API-n keresztül lehetett elérni, amellyel 2D és 3D grafikát lehetett létrehozni az SGI munkaállomásain. A 90-es évek elejére az SGI lett a piacvezető a 3D grafika terén, és az IrisGL de facto szabvánnyá vált.
A többi gyártó (Sun Microsystem, Hewlett-Packard, IBM) is kezdte behozni a lemaradását a 3D grafika terén, ezért az SGI szabványosította az IrisGL-t, a grafikához nem kötődő részeket kidobta belőle, és 1992-ben kiadta az OpenGL specifikációt, amely alapján a gyártók elkészítették az OpenGL implementációjukat. A hardvergyártók eszközmeghajtókat írtak (device drivers), amellyel a saját hardverüket lehetett elérni az OpenGL API-n keresztül. A szoftvergyártók kiegészítették az operációs rendszerüket az OpenGL specifikus részekkel.
Microsoft a Windows NT 3.5-ös verzióban implementálta először az OpenGL-t, amely 1994-ben jelent meg. Az implementáció teljesen szoftveres volt, mert akkoriban még nem volt dedikált 3D grafikát számoló hardver PC-re. Ezután a Microsoft 1995-ben kiadta a saját 3D programozásra, főleg játék programozásra szánt API-ját a DirectX-et, amely csak Windows operációs rendszeren fut. A DirectX több játékprogramozással kapcsolatos API-ból áll (pl. beviteli eszköz, joystick-kezelés, hangkezelés, 2D grafika), 3D grafikát a Direct3D-n keresztül lehet programozni. A Direct3D kezdetben nehézkesen használható volt, de a Microsoft folyamatosan fejlesztett rajta, és az 5.0-s verziótól kezdve vált felhasználóbaráttá. A Microsoft úgy tartotta, hogy a komoly, „professzionális” alkalmazásokra (például CAD) való az OpenGL, ezért kezdetben csak a Windows NT vonal támogatta, a Windows 95 nem.
1996-ban adta ki az idSoftware a Quake nevű 3D-s FPS játékát, amelynek 1997-ben jelent meg egy módosított változata GLQuake néven. A GLQuake a 3D megjelenítéshez a 3D grafikus hardvert használta az OpenGL API-n keresztül. Később OpenGL alapon még több PC-s játék jelent meg, például Quake 2, Unreal, Half-Life.
Ebben az időben jelentek meg az első dedikált 3D grafikus hardverek is. Az elsők között volt a 3Dfx Interactive által gyártott Voodoo Graphics kártya. A piacon lévő többi kártyát tudásban, és teljesítményben is felülmúlta, nagy siker volt. Programozása a 3Dfx saját Glide nevű API-ján keresztül történt. Az API gyártóspecifikus volt, ezért néhány év múlva veszített a népszerűségéből. Az Nvidia 1999-ben jelentette meg a GeForce 256 kártyát, amelyet GPU-nak (Graphics Processing Unit) nevezett el. Újdonsága a T&L (Transform & Lighting) technológia volt, amely a csúcspont (vertex) transzformációk számítását a kártyán valósította meg a CPU helyett. 3Dfx sose implementálta a hardveres T&L-t, a cég hanyatlani kezdett, végül 2002-ben csődbe ment, és megvette az Nvidia. A 2000-es évekre a két fő GPU gyártó az Nvidia, és az ATI lett. Mindkét gyártó GPU-i egyaránt támogatták az OpenGL-t és a Direct3D-t is.
Az OpenGL szabványban rögzített funkciók mellett lehetőség van gyártó specifikus kiterjesztéseket (extension) is használni. Az OpenGL szabványt 1992-től az OpenGL ARB (OpenGL Architectural Review Board) nevű csoport fejlesztette az SGI vezetésével. A csoport több OpenGL érdekelt gyártóból állt, és szavazással döntöttek a fejlesztés irányáról. Például az eddig csak kiterjesztésként létező funkciókat szabványosítják, és az OpenGL egy új verziójában specifikálják. A 2006-os SIGGRAPH konferencián jelentették be, hogy az OpenGL ARB helyett a Khronos konzorcium veszi át a szabvány fejlesztését és karbantartását.
Az OpenGL segítségével egyszerű, térben elhelyezkedő 3D-s grafikus primitíveket jeleníthetünk meg a képernyőn. Ennek során megválaszthatjuk a térből síkba való leképezés módját, az objektumok színét, megvilágítását és mintázatát. A síkbeli képen többféle módon is figyelembe vehetjük azt, hogy az elemek a térbeli elhelyezkedésből következően takarják egymást.
Ha az OpenGL-t használjuk, akkor az előírt grafikus megjelenítés nem kerül azonnal a képernyőre (a képernyőkártya memóriájába), hanem bekerül egy feldolgozási sorba, a „megjelenítési csőbe”. A megjelenítési cső egyes műveleteit a 4.1. ábra szemlélteti.
A GPU-k csak primitíveket, azaz pontokat, vonalakat és háromszögeket tudnak nagy sebességgel megjeleníteni. A primitívek a kirajzoláshoz a grafikus csővezeték (4.1. ábra) különböző fázisain mennek keresztül az alkalmazás rajzolás kérésétől egészen a képernyőig.
Az OpenGL 2.0 egyik újdonsága volt, hogy a szabványban rögzítették a csővezeték programozhatóságát. Korábban a vertex shader és a fragment shader előre meghatározott műveleteket végzett el hardverből (fix-function pipeline), amelyek paraméterezhetőek voltak. Az egyre gyorsabb GPU-kal lehetővé váltak számításigényesebb feladatok valósidejű elvégzése is. Így egyéni árnyalás modellek és effektek is megvalósíthatóak, amelyeket a gyártók nem tudnak előre implementálni hardverben. A testreszabhatóságot a csővezeték programozhatóságával érték el.
A első ábrát (4.1. ábra) tanulmányozva végigkövethetjük az OpenGL képalkotási folyamatának lehetőségeit és lépéseit. Első lépésként az alkalmazás összeállítja a kirajzolandó primitív csúcspontjait (vertex), amelyek a rajzoláshoz szükséges attribútumokat tartalmazzák. Ilyen attribútum a pozíció, a szín, a textúra-koordináta vagy -koordináták, a normál vektor. Lehetőségünk van arra, hogy térbeli objektumokat leíró csúcspontjaikkal (vertex) jellemezve adjunk át megjelenítésre. Szükség esetén azonban magunk is definiálhatjuk a pixeleken megjeleníteni kívánt információt. Érdemes már itt megemlíteni, hogy az alapobjektumok sorozatát listába szervezhetjük (display lista), amelyet ezek után egyetlen rajzelemként kezel a rendszer.
Az alkalmazástól érkező vertexek mindegyikén lefut egy program, a vertex árnyaló (vertex shader), amelynek a feladata a vertexek képernyőre vetítése. Ez 3D grafikában rendszerint perspektivikus vetítéssel történik. A vertex shader kimenete a transzformált vertex pozíciója homogén koordinátákban kifejezve. Emellett még egyéb számításokat is végezhet, például a fényforrások alapján meghatározhatja a vertex árnyalt színét is.
A raszterizálás során a topológia figyelembe vételével megtörténik a vágás, azaz a képernyőből kilógó háromszögeket, vagy azok kilógó részét eldobja a rendszer. Ezután a raszterizáló a háromszöget felosztja pixel méretű fragmensekre (fragment). Egy fragmens tartalmazza a pixel framebuffer-beli címét, illetve a vertexek egyéb paramétereit, amelyet a hardver lineárisan interpolál (4.2. ábra).
A raszterizálás során előálló fragmensek mindegyikére vonatkozóan lefut egy program, a fragmens árnyaló (fragment shader), amelynek feladata a fragmens színének meghatározása. Ehhez a fragmensben lévő interpolált paramétereket is használhatja, amely alapján például textúrát mintavételezhet, vagy csak szimplán kiírja a színt. A fragmens shader kimenete az adott fragmens színe.
A raszterműveletek (raster operations, ROP) feladata fragment shader-ből előálló fragmensek egyesítése a framebuffer-ben lévő pixelekkel. Ez lehet szimpla felülírás is, vagy részben átlátszó fragmensek esetén az új pixel színéhez figyelembe kell venni a framebuffer-ben lévő pixel színét is (blending). 3D esetén a takarási feladat is itt kerül megoldásra.
A raszterműveletek kimenete a framebuffer-be kerül, amely több puffer együttese. Minimum kell egy színpuffer, amely az összes pixelt tartalmazza tipikusan RGB színtérben. Emellett lehetnek egyéb pufferek is, például mélységpuffer a takarási problémák megoldásához, vagy alkalmazástól függően akár több szín pufferbe is lehet írni egyszerre. Például a sztereó 3D megjelenítésnél külön szín- és mélységpuffere van a bal és a jobb szemnek.
Az alapértelmezett framebuffer a képernyő, azonban lehetőség van saját framebuffer-t létrehozni. Ilyenkor a pixelek nem jelennek meg a képernyőn, hanem a memóriába kerülnek, amelyet textúraként később fel lehet használni. Ezzel érdekes effekteket lehet létrehozni.
Az Open GL rendszert használhatjuk klasszikus módon, illetve az egy a felhasználást könnyítő könyvtárral.
Az OpenGL rendszer API függvényeit az OpenGL32.lib
, a glu32.lib
, a gdi32.lib
és a User32.lib
könyvtárak tartalmazzák. A könyvtárakat be kell építeni programunkba (Project Properties/ Configuration Properties/ Linker/ Input/ Additional Dependencies = openGL32.lib glu32.lib gdi32.lib User32.lib $(NoInherit)) eléréséhez be kell építenünk program stdafx.h
-ba a kapcsolódó deklarációs állományokat:
#include <windows.h> #include <winuser.h> #include <GL/gl.h> #include <GL/glu.h>
A fenti könyvtárak és fejállományok beépítése a projektünkbe lehetővé teszi, hogy használjuk a rendszer konstansait, típusait és függvényeit.
Az OpenGL pixeles eszközökön működik. Első lépésben definiálnunk kell, hogy mely lehetőségeit szeretnénk használni a raszteres eszköznek. Ha ezt megtettük, informálódni kell arról, hogy az aktuális konfiguráció mit támogat az igényeink közül. Az így meghatározott lehetőségek alapján OpenGL-eszközkapcsolatot építhetünk, és kezdhetjük a geometriai modellezést. Ha már nincs szükségünk az OpenGL-eszközkapcsolatra, töröljük annak adatait.
A Windows API (Application Programming Interface) függvényei az objektumokat számokkal azonosítják. Ezeket a számokat leírónak (handle) hívjuk. Az ablakok leírója a HWND
típus (Handle of WiNdoW). Hasonlóan a grafikus eszközök rajzlapjainak objektumát egy számmal azonosíthatjuk HDC
típus (Handle of Device Context). Az API rajzoló függvényeket is használhatjuk ezekkel az azonosítókkal. A .NET rendszerben az objektumok Handle
tulajdonsága tartalmazza a leírókat.
Az alábbi példa a HWND
típusú hwnd változóba lekéri az ablakleírót, majd a GetDC()
API függvénnyel lekéri az ablak rajzlapjának leíróját, és a HDC
típusú m_hDC változóba helyezi.
HWND hwnd=(HWND)this->Handle.ToInt32(); HDC m_hDC = GetDC(hwnd);
PictureBox-ba is rajzolhatunk, csak akkor a PictureBox ablakleírójával kell dolgoznunk.
hwnd=(HWND)pictureBox1->Handle.ToInt32(); m_hDC = GetDC(hwnd);
Az OpenGL használatához meg kell határozzuk, hogyan használjuk a pixeleket. A pixelformátum megadásához a PIXELFORMATDESCRIPTOR
struktúrát alkalmazhatjuk. A struktúra adattagjaival előírhatjuk, miként szeretnénk használni a grafikus hardver lehetőségeit. Beállíthatjuk a takartvonalas ábrázolásra, a színezésre, a pufferek használatára vonatkozó igényeinket. A pixelformátum struktúra definíciója:
typedef struct tagPIXELFORMATDESCRIPTOR { // pfd WORD nSize; WORD nVersion; DWORD dwFlags; BYTE iPixelType; BYTE cColorBits; BYTE cRedBits; BYTE cRedShift; BYTE cGreenBits; BYTE cGreenShift; BYTE cBlueBits; BYTE cBlueShift; BYTE cAlphaBits; BYTE cAlphaShift; BYTE cAccumBits; BYTE cAccumRedBits; BYTE cAccumGreenBits; BYTE cAccumBlueBits; BYTE cAccumAlphaBits; BYTE cDepthBits; BYTE cStencilBits; BYTE cAuxBuffers; BYTE iLayerType; BYTE bReserved; DWORD dwLayerMask; DWORD dwVisibleMask; DWORD dwDamageMask; } PIXELFORMATDESCRIPTOR;
A struktúra egyes adattagjait a Windows nem használja, a támogatott adattagok az alábbiak:
nSize |
A struktúra mérete sizeof(PIXELFORMATDESCRIPTOR) |
|
nVersion |
Ennek értéke 1 kell legyen. |
|
dwFlags |
A megadott konstansokat bitenkénti vagy (|) művelettel adjuk meg. Néhány jól használható beállítás: |
|
|
Akkor használjuk a konstanst, ha azt szeretnénk, hogy ablakban jelenjen meg a rajz. Alkalmazhatjuk még a |
|
|
Az OpenGL lehetőségeit használjuk, egyébként |
|
|
Több puffert alkalmazva a képeket cserélgethetjük (pl. animáció). |
|
iPixelType |
|
RGBA színmegadás |
|
Színmegadás palettaindexszel |
|
cColorBits |
Színdefiniáló bitek száma. |
|
cAcumBits |
A speciális tárolópuffer színdefiniáló bitjeinek száma. |
|
cDepthBits |
A Z-puffer bitjeinek száma. |
|
cStencilBits |
A stencilpuffer bitjeinek száma. |
|
iLayerType |
|
Rajzolás az alap fóliára. |
|
Rajzolás a felső fóliára |
|
|
Rajzolás az alsó fóliára |
Ha az adatokat a struktúrában rögzítettük, akkor a ChoosePixelFormat()
függvény hívásával lekérdezhetjük a sorszámát annak a pixelformátumnak, amelyet OpenGL-eszközkapcsolatunk a igényeinkhez legjobban illeszkedőnek talál, és támogat. Ezt a sorszámot használjuk majd az OpenGL eszközkapcsolat megteremtéséhez:
int ChoosePixelFormat( HDC hdc, CONST PIXELFORMATDESCRIPTOR * ppfd );
A hdc paraméter az eszközkapcsolat-leíró, a ppfd a formátumdefiniáló struktúrára mutató pointer. A függvény sikeres választás esetén 0-tól különböző értékkel tér vissza.
A kiválasztott pixelformátum tényleges adatai lekérdezhetők a DescribePixelFormat()
függvénnyel.
int WINAPI DescribePixelFormat( HDC hdc, int iPixelFormat, UINT nBytes, LPPIXELFORMATDESCRIPTOR ppfd);
a hdc az eszközkapcsolat leíró, megadhatjuk a kiválasztott pixelformátumot iPixelFormat, az nBytes a ppfd által mutatott struktúra mérete. A visszatérési érték az adott eszközkapcsolaton használható formátumok száma.
Ha megvan a legközelebbi pixelformátum, akkor azt a
bool SetPixelFormat(HDC hdc, int iPixelFormat, PIXELFORMATDESCRIPTOR * ppfd)
függvénnyel állíthatjuk be egy hDC-re. A hdc az eszközkapcsolat leíró, az iPixelFormat egész a meghatározott legközelebbi pixelformátum, a ppfd PIXELFORMATDESCRIPTOR
-ra mutató pointer.
A
HGLRC wglCreateContext(HDC hdc);
függvény segítségével, az eszközkapcsolat-leírót felhasználva létrehozhatunk OpenGL megjelenítési kapcsolatokat (HGLRC
típusú Handle of GL Rendering Context):
A
BOOL wglMakeCurrent( HDC hdc, HGLRC hglrc);
függvénnyel, melynek paraméterei az eszköz- és megjelenítési kapcsolat, beállíthatjuk azt, hogy az esetlegesen több megjelenítési kapcsolat közül melyik legyen az aktuális. Ha sikerrel jártunk, a visszatérési érték igaz.
Az aktuális beállítások lekérdezésére a HGLRC típusú wglGetCurrentContext()
és a HDC típusú wglGetCurrentDC()
függvényeket használhatjuk.
Az adott programszál megjelenítési kapcsolatának felszabadításához először meg kell szüntetnünk a kapcsolat kiválasztott állapotát, a wglMakeCurrent(0, 0)
; hívással, majd törölhetjük azt a
BOOL wglDeleteContext ( HGLRC hglrc);
függvénnyel, melynek visszatérési értéke a törlés sikerességét jelzi.
Az alábbi példa MySetPixelFormat()
függvénye a hdc eszközkapcsolattal beállítja az m_hglrc
megjelenítési kapcsolatot.
HDC m_hDC; HWND hwnd; HGLRC m_hglrc; GLint MySetSetPixelFormat(HDC hdc) { static PIXELFORMATDESCRIPTOR pfd= { sizeof(PIXELFORMATDESCRIPTOR),// méret 1, // verzió PFD_DRAW_TO_WINDOW | // ablakba PFD_SUPPORT_OPENGL | // OpenGL PFD_DOUBLEBUFFER, // Dupla puffer PFD_TYPE_RGBA, // Színmodell 32, // Színmélység 0, 0, 0, 0, 0, 0, // Color Bits Ignored 0, // Nincs alfa 0, // Nincs eltolás bit 0, // Nincs akkumulátorpuffer 0, 0, 0, 0, // Akku bitek nincsenek 32, // 32Bit Z-puffer 0, // Nincs stencilpuffer 0, // Nincs Auxpuffer PFD_MAIN_PLANE, // Rajz a fősíkra 0, // Foglalt 0, 0, 0 // Nincsenek Layer mMaszkok }; GLint iPixelFormat; if((iPixelFormat = ChoosePixelFormat(hdc, &pfd)) == 0) { MessageBox::Show("ChoosePixelFormat nem OK"); return 0; } if(SetPixelFormat(hdc, iPixelFormat, &pfd) == FALSE) { MessageBox::Show("SetPixelFormat nem OK "); return 0; } if((m_hglrc = wglCreateContext(m_hDC)) == NULL) { MessageBox::Show("wglCreateContext nem OK "); return 0; } if((wglMakeCurrent(m_hDC, m_hglrc)) == NULL) { MessageBox::Show("wglMakeCurrent nem OK "); return 0; } return 1; }
A megjelenítési kapcsolat felépítése a Load
eseményben:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) { hwnd=(HWND)this->Handle.ToInt32(); m_hDC = GetDC(hwnd); if(m_hDC) { MySetPixelFormat(m_hDC); } }
A megjelenítési kapcsolat megszüntetése a FormClosing
eseményben:
private: System::Void Form1_FormClosing(System::Object^ sender, System::Windows::Forms::FormClosingEventArgs^ e) { wglMakeCurrent(0, 0); wglDeleteContext(m_hglrc); ReleaseDC(m_hDC); }
Az OpenGL ablakozó rendszer független, azaz ablak létrehozását, a felhasználói események kezelését mindig az adott platform szerint kell megvalósítani. Az OpenGL-t inicializálni kell egy létrehozott ablakban, hogy OpenGL-lel lehessen rá rajzolni. A szabvány erről nem mond semmit, hanem ablakozó-rendszer-függő kiterjesztésekkel kell ezt megtenni. Például Windows-on a Win32 API-val létrehozott ablakokon a WGL kiterjesztéssel lehet OpenGL-t inicializálni. UNIX-on, vagy Linuxon X11 felett az XGL, Mac OS X-en Carbon felett az AGL, vagy Cocoa felett az NSOpenGL API-kkal tehető ez meg.
Azonban ehhez az ablakozó rendszerek működésének bővebb ismerete szükséges. Szerencsére erre nincs szükség, mert a GLUT (OpenGL Utility Toolkit) a platformfüggő részeket elfedi, és egy egységes API-n keresztül elérhetővé teszi a szükséges funkciókat. Hátránya, hogy a GLUT tudása limitált, bonyolultabb felhasználói felületet nem lehet vele könnyen létrehozni, azonban példaprogramokhoz, animációkhoz, játékokhoz tökéletes.
Az elkészítendő alkalmazások közvetlenül használják majd a GLUT-ot, amely az adott platformon elérhető API-val létrehozza az ablakot, kezeli a felhasználói eseményeket. Emellett az alkalmazások használják a GLU, és a GL könyvtárak függvényeit is. Ezek az operációs rendszeren keresztül eljutnak az eszköz illesztőprogramjához, amely megszólítja a grafikus hardvert (4.3. ábra). Látszik, hogy csak platform független könyvtárakat fognak használni az alkalmazások, így ugyanazt a kódot le lehet fordítani Windows-on, Linux-on, és Mac OS X-en is.
A http://user.xmission.com/~nate/glut.html címről le kell tölteni a glut-3.7.6-bin.zip
fájlt. A zip fájlban lévő glut32.dll
-t vagy közvetlenül a GLUT-ot használó .exe
fájl mellé kell rakni, vagy a c:\ Windows\ System32\
könyvtárba, így minden program meg fogja találni.
A .h
és .lib
fájlokat vagy egy külön könyvtárban kell elhelyezni, és így a fejlesztő környezetben külön be kell állítani a könyvtár helyét, vagy Visual Studio verziótól függően a következő helyre kell másolni:
Visual Studio 2005
glut.h
-> C:\ Program Files\ Microsoft Visual Studio 8\ VC\ PlatformSDK\ Include\ gl
glut.lib
-> C:\ Program Files\ Microsoft Visual Studio 8\ VC\ PlatformSDK\ Lib
Visual Studio 2008
glut.h
-> C:\ Program Files\ Microsoft SDKs\ Windows\ v6.0A\ Include\ gl
glut.lib
-> C:\ Program Files\ Microsoft SDKs\ Windows\ v6.0A\ Lib
Visual Studio 2010
glut.h
-> C:\ Program Files\ Microsoft SDKs\ Windows\ v7.0A\ Include\ gl
glut.lib
-> C:\ Program Files\ Microsoft SDKs\ Windows\ v7.0A\ Lib
Az OpenGL függvényeinek nevében mindig találunk egy előtagot (prefix) és egy utótagot (postfix) is.
prefixNévpostfix()
Az előtag mindig a függvény felhasználási területére utal. A Windows rendszerrel való kapcsolat kiépítésére és kezelésére szolgáló függvények neve wgl
előtaggal kezdődik, az alapvető OpenGL függvények előtagja mindig gl
. A segédprogram-könyvtár függvényeinek előtagja pedig a glu
.
Az OpenGL használata során definiálhatunk geometriai adatokat síkban, ilyen esetben a pontoknak két koordinátáját kell megadni. Térbeli modellezéskor a pontok helyzetét három koordinátájukat megadva rögzíthetjük. Komolyabb térbeli modellezéskor használnunk kell a homogén koordinátákat is (3.2.2. szakasz fejezet), ilyenkor egy térbeli pont az (x,y,z,1) vektorral rögzíthető, azaz négy koordinátát kell megadnunk. A színek esetén alkalmazhatjuk a már ismert RGB-modellt, ahol a szín három adattal jellemezhető. Lehetőség van azonban az RGBA színmodell használatára is, amely a három alapszín keverése mellett, a negyedik α adattal a kérdéses pontban a háttérszínének hatását jellemzi. A függvények hívásakor az adatokat paraméterek segítségével adhatjuk meg. A függvények nevében szereplő utótag egy szám és egy betű, ahol a szám a függvényparamétereinek számát, a betű pedig a paraméterek típusát rögzíti. Például az i
az int
, az f
a float
és a d
a double
típusú paraméterekre utal. Minden az OpenGL rendszer által definiált konstans a GL_
előtaggal rendelkezik és csak nagybetűkből áll.
Az OpenGL 3 dimenziós grafikus alprogramrendszer. Ahhoz, hogy megteremtett eszközkapcsolat segítségével rajzolni tudjunk, tisztában kell lennünk néhány alapfogalommal. A rendszer RGBA színeket használ az objektumok megjelenítésére, színes elemek megjelenítésére használhatjuk az OpenGL függvényeit. Az az elem jelenik meg amit valamilyen rajzrutinnal beírunk a megjelenítési pufferekbe. A rajzrutinok térbeli objektumokat képeznek le a síkra, ahhoz, hogy megjelenjenek a leképezéseket szintén rajzrutinokkal állíthatjuk be. Végül mivel az OpenGL megjelenítési csövet használ (a rajzrutinok a rajzutasításokat „bedobálják” 4.1. ábra bal oldali bemenetébe, gondoskodnunk kell arról, hogy megjelenjen a rajzolat a képernyőn (a cső alján).
A GDI+-hoz hasonlóan az OpenGL rendszerben is használhatjuk az RGBA színmodellt. Ha egy objektum színét szeretnénk megadni, akkor meg kell adni a szín piros (r), zöld(g) és kék(b) és esetleg alfa(a) – az áttetszőséget szabályozó – összetevőjét. A kapcsos zárójelek között a választható típusjelölők vannak.
void glColor3{b s i f d ub us ui} (TYPE r, TYPE g, TYPE b); void glColor4{b s i f d ub us ui} (TYPE r, TYPE g, TYPE b, TYPEa);
Mivel az egyes színek intenzitását 0 és 1 közé eső valós számokkal jellemezhetjük. Ha a függvény valós számokat használ paraméterként, mint például a
void glColor3f (GLfloat red, GLfloat green, GLfloat blue);
függvény, amelyik 3 valós argumentummal határozza meg az RGB színt. Ha az alfa színösszetevőt is szeretnénk használni akkor a
void glColor4f (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
függvényt használhatjuk, az argumentumok közvetlen 0 és 1 közé állításával. Lehetőségünk van például arra is, hogy byte
vagy int
típusú adatokat használjunk az intenzitások megadására. A rendszer ebben az esetben is a [0,1] intervallumon képzi az intenzitást, az adott típus legnagyobb értékéhez viszonyítva a megadott argumentumot. Például, az alábbi színdefiníciók egyenértékűek.
glColor3f (1.0, 1.0, 1.0); glColor3b (127, 127, 127);
Ha megadunk egy színt, akkor az lesz a létrehozott elemek jellemző színe.
Használhatunk egy indexet egy színpaletta adott pozíciójának kiválasztására (ha a PIXELFORMATDESCRIPTOR iPixelType
adattagja PFD_TYPE_COLORINDEX
) és ezzel az aktuális szín kiválasztására. Ha a színindexet szeretnénk használni, akkor a paletta indexszel adhatjuk meg a színt a
void glIndex{s i f d ub}(TYPE c);
függvénnyel.
Már a megjelenítési kapcsolat kialakításánál találkoztunk a színeket- és a Z-távolságokat tároló, a takarásokat és az egyéb képtároló lehetőségeket támogató segédpufferekkel. A kép megjelenítésének kezdetén célszerű ezen adatterületeket törölni. A
void glClear(GLbitfield mask);
függvényt használhatjuk az adatpufferek törlésére. A függvény mask paraméterének bitjei a különböző puffereket azonosítják. A GL_COLOR_BUFFER_BIT
bit a színek törléséről, a GL_DEPTH_BUFFER_BIT
a Z-puffer adatainak törléséről, a GL_ACCUM_BUFFER_BIT
a akkumulátor puffer törléséről, a GL_STENCIL_BUFFER_BIT
pedig a stencilpuffer törléséről gondoskodik.
Az alábbi hívás szín- és a Z-puffert egyaránt törli:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
A pufferek törlésére használt adatokat is megadhatjuk a
void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); void glClearIndex(GLfloat index); void glClearDepth(GLdouble depth); void glClearStencil(GLint s); void glClearAccum(GLfloat red, GLfloat green, GLfloat blue,GLfloat alpha);
függvényeket használva. Szürke hátteret készíthetünk például az alábbi hívással. (Az első három az rgb argumentum, az utolsó pedig az α, melynek értelmezésével a későbbiekben foglalkozunk.)
glClearColor( 0.8, 0.8, 0.8,1.0 );
Ha indexelt színnel törlünk, akkor a
void glClearIndex(TYPE c))
függvényt használhatjuk a szín c paraméterrel való kiválasztásával.
Felmerül a kérdés, hogy mikor rajzoljuk meg az OpenGL rajzokat. Ha a kép statikus, akkor rajzolhatjuk a Paint
vagy a Resize
esemény kezelőjében. Ha folyamatosan változó képet szeretnénk megjeleníteni, akkor használhatjuk a Timer
komponenst, szervezhetünk a rajzolásnak önálló programszálat is.
Mivel a megjelenítés az OpenGL-rendszerben a „megjelenítési csövön keresztül” történik, és a rajzoló és a megjelenítő gép akár különbözőek is lehetnek. A cső előtt még van egy parancssor, amibe az OpenGL gyűjti a rajzparancsokat. Ha ez megtelik, akkor rajzolja ki, így a GPU-t jobban ki tudja használni. Ha nem telik meg a sor, akkor szerepe van a kirajzolást kezdeményező
void glFlush() és void glFinish()
függvényeknek. Az előző kezdeményezi a kirajzolást, azonban a program futása folytatódik, az utóbbi nem tér addig vissza, míg a kirajzolás be nem fejeződik.
Ha a megjelenítési kapcsolat kialakításakor több puffer használatát írtuk elő (PFD_DOUBLEBUFFER
), akkor a
void SwapBuffers(HDC hdc)
függvénnyel cserélhetjük az adott eszközkapcsolat (hdc) „első” és „hátsó” adatterületét.
Az alábbi példa C++/CLI form Paint
eseményében töröl szürkével az ablakot.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) { this->Text=”Hello OpenGL”; hwnd=(HWND)this->Handle.ToInt32(); m_hDC = GetDC(hwnd); if(m_hDC) { MySetPixelFormat(m_hDC); } } private: System::Void Form1_FormClosing(System::Object^ sender, System::Windows::Forms::FormClosingEventArgs^ e) { wglMakeCurrent(0, 0); wglDeleteContext(m_hglrc); } private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glClearColor(0.5,0.5,0.5,1); glClear(GL_COLOR_BUFFER_BIT ); glFinish(); }
Első lépésként a glut.h
fejléc (header) fájlt be kell építeni a forrásfájlba, és a glut32.lib
fájlt hozzá kell szerkeszteni (link) a programhoz. A glut.h
automatikusan beépíti a GL/gl.h
és a GL/glu.h
header fájlokat, a szükséges platformfüggő fejállományokkal együtt.
A GLUT használata előtt a GLUT-ot inicializálni kell a
void glutInit(int *argcp, char **argv);
függvénnyel, amelynek paraméterként meg kell adni a main
-ben kapott parancssori argumentumokat tartalmazó paramétereket (argc, argv). Így lehetőség van a GLUT néhány paraméterét parancssorból is állítani. Ezután a
void glutInitWindowSize(int width, int height);
függvény az ablak szélességét és magasságát (width, height) állítja be, a
void glutInitWindowPosition(int x, int y);
pedig meghatározza a létrehozandó ablak pozícióját (x, y) lehet beállítani.
A
void glutInitDisplayMode(unsigned int mode);
függvénnyel lehet beállítani a létrehozandó ablakban lévő framebuffer tulajdonságait. A mode paraméter értékei a PIXELFORMATDESCRIPTOR
adattagjainak elérését szolgálják. (a GLUT_RGBA
, GLUT_INDEX
a színek megadása, a GLUT_SINGLE
, GLUT_DOUBLE
a pufferelés módja, a GLUT_ACCUM, GLUT_ALPHA, GLUT_DEPTH, GLUT_STENCIL
– a pufferek megadása stb.)
A GLUT_RGBA
konstanssal jelezzük, hogy a framebuffer a színeket RGBA-ként tárolja, azaz minden pixelre jut egy vörös, zöld, kék és alfa összetevő.
A
int glutCreateWindow(char *name);
függvénnyel létrehozunk egy ablakot, amelynek feliratát argumentumként meg kell adni. Az ablak, a korábban a glutInit
kezdetű függvényekkel beállított paraméterek alapján jön létre.
#include <GL/glut.h> int main(int argc, char* argv[]){ glutInit(&argc, argv); glutInitWindowSize(640, 480); glutInitWindowPosition(0, 0); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(”Hello OpenGL”);
Ezután be kell regisztrálni a callback függvényeket, amelyeket az ablakon történő egyes események hatására a GLUT fog meghívni. Ehhez adott prototípusú függvényekre mutató pointereket kell megadni, azaz a függvény nevét függvényhívó operátor nélkül. Végül a glutMainLoop()
függvénnyel elindítjuk az eseménykezelő hurkot. Ez egy végtelen ciklus, amely folyamatosan várakozik a külső eseményekre, és meghívja a megfelelő beregisztrált függvényt. Innen a vezérlés nem fog vissza térni a main()
-be. (A main()
végén a return 0
;-ra azért van szükség, hogy a fordító ne figyelmeztessen, hogy a visszatérési érték hiányzik).
Az OpenGL inicializálása utána az alkalmazás is végezhet incializálást, amelyet csak egyszer, a program legelején szükséges elvégezni. Ezért az eseménykezelő hurok indítása előtt meghívunk egy saját inicializáló függvényt. A tartalmát a main()
-be is lehetne írni, de így áttekinthetőbb.
glutDisplayFunc(onDisplay); onInit(); glutMainLoop();
Ezután írjuk meg a hiányzó két függvényt.
void onInit() { glClearColor(0.1f, 0.2f, 0.3f, 1.0f); } void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); glFinish(); }
Az onInit()
a glClearColor()
függvénnyel átállítja a színt, amellyel a színpuffert töröljük. Az onDisplay()
törli a színpuffer tartalmát a korábban beállított színnel. Paraméterként több puffer azonosítóját is meg lehet adni logikai VAGY kapcsolattal, de most a szín pufferen kívül nincs más puffer, ezért csak ezt töröljük.
Az OpenGL függvényhívások asszinkron hívások, azaz rögtön visszatérnek, mielőtt a művelet ténylegesen végrehajtódott volna. A glFinish()
függvénnyel azt kérjük a rendszertől, hogy a kiadott parancsokat hajtsa végre, és ezt meg is várjuk. A programot lefordítva és futtatva a 4.5. ábra képét kapjuk.
Ha a megjelenítési kapcsolat kialakításakor több puffer használatát írt/uk elő (glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE)
), akkor a
void glutSwapBuffers();
függvényt hívhatjuk a pufferek cseréjére.
A továbbiakban az egyes lehetőségek bemutatása során a példákat elsősorban GLUT rendszerben készítjük el. Egyes esetekben, ahol interaktív lehetőségeket is megvalósítunk, visszatérünk a C++/CLI rendszerhez.
Az OpenGL térben (esetleg homogén koordinátákkal) definiált objektumokból készít - a képernyőn - síkbeli képet. Ahhoz, hogy a rendszer segítségével rajzot jelenítsünk meg, tisztában kell lennünk a képalkotás módszerével. Képzeljük el, hogy van egy munkaterünk, amelyben térbeli objektumokat helyezünk el! A cél az, hogy valamilyen irányból fényképet készítsünk az elhelyezett testekről és a fénykép jelenjen meg a képernyőn.
Alaphelyzetben a megjelenítési kapcsolat kialakításakor az ablak méretei határozzák meg a munkateret. A munkatér olyan, hogy a határoló tégla alaprajzát az ablak szélessége és magassága határozza meg, és a tégla magassága is megegyezik az ablak magasságával. A tér koordináta-rendszere olyan, hogy az origó a tégla közepére kerül, és a tégla szélei minden irányban 1 távolságra vannak az origótól.
A kamera az origóban helyezkedik el és negatív Z irányba „néz”. (4.6. ábra). A kép alapesetben úgy jön létre, hogy a kamera a végtelenből érkező, Z-tengely irányú, párhuzamos vetítősugarak képét rögzíti.
Az OpenGL rendszerben történő megjelenítés alapobjektumai az úgynevezett primitívek. Az alapvető primitívek a pont, a vonalszakasz, a szakaszokból alkotott vonallánc, a háromszög, a négyszög és a poligon. Ezek mellett használhatunk többféle felületi objektumot is.
Minden egyes primitív létrehozását a glBegin()
függvény hívása kezdeményezi, és glEnd()
zárja. A két függvény között definiált pontok (vertexek) határozzák meg a primitívek térbeli elhelyezkedését. A glBegin()
függvény GLenum
típusú mode paraméterétől függ, hogy milyen primitívet hozunk létre.
void glBegin(GLenum mode);
A mode paraméter lehetséges értékeit és azok értelmezését az alábbiakban foglaljuk össze:
|
A vertexek pontokat határoznak meg. |
|
Minden pár sarokpont egy vonalszakaszt határoz meg. A szakaszok nem alkotnak láncot. Ha páros n pontot adunk meg n/2 darab szakasz jön létre. |
|
Minden pár sarokpont egy vonalszakaszt definiál. A szakaszok láncot alkotnak, n pont (n-1) darab szakaszt határoz meg. |
|
A szakaszok zárt láncot alkotnak. Az n pont n szakaszt definiál. |
|
Minden ponthármas egy háromszöget határoz meg. A kifestett háromszögek nem alkotnak láncot. Hárommal osztható n pont esetén n/3 háromszög jön létre. |
|
A megadott pontok egymáshoz oldalaival illeszkedő, kifestett háromszögekből álló láncot definiálnak. Ha n>3, akkor n-2 darab háromszög keletkezik. |
|
Záródó, kifestett háromszöglánc, melynek kezdőpontja közös (n>3 esetén n-2 darab háromszög). |
|
Minden pontnégyes egy kifestett négyszöget határoz meg. A négyszögek nem alkotnak láncot. Ha n a 4 többszöröse, akkor n/4 négyszög keletkezik. |
|
A kifestett négyszögekből alkotott lánc, páros n esetén n/2 elemmel. |
|
A pontok n oldalú, kifestett poligont határoznak meg. |
A csúcspontokat (vertex) a glVertex
számtípus()
alakú függvényekkel adhatjuk meg a glBegin()
és a glEnd()
hívások között. Ha csak két koordinátát adunk meg, akkor a Z-koordináta automatikusan 0 lesz. Mivel dolgozhatunk a homogén koordinátás térben is, a rendszer automatikusan kezeli a negyedik koordinátát. Ha csak három koordinátát definiálunk, a negyedik - homogén - koordináta automatikusan az 1 értéket kapja.
Az alábbi példa téglalapot rajzol az ablakba. Azért téglalap, mert az 1 abszolút értékű koordináták a téglalap alakú ablak széleihez tartoznak. A módosított onDisplay()
a következő lesz.
void onDisplay(){ glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_TRIANGLE_STRIP); glVertex2f(-0.5f, 0.5f); glVertex2f(0.5f, 0.5f); glVertex2f(-0.5f, -0.5f); glVertex2f(0.5f, -0.5f); glEnd(); glFinish(); }
Négyszög rajzoláshoz használhattuk volna a GL_QUADS
primitív típust is, de inkább GL_TRIANGLE_STRIP
-et használtuk. A GL_QUADS
, GL_QUAD_STRIP
, GL_POLYGON
OpenGL 2.0-ban még támogatott, azonban az OpenGL későbbi verzióiból kivették hatékonysági okokból.
Az eddig megismertek alapján be tudjuk állítani a pixelnyi pontok, a vonalak, a kifestett poligonok és a kifestett felületek színét, megjelenítési módját.
Pontok rajzolásakor (GL_POINTS
) lehetőségünk van a pont megjelenítésének módosítására, a vonalak vastagságának és mintázatának meghatározására, a felületelemek oldalainak különböző módszerrel történő ábrázolására is.
A megjelenített pontok raszterpontban mért átmérőjét (size) állíthatjuk be a
void glPointSize (GLfloat size);
függvénnyel. Az alábbi példa az ablak közepére rajzol egy zöld 10 pixel méretű pontot
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); glPointSize(10); glColor3f(0,1,0); // a zöld szín glBegin(GL_POINTS); glVertex3f( 0, 0,0); glEnd(); glFinish(); }
Vonalak megjelenítésekor (GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP
) a vonalak pixelben mért vastagságát (width) szabályozza az alábbi függvény:
void glLineWidth (GLfloat width);
Ha a vonalmintázatok használatát engedélyezzük glEnable(GL_LINE_STIPPLE)
, akkor saját mintázatú vonalakat hozhatunk létre. A vonal mintázatát a
void glLineStipple (GLint factor, GLushort pattern);
függvény argumentumaival határozhatjuk meg. A 16 bites pattern paraméter nem 0 bitjei definiálják a rajzolandó pontokat. Azért, hogy a mintázat ne csak 16-pontos legyen, használhatjuk a factor paramétert, melynek hatására a pattern minden egyes bitje factor darab, egymás utáni pontot jelent a vonalon kirajzolva. Az alábbi példa vastag folyamatos és vastag szaggatott vonallal rajzolt átlót jelenít meg.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); glLineWidth(10); glColor3f(1,1,0); // a sárga szín glBegin(GL_LINES); glVertex3f( -1,-1,0); glVertex3f( 1,1,0); glEnd(); glColor3f(0,1,1); // a magenta szín glLineStipple (5, 0x0C0F); glEnable(GL_LINE_STIPPLE); glBegin(GL_LINES); glVertex3f( -1,1,0); glVertex3f( 1,-1,0); glEnd(); glDisable(GL_LINE_STIPPLE); glFinish(); }
Konvex poligonok (GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_QUADS, GL_QUAD_STRIP, GL_POLYGON
) megjelenésekor a térbeli sokszögeknek (poligonok) van első és hátsó oldaluk. Alapesetben az a poligon első oldala, amely irányból nézve a pontok óramutató járásával ellentétes körüljárást adnak. A sokszögek első oldala kiválasztható a
void glFrontFace(GLenum mode);
függvénnyel – a mode paraméterben a GL_CCW
az alapesetet, a GL_CW
pedig az alapesettel ellentétes oldalt meghatározó argumentumok megadásával.
A sokszögek rajzolásakor dönthetünk arról, hogyan jelenjen meg az alakzat első és a hátsó oldala. Alaphelyzetben a poligonok kifestettek. A kirajzolás azonban történhet csak a sarokpontokkal, az élekkel és végül a felületek lehetnek kifestettek. A
void glPolygonMode(GLenum face, GLenum mode);
függvényt használjuk az adatok beállítására. A face paramétertől függ, hogy melyik oldalt állítjuk be (GL_FRONT
, GL_BACK
, GL_FRONT_AND_BACK
– felső, hátsó vagy mindkettő), a mode paraméter értékei pedig megjelenítés típusát adják (GL_POINT
, GL_LINE
, GL_FILL
).
A nem láncban (STRIP
) rajzolt sokszögek éleinek láthatóságát is szabályozhatjuk, így lehetőségünk van arra, hogy a nem konvex alakzatokat konvex alakzatokból összeállítva rajzoljuk ki. Az élek láthatóságát a megelőző vertex-nél határozhatjuk meg a
void glEdgeFlag(GLboolean flag);
függvénnyel. A flag logikai paraméter megadásakor használhatjuk a GL_TRUE
és a GL_FALSE
konstansokat is. Az élek láthatósága a poligonban nem változik egészen a következő glEdgeFlag()
függvényhívásig.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); glLineWidth(10); glColor3f(1,1,1); // a fehér szín glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glBegin(GL_POLYGON); glEdgeFlag(GL_TRUE); glVertex3f(-0.5,-0.5,0); glEdgeFlag(GL_FALSE); glVertex3f(0.5,-0.5,0); glEdgeFlag(GL_TRUE); glVertex3f(0,0.5,0); glEnd(); glFinish(); }
Mintázatot is definiálhatunk a sokszögeknek a
void glPolygonStipple(const GLubyte *mask);
függvénnyel, ahol a mask tömb egy 32*32 méretű bitkép pontjainak megjelenését definiálja. Alaphelyzetben ennek értelmezése: ahol 1 van ott van poligon szín, ahol 0 ott nincs. Ezt a képet mint tapéta mintázatot használjuk. Ahhoz, hogy a mintázat megjelenjen, használnunk kell a glEnable(GL_POLYGON_STIPPLE)
függvényhívást. Megszüntethetjük a mintázást a glDisable(GL_POLYGON_STIPPLE)
függvényhívással. Az alábbi példa téglalapot jelenít meg véletlen mintázattal
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); GLubyte vl[128]; // 32*32/8 for (int i=0 ; i<128 ; i++) { vl[i]=rand()%128+1; // stdlib.h } glColor4b(127,127,0,64); // sárga glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glEnable (GL_POLYGON_STIPPLE); glPolygonStipple (vl); glBegin(GL_POLYGON); glVertex3f( -0.6, -0.6,-0.5); glVertex3f( 0.6,-0.6,-0.5); glVertex3f(0.6,0.6,-0.5); glVertex3f(-0.6, 0.6,-0.5); glEnd(); glDisable (GL_POLYGON_STIPPLE); glFinish(); }
Ha megvilágítottan és árnyalva szeretnénk megjeleníteni a felületet, akkor szükségünk van a normálvektorokra. A háromszög három pontja meghatározza a sík normálvektorát. Azonban így egyik háromszögről a másikra áthaladva a normálvektor nem folytonosan változik, ezért az élek kiemelődnek. Elkerülhetjük ezt a jelenséget, ha minden ponthoz egy normálvektort is definiálunk. A később tárgyalásra kerülő árnyalt ábrázoláshoz a poligon minden sarokpontjába normálist is definiálhatunk a glNormalszámTípus() függvénnyel megadva a normális koordinátáit.
A glVertex()
hívással hozunk létre egy vertexet, azaz bekerül a feldolgozandó vertexek közé. Az OpenGL egy nagyállapot gép, ezért a vertex attributúmait a legutoljára beállított állapot határozza meg. A glColor()
függvénnyel akár vertexenként is beállíthatjuk a színeket.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_TRIANGLE_STRIP); glColor3f(1.0f, 0.0f, 0.0f); // vertex szín attribútum glVertex2f(-0.5f, 0.5f); // vertex létrehozás glColor3f(0.0f, 1.0f, 0.0f); // vertex szín attribútum glVertex2f(0.5f, 0.5f); // vertex létrehozás glColor3f(0.0f, 0.0f, 1.0f); // vertex szín attribútum glVertex2f(-0.5f, -0.5f); // vertex létrehozás glColor3f(1.0f, 1.0f, 0.0f); // vertex szín attribútum glVertex2f(0.5f, -0.5f); // vertex létrehozás glEnd(); glFinish(); }
A grafikus csővezetékben a raszterizáló a létrejövő fragmenseken interpolálja a vertex attribútumokat. Ennek módja a
void glShadeModel( GLenum mode );
függvénnyel állítható. GL_SMOOTH
hatására egy háromszögnél mind a három vertex attribútumai alapján interpolálja a fragmensek attribútumait (4.13. ábra). A GL_FLAT
hatására az egész háromszög fragmensei az egyik vertex attributúmait veszik át (4.14. ábra). Ez az onInit()-
ben állítható.
void onInit() { glClearColor(0.1f, 0.2f, 0.3f, 1.0f); glShadeModel(GL_SMOOTH); }
A poligonok vertexek megadásával történő rajzolása helyett téglalapok rajzolására használhatjuk a
void glRecttipus(tipus x1, tipus y1, tipus x2, tipus y2);
függvényeket. Az alábbi példa is egy előző ábrán (4.8. ábra) látható téglalapot rajzolja ki csak pirossal.
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glColor3f(1,0,0); // a piros szín glRectf(-0.6, -0.6,0.6, 0.6); glFinish(); }
Görbevonalú alakzatok határát úgy rajzoljuk, hogy kiszámoljuk annak pontjait, és azokat egyenessel kötjük össze. Kör esetén a kör parametrikus egyenletébe néhányszor behelyettesítünk, és így hozzájutunk a kör néhány pontjához, amelyeket a kör középpontjával összekötve háromszögek alakulnak ki (4.15. ábra). Egy egyenletekkel megadott alakzat háromszögekké alakítása a tesszeláció. Látszik, hogy minél többször mintavételezzük a körvonalat, annál íveltebb lesz a háromszögelt kör. Cserébe több ponton kell a GPU számoljon.
void drawCircle(float radius, int tesselation = 32) { glBegin(GL_TRIANGLE_FAN); glColor3f(1.0f, 1.0f, 0.0f); glVertex3f(0.0f, 0.0f, 0.0f); // középső vertex glColor3f(1.0f, 0.0f, 0.0f); // köríven lévő vertexek színe float delta = 2.0f * static_cast<float>(M_PI) / tesselation; for (int i = 0; i <= tesselation; ++i) { float t = i * delta; glVertex3f(radius * cosf(t), radius * sinf(t), 0.0f); } glEnd(); }
A kör középpontja minden háromszögnek az egyik pontja lesz, ezért GL_TRIANGLE_FAN
-ként adjuk meg a csúcspontokat. A középpontnak, és a köríven lévő pontoknak különböző színt állítunk be. Ezután a tesszeláció mértéke alapján kiszámoljuk, hogy mekkora szöget kell fordulni minden mintavételkor (delta). Majd egy for
ciklussal végig mintavételezzük a kör parametrikus egyenletét. Az egyenlet itt egyszerűbb, mint a fenti képletben, mert feltételezzük, hogy a kör mindig az origóban lesz. A for
ciklus összesen tesselation + 1-szer fut le. Az utolsó vertex a 2π-nél van, azaz ott, ahonnan elindultunk. Erre azért van szükség, hogy az utolsó háromszög összekösse az utolsó pontot az elsővel.
Az alábbi példa az ablak arányaihoz igazított torzított körlapot rajzol a DrawCircle()
függvénnyel.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); drawCircle(0.50F); glFinish(); }
Hogyan jön létre a térbeli háromdimenziós - vagy akár homogén-koordinátákkal négydimenziós – objektumok képe a képernyőn? Ahhoz, hogy egy térbeli pontot leképezzünk a képernyő egy ablakába, több geometriai transzformációt is végre kell hajtanunk. (4.17. ábra)
A képet alkotó objektumokat úgy helyezhetjük el a térben, hogy felépítjük a geometriát, mondjuk az origó körül, és aztán azt geometriai transzformációval a kamerához képest a megfelelő térbeli pozícióba mozgatjuk (nézeti transzformáció).
A térbeli alakzatok síkbeli ábrázolásához először síkra kell vetíteni a képet. Ha a síkba történő vetítés sugarai párhuzamosak, akkor azt axonometriának hívjuk. Ha a szem működését szeretnénk modellezni, akkor centrális vetítést kell alkalmaznunk. (vetítő transzformáció)
Ha a síkba vetített képet ténylegesen a képernyő egy ablakában szeretnénk látni, akkor alkalmaznunk kell egy téglalapot, és a téglalapba konvertáló transzformációt (Window-Viewport transzformáció).
Az OpenGL a transzformációkat homogén koordinátákkal, 4*4-es mátrixszorzással végzi úgy, hogy egyetlen modellnézeti transzformációs és egyetlen vetítési transzformációs mátrixot használ. A képet alkotó elemek mindegyik pontja a modellezési transzformációnak megfelelően elmozdul, és a vetítési transzformációnak megfelelően kerül a síkbeli képre.
|
(4.1) |
Minden újabb transzformáció az aktuális vetítési vagy modellezési transzformációs mátrix szorzását jelenti a megadott transzformációval.
A módosítás előtt a glMatrixMode()
függvénnyel kiválaszthatjuk, hogy melyik mátrixra vonatkoznak a műveletek:
void glMatrixMode(GLenum mode);
Ha a mode paraméter értéke GL_MODELVIEW
, akkor modellezési-, ha GL_PROJECTION
, akkor vetítési transzformációt adunk meg. (A glGet(GL_MATRIX_MODE)
hívással lekérdezhetjük, hogy éppen melyik mód az aktuális). A programban rendszerint a vetítési transzformáció megelőzi a modellezési transzformációt, hiszen először a kép méreteit, a kamera jellegzetességeit kell beállítanunk, és azután megfelelő pozícióból elkészíteni a képet.
Akármilyen mátrixmódban is dolgozunk, a glLoadIdentify()
hívás hatására az aktuális transzformációs mátrixunk az egységmátrix lesz, ami a helyben hagyásnak felel meg. Lehetőségünk van arra, hogy 16 darab valós számmal definiáljuk a transzformációs mátrix koordinátáit.
void glLoadMatrixd (const GLdouble *m); void glLoadMatrixf (const GLfloat *m);
Az m paraméter double
(d
eset), illetve float
(f
eset) típusú elemeket oszlopfolytonosan (!) tartalmazó tömb mutatója.
Az aktuális mátrixot akár be is szorozhatjuk egy argumentumként megadott tömbbel a glMultMatrixf()
és a glMultMatrixd()
függvények segítségével.
A fenti lehetőségekkel minden geometriai transzformáció végrehajtható, azonban gyakrabban használjuk a szemléletes, geometriai jelentést hordozó transzformációkat megvalósító függvényeket.
A modellezési transzformációk glMatrixMode(GL_MODELVIEW)
beállítás esetén kétféle szemléletmóddal is értelmezhetők. Felfoghatjuk a transzformációkat úgy, mint a térben elhelyezett objektumok mozgatását a fix helyzetben lévő kamera előtt (modellező transzformációk), illetve úgy is, hogy a kamera pozíciója és iránya változik (nézeti transzformáció) a képalkotáskor. Célszerű azonban a kétféle gondolkodás közül az egyiket kiválasztani, és annak tükrében értelmezni a geometriai transzformációkat. Az alábbiakban mi is ezt tesszük, a transzformációkat, mint a térbeli objektumok mozgatását fogjuk fel.
A legegyszerűbb geometriai transzformáció az eltolás, melyet az argumentumok típusától függően a
void glTranslated (GLdouble x, GLdouble y, GLdouble z); void glTranslatef (GLfloat x, GLfloat y, GLfloat z);
függvényekkel valósíthatunk meg. Mindkét függvény hívásának hatása az, hogy a homogén koordinátás modell-transzformációs mátrixot megszorozza az (x,y,z) eltolást megvalósító mátrixszal.
Térbeli origón átmenő tengely körüli forgatásnak megfelelő transzformációt eredményeznek a
void glRotated(GLdouble angle,GLdouble x,GLdouble y,GLdouble z) void glRotatef(GLfloat angle,GLfloat x,GLfloat y,GLfloat z)
függvények. Az elforgatás tengelyvektorának irányát az (x,y,z) paraméterek, fokban mért szögét pedig az alpha paraméter határozzák meg.
Transzformációk sorozatát írhatjuk elő a segédprogramok könyvtárának - nézeti transzformációs szemléletű - gluLookAt()
függvényével.
void gluLookAt(GLdouble eyex, GLdouble eyey, GLdouble eyez, GLdouble centerx, GLdouble centery, GLdouble centerz, GLdouble upx, GLdouble upy, GLdouble upz );
A függvény paraméterei a kamera pozícióját (eyex,eyey,eyez), a kamera által „megcélzott” térbeli pontot (centerx,centery,centerz) és a kamera felső oldalának irányát (upx,upy,upz) rögzítik. A transzformáció a modelltér koordináta-rendszerét (X,Y,Z) transzformálja új pozícióba (X’,Y’,Z’)
A függvény által megvalósított transzformáció a modellmátrix transzformációs sorának végére kerül, azaz először a többi geometriai transzformáció kerül sorra.
Az alábbi példa sárga, térben elforgatott téglalapot rajzol. A téglalap mellé még pirossal a koordináta-tengelyeket is megrajzoltuk.
void onDisplay() { glMatrixMode(GL_MODELVIEW); glRotatef(60,10,10,10); glColor3f(1,1,0); glRectf(-0.6, -0.6,0.6, 0.6); glColor3f(1.0, 0.0, 0.0); glBegin(GL_LINES); glVertex3f( 0,0,0); glVertex3f( 0.2,0,0); glVertex3f( 0,0,0); glVertex3f( 0,0.2,0); glVertex3f( 0,0,0); glVertex3f( 0,0,0.2); glEnd(); glFinish(); }
A 3.4.1. szakasz fejezetben áttekintettük a tér síkba vetítésének matematikai hátterét.
A vetítési transzformációk glMatrixMode(GL_PROJECTION)
beállítás segítségével módosíthatjuk az alaphelyzet (4.20. ábra) merőleges párhuzamos vetítési előírása mellett a modelltér méreteit is. Alaphelyzetben a munkatér +X, -X, +Y, -Y és –Z irányban 1 kiterjedésű. A merőleges leképezés előírása mellett, a munkatér méreteit is meghatározza az alábbi függvény:
void glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far );
A left és right paraméterek a téglalap alakú munkatér jobb- és bal oldalának centrumtól mért távolságát, a top és bottom paraméterek pedig a munkatér felső és alsó szélének középponttól vett távolságát definiálják. A munkatér egy első és egy hátsó vágósíkkal határolt, melyek origótól való távolságát a near és far paraméterek határozzák meg (4.20. ábra).
A párhuzamos vetítés eltér a szem képalkotási módszerétől, így képen a távolabbi objektumok mérete ugyanakkora, mint a közelebbieké. Ha a szem képalkotásához szeretnénk illeszkedni, akkor centrális vetítést kell használnunk.
Centrális leképezést valósíthatunk meg a glFrustum()
, illetve a segédkönyvtár gluPerspective()
függvényeinek segítségével. Mindkét függvény definiálja a munkateret és a nézeti ablakot.
A paramétereinek értelmezéséhez az 4.21. ábra lehet segítségünkre.
void glFrustum(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far); void gluPerspective(GLdouble alpha, GLdouble aspect, GLdouble near, GLdouble far);
Az alpha értéket fokokban kell megadni, az aspect paraméter csak a nézeti ablak oldalainak arányát rögzíti (4.2).
aspect=(left+right)/(top+bottom) |
(4.2) |
További, a
void glSetClipPlane(GLenum plane, GLdouble *equation);
függvény segítségével definiált vágósíkok is megadhatók (GL_MAX_CLIP_PLANES
darab). Az equation pointer az Ax+By+Cz+D = 0 egyenlet együtthatóit tartalmazó mátrix pointere. Lekérdezhetjük az adatokat a
void glGetClipPlane(GLenum plane, GLdouble *equation);
A vágósíkok használatát egyenként engedélyezhetjük a glEnable(GL_CLIP_PLANE
i) hívással.
A 3.4.2. szakasz fejezetben láttuk a Window-ViewPort leképezést. Ez is programozható OpenGL-ben.
A megjelenő nézeti ablak mérete alapértelmezés szerint az ábrának otthont adó ablak teljes mérete. A glViewport()
függvénnyel lehetőségünk nyílik arra, hogy az aktív területnél kisebb ablakot jelöljünk ki.
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
Az x, y paraméter a nézeti ablak bal alsó sarokpontját, a width és a height a szélességet és magasságot definiálja. Mindegyik paraméterérték pixelben értendő.
Példaként állítsuk be egy olyan vetítés-mátrixot, hogy a kirajzolandó objektumokat az ablak megszokott grafikus koordináta-rendszerében lehessen megadni! Gondot jelent azonban ha az ablakot elkezdjük méretezni kézzel, akkor az elején beállított arány már nem lesz jó. CLI-ben ez nem okoz problémát, GLUT esetén a glutReshapeFunc()
függvénnyel be lehet regisztrálni egy callback függvényt, amely az ablak méretezéskor hívódik meg az ablak új méreteivel. Ez az esemény az ablak létrejöttekor is aktiválódik.
int main(int argc, char* argv[]) { //glutInit... glutDisplayFunc(onDisplay); glutReshapeFunc(onResize); onInit(); glutMainLoop(); return 0; }
A bal felső sarok az origó, az X tengely jobbra, az Y tengely lefelé nő, és mindkét tengelyen 1 egység 1 pixelnek feleljen meg!
A transzformációt a gluOrtho2D()
függvénnyel lehet beállítani. A Z tengely -1 és +1 közötti része lesz látható. A gluOrtho2D()
azonban az aktuális mátrixot szorozza meg ezzel a transzformációs mátrix-szal, így a glLoadIdentity()
függvénnyel előtte inicializálni kell a transzformációt, különben érdekes meglepetések érhetnek minket az ablak méretezéskor. Az új onResize()
függvény tehát így néz ki:
void onResize(int width, int height) { glViewport(0, 0, width, height); // viewport állítás if (height == 0) height = 1; glMatrixMode(GL_PROJECTION); // projection mátrix kiválasztása glLoadIdentity(); // egység mátrix beállítása gluOrtho2D(0.0, width, height, 0.0); // transzformáció beállítása glMatrixMode(GL_MODELVIEW); // modelview mátrix kiválasztása }
A kört viszont mindig az origóba rajzoljuk, amely most az ablak bal felső sarkában található. A glTranslatef()
függvénnyel az aktuális mátrixhoz egy eltolás transzformációt lehet hozzáadni. Az aktuális mátrix a modelview, mert az onResize()
végén erre álltunk vissza. Mivel a modelview mátrixban ott maradhat az előző transzformáció, ezért ezt is a glLoadIdentity()
-vel egység mátrixba állítjuk.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); glTranslatef(100.0f, 100.0f, 0.0f); drawCircle(50.0f, 64); glFinish(); }
Így a színes kör középpontja a 100,100 pontba kerül.
A transzformációk tárolására típusonként verem áll rendelkezésünkre. Ebbe a verembe menthetjük az aktuális mátrixot a glPushMatrix()
hívással, illetve az utolsónak mentett mátrix aktuálissá tehető a glPopMatrix()
hívással.
A nem látható elemek eltüntetésére a legegyszerűbb megoldás a hátsó lap eldobásának módszere. (3.4.3. szakasz fejezet) Ennek alapkoncepciója az, hogy ha a felületet (testet) alkotó poligonháló kifelé mutató normálisa n egy irányba mutat a ránézés irányával r , n * r> 0, akkor az a háromszög nem látható.
A hátsó lapok eldobásának szabályozására használható a
void glCullFace(GLenum mode);
A mode paraméter határozza meg, hogy mely poligonokat dobjuk el (GL_FRONT
– az elsőket, GL_BACK
– a hátsókat, vagy GL_FRONT_AND_BACK
– mindkettőt). A működés beállítható a glEnable(GL_CULL_FACE)
hívással, és letiltható a glDisable(GL_CULL_FACE)
függvény aktiválásával. Ha hagyományos – PIXELFORMATDESCRIPTOR strukúrával inicializálunk, akkor a z-puffer az alapértelmezett (Letilthatjuk a dwFlag PFD_DEPTH_DONTCARE bitjével). GLUT használatakor a
glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
hívás alkalmazható.
A takart részek eltávolítására az OpenGL a manapság már legelterjedtebb algoritmust a 3.4.3. szakasz fejezetben ismertetett Z-puffer algoritmust használja.
A takarás vizsgálata csak akkor történik meg, ha azt engedélyeztük. Alapállapotban nincs távolságvizsgálat, és az objektumok a rajzolás sorrendjében a képernyőre kerülnek.
A mélységi teszt a glEnable(GL_DEPTH_TEST)
függvény hívásával engedélyezhető. Az engedélyezett műveletet természetesen tilthatjuk is a glDisable(GL_DEPTH_TEST)
függvénnyel.
Azt is szabályozhatjuk, hogyan történjen a Z-koordináták összehasonlítása. A
void glDepthFunc (GLenum func);
függvényt többféle GLenum
típusú konstanssal is hívhatjuk. Például, ha engedélyezett távolságvizsgálat esetén a GL_LESS
-t használjuk argumentumként, akkor a pont csak akkor jelenik meg, ha z-koordinátája kisebb, mint az aktuálisan tárolt z-érték. A vizsgálat fordítva is történhet, ha a GL_GREATER
konstanst használjuk.
Az eddigi példák alapján vizsgáljuk meg a takarást! A kör koordinátáinak z komponense 0 volt. A gluOrtho2D()
olyan transzformációs mátrixot készít, hogy az objektumok a z=-1 és +1 között kerülnek a képernyőre. Kipróbálhatjuk a takarást, ha a z=0 síkra kört, a z=-1 síkra pedig négyszöget rajzolunk.
Ha először kirajzoljuk a négyszöget, és utána a kört, akkor ez biztos jó lesz. Ezt hívják „painters” algoritmusnak, amikor először a legtávolabbi objektumot rajzoljuk ki, és utána rá a többit. De az OpenGL használható 3D grafikára is, így van más módja is a takarási probléma megoldásának.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT); // kör rajzolás glLoadIdentity(); glTranslatef(100.0f, 100.0f, 0.0f); drawCircle(50.0f, 64); // négyszög rajzolás glLoadIdentity(); glColor3f(0.0f, 1.0f, 0.0f); glBegin(GL_TRIANGLE_STRIP); glVertex3f(100.0f, 50.0f, -1.0f); glVertex3f(200.0f, 40.0f, -1.0f); glVertex3f( 90.0f, 160.0f, -1.0f); glVertex3f(210.0f, 140.0f, -1.0f); glEnd(); glFinish(); }
A kört eltoljuk a (100,100) pontba. A modelview mátrix a glTranslatef()
után tartalmaz egy eltolás transzformációt, így minden utána következő objektum el lesz tolva. Mivel a négyszög koordinátáit közvetlenül világ koordinátákban adjuk meg, ezért az eltolásra már nincs szükség, így a modelview mátrixot a glLoadIdentity()
függvénnyel egység mátrixba állítjuk.
A négyszög a kör elé került, pedig mögötte kellene lennie. A takarási probléma megoldására szükség van a mélységi pufferre (depth puffer, z-puffer). A mélységi puffer tárolja minden pixelre a pixel mélységi értékét. Amikor a fragmens shader után a fragmens a raszter műveletekhez jut, a pixel framebufferbe írása előtt a rendszer összehasonlítja az új fragmens mélységi értékét a mélységi pufferben lévő értékkel. Ha az új érték kisebb, mint a régi, azaz közelebb van a kamerához, akkor a pixel felül írja a framebuffer-ben lévő pixelt. Ha nagyobb, akkor a rendszer eldobja (4.24. ábra) a pixelt.
Mint láttuk, a glutInitDisplayMode()
függvénynek meg lehet adni, hogy az RGBA típusú színpuffer (GLUT_RGBA
) mellé kérünk még egy mélységi puffert (GLUT_DEPTH
) is. Ezután az OpenGL-ben engedélyezni kell a mélység tesztelést a glEnable(GL_DEPTH_TEST)
hívással. A glDepthFunc()
függvénnyel megadhatjuk a mélységi értékek összehasonlítási módját. Ez alapértelmezetten GL_LESS, azaz a mélységi teszten csak a kisebb mélységű pixelek fognak átjutni. Végül még arra is figyelni kell, hogy a rajzolások előtt ne csak a színpuffert töröljük, hanem a mélységi puffert is.
int main(int argc, char* argv[]) { //glutInit... glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH); glutCreateWindow(”Hello Circle”); //... } void onInit() { glClearColor(0.1f, 0.2f, 0.3f, 1.0f); glShadeModel(GL_SMOOTH); glEnable(GL_DEPTH_TEST); } void onDisplay() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // kör és négyszög rajzolás glFinish(); }
Így az objektumok az elvártnak megfelelően takarják egymást.
Az animáció két problémát is felvet. Biztosítani kell, hogy a geometria többször, villogásmentesen kerüljön a képernyőre.
A CLI-rendszerben a Timer
komponens kezdeményezhet kirajzolást, és a Timer
méri az eltelt időt is. A GLUT rendszerben a glutIdleFunc()
regisztrál egy olyan callback függvényt, amelyet a GLUT akkor hív, ha éppen nincs semmilyen más esemény (ablakméretezés, rajzolás). Ha fizikai mozgást szeretnénk modellezni akkor a glutGet(GLUT_ELAPSED_TIME)
hívással le lehet kérdezni a program indítása óta eltelt időt ezredmásodpercben.
Ha mindig ugyanabba a pufferbe rajzolunk, ami a képernyőn megjelenik, akkor az a kép villogását okozhatja. Az OpenGL parancsok asszinkron műveletek, így nem rögtön hajtódnak végre, először egy műveleti parancssorba kerülnek. A GPU-n lévő ütemező dönt a végrehajtás idejéről. A problémára megoldás a dupla pufferelés. Ekkor két framebuffer van, az egyik jelenik meg a képernyőn, miközben a másikba rajzolunk. A képernyőn lévő puffert front buffernek, az aktuális puffert back buffernek nevezzük. A rajzolás befejezésekor a két puffert ki kell cserélni, így a teljesen megrajzolt kép fog a felhasználónak megjelenni (4.26. ábra).
Hagyományos módon történő inicializálás esetén a PIXELFORMATDESCRIPTION
struktúra dwFlag
adattagjába be kell állítania PFD_DOUBLEBUFFER
bitet, és a SwapBuffers()
függvény hívásával cserélhetjük a puffereket.
GLUT esetén a dupla pufferelést a glutInitDisplayMode(GLUT_DOUBLE);
függvényhívással lehet aktivizálni. A pufferek cserélését pedig a glutSwapBuffers()
függvénnyel lehet kérni. A glutSwapBuffers()
meghívja a glFinish()
függvényt, így erre külön nincs szükség.
Példaként módosítsuk a 4.25. ábra programját, hogy mozogjon a labda. Ehhez szükség lesz egy 2-dimenziós Vector osztályra, amellyel a vektor műveletek kényelmesen elvégezhetőek.
struct Vectorú { float x, y; Vector(float x = 0.0f, float y = 0.0f) : x(x), y(y) { } Vector& operator +=(const Vector& v) { x += v.x; y += v.y; return *this; } } ; Vector operator *(const float& a, const Vector& v) { return Vector(a*v.x, a*v.y); }
Ezután deklaráljunk néhány globális változót, amelyek tartalmazzák majd a labda pozícióját (position), sebességét (velocity), az ablak méretét, illetve az animációhoz szükséges utolsó frissítés idejét (lastTime).
Vector position(320.0f, 240.0f), velocity(100.0f, 100.0f); int lastTime = 0; float windowWidth = 640.0f, windowHeight = 480.0f;
A glutIdleFunc()
függvénnyel regisztráljuk azt a callback függvényt, amelyet a GLUT fog hívni.
int main(int argc, char* argv[]) { //glutInit... glutDisplayFunc(onDisplay); glutReshapeFunc(onResize); glutIdleFunc(onIdle); onInit(); glutMainLoop(); return 0; } void onIdle() { // idő mérés int now = glutGet(GLUT_ELAPSED_TIME); float dt = (now - lastTime) / 1000.0f; lastTime = now; // labda mozgatás position += dt * velocity; // ha a labda kiért a képernyőből // ... glutPostRedisplay(); }
A glutGet(GLUT_ELAPSED_TIME)
kérdezi le a program indítása óta eltelt időt ezredmásodpercben. Ebből kivonjuk az utolsó lekérdezés idejét (lastTime), majd osztjuk 1000-el, így megkapjuk az előző onIdle()
hívás óta eltelt időt másodpercben. Végül a lastTime változót frissítjük a mostani idővel.
Az eltelt idő, és a labda sebessége alapján frissítjük a labda pozícióját, majd megkérjük a rendszert, hogy frissítse az ablak tartalmát (glutPostRedisplay()
). Így az alkalmazás eseménysorába bekerül az ablak újrarajzolását kiváltó esemény, és meghívódik az onDisplay()
függvény. Közvetlenül is meg lehetne hívni az onDisplay()
-t a glutPostRedisplay()
helyett, de ez problémákhoz vezethet, ha egy bonyolult grafikánál az onDisplay()
sokáig futna.
Azonban egy idő után a labda kiér a képernyőből. Ilyenkor a labdát vissza kellene rakni a képernyő közepére, majd egy véletlenszerű irányba elindítani.
// ha a labda kiért a képernyőből if (position.x <= 0.0f || position.x >= windowWidth || position.y <= 0.0f || position.y >= windowHeight) { position.x = windowWidth / 2.0f; position.y = windowHeight / 2.0f; float length = 200.0f * randomFloat(0.5f, 1.0f); float angle = 2.0f * M_PI * randomFloat(); velocity.x = length * cosf(angle); velocity.y = length * sinf(angle); }
A randomFloat()
függvény egy minimum és egy maximum érték között visszaad egy véletlen számot.
float randomFloat(float minValue = 0.0f, float maxValue = 1.0f) { float randomBetween0and1 = static_cast<float>(rand()) / static_cast<float>(RAND_MAX); return minValue + randomBetween0and1 * (maxValue - minValue); } #define _USE_MATH_DEFINES #include <cmath> #include <cstdlib> #include <ctime> #include <GL/glut.h> void onInit() { srand(time(NULL)); glClearColor(0.1f, 0.2f, 0.3f, 1.0f); glShadeModel(GL_SMOOTH); glEnable(GL_DEPTH_TEST); lastTime = glutGet(GLUT_ELAPSED_TIME); }
Az onInit()-
ben a véletlenszám generátor inicializálása, és az OpenGL működésének beállítása mellett még beállítjuk az eltelt idő számításánál használt lastTime változó kezdő értékét is.
Végül már csak a labdát kell a megfelelő helyre rajzolni, illetve az ablak átméretezésekor az ablak méreteit eltárolni a windowWidth, windowHeight változókban, hogy a következő labda indításkor is középről induljon a labda.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // kör rajzolás glLoadIdentity(); glTranslatef(position.x, position.y, 0.0f); drawCircle(50.0f, 64); // négyszög rajzolás //... glFinish(); } void onResize(int width, int height) { windowWidth = static_cast<float>(width); windowHeight = static_cast<float>(height); glViewport(0, 0, width, height); // viewport állítás // ... }
Így elértük, hogy mozogjon a képernyőn a labda. Ebből egy mozzanat látható az alábbi képernyőképen.
int main(int argc, char* argv[]) { glutInit(&argc, argv); glutInitWindowSize(640, 480); glutInitWindowPosition(0, 0); glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE); glutCreateWindow(”Hello Circle”); //... } void onDisplay() { //... glutSwapBuffers(); // glFinish helyett }
Az alábbi CLI példa perspektívában forgó, csúszkával nagyítható kockát rajzol. A
static GLfloat z=30; static GLfloat aspect=1; static GLfloat szog=0; int meret;
globális változók a zoom érték (z) az arány (aspect) és a szög (szog), illetve az ablakméret (meret). Az átméretezés esemény beállítja a méret értékei.
private: System::Void Form1_Resize(System::Object^ sender, System::EventArgs^ e) { meret=min(this->Width,this->Height); glViewport((this->Width-meret)/ 2, (this->Height-meret) / 2 , meret, meret); Invalidate(); Refresh(); }
Az időzítő forgat:
private: System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e) { szog+=5; Invalidate(); Refresh(); }
A csúszkával a nagyítás állítható:
private: System::Void Zoom_Scroll(System::Object^ sender, System::Windows::Forms::ScrollEventArgs^ e) { z=Zoom->Value; Refresh(); }
Végül minden kirajzolást a Paint esemény végez:
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(z, 1, 1.0, 20.0); meret=min(this->Width,this->Height); glViewport((this->Width-meret) / 2, (this->Height-meret) / 2 , meret, meret); glClearColor(0.5,0.5,0.5,1); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, -2.0, -15.0); glRotated(szog,0,1,0); glColor3f(1.0, 0.0, 0.0); glBegin(GL_POLYGON); glVertex3f(1.0, 1.0, 1.0); glVertex3f(-1.0, 1.0, 1.0); glVertex3f(-1.0, -1.0, 1.0); glVertex3f(1.0, -1.0, 1.0); glEnd(); glColor3f(0, 1.0, 0.0); glBegin(GL_POLYGON); glVertex3f(1.0, 1.0, 3.0); glVertex3f(-1.0, 1.0, 3.0); glVertex3f(-1.0, -1.0, 3.0); glVertex3f(1.0, -1.0, 3.0); glEnd(); glColor3f(0, 0.0, 1.0); glBegin(GL_POLYGON); glVertex3f(1.0, 1.0, 1.0); glVertex3f(1.0, 1.0, 3.0); glVertex3f(1.0, -1.0, 3.0); glVertex3f(1.0, -1.0, 1.0); glEnd(); glColor3f(1.0, 1.0, 0.0); glBegin(GL_POLYGON); glVertex3f(-1.0, 1.0, 1.0); glVertex3f(-1.0, 1.0, 3.0); glVertex3f(-1.0, -1.0, 3.0); glVertex3f(-1.0, -1.0, 1.0); glEnd(); glColor3f(1.0, 0.0, 1.0); glBegin(GL_POLYGON); glVertex3f(-1.0, 1.0, 1.0); glVertex3f(-1.0, 1.0, 3.0); glVertex3f(1.0, 1.0, 3.0); glVertex3f(1.0, 1.0, 1.0); glEnd; glColor3f(0.0, 1.0, 1.0); glBegin(GL_POLYGON); glVertex3f(-1.0, -1.0, 1.0); glVertex3f(-1.0, -1.0, 3.0); glVertex3f(1.0, -1.0, 3.0); glVertex3f(1.0, -1.0, 1.0); glEnd(); glFlush(); }
A segédkönyvtár lehetővé teszi, hogy olyan egyszerű térbeli felületeket hozzunk létre, mint a gömb, a tárcsa, a henger.
A létrehozáshoz deklarálni kell egy (GLUquadricObj
típusra mutató) felületobjektum-pointert:
GLUquadricObj *quadObj;
Ezek után létrehozhatjuk az új felületobjektumot a
quadObj = gluNewQuadric ();
hívással. A felületeket az
|
(4.3) |
egyenlet definiálja.
Az alapállapotban kifestett módon megjelenő felületek színét a már ismert színbeállítással lehet megadni. A térbeli felületeket a rendszer poligonokkal közelíti, ezért az elemek létrehozásakor meg kell adnunk a felosztások számát két független felületi irányban.
Középen lyukas lemezt hozhatunk létre az X-Y síkban az origó körül a
void gluDisk(GLUquadricObj *qobj, GLdouble innerRadius, GLdouble outerRadius, GLint slices, GLint loops );
függvénnyel. A függvény paraméterei a létrehozott felületobjektum (*qobj), a belső- (innerRadius) és a külső sugár (outerRadius). Megadhatjuk azt is, hogy a lemez hány cikkből (slices) és hány gyűrűből (loops) álljon.
A
void gluPartialDisk(GLUquadricObj *qobj, GLdouble innerRadius, GLdouble outerRadius, GLint slices, GLint loops, GLdouble startAngle, GLdouble sweepAngle);
egy fokokban megadott kezdőszög (startAngle) és a középponti szög (sweepAngle) paraméterrel jellemzett lemezcikket hoz létre.
Gömbfelületet készíthetünk az origó körül a
void gluSphere(GLUquadricObj *qobj, GLdouble radius, GLint slices, GLint stacks);
függvénnyel. A radius a sugarat a slices és a stacks a szélességi és hosszúsági körök számát határozza meg.
A
void gluCylinder(GLUquadricObj *qobj, GLdouble baseRadius, GLdouble topRadius, GLdouble height, GLint slices, GLint stacks);
függvény nevével ellentétben csonkapúpot (felfogható általánosított hengerfelületnek) készít. Az alapkör középpontja az origó, sugara a baseRadius. A +Z irányú kúp magassága height, a fedőlap sugara pedig topRadius. A slices és a stacks a felosztások száma.
Ha már nincs szükségünk a létrehozott objektumra megszüntethetjük a
void gluDeleteQuadric(quadObj);
függvény hívásával, melynek argumentuma az objektumra mutató pointer.
Gondoskodhatunk a térbeli objektumok megjelenítéséről is a felületobjektumok tulajdonságainak beállításával.
Az első és hátsó oldal szempontjából a sokszögekhez hasonló a helyzet a felületi elemek esetében. Az alábbi függvénnyel beállíthatjuk, hogy a felületi objektumnak melyik legyen az első felülete:
void gluQuadricOrientation (GLUquadricObj *qobj, GLenum orientation);
A paraméterként megadott orientation lehetséges értékei GLU_OUTSIDE
és GLU_INSIDE
lehetnek. A különböző felületi elemek esetén persze másképpen értelmezhető a külső és belső oldal.
A felületi elemeket határoló sokszögek kifestési módját a
void gluQuadricDrawStyle(GLUquadricObj *qobj, GLenum drawStyle);
függvénnyel állíthatjuk be. A gobj által mutatott elemre vonatkozó beállítások a (a drawstyle lehetséges értékei GLU_FILL
- a kifestett poligonok az irányítottságot meghatározó normálvektor szerint kerülnek a képre, GLU_LINE
- a felületek foltjaik határvonalával jelennek meg, GLU_SILHOUETTE
- a felületfoltok határvonalai jelennek meg, ha a kapcsolódó felületfoltok nem párhuzamosak, GLU_POINT
- a felület csúcspontokkal jelenik meg.
Poligonoknál is használhatunk kifestési mintázatot, amelyet a glEnable(GL_POLYGON_STIPPLE);
hívással engedélyezünk, és a
void glPolygonStipple(const GLubyte *mask);
függvénnyel definiálunk. A mask paraméter egy 32*32-bites kétszínű bitkép.
Ha az előző fejezet végén a forgó kocka rajz Paint eseményében a kocka helyett gömböt, hengert, kúpot és poligont rajzolunk, kipróbálhatjuk a glu objektumok rajzolását is (4.29. ábra).
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(z, 1, 1.0, 20.0); meret=min(this->Width,this->Height); glViewport((this->Width-meret) / 2, (this->Height-meret) / 2 , meret, meret); glClearColor(0.5,0.5,0.5,1); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, -1.0, -15.0); glRotated(szog,0,1,0); GLUquadricObj *quadObj_gomb; quadObj_gomb = gluNewQuadric (); glColor3f(1,0,0); gluQuadricDrawStyle (quadObj_gomb, GLU_LINE); gluSphere (quadObj_gomb, 1.5, 16, 16); gluDeleteQuadric(quadObj_gomb); glColor3f(0,1,0); GLUquadricObj *quadObj_henger; quadObj_henger = gluNewQuadric (); gluQuadricDrawStyle (quadObj_henger, GLU_FILL); gluCylinder(quadObj_henger, 0.3, 0.0, 0.6, 15, 10); gluDeleteQuadric(quadObj_henger); glColor3f(0,0,1); glPushMatrix (); glRotatef ((GLfloat)90.0, (GLfloat)1.0, (GLfloat)0.0, (GLfloat)0.0); glTranslatef ((GLfloat)0.0, (GLfloat)0.0, (GLfloat)-1.0); GLUquadricObj *quadObj_kup; quadObj_kup = gluNewQuadric (); gluQuadricDrawStyle (quadObj_kup, GLU_FILL); gluCylinder (quadObj_kup, 0.3, 0.3, 0.6, 12, 2); gluDeleteQuadric(quadObj_kup); glPopMatrix (); glPolygonMode(GL_FRONT_AND_BACK , GL_FILL); glColor3f(1,1,0); glBegin(GL_POLYGON); glVertex3f(0.5, 0.5, 1.0); glVertex3f(-0.5, 0.5, 1.0); glVertex3f(-0.5, -0.5, 1.0); glVertex3f(0.5, -0.5, 1.0); glEnd(); glFlush(); SwapBuffers(m_hDC); }
Többféle görbét és felületet is használhatunk.
Az OpenGL a tartópontok által meghatározott Bezier-görbék és -felületek kiszámolt pontjainak használatát is lehetővé teszi.
A 3.3.5. szakasz fejezetben megismertek alapján tetszőleges [u 1 , u 2 ] intervallumra könnyen felírható a Bezier-görbe. Az OpenGL az alábbi függvényeket kínálja az interpoláció végzésére:
void glMap1d(GLenum target, GLdouble u1, GLdouble u2, GLint stride, GLint order, const GLdouble *points); void glMap1f(GLenum target, GLfloat u1, GLfloat u2, GLint stride, GLint order, const GLfloat *points);
A függvények target paramétere (GL_MAP1_VERTEX_3
, GL_MAP1_VERTEX_4
) szabályozza, hogy 3 vagy négy dimenzióban dolgozunk. Az u
1
, u
2
a paramétertartományt jelöli ki. A stride paraméter az egy ponthoz tartozó valós adatok számát jelzi, az order pedig a közelítő polinom fokszámát rögzíti. A pont adatokat a points mutató által jelzett tömbben kell elhelyezni.
Az így definiált görbe pontjait használhatjuk kirajzolásra a glBegin(GL_LINRSTRIP)
és a glEnd()
hívások között megadott pontokkal. Azonban a pontokat nem a glVertex()
függvények, hanem a glEvalCoords1
típus()
függvényekkel kell meghatározni.
A kijelölt interpoláció alapján a görbe tetszőleges u∈[u
1
,u
2
] paraméterhez tartozó pontját a (glEnable(GL_MAP1_VERTEX_3),
vagy a glEnable(GL_MAP1_VERTEX_4)
) vagy akár a normálisát (glEnable(GL_MAP1_NORMAL)
) a
void glEvalCoord1d (GLdouble u); void glEvalCoord1f (GLfloat u);
függvényekkel számíthatjuk.
Az alábbi példában a GLUT onDisplay()
függvénye egy síkbeli Bezier görbét és tartópontjait rajzolja.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT ); GLfloat ctrlpoints[4][3] = {{ -0.8, -0.8, 0.0}, { -0.4, 0.8, 0.0}, {0.4, -0.8, 0.0}, {0.8, 0.8, 0.0}}; glMap1f(GL_MAP1_VERTEX_3, 0.0, 1.0, 3, 4, (GLfloat*)&ctrlpoints[0][0]); glEnable(GL_MAP1_VERTEX_3); glColor3f(1.0, 0.0, 0.0); glLineWidth(2.0); glBegin(GL_LINE_STRIP); for (int i = 0; i <= 30; i++) glEvalCoord1f((GLfloat) i/30.0); glEnd(); glPointSize(4); glColor3f(0.0, 1.0, 0.0); glBegin(GL_POINTS); for (int i = 0; i < 4; i++) glVertex3fv((GLfloat*)&ctrlpoints[i][0]); glEnd(); glFinish(); }
A görbékhez hasonlóan járhatunk el Bezier-felületek esetén.
A két paraméteren történő interpolációhoz az alábbi függvényeket használhatjuk:
void glMap2d(GLenum target, GLdouble u1, GLdouble u2, GLint ustride, GLint uorder, GLdouble v1, GLdouble v2, GLint vstride, GLint vorder, const GLdouble *points); void glMap2f(GLenum target, GLfloat u1, GLfloat u2, GLint ustride, GLint uorder, GLfloat v1, GLfloat v2, GLint vstride, GLint vorder, const GLfloat *points);
Hasonlóan a görbékhez, attól függően, hogy a glEnable()
függvény paramétereként a GL_MAP2_VERTEX_3
, GL_MAP2_VERTEX_4
vagy a GL_MAP2_NORMAL
értéket adjuk meg a
void WINAPI glEvalCoord2f( GLfloat u, GLfloat v); void WINAPI glEvalCoord2d( GLdouble u, GLdouble v);
függvényeket használhatjuk kirajzoláskor a felületi görbék csúcspontjai helyett.
Ha 4.6.6. szakasz fejezetben látható (quadric elemeket forgató) példa Paint
eseményét az alábbira cseréljük, akkor Bezier-felületek paramétervonalait rajzoljuk ki.
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(z, 1, 1.0, 20.0); meret=min(this->Width,this->Height); glViewport((this->Width-meret) / 2, (this->Height-meret) / 2 , meret, meret); glClearColor(0.5,0.5,0.5,1); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, -1.0, -15.0); glRotated(szog,0,1,0); GLfloat ctrlpoints[4][4][3] = {{{-1.5, -1.5, 4.0}, {-0.5, -1.5, 2.0}, {0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}}, {{-1.5, -0.5, 1.0}, {-0.5, -0.5, 3.0}, {0.5, -0.5, 0.0}, {1.5, -0.5, 1.0}}, {{-1.5, 0.5, 4.0}, {-0.5, 0.5, 0.0}, {0.5, 0.5, 3.0}, {1.5, 0.5, 4.0}}, {{-1.5, 1.5, 2.0}, {-0.5, 1.5, 2.0}, {0.5, 1.5, 0.0}, {1.5, 1.5, 1.0}} }; glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4,0, 1, 12, 4, (GLfloat*)&ctrlpoints[0][0][0]); glEnable(GL_MAP2_VERTEX_3); glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0, 0.0, 0.0); glLineWidth(2); for (int j = 0; j <= 8; j++) { glBegin(GL_LINE_STRIP); for (int i = 0; i <= 30; i++) glEvalCoord2f((GLfloat)i/30.0, (GLfloat)j/8.0); glEnd(); glBegin(GL_LINE_STRIP); for (int i = 0; i <= 30; i++) glEvalCoord2f((GLfloat)j/8.0, (GLfloat)i/30.0); glEnd(); } glFlush(); SwapBuffers(m_hDC); }
A 3.3.8.3. szakasz és 3.3.8.4. szakasz fejezetben megismert B-spline és NURBS görbék és felületek is megjeleníthetők.
A segédprogramok könyvtára a NURBS-görbék és -felületek létrehozását egyaránt támogatja. A Bezier-elemekkel ellentétben görbe-, vagy felületobjektum létrehozásakor nem kell törődnünk az interpolált pontok alapján történő megjelenítéssel. A NURBS-objektumot a
GLUnurbsObj* gluNewNurbsRenderer (void);
függvény hívásával hozhatunk létre. Ha már nincs szükség rá, az objektumot az alábbi függvénnyel törölhetjük:
void gluDeleteNurbsRenderer (GLUnurbsObj *nobj);
Ha görbét szeretnénk létrehozni, annak adatait a
void gluBeginCurve( GLUnurbs *nobj);
és a
void gluEndCurve( GLUnurbs *nobj);
a felület adatait pedig a
void gluBeginSurface( GLUnurbs *nobj);
és a
void gluEndSurface( GLUnurbs *nobj);
függvények hívásai között kell megadnunk.
NURBS-görbe adatainak megadása a
void gluNurbsCurve (GLUnurbsObj *nobj, GLint nknots, GLfloat *knot, GLint stride, GLfloat *ctlarray, GLint order, GLenum type);
függvénnyel történik. A paraméterek közül az nobj az objektumot azonosítja. NURBS-elemek esetén a nem egységes paraméterezést úgy valósíthatjuk meg, hogy a paraméterintervallumon a tartópontok számához a közelítés fokát hozzáadjuk, és ennek megfelelő számban nem csökkenő értékű csomópontsort hozunk létre. A knots a csomópontok számát tartalmazza a knot pedig a csomóponttömbre mutat. A stride az egy vezérlőponthoz tartozó adatok száma, míg a ctrlarray a tartópontok koordinátáit tartalmazó tömbre mutat. Az order a közelítés fokszáma +1, a type pedig a már ismert GL_MAP1_VERTEX_3
és GL_MAP1_VERTEX_4
értékek közül valamelyik.
Mivel a NURBS-felület kétparaméteres, ezért mindkét paraméterirányba meg kell adni az adatokat:
void gluNurbsSurface (GLUnurbsObj *nobj, GLint uknot_count, GLfloat *uknot, GLint vknot_count, GLfloat *vknot, GLint u_stride, GLint v_stride, GLfloat *ctlarray, GLint uorder, GLint vorder, GLenum type);
a type pedig a már ismert GL_MAP2_VERTEX_3
és GL_MAP2_VERTEX_4
értékek közül valamelyik.
A NURBS-felületeknek nem kell négyzet topológiával rendelkezniük, levághatjuk a széleket a gluBegintTrim()
és a gluEndTrim()
függvények hívása között megadott zárt görbével. A gluNurbsCurve()
függvény első (nobj) paramétere a létrehozott NURBS-objektumra mutató pointer A vágáshoz a gluPwlCurve()
függvényt használhatjuk, amely a paramétertér pontjaival definiálja a vágást. Ilyen esetben csak a GLU_MAP1_TRIM_2
, és a GLU_MAP1_TRIM_3
használható a type paraméterben.
A
void gluNurbsProperty( GLUnurbs *nobj, GLenum property, GLfloat value);
függvény a NURBS megjelenítési módját szabályozza. A nobj paraméter a NURBS-t azonosítja, a property a megjelenítési beállítást tartalmazza, ha GLU_DISPLAY_MODE
, akkor a value - GLU_FILL
–kitöltés, GLU_OUTLINE_PATCH
körvonal, GLU_OUTLINE_POLYGON
– megjelenítő poligonok körvonala. Ha például a property GLU_U_STEP
, vagy GLU_V_STEP
, akkor a value a felosztást szabályozza (100 az alapérték).
A glu objektumok felületi normálisainak beállításával a megjelenítés minősége állítható. A
void gluQuadricNormals(GLUquadric *quadObject,GLenum normals);
függvénnyel a megadott objektumra (quadObject) beállíthatjuk az felületi normálisok megadásának módját. A normals paraméter értékei (GLU_NONE
nincs normális megadva, GLU_FLAT
-felületdarabonként egy normális, GLU_SMOOTH
– minden vertexnél egy normális).
Az alábbi példában NURBS felületként készítünk Bezier felületet. A 4.6.2. szakasz fejezet példájában csak a Paint
eseményt cseréljük.
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(z, 1, 1.0, 20.0); meret=min(this->Width,this->Height); glViewport((this->Width-meret) / 2, (this->Height-meret) / 2 , meret, meret); glClearColor(0.5,0.5,0.5,1); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, -1.0, -15.0); glRotated(szog,0,1,0); GLfloat ctrlpoints[4][4][3] = {{{-1.5, -1.5, 4.0}, {-0.5, -1.5, 2.0}, {0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}}, {{-1.5, -0.5, 1.0}, {-0.5, -0.5, 3.0}, {0.5, -0.5, 0.0}, {1.5, -0.5, 1.0}}, {{-1.5, 0.5, 4.0}, {-0.5, 0.5, 0.0}, {0.5, 0.5, 3.0}, {1.5, 0.5, 4.0}}, {{-1.5, 1.5, 2.0}, {-0.5, 1.5, 2.0}, {0.5, 1.5, 0.0}, {1.5, 1.5, 1.0}} }; GLfloat csp[8]={0.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0}; glColor3f(1,1,0); GLUnurbsObj * nurbs; gluNurbsProperty (nurbs, GLU_DISPLAY_MODE,GLU_OUTLINE_POLYGON); nurbs=gluNewNurbsRenderer(); gluBeginSurface(nurbs); gluNurbsSurface(nurbs, // az objektum 8,csp, // az u-irányú csomópontok 8,csp, // az v-irányú csomópontok 4*3, // két u-ir. szomszédos pont // távolsága a tömbben 3, // két v-ir. szomszédos pont // távolsága a tömbben (GLfloat*)&ctrlpoints[0][0][0], // a pontokat tároló tömb 4,4, // a spline fokszám+1 u,v irányban GL_MAP2_VERTEX_3 // háromdimenziós csúcspontok ); gluEndSurface(nurbs); glFlush(); SwapBuffers(m_hDC); }
A fejezet elején, a megjelenítési cső tárgyalásakor láttuk, hogy objektumokból listát készíthetünk, amelyet aztán egyetlen objektumként jeleníthetünk meg. A lista létrehozása a
void glNewList (GLuint list, GLenum mode);
függvény hívásával történik. Az (egész típusú) list paraméter egyértelműen azonosítja a listát. A mode paraméter meghatározza, hogy a hívást követően a listaelemek (primitívek) csak hívásukkal (GL_COMPILE
), vagy végrehajtva (kirajzolva) kerüljenek a listába (GL_COMPILE_AND_EXECUTE
).
A listát a
void glEndList(void);
függvény hívásával zárjuk.
A lista „lejátszható” (megjeleníthető), ha a
void glCallList(GLuint list);
függvény egyetlen argumentumaként (list) a lista azonosítóját adjuk meg.
Az alábbi példában egy piros gömböt és egy sárga lemezcikket tartalmazó listát készítünk a form betöltésekor:
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) { hwnd=(HWND)this->Handle.ToInt32(); m_hDC = GetDC(hwnd); if(m_hDC) { MySetPixelFormat(m_hDC); } GLUquadricObj *quadObj; glNewList(1, GL_COMPILE); quadObj = gluNewQuadric (); gluQuadricDrawStyle (quadObj, GLU_LINE); glColor3f(1,0,0); gluSphere (quadObj, .5, 16, 16); glColor3f(1,1,0); gluPartialDisk(quadObj, .3,.6,20,20,90,180); gluDeleteQuadric(quadObj); glEndList(); gluDeleteQuadric(quadObj); }
A Paint
eseményben csak a (4.6.6. szakasz fejezetben látható, a quadric elemeket forgató példának megfelelő) forgatás, a kameraállítás és a lista kirajzolása történik.
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e){ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(z, 1, 1.0, 20.0); meret=min(this->Width,this->Height); glViewport((this->Width-meret) / 2, (this->Height-meret) / 2 , meret, meret); glClearColor(0.5,0.5,0.5,1); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -15.0); glRotated(szog,0,1,0); glCallList(1); glFlush(); SwapBuffers(m_hDC); }
Ha a megjelenítési kapcsolatban az RGBA színmodellt használjuk (PFD_TYPE_RGBA), akkor a felületek áttetsző módon jelenhetnek meg. Az OpenGL úgy készít áttetsző felületet, hogy a pont színének meghatározásakor az adott pixelen már definiált színt is figyelembe veszi.
Az áttetsző megjelenítéshez engedélyeznünk kell a már kirajzolt és a rajz színének keverését a glEnable(GL_BLEND)
hívással. Letilthatjuk a glDisable(GL_BLEND)
hívással.
Azt, hogy a megjelenítendő objektum színe és az adott pixelre már betöltött szín milyen módon keveredik, a
void glBlendFunc( GLenum sfactor, GLenum dfactor);
függvény paramétereivel szabályozhatjuk. Arról van szó, hogy szorzófaktorokat definiálunk színenként a megjelenítendő (forrás - source - sfactor) színkomponensekre (S r ,S g ,S b ,S a ) és a már pixelen lévő (cél - destination - dfactor) színkomponensekre (D r ,D g ,D b ,D a ). Ha a megjelenítendő szín (R s ,G s ,B s ,A s ), és a pixel már kifestett színe (R d ,G d ,B d ,A d ), akkor a végső RGBA színdefiníció az alábbi kifejezés szerint adódik:
RGBA=( R s * S r + R d * D r , G s * S g + G d * D g , B s * S b + B d * D b , A s * S a + A d * D a )
Az S és D faktorok lehetséges értékei:
|
(0,0,0,0) |
|
(1,1,1,1) |
|
(R d ,G d ,B d ,A d ) |
|
(R s ,G s ,B s ,A s ) |
|
(1,1,1,1) - (R d ,G d ,B d ,A d ) |
|
(1,1,1,1) - (R s ,G s ,B s ,A s ) |
|
(A s , A s , A s , A s ) |
|
(1,1,1,1) - (A s , A s , A s , A s ) |
|
(A d , A d , A d , A d ) |
|
(1,1,1,1) - (A d , A d , A d , A d ) |
|
(f, f, f, f ) ahol f=min(A s , 1- A d ) |
Az alábbi
void glAlphaFunc(GLenum func, GLclampf ref );
függvénnyel előírhatjuk, hogy a fenti összevetést az alpha értékek függvényében hogyan használjuk. Az első paraméter az összevetés módját (GL_NEVER
- soha, GL_ALWAYS
– mindig, GL_LESS
– kisebb, GL_GREATER
nagyobb), a második pedig a küszöbértéket szabályozza. Alapértelmezés szerint az összevetés ki van kapcsolva.
Az alábbi példában a forgó kocka színeit áttetszőnek definiáljuk. Az áttetszőséget az alpha csúszkával szabályozhatjuk. Ilyenkor a PIXELFORMATDESCRIPTOR iPixelType
adattagja PFD_TYPE_RGBA
kell, legyen.
private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(z, 1, 1.0, 20.0); meret=min(this->Width,this->Height); glViewport((this->Width-meret) / 2, (this->Height-meret) / 2 , meret, meret); glClearColor(0.5,0.5,0.5,1); glEnable(GL_DEPTH_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, -2.0, -15.0); glRotated(szog,0,1,0); // A színkeverés (alpha) figyelembevétele glEnable (GL_BLEND); // Az RGBA szinmodellben a rajzolt lapok alpha faktorát használjuk glBlendFunc (GL_SRC_ALPHA, GL_ONE); // Az alpha értéket a csúszka poziciója változtatja float alp=(float)alpha->Value/alpha->Maximum; glColor4f(0,0,1,alp); glBegin(GL_POLYGON); glVertex3f(1.0, 1.0, 1.0); glVertex3f(-1.0, 1.0, 1.0); glVertex3f(-1.0, -1.0, 1.0); glVertex3f(1.0, -1.0, 1.0); glEnd(); // Az összes lapra hasonlóan … glFlush(); SwapBuffers(m_hDC); }
A térben elhelyezett felületi objektumokat megvilágíthatjuk, illetve megadhatjuk, hogyan verik vissza a fényt.
Ha a fényforrásokat engedélyezni szeretnénk, akkor a glEnable(GL_LIGHTING)
függvényhívást kell alkalmaznunk.
Legfeljebb 8 fényforrást definiálhatunk GL_LIGHT0
-GL_LIGHT7
-ig sorszámozva, melyek közül a GL_LIGHT0
-nak kitüntetett szerepe van. A fényforrások adatait az alábbi függvényekkel állíthatjuk be:
void glLightf (GLenum light, GLenum pname,GLfloat param ); void glLighti (GLenum light, GLenum pname,GLint param ); void glLightfv (GLenum light,GLenum pname,const GLfloat *params); void glLightiv (GLenum light,GLenum pname,const GLint *params );
Mint ismeretes azokat a függvényeket, amelyek az i típusjelzőt hordozzák nevükben int adatokkal hívjuk, míg az f típusjelző float adatokra utal. Vannak olyan fényforrás beállítások, melyeket több adat is jellemez. Ilyenkor a jellemzők tömbjét használjuk argumentumként, ha a v típusjelzőt hordozó nevű függvényt hívjuk.
A függvények light paramétere a fényforrás sorszámát. a pname paraméter pedig a fényforrás típusát, illetve a fényforrást definiáló adat típusát rögzíti.
|
Négy paraméter a környezeti szórt fény RGBA intenzitását definiálja. Az alapérték (0,0,0,1.0). |
|
Négy paraméter a sugárzó fény RGBA intenzitását adja. Az alapérték a GL_LIGHT0 esetén (1.0, 1.0, 1.0, 1.0), különben (0, 0, 0, 1.0). |
|
A négy paraméter a tükröződő fény RGBA intenzitása. Az alapérték a GL_LIGHT0 esetén (1.0, 1.0,1.0, 1.0), különben (0,0, 0,1.0). |
|
A fényforrás homogén-koordinákban megadott térbeli helye. A négy paraméter alapértéke (0, 0, 1.0, 0). |
|
A fényforrás térkoordinátákban megadott iránya. A három koordináta alapértéke (0, 0, -1.0). |
|
A fényforrás fókuszáltsága, azaz mennyivel csökken a visszaverődés intenzitása a beesési merőlegestől távolodva. A paraméter értéke 0-128 között kell, hogy legyen. Minél nagyobb az érték, annál inkább fókuszált a fény. Az alapérték 0. |
|
A megvilágítás terjedési szöge fokokban. A paraméter alapértéke 180. |
|
A fényforrás távolsággal való intenzítás-csökkenése egy másodfokú polinom reciprokaként változik. f=1/(c+l*d+q*d2) A megadható paraméterek: a konstans (c), a lineáris (l) és a másodfokú tag (q) együtthatója. Az alapértékek (1, 0, 0). |
A megvilágítási modell paramétereit az alábbi egész vagy valós, egyparaméteres vagy paramétervektort alkalmazó függvényekkel is beállíthatjuk:
void glLightModelf (GLenum pname, GLfloat param); void glLightModeli (GLenum pname, GLint param); void glLightModelfv (GLenum pname, const GLfloat *params); void glLightModeliv (GLenum pname, const GLint *params);
A paraméterek értelmezése:
|
A négy paraméter a teljes modell szórt megvilágításának [0,1.0] közti RGBA értékét definiálja. Az alapérték (0.2, 0.2, 0.2, 1.0) |
|
Egyetlen egész, vagy valós paraméter szabályozza, hogyan használja a rendszer ránézési irányt. 0 esetén a ránézési irány a –Z-tengely, egyébként a kamerát és az aktuális pontot összekötő egyenes. Az alapérték 0. |
|
Egyetlen egész, vagy valós paraméter szabályozza, hogy a felületek megvilágításakor egy vagy két oldalt vegyen figyelembe a rendszer. Az alapérték 0. |
Érdemes megjegyezni, hogy akár ködös képet is készíthetünk a glEnable(GL_FOG)
hívással. A glFogi()
, glFogf()
, glFogiv()
és a glFogfv()
függvényeket használhatjuk a köd paramétereinek beállítására.
A felületek megjelenése nemcsak a megvilágítás tulajdonságaitól függ, hanem attól is, milyen „anyagtulajdonságai” vannak a megvilágított felületeknek.
A felületek normálisának fontos szerepe van a felületek oldalainak megkülönböztetésében, illetve a visszaverődés adatainak számításakor. A felületelem definiálásakor - a glBegin()
és a glEnd()
között - az aktuális elem normálvektora beállítható glNormal3típus()
függvényekkel a vektor három koordinátáját megadva, illetve a glNormal3típusv()
függvényekkel, melyek paramétere a normálvektort tartalmazó tömb kezdőcíme.
A megjelenítéskor az anyagtulajdonságokkal rendelkező elemek színe az alábbi tényezőkből számítva keletkezik:
szín=színanyag + megvilágításkörnyezet * megvilágításanyag + fényforrások(l, n, v)
A megjelenítés színe tehát függ az anyag színétől, a környezeti megvilágítástól, és attól, hogyan veri vissza az anyag a fényforrások felől érkező fénysugarakat. A fényforrások hatásában vehetjük figyelembe, hogy azok reflektorszerűek is lehetnek, azaz a megvilágítás iránya (d), a normálvektor (n), és a fényforrást és a pontot összekötő vektor (v), valamint a nézőpontot és a pontot összekötő vektor (l) hogyan határozzák meg a fény visszaverődését. Az alábbi tájékoztató jellegű képletben a vektorok egységvektorok és a „(,)” jelzés a skaláris szorzást jelenti amennyiben a visszaverődés értelmezett.
fényforrások(l, n, v) = intenzításfényforrások * (v,d) * (megvilágításkörnyezet * megvilágításanyag + (v,n)*szórtfényforrások * szórtanyag + (v+l,n)*tükrfényforrások * tükranyag)
Az anyag reflexiós tulajdonságainak beállításához egész és valós paraméterekkel rendelkező függvényeket használhatunk:
void glMateriali (GLenum face, GLenum pname, GLint param); void glMaterialf (GLenum face, GLenum pname, GLfloat param);
A függvények paramétereit az alábbi táblázat segítségével értelmezhetjük:
face |
Megadhatjuk, hogy a felület első ( |
|
pname |
Az anyag reflexiós tulajdonsága: |
|
|
azonos fényesség a felületen, |
|
|
minden irányban szórt fény, |
|
|
a fenti két elem összege, |
|
|
tükröződő anyag, |
|
|
a fényesség nem függ a megvilágítás irányától, |
|
|
világító anyag, |
|
|
a megvilágítás színe 0-s a indexű szín a szórtat és a tükröződőt a 1-s indexű szín definiálja. |
|
param |
A megadott anyagtípus RGBA paraméterei |
Pontosabb beállításokhoz többparaméteres függvényeket használhatunk, ahol a params a beállítási adatokat tartalmazó vektor kezdőcíme.
void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params); void glMaterialiv (GLenum face, GLenum pname, const GLint *params);
Ha a glEnable(GL_COLOR_MATERIAL)
függvényt meghívjuk, akkor az anyagtulajdonságokat a színek határozzák meg. Ilyenkor szín adatai alapján az anyag reflexiós tulajdonságait a
void glColorMaterial (GLenum face, GLenum mode);
függvénnyel állíthatjuk be. A face paraméter lehetséges értékei itt is a felület kérdéses oldalát jelölik. A mode paraméter értéke pedig a GL_EMISSION
, GL_AMBIENT
, GL_DIFFUSE
, GL_SPECULAR
, vagy a GL_AMBIENT_AND_DIFFUSE
értékek egyike lehet (az utolsó az alapértelmezett).
Az alábbi GLUT példa mozgó, tartópontokkal modellezett, árnyalt Bezier felületet mutat.
A mozgatáshoz szükséges adatok:
// ránézés távolsága GLdouble tav; // a tartópontok tömbje GLfloat ctlpoint[4][4][3]; // a NURBS objektum GLUnurbsObj * nurbs;
Az onInit()
függvényben gondoskodunk a felület megvilágításáról és fényvisszaverés tulajdonságairól.
void onInit() { glClearColor(0.1f, 0.2f, 0.3f, 1.0f); // A felület szórt fényre való reakciója GLfloat szort[4] ={0.6,0.6,0.0,1.0}; // A felület tükröződési adatai GLfloat tukros[4] ={1.0,1.0,0.0,1.0}; // A felület fényessége GLfloat fenyes[1] ={100.0}; GLfloat viszony; glClearDepth( 1.0 ); glEnable(GL_DEPTH_TEST); // A felület fényvisszaverési adatai glMaterialfv(GL_FRONT,GL_DIFFUSE,szort); // szórt fény glMaterialfv(GL_FRONT,GL_SPECULAR,tukros); // tükröződés glMaterialfv(GL_FRONT,GL_SHININESS,fenyes); // fényesség // A default megvilágítás glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_DEPTH_TEST); // A felület normálvaktorainak számítása tükröződéshez glEnable(GL_AUTO_NORMAL); glEnable(GL_NORMALIZE); // A NURBS objektum létrehozása nurbs=gluNewNurbsRenderer(); // A mintavétel sűrüsége az arnyaláskor (pixel) gluNurbsProperty(nurbs,GLU_SAMPLING_TOLERANCE,25.0); // A NURB megjelenítése gluNurbsProperty(nurbs,GLU_DISPLAY_MODE,GLU_FILL); }
Az onIdle()
függvény időzít
void onIdle() { // idő mérés int now = glutGet(GLUT_ELAPSED_TIME); float dt = (now - lastTime) / 1000.0f; if (dt>0.1) { lastTime = now; glutPostRedisplay(); } }
A kirajzolást az onDisplay()
függvény végzi.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); int u,v; // A tartópontok véletlen meghatározása for (u=0;u<4;u++) { for (v=0;v<4;v++) { ctlpoint[u][v][0]=2.0*((GLfloat)u-1.5); // x ctlpoint[u][v][1]=2.0*((GLfloat)v-1.5); // y ctlpoint[u][v][2]=3.0*rand()/(float)RAND_MAX; // f(x,y) } } glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glPushMatrix(); glTranslated(0.0, 0.0, -tav); glRotated(-30, 1.0, 0.0, 0.0); glScalef(.5,.5,.5); glPolygonMode(GL_FRONT_AND_BACK , GL_FILL); GLfloat csp[8]={0.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0}; // A NURBS felület létrehozása gluBeginSurface(nurbs); gluNurbsSurface(nurbs, // az objektum 8,csp, // az u irányú csomópontok 8,csp, // az v irányú csomópontok 4*3, // két u-ir. szomszédos pont táv. a tömbben 3, // két v-ir. szomszédos pont táv. a tömbben &ctlpoint[0][0][0], // a pontokat definiáló tömb 4,4, // a spline fokszám+1 u,v irányban GL_MAP2_VERTEX_3 // háromdimenziós csúcspontok ); gluEndSurface(nurbs); glPopMatrix(); glutSwapBuffers(); }
Végül az időzítő duplán pufferelő főprogram.
int main(int argc, char* argv[]) { glutInit(&argc, argv); glutInitWindowSize(640, 480); glutInitWindowPosition(0, 0); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); glutCreateWindow("Árnyalás"); glutIdleFunc(onIdle); onInit(); glutReshapeFunc(onResize); glutDisplayFunc(onDisplay); glutMainLoop(); return 0; }
A
void glShadeModel (GLenum mode);
függvénnyel a rajzolás árnyalási modelljét állíthatjuk be. A GL_SMOOTH
(az alapértelmezett) normálist a vertexekben számítja a rendszer (4.35. ábra/a), vagy GL_FLAT
az egyszerű felületdarabonként csak egy normális használatos. Ha az onInit()
függvényben beállítjuk a
glShadeModel(GL_FLAT);
értéket, akkor a zászló kockássá válik (4.35. ábra/b).
Az anyagminták (textúrák) használatának lehetőségeire vonatkozó ízelítővel zárjuk az OpenGL bemutatkozását célzó fejezetet. Az anyagminták használatának első lépése az anyagminta létrehozása. A textúra lehet egydimenziós, azonban az alapeset a kétdimenziós kép, melyet felületen szeretnénk megjeleníteni. Általában egyetlen képet használunk, és azt feszítjük rá a felületekre. Azonban arra is van lehetőségünk, hogy egyetlen képet több felbontásban is elkészítsünk, és mindig a leggazdaságosabban megjeleníthetőt használjuk (mip-map), ennek részleteivel azonban nem foglalkozunk.
A textúrát egy pontjaival meghatározott kép alapján készíthetjük el a
void glTexImage2D (GLenum target, GLint level, GLint components, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
függvény segítségével. A függvény target paramétere GL_TEXTURE_2D
érték kell legyen. A level paraméter a mip-map-ek számát adja meg, ha csak egy kép van, akkor értéke 0. A components paraméterben a színkomponensek számát definiálhatjuk. A kép méreteit a width, height, keretét a border paraméter tartalmazza. A format paraméter definiálja a használt színmodellt (pl. GL_RGB
, GL_RGBA
stb.). A type paraméter a színadatok típusát tárolja (GL_INT, GL_FLOAT
stb.) és végül a pixels a pontokat definiáló színeket tartalmazó tömb kezdőcíme.
Az alábbi függvények a textúra-kép használatát szabályozzák:
void glTexParameterf(GLenum target, GLenum pname, GLfloat param); void glTexParameteri(GLenum target, GLenum pname, GLint param);
A target paraméter értéke mindkét esetben GL_TEXTURE_2D
kell, legyen. A pname paraméter nevezi meg azt a tulajdonságot, melyet a textúrára vonatkozóan be szeretnénk állítani, a param pedig a beállító értéket tartalmazza. Ha a pname értéke például a GL_TEXTURE_WRAP_S
, vagy GL_TEXTURE_WRAP_T
, akkor a param segítségével a különböző irányokban előírhatjuk, hogy a kép a felületre nyújtva (GL_CLAMP
), vagy eredeti méretben ismétlődésekkel tölti ki a felületet (GL_REPEAT
ez az alapérték). Ha a pname értéke GL_TEXTURE_MIN_FILTER
vagy GL_TEXTURE_MAG_FILTER
, akkor a textúraelem pixelre történő kicsinyítésének, illetve nagyításának módját írhatjuk elő. A param GL_NEAREST
értéke esetén a több leképzett képpont közül a pixelhez legközelebbi pont színe a meghatározó, a GL_LINEAR
(alapérték) esetben pedig a szóba kerülő pontok színe átlagolódik.
A textúra leképezés során azt, hogy a felület színe és a képpontok színe milyen módon kerül összevetésre a glTexEnvtipus()
függvénnyel szabályozhatjuk:
void glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GLfloat param);
Az első két paraméter a megadott konstans kell, legyen. A param paraméter GL_DECAL
beállításkor a kép színe kerül a felületre, mintha matricaként rátennénk, egyébként (GL_MODULATE, GL_BLEND
) a textúra keveredik a háttérszínnel. Az ilyen módon meghatározott textúra megjelenik minden olyan felületen, amely a glEnable(GL_TEXTURE_2D)
beállítással jött létre.
Az alábbi programrészletben egy legfeljebb MERET*MERET nagyságú bitkép adataival töltjük fel a textúrát. A bitképek egyszerű kezelése érdekében – CLI-t használunk.
private: System::Void Form1_Load(System::Object^ sender, System::EventArgs^ e) { hwnd=(HWND)this->Handle.ToInt32(); m_hDC = GetDC(hwnd); if(m_hDC) { MySetPixelFormat(m_hDC); } try { // létrehozunk egy TBitmap objektumot és abba töltjük a képet Bitmap ^ Bm = gcnew Bitmap("C:\\M\\Mogi.png"); BYTE kep[MERET][MERET][3]; // Áttöltjük a bitkép színeit a képafdat tömbbe for (int i=0;i<Bm->Width;i++) for (int j=0;j<Bm->Height;j++) { kep[i][j][0]=Bm->GetPixel(i,j).R; kep[i][j][1]=Bm->GetPixel(i,j).G; kep[i][j][2]=Bm->GetPixel(i,j).B; } // A kétdimenziós mintázat definíciója glTexImage2D(GL_TEXTURE_2D,0, // szintek a nagyításhoz 3, // színkomponensek száma Bm->Height,Bm->Width,// méretek 0, // a keret vastagsága GL_RGB, // színformátum GL_UNSIGNED_BYTE, // színadatok &kep // az adatok tömbje ); // A mindkét irányban a 0,1 paraméterekhez kapcsolódunk glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP); // A pixel textúra elemre való nagyításának, // kicsinyítésének módja glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); // A mintázat matricaként kerül a felületre takarva azt. glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_DECAL); glEnable(GL_TEXTURE_2D); // Használat után letöröljük a bitkép objektumot delete Bm; } catch (...) { } }
A 3D-s objektumok megadása is csúcspont és attribútumaik segítségével történik. Ezeket kiszámolhatjuk kézzel is, mint ahogy eddig tettük, vagy egy 3D modellező programban megrajzolhatjuk az objektumokat, amelyeket betölt a programunk.
Az objektumok betöltéséhez ismerni kell az objektumot leíró fájl formátumát. Ilyen formátum pl. a szöveges Wavefront OBJ (.obj
), a bináris 3D Studio (.3ds
), vagy az XML alapú Collada (.dae
). A fájl betöltéséhez az Open Asset Import Library (Assimp) könyvtárat fogjuk használni. Az Assimp-et C++-ban írták, sok különböző típusú formátumot képes kezelni, amelyekhez egy egységes API-n keresztül lehet hozzáférni.
Gyakran előfordul, hogy egy modell különböző háromszögeinek ugyanaz a csúcspont is a része, ilyenkor felesleges újra eltárolni a vertexet. Ehelyett egy tömbben fogjuk tárolni az összes csúcspontot, és az attribútumokat, és egy külön tömbben tároljuk a háromszögeket alkotó vertexek indexeit (4.37. ábra).
3D grafikánál az árnyaláshoz a pozíciók mellett meg kell adni a csúcsokban lévő normál vektorokat is. A normál vektor egy egység hosszú vektor, amely merőleges a felületre. Definiáljunk két típust: egy 3-elemű vektort, amellyel a pozíciókat és normál vektorokat tudjuk leírni, illetve egy struktúrát amely egy oldal indexeit fogja tartalmazni.
struct vec3 { float x, y, z; vec3(float x, float y, float z) : x(x), y(y), z(z) { } } ; struct Face { unsigned int index[3]; } ;
Ezeket felhasználva készítjük el a példaprogramot! A 3D-s objektumokon is bemutatjuk az eddig megismert ábrázolásokat. Definiáljunk egy 3D-s objektumot leíró osztályt! Ez tartalmazni fogja a megfelelő puffereket, amelyeket tömb helyett a Standard Library-ben lévő std::vector
generikus típusú objektumban fogunk tárolni. Ennek előnye a hagyományos tömbkezeléssel szemben, hogy a memóriát magától fel fogja szabadítani az objektum megszűnésekor. Emellett definiáljunk két metódust, az egyik betölti az objektumot, a másik kirajzolja azt.
#include <vector> #include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> class Mesh { std::vector<vec3> vertexes; std::vector<vec3> normals; std::vector<Face> indexes; public: void load(const char* filename); void render(); } ;
Írjuk meg a load()
metódust, amely betöltet az Assimp-el egy 3D-s fájlt, majd ebből a szükséges attribútumokat átalakítja a saját adatszerkezetünkre. Az Assimp használatához szükség van néhány fejállományra, illetve hozzá kell szerkeszteni az assimp.lib
fájlt a programhoz.
void Mesh::load(const char* filename) { // mesh betöltés Assimp::Importer importer; const aiScene* pScene = importer.ReadFile(filename, aiProcess_Triangulate); if (!pScene || pScene->mNumMeshes == 0) return; // mesh const aiMesh* pMesh = pScene->mMeshes[0]; …
Az Assimp::Importer
osztályon keresztül lehet betölteni egy fájlt, amelyre vissza ad egy const aiScene
pointert. Ez az adatstruktúra tartalmazza többek között az objektumokat (mesh). Emellett tartalmazhatja a színteret is, azaz, hogy az egyes mesh-ek milyen hierarchikus viszonyban vannak egymással, és hol helyezkednek el. A ReadFile()
metódusnak megadtuk az aiProcess_Triangulate
flaget, amivel elérjük, hogy az Assimp
a sok pontból álló poligonokat alakítsa át háromszögekké.
A példában egy .obj
fájlból töltünk be egy teáskannát, és feltételezzük, hogy csak egy mesh lesz a fájlban. Ezután a saját adatszerkezetünkre alakítjuk át az aiMesh
-ben lévő információt.
// memóriafoglalás vertexes.reserve(pMesh->mNumVertices); normals.reserve(pMesh->mNumVertices); indexes.reserve(pMesh->mNumFaces); // vertexek másolása for (unsigned int i = 0; i < pMesh->mNumVertices; ++i) vertexes.push_back(vec3( pMesh->mVertices[i].x, pMesh->mVertices[i].y, pMesh->mVertices[i].z)); // normálisok másolása for (unsigned int i = 0; i < pMesh->mNumVertices; ++i) normals.push_back(vec3( pMesh->mNormals[i].x, pMesh->mNormals[i].y, pMesh->mNormals[i].z)); // indexek másolása for (unsigned int i = 0; i < pMesh->mNumFaces; ++i) { Face face; face.index[0] = pMesh->mFaces[i].mIndices[0]; face.index[1] = pMesh->mFaces[i].mIndices[1]; face.index[2] = pMesh->mFaces[i].mIndices[2]; indexes.push_back(face); } }
Az std::vector
magától nagyobb memóriát foglal, ha betelik az előre lefoglalt terület, azonban ezt elkerülhetjük, mert tudjuk, hogy pontosan hány elem fog belekerülni. Ezután az aiMesh
struktúrából átmásoljuk a számunkra érdekes részeket.
A rendereléskor végiglépkedünk az összes háromszögen, és az indexek alapján kirajzoljuk a megfelelő csúcspontokat.
void Mesh::render() { glBegin(GL_TRIANGLES); for (std::vector<Face>::const_iterator it = indexes.begin(); it != indexes.end(); ++it) { for (int j = 0; j < 3; ++j) { glNormal3f( normals[it->index[j]].x, normals[it->index[j]].y, normals[it->index[j]].z); glVertex3f( vertexes[it->index[j]].x, vertexes[it->index[j]].y, vertexes[it->index[j]].z); } } glEnd(); }
A main()
függvény a szokásos módon néz ki, létrehoz egy ablakot mélység pufferrel, és dupla puffereléssel.
int main(int argc, char* argv[]) { glutInit(&argc, argv); glutInitWindowSize(640, 480); glutInitWindowPosition(0, 0); glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE); glutCreateWindow(”Hello 3D”); glutDisplayFunc(onDisplay); glutReshapeFunc(onResize); glutIdleFunc(onIdle); onInit(); glutMainLoop(); return 0; }
A nemrég megírt Object
osztályból hozzunk létre egy példányt globális változóként, majd az onInit()-
ben töltsük be a modellt. Később forgatni fogjuk a modellt, amihez deklaráljunk még 2 globális változót!
Mesh teapot; float angle = 0.0f; // forgatás mértéke fokokban int lastTime = 0; void onInit() { glClearColor(0.1f, 0.2f, 0.3f, 0.0f); glShadeModel(GL_SMOOTH); glEnable(GL_DEPTH_TEST); lastTime = glutGet(GLUT_ELAPSED_TIME); teapot.load(”teapot.obj”); }
Az onResize()
függvényben kezeljük az ablak átméretezését, és itt beállítunk – az eddigiekkel ellentétben – egy perspektivikus vetítést a már ismert gluPerspective()
függvénnyel.
void onResize(int width, int height) { glViewport(0, 0, width, height); if (height == 0) height = 1; double aspect = static_cast<double>(width) / static_cast<double>(height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(60.0, aspect, 0.1, 100.0); glMatrixMode(GL_MODELVIEW); }
Az onIdle()
függvény a szokásos módon méri az utolsó hívás óta eltelt időt, és frissíti a forgatáshoz használt angle nevű változót.
void onIdle() { // idő mérés int now = glutGet(GLUT_ELAPSED_TIME); float dt = (now - lastTime) / 1000.0f; lastTime = now; angle += 36.0 * dt; glutPostRedisplay(); }
Végül már csak az onDisplay()
-t kell megírni.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt( -0.5, 2.0, 3.0, 0.0, 0.5, 0.0, 0.0, 1.0, 0.0 ); teapot.render(); glutSwapBuffers(); }
Első lépésként töröljük a szín-, és mélységi puffer tartalmát. Ezután egységmátrixot állítunk a modelview mátrixba. Az onResize()
végén a projection mátrix után újra a modelview mátrixot választottuk ki, így ezt módosítjuk.
Ezután beállítunk egy nézeti transzformációt a gluLookAt()
függvénnyel, amelynek meg kell adni a kamera pozícióját (első 3 paraméter), azt, hogy melyik pontba néz (második 3 paraméter), illetve a felfele irányt (harmadik 3 paraméter). A felfele irány megadásával lehet a kamerát az optikai tengelye mentén forgatni. Majd kirajzoljuk az objektumot.
Habár megjelent a betöltött modell, de nem pont ezt vártuk. Igazából jól működik az OpenGL, mert ő csak a megadott háromszögeket rajzolja ki, és mivel nem adtunk meg más színt, ezért minden az alapértelmezett fehér színnel jelenik meg.
Az árnyalás a fényforrás iránya, és a normál vektorok iránya alapján történik. A normál vektorokról azt gondolja az OpenGL, hogy egység hosszúak, azonban a különböző transzformációk hatására ez változhat. Ezért a GL_NORMALIZE
kapcsolóval megkérhetjük az OpenGL-t, hogy az árnyalás előtt normalizálja a normál vektorokat. Ez persze nincs ingyen, de a mai GPU-kon ez gyorsan végbemegy, ezért érdemes bekapcsolni.
void onInit() { glClearColor(0.1f, 0.2f, 0.3f, 0.0f); glShadeModel(GL_SMOOTH); glEnable(GL_DEPTH_TEST); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_NORMALIZE); lastTime = glutGet(GLUT_ELAPSED_TIME); teapot.load(”teapot.obj”); }
Így már azt a képet kapjuk, amelyet az előbb vártunk.
Az onIdle()
-ben folyamatosan számoljuk a forgatási szöget, de eddig még nem használtuk sehol. Az onDisplay()
-ben a gluLookAt()
hívás után a glRotatef()
függvénnyel beállíthatunk egy forgatás transzformációt az objektumra. Az első paramétere a forgatás mértéke fokokban, a többi 3 pedig a tengely, ami körül a forgatás történik, ami most az Y-tengely lesz.
void onDisplay() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt( -0.5, 2.0, 3.0, 0.0, 0.5, 0.0, 0.0, 1.0, 0.0 ); glRotatef(angle, 0.0f, 1.0f, 0.0f); teapot.render(); glutSwapBuffers(); }
Az OpenGL-ben megadott transzformációkat “alulról felfelé” kell megadni a mátrixszorzás sorrendje miatt. Azaz az objektumot először forgatni fogjuk az Y tengely körül, ezután kerül be a kamera koordináta-rendszerbe. Ezt a modelview mátrix tartalmazza. Ezután a kamera koordináta-rendszerből a projection mátrix átalakítja a vertexeket normalizált eszköz koordinátákba, és megtörténik a raszterizálás.
A teáskanna nagyjából tömör, így nem látjuk a hátul lévő háromszögeket. Azonban ezeket is kirajzoljuk, a depth puffer algoritmus azonban megoldja, hogy ezek ne látszódjanak. A képernyőre vetítve egy háromszöget, a vertexek megadási sorrendje meghatároz egy körül járási irányt, amely lehet az óra mutató járásával megegyező, vagy ellentétes. A háromszögeket az óra mutató járásával ellentétes irányba szokás megadni. Így az elöl lévő háromszögek ebben az irányban lesznek, a hátul lévők megfordulnak, és az óramutató járásával megegyező irányban állnak.
A glEnable(GL_CULL_FACE)
hívással engedélyezhetjük, hogy a hátsó lapokat, a körül járási irány alapján, a raszterizálás előtt eldobja a rendszer (culling). A glFrontFace()
függvénnyel lehet megadni, hogy az óramutató járásával ellentétes (GL_CCW
), vagy megegyező irányú (GL_CW
) háromszögeket kezelje elöl lévőként az OpenGL. Az onInit()
-be szúrjuk be a következő sorokat:
glEnable(GL_CULL_FACE); glFrontFace(GL_CCW);
Így ugyanazt látjuk, mint eddig. Ha megfordítjuk a körül járási irányt (glFrontFace(GL_CW)
), akkor az objektum belsejét fogjuk látni.
A hátsó lap eldobás hatását másképp is meg lehet nézni. A glPolygonMode()
hívással be lehet állítani, hogy az elöl és hátul lévő háromszögek hogyan legyenek raszterizálva (pontként - GL_POINT
, élként - GL_LINE
, kitöltéssel - GL_FILL
). Az onInit()
-be szúrjuk be valahova a következő sort, és próbáljuk ki a kódot glEnable(GL_CULL_FACE)
hívással, és nélküle is!
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);