4. fejezet - OpenGL

Tartalom
4.1. A Grafikus csővezeték
4.1.1. Alkalmazás
4.1.2. Vertex Shader
4.1.3. Raszterizálás
4.1.4. Fragment Shader
4.1.5. Raszterműveletek
4.1.6. Framebuffer
4.2. Az OpenGL Használata
4.2.1. Az OpenGL klasszikus inicializálása
4.2.1.1. Az OpenGL eszközkapcsolata
4.2.1.2. A használni kívánt megjelenítési kapcsolat meghatározása
4.2.1.3. A pixelformátum meghatározása
4.2.1.4. A megjelenítési kapcsolat létrehozása, kiválasztása és törlése
4.3. GLUT
4.3.1. GLUT telepítés Windows rendszerben
4.4. Adatok, konstansok és függvények az OpenGL rendszerben
4.5. Első lépések az OpenGL-ben
4.5.1. Színek megadása
4.5.2. Rajzpufferek, a pufferek előkészítése
4.5.3. Kirajzolás hagyományos módon
4.5.4. Hello OpenGL hagyományosan a C++/CLI-vel
4.5.5. Hello OpenGL a glut-tal
4.6. Rajzolás
4.6.1. Alapvető rajzelemek
4.6.2. A geometriai objektumok és megjelenítési módjaik
4.6.2.1. Pontok
4.6.2.2. Vonalak
4.6.2.3. Konvex sokszögek
4.6.2.4. Színek, interpolálás
4.6.2.5. Görbevonalú alakzatok rajzolása, tesszalláció
4.6.3. Transzformációk
4.6.3.1. Modellezési transzformációk
4.6.3.2. Vetítési transzformációk
4.6.3.3. Viewport-transzformációk
4.6.3.4. Rajzolás pixel koordinátákkal
4.6.3.5. Mátrix verem
4.6.4. Takarások kezelése - a Z-puffer
4.6.5. Animáció
4.6.5.1. Dupla pufferelés
4.6.6. A glu segédkönyvtár térbeli alapobjektumai
4.6.6.1. Szabad formájú görbék és felületek
4.6.6.2. Bezier görbék és felületek
4.6.6.3. A glu NURBS görbéi és felületei
4.6.6.4. Listák
4.6.7. Áttetsző anyagok, az RGBA színmodell használata
4.6.8. Megvilágítások
4.6.9. Anyagok
4.6.10. Árnyalások
4.6.11. Textúra
4.6.11.1. A textúra elkészítése
4.7. 3D-s objektumok
4.7.1. 3D-s objektumok megadása
4.7.2. 3D-s objektumok rajzolása
4.7.3. Objektum megvilágítása
4.7.4. Az objektum forgatása
4.7.5. Takart vonalak - hátsó lap eldobás

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.

4.1. A Grafikus csővezeték

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.

Az Open GL megjelenítési csővezeték
4.1. ábra - Az Open GL megjelenítési csővezeték


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.

4.1.1. Alkalmazás

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.

4.1.2. Vertex Shader

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.

4.1.3. Raszterizálás

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).

Háromszög raszterizálás, interpolálás
4.2. ábra - Háromszög raszterizálás, interpolálás


4.1.4. Fragment Shader

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.

4.1.5. Raszterműveletek

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.

4.1.6. Framebuffer

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.

4.2. Az OpenGL Használata

Az Open GL rendszert használhatjuk klasszikus módon, illetve az egy a felhasználást könnyítő könyvtárral.

4.2.1. Az OpenGL klasszikus inicializálása

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.

4.2.1.1. Az OpenGL eszközkapcsolata

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.

4.2.1.2. A használni kívánt megjelenítési kapcsolat meghatározása

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);

4.2.1.3. A pixelformátum meghatározása

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:

PFD_DRAW_TO_WINDOW

Akkor használjuk a konstanst, ha azt szeretnénk, hogy ablakban jelenjen meg a rajz. Alkalmazhatjuk még a PFD_DRAW_TO_BITMAP konstanst is.

PFD_SUPPORT_OPENGL

Az OpenGL lehetőségeit használjuk, egyébként PFD_SUPPORT_GDI.

PFD_DOUBLEBUFFER

Több puffert alkalmazva a képeket cserélgethetjük (pl. animáció).

iPixelType

PFD_TYPE_RGBA

RGBA színmegadás

PFD_TYPE_COLORINDEX

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

PFD_MAIN_PLANE

Rajzolás az alap fóliára.

PFD_OVERLAY_PLANE

Rajzolás a felső fóliára

PFD_UNDERLAY_PLANE

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.

4.2.1.4. A megjelenítési kapcsolat létrehozása, kiválasztása és törlése

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);
}

4.3. GLUT

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.

OpenGL, GLUT, és az alkalmazás helye a szoftveres veremben
4.3. ábra - OpenGL, GLUT, és az alkalmazás helye a szoftveres veremben


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.

4.3.1. GLUT telepítés Windows rendszerben

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

4.4. Adatok, konstansok és függvények az OpenGL rendszerben

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.

4.5. Első lépések az OpenGL-ben

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).

4.5.1. Színek megadása

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 meg­adá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.

4.5.2. Rajzpufferek, a pufferek előkészítése

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.

4.5.3. Kirajzolás hagyományos módon

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.

4.5.4. Hello OpenGL hagyományosan a C++/CLI-vel

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();
}

Hagyományos OpenGL alkalmazás
4.4. ábra - Hagyományos OpenGL alkalmazás


4.5.5. Hello OpenGL a glut-tal

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.

GLUT alkalmazás
4.5. ábra - GLUT alkalmazás


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.

4.6. Rajzolás

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.

A kamera analógia
4.6. ábra - A kamera analógia


4.6.1. Alapvető rajzelemek

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:

GL_POINTS

A vertexek pontokat határoznak meg.

GL_LINES

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.

GL_LINE_STRIP

Minden pár sarokpont egy vonalszakaszt definiál. A szakaszok láncot alkotnak, n pont (n-1) darab szakaszt határoz meg.

GL_LINE_LOOP

A szakaszok zárt láncot alkotnak. Az n pont n szakaszt definiál.

GL_TRIANGLES

Minden ponthármas egy háromszöget határoz meg. A kifestett három­szögek nem alkotnak láncot. Hárommal osztható n pont esetén n/3 három­szög jön létre.

GL_TRIANGLE_STRIP

A megadott pontok egymáshoz oldalaival illeszkedő, kifestett három­szögekből álló láncot definiálnak. Ha n>3, akkor n-2 darab háromszög keletkezik.

GL_TRIANGLE_FAN

Záródó, kifestett háromszöglánc, melynek kezdőpontja közös (n>3 esetén n-2 darab háromszög).

GL_QUADS

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.

GL_QUAD_STRIP

A kifestett négyszögekből alkotott lánc, páros n esetén n/2 elemmel.

GL_POLYGON

A pontok n oldalú, kifestett poligont határoznak meg.

A csúcspontokat (vertex) a glVertexszá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 alakzatok
4.7. ábra - Az alakzatok


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();
}

Az alakzatok
4.8. ábra - Az alakzatok


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.

4.6.2. A geometriai objektumok és megjelenítési módjaik

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.

4.6.2.1. Pontok

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();
}

10 pixeles zöld pont
4.9. ábra - 10 pixeles zöld pont


4.6.2.2. Vonalak

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();
}

Vonaltípusok
4.10. ábra - Vonaltípusok


4.6.2.3. Konvex sokszögek

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();
}

Háromszög láthatatlan éllel
4.11. ábra - Háromszög láthatatlan éllel


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();
}

Mintázott sokszög
4.12. ábra - Mintázott sokszög


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.

4.6.2.4. Színek, interpolálás

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);
}

GL_SMOOTH
4.13. ábra - GL_SMOOTH


GL_FLAT
4.14. ábra - GL_FLAT


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();
}

4.6.2.5. Görbevonalú alakzatok rajzolása, tesszalláció

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.

Kör tesszeláció
4.15. ábra - Kör tesszeláció


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();
}

Körlap
4.16. ábra - Körlap


4.6.3. Transzformációk

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ó).

A képalkotási transzformációk sora
4.17. ábra - A képalkotási transzformációk sora


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.

 

v , _ = M _ _ v e t í t é s M _ _ mod e l l v _

(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.

4.6.3.1. Modellezési transzformációk

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 „meg­cé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.

A gluLookAt() függvény kamera-pozíciója
4.18. ábra - A gluLookAt() függvény kamera-pozíciója


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 forgó téglalap
4.19. ábra - A forgó téglalap


4.6.3.2. Vetítési transzformációk

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 glOrtho() leképezés
4.20. ábra - A glOrtho() leképezés


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_PLANEi) hívással.

A centrális leképezés
4.21. ábra - A centrális leképezés


4.6.3.3. Viewport-transzformációk

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ő.

4.6.3.4. Rajzolás pixel koordinátákkal

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.

Kör pixel koordinátákkal
4.22. ábra - Kör pixel koordinátákkal


4.6.3.5. Mátrix verem

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.

4.6.4. Takarások kezelése - a Z-puffer

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 takarás kirajzolási sorrenddel
4.23. ábra - A takarás kirajzolási sorrenddel


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();
}

Mélység teszt algoritmus
4.24. ábra - Mélység teszt algoritmus


Takarás mélység teszt algoritmussal
4.25. ábra - Takarás mélység teszt algoritmussal


Így az objektumok az elvártnak megfelelően takarják egymást.

4.6.5. Animáció

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.

4.6.5.1. Dupla pufferelés

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).

Dupla pufferelés
4.26. ábra - Dupla pufferelés


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.

Dupla pufferelés
4.27. ábra - Dupla pufferelés


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();
}

Forgó perspektivikus kocka
4.28. ábra - Forgó perspektivikus kocka


4.6.6. A glu segédkönyvtár térbeli alapobjektumai

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

 

a 1 x   2 +   a 2 y 2   +   a 3 z 2   + a 4 x y   +   a 5 y x   +   a 6 x z   + a 7 x   +   a 8 y   +   a 9 z   +   a 10   =   0

(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);
}

A glu elemek
4.29. ábra - A glu elemek


4.6.6.1. Szabad formájú görbék és felületek

Többféle görbét és felületet is használhatunk.

4.6.6.2. Bezier görbék és felületek

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 glEvalCoords1tí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 Bezier görbe
4.30. ábra - A Bezier görbe


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 Bezier felület
4.31. ábra - A Bezier felület


4.6.6.3. A glu NURBS görbéi és felületei

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 Bezier felület
4.32. ábra - A Bezier felület


4.6.6.4. Listák

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);
}

A Bezier felület
4.33. ábra - A Bezier felület


4.6.7. Áttetsző anyagok, az RGBA színmodell használata

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:

GL_ZERO

(0,0,0,0)

GL_ONE

(1,1,1,1)

GL_DST_COLOR

(R d ,G d ,B d ,A d )

GL_SRC_COLOR

(R s ,G s ,B s ,A s )

GL_ONE_MINUS_DST_COLOR

(1,1,1,1) - (R d ,G d ,B d ,A d )

GL_ONE_MINUS_SRC_COLOR

(1,1,1,1) - (R s ,G s ,B s ,A s )

GL_SRC_ALPHA

(A s , A s , A s , A s )

GL_ONE_MINUS_SRC_ALPHA

(1,1,1,1) - (A s , A s , A s , A s )

GL_DST_ALPHA

(A d , A d , A d , A d )

GL_ONE_MINUS_DST_ALPHA

(1,1,1,1) - (A d , A d , A d , A d )

GL_SRC_ALPHA_SATURATE

(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);
}

RGBA színek
4.34. ábra - RGBA színek


4.6.8. Megvilágítások

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.

GL_AMBIENT

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).

GL_DIFFUSE

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).

GL_SPECULAR

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).

GL_POSITION

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).

GL_SPOT_DIRECTION

A fényforrás térkoordinátákban megadott iránya. A három koordináta alap­értéke (0, 0, -1.0).

GL_SPOT_EXPONENT

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.

GL_SPOT_CUTOFF

A megvilágítás terjedési szöge fokokban. A paraméter alapértéke 180.

GL_CONSTANT_ATTENUATION

GL_LINEAR_ATTENUATION

GL_QUADRATIC_ATTENUATION

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:

GL_LIGHT_MODEL_AMBIENT

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)

GL_LIGHT_MODEL_LOCAL_VIEWER

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.

GL_LIGHT_MODEL_TWO_SIDE

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.

4.6.9. Anyagok

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ő (GL_FRONT), hátsó (GL_BACK), vagy mindkét oldalára (GL_FRONT_AND_BACK) érvényes-e a beállítás.

pname

Az anyag reflexiós tulajdonsága:

 

GL_AMBIENT

azonos fényesség a felületen,

GL_DIFFUSE

minden irányban szórt fény,

GL_ AMBIENT_AND_DIFFUSE

a fenti két elem összege,

GL_SPECULAR

tükröződő anyag,

GL_SHINESS

a fényesség nem függ a megvilágítás irányától,

GL_EMISSION

világító anyag,

GL_COLOR_INDEXES

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;
}

Árnyalt zászló
4.35. ábra - Árnyalt zászló


4.6.10. Árnyalások

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).

4.6.11. Textúra

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.

4.6.11.1. A textúra elkészítése

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 (...) {
}
}

Textúra
4.36. ábra - Textúra


4.7. 3D-s objektumok

4.7.1. 3D-s objektumok megadása

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).

Vertex és index puffer
4.37. ábra - Vertex és index puffer


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();
}

4.7.2. 3D-s objektumok rajzolása

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.

3D-s objektum
4.38. ábra - 3D-s objektum


4.7.3. Objektum megvilágítása

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.

A megvilágított 3D-s objektum
4.39. ábra - A megvilágított 3D-s objektum


4.7.4. Az objektum forgatása

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 megvilágított 3D-s objektum
4.40. ábra - A megvilágított 3D-s objektum


4.7.5. Takart vonalak - hátsó lap eldobá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);

glDisable(GL_CULL_FACE)
4.41. ábra - glDisable(GL_CULL_FACE)


glEnable(GL_CULL_FACE)
4.42. ábra - glEnable(GL_CULL_FACE)