Lekcja 18. Kwadryki
Autor: Rafał 'Revo' Kozik
Oryginał: Quadrics (GB Schmick)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson18.zip

Dzięki kwadrykom można w prosty sposób rysować złożone obiekty. Sposoby tradycyjne przeważnie zajmują kilka pętli FOR i wymagają stosownej podstawy trygonometrycznej.

Będziemy używać kodu z lekcji siódmej. Dodamy 7 zmiennych i zmodyfikujemy teksturę, żeby było ciekawiej.

#include <windows.h>         // Plik nagłówkowy Windows
#include <stdio.h>         // Plik nagłówkowy standardowego wejścia/wyjścia
#include <gl\gl.h>         // Plik nagłówkowy biblioteki OpenGL32
#include <gl\glu.h>         // Plik nagłówkowy biblioteki GLu32
#include <gl\glaux.h>         // Plik nagłówkowy biblioteki GLaux
HDC        hDC=NULL;         // Prywatny kontekst urządzenia GDI
HGLRC        hRC=NULL;         // Stały kontekst renderowania
HWND        hWnd=NULL;         // Przechowuje uchwyt okna
HINSTANCE    hInstance;         // Przechowuje instancję aplikacji
bool    keys[256];         // Tablica używana do obsługi klawiatury
bool    active=TRUE;         // Znacznik aktywności okna
bool    fullscreen=TRUE;         // Znacznik trybu pełnoekranowego
bool    light;         // Oświetlenie włączone/wyłączone
bool    lp;         // Klawisz L wciśnięty?
bool    fp;         // Klawisz F wciśnięty?
bool sp;         // Klawisz spacji wciśnięty? ( NOWE )
int    part1;         // Początek dysku ( NOWE )
int    part2;         // Koniec dysku ( NOWE )
int    p1=0;         // Przyrost 1 ( NOWE )
int    p2=1;         // Przyrost 2 ( NOWE )
GLfloat    xrot;         // Kąt obrotu wokół osi X
GLfloat    yrot;         // Kąt obrotu wokół osi Y
GLfloat xspeed;         // Prędkość obrotu wokół osi X
GLfloat yspeed;         // Prędkość obrotu wokół osi Y
GLfloat    z=-5.0f;         // Głębokość względem ekranu
GLUquadricObj *quadratic;         // Tak będziemy przechowywać nasze Kwadryki ( NOWE )
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f };         // Wartości oświetlenia otoczenia
GLfloat LightDiffuse[]=     { 1.0f, 1.0f, 1.0f, 1.0f };         // Wartości światła rozproszonego
GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f };         // Pozycja światła
GLuint    filter;         // Którego filtru użyć
GLuint    texture[3];         // Miejsce na trzy tekstury
GLuint object=0;         // Który obiekt rysować ( NOWE )
LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // Deklaracja WndProc
    

Teraz przejdziemy do funkcji InitGL(). Dodamy tu 3 linie kodu, które zainicjalizują naszą kwadrykę. Dodaj te trzy linie po włączeniu light1, ale jeszcze zanim zwrócisz true. Pierwsza linia inicjalizuje kwadrykę i ustawia wskaźnik na jej adres w pamięci. Jeżeli nie będzie można go utworzyć, to zwrócone zostanie 0. Druga linia tworzy gładkie wektory normalne na naszej kwadryce, więc oświetlenie będzie dobrze wyglądać. Innymi możliwymi wartościami są GLU_NONE i GLU_FLAT. Na koniec włączamy mapowanie tekstury na naszej kwadryce. Mapowanie tekstury jest dosyć dziwnym procesem i nigdy nie przebiega w pełni zgodnie z oczekiwaniami.

    quadratic=gluNewQuadric();         // Ustawia wskaźnik na nową kwadrykę ( NOWE )
    gluQuadricNormals(quadratic, GLU_SMOOTH);         // Tworzy gładkie normalne ( NOWE )
    gluQuadricTexture(quadratic, GL_TRUE);         // Tworzy współrzędne tekstury ( NOWE )
    

Zdecydowałem, że zachowam sześcian w tym tutorialu, więc można zobaczyć, jak mapowana będzie tekstura na kwadryce. Postanowiłem, że przeniosę sześcian do osobnej funkcji, więc po napisaniu funkcji rysującej wszystko stanie się bardziej przejrzyste.

GLvoid glDrawCube()         // Rysuj sześcian
{
    glBegin(GL_QUADS);         // Rozpocznij rysowanie czworokątów
        // Przednia ściana        
        glNormal3f( 0.0f, 0.0f, 1.0f);         // Normalna zwrócona w stronę widza
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);         // Punkt 1 (Przód)    
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);         // Punkt 2 (Przód)    
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);         // Punkt 3 (Przód)    
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);         // Punkt 4 (Przód)
        // Tylna ściana        
        glNormal3f( 0.0f, 0.0f,-1.0f);         // Normalna zwrócona w głąb ekranu
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);         // Punkt 1 (Tył)    
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);         // Punkt 2 (Tył)    
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);         // Punkt 3 (Tył)    
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);         // Punkt 4 (Tył)
        // Górna ściana        
        glNormal3f( 0.0f, 1.0f, 0.0f);         // Normalna zwrócona w górę        
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);         // Punkt 1 (Góra)        
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);         // Punkt 2 (Góra)    
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);         // Punkt 3 (Góra)    
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);         // Punkt 4 (Góra)
        // Dolna ściana        
        glNormal3f( 0.0f,-1.0f, 0.0f);         // Normalna zwrócona w dół        
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);         // Punkt 1 (Dół)    
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);         // Punkt 2 (Dół)    
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);         // Punkt 3 (Dół)    
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);         // Punkt 4 (Dół)
        // Prawa ściana        
        glNormal3f( 1.0f, 0.0f, 0.0f);         // Normalna zwrócona w prawo        
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);         // Punkt 1 (Prawa)    
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);         // Punkt 2 (Prawa)    
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);         // Punkt 3 (Prawa)    
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);         // Punkt 4 (Prawa)
        // Lewa ściana        
        glNormal3f(-1.0f, 0.0f, 0.0f);         // Normalna zwrócona w lewo        
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);         // Punkt 1 (Lewa)    
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);         // Punkt 2 (Lewa)    
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);         // Punkt 3 (Lewa)    
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);         // Punkt 4 (Lewa)    
    glEnd();         // Skończ rysowanie czworokątów
}
    

Następnie mamy funkcję DrawGLScene, w której dopisałem prostą instrukcję warunkową w celu sprawnego rysowania różnych obiektów. Aby było bardziej przejrzyście, przepiszę całą funkcję DrawGLScene.

Zauważysz, że gdy mówię o użytych parametrach, to ignoruję pierwszy parametr (quadratic). Wynika to z tego, że używam go do rysowania wszystkich obiektów z wyjątkiem sześcianu.

                                
int DrawGLScene(GLvoid)         // Tu wszystko rysujemy
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Czyści ekran i bufor głębokości
    glLoadIdentity();         // Resetuje obecną matrycę modeli
    glTranslatef(0.0f,0.0f,z);         // Przesuń w głąb/na zewnątrz ekranu o z
    glRotatef(xrot,1.0f,0.0f,0.0f);         // Obróć o xrot wokół osi X
    glRotatef(yrot,0.0f,1.0f,0.0f);         // Obróć o xrot wokół osi Y
    glBindTexture(GL_TEXTURE_2D, texture[filter]);         // Wybierz teksturę w oparciu o zmienną filter
        // Ta część kodu jest nowa ( NOWE )
    switch(object)         // Sprawdzamy, który obiekt mamy rysować
    {
    case 0:         // Rysujemy obiekt 1
        glDrawCube();         // Rysuje nasz sześcian        
        break;         // Gotowe
    

Drugim tworzonym obiektem będzie cylinder. Pierwszy parametr (1.0f) jest promieniem dolnej podstawy cylindra. Drugi (1.0f) jest promieniem górnej podstawy. Trzeci parametr (3.0f) określa wysokość cylindra (jaki jest długi). Czwarty parametr (32) określa liczbę segmentów wokół osi Z, a piąty parametr określa ilość segmentów wzdłuż osi Z. Im więcej segmentów, tym bardziej szczegółowy obiekt. Zwiększając ilość segmentów, dodajemy więcej wielokątów do obiektu. Zatem poprawiamy jakość kosztem prędkości. Zazwyczaj łatwo jest znaleźć optymalne rozwiązanie.

    case 1:         // Rysujemy obiekt 2
        glTranslatef(0.0f,0.0f,-1.5f);         // Ustawiamy cylinder na środku
        gluCylinder(quadratic,1.0f,1.0f,3.0f,32,32);         // Rysujemy cylinder
        break;         // Gotowe
    

Trzecim obiektem będzie dysk w kształcie płyty CD. Pierwszy parametr (0.5f) określa wewnętrzny promień dysku. Ta wartość może być równa zero, co będzie oznaczało, że nie będzie pośrodku otworu. Im większy wewnętrzny promień, tym większy będzie otwór pośrodku dysku. Drugi parametr (1.5f) to promień zewnętrzny. Powinien on być większy niż promień wewnętrzny. Jeżeli ta wartość będzie niewiele większa od wewnętrznego promienia, to otrzymamy cieńki pierścień. Trzeci parametr (32) to ilość kawałków składających się na dysk. Pomyśl o nich jak o kawałkach pizzy. Więcej kawałków da nam gładszą zewnętrzną krawędź. Czwarty parametr (32) określa liczbę pierścieni tworzących dysk. Pierścienie są podobne do ścieżek na winylowych płytach. Okrąg w okręgu. Łączą się odpowiednio zewnętrznym i wewnętrznym promieniem, dając więcej detali. Analogicznie, im więcej będzie takich segmentów, tym wolniej będzie to działać.

    
    case 2:         // Rysujemy obiekt 3
        gluDisk(quadratic,0.5f,1.5f,32,32);         // Rysujemy dysk (w kształcie płyty CD)
        break;         // Gotowe
    

Nasz czwarty obiekt powinniście bez większych problemów rozpoznać. To kula! Ten obiekt jest całkiem prosty. Pierwszy parametr to promień kuli. Jeżeli nie spotkałeś się dotąd z tym pojęciem, to definiuje się je następująco: promień jest to odległość od środka obiektu do jego powierzchni. W tym wypadku promień wynosi 1.3f. Następnie mamy segmenty wokół osi z (32) oraz segmenty wzdłuż osi z (32). Więcej segmentów spowoduje, że kula będzie gładsza. Kula zazwyczaj potrzebuje sporo segmentów, aby wyglądała gładko.

    case 3:         // Rysujemy obiekt 4
        gluSphere(quadratic,1.3f,32,32);         // Rysujemy kulę
        break;         // Gotowe
    

Kolejny obiekt utworzymy przy użyciu tej samej komendy, która tworzyła cylinder. Jak pamiętasz, pierwsze dwa parametry kontrolowały promienie dolnej i górnej podstawy cylindra. Aby zrobić stożek, wystarczy podać zero jako jeden z promieni. W ten sposób na jednym końcu cylindra powstanie punkt. W poniższym kodzie podajemy promień górnej podstawy równy zero.

    case 4:         // Rysujemy obiekt 5
        glTranslatef(0.0f,0.0f,-1.5f);         // Ustawiamy stożek na środku
        gluCylinder(quadratic,1.0f,0.0f,3.0f,32,32);         // Stożek z promieniem górnej podstawy 0 i wysokością 3
        break;         // Gotowe
    

Nasz szósty obiekt jest tworzony przy użyciu funkcji gluPartialDisc. Stworzony przy użyciu tej komendy obiekt będzie wyglądał dokładnie tak, jak dysk, który zrobiliśmy poprzednio, ale funkcja gluPartialDisc przyjmuje jeszcze dwa nowe parametry. Piąty parametr (part1) jest kątem początkowym, od którego chcemy zacząć rysować dysk. Szósty parametr to kąt łuku. Kąt łuku to odległość do przejścia od aktualnego kąta. Zwiększając kąt łuku, spowodujemy, że dysk będzie powoli rysowany na ekranie, zgodnie z ruchem wskazówek zegara. Gdy kąt łuku przekroczy 360 stopni, zaczniemy zwiększać kąt początkowy. Będzie to wyglądało tak, jakby dysk był zmazywany i wszystko zaczyna się od początku!

    case 5:         // Rysujemy obiekt 6
        part1+=p1;         // Zwiększamy kąt początkowy
        part2+=p2;         // Zwiększamy kąt łuku
        if(part1>359)         // 360 stopni
        {
            p1=0;         // Przestajemy zwiększać kąt początkowy
            part1=0;         // Kąt początkowy ma być równy 0
            p2=1;         // Przestajemy zwiększać kąt łuku
            part2=0;         // Kąt łuku ma być równy 0
        }
        if(part2>359)         // 360 stopni
        {
            p1=1;         // Zaczynamy zwiększać kąt początkowy
            p2=0;         // Przestajemy zwiększać kąt końcowy
        }
        gluPartialDisk(quadratic,0.5f,1.5f,32,32,part1,part2-part1);         // Dysk, taki jak poprzednio
        break;         // Gotowe
    };
    xrot+=xspeed;         // Zwiększamy obrót wokół osi x
    yrot+=yspeed;         // Zwiększamy obrót wokół osi y
    return TRUE;         // Wszystko poszło dobrze
}
    

W funkcji KillGLWindow() musimy usunąć naszą kwadrykę, aby zwolnić zasoby systemowe. Zrobimy to przy użyciu komendy gluDeleteQuadric.

GLvoid KillGLWindow(GLvoid)         // Niszczy okno
{
    gluDeleteQuadric(quadratic);         // Usuwamy kwadrykę - zwalniamy zasoby
    

W ostatniej części zajmiemy się reakcją na klawisze. Po prostu dodaj ten kod tam, gdzie znajduje się reszta kodu dotycząca klawiszologii.

            if (keys[' '] && !sp)         // Czy naciśnięto spację?
            {
                sp=TRUE;         // Jeżeli tak, to sp ustawiamy na TRUE
                object++;         // Kolejny obiekt
                if(object>5)         // Czy obiekt jest większy niż 5?
                {
                    object=0;         // Jeżeli tak, to ustawiamy na 0
                }
            }
            if (!keys[' '])         // Czy spacja została puszczona?
            {
                sp=FALSE;         // Jeżeli tak, to sp ustawiamy na FALSE
            }
    

To wszystko! Teraz możesz już rysować kwadryki w OpenGL. Połączenie morphingu i kwadryk może dać naprawdę imponujące efekty. Animowany dysk jest przykładem prostego morphingu.