SDL - Wstęp do grafiki 3D

Trójwymiarowa grafika komputerowa jest dynamicznie rozwijającą się dziedziną informatyki. Jakość obrazów komputerowych osiągnęła dzisiaj już taki poziom, iż film lub zdjęcie nie może być dowodem w sądzie (przykładem był pewien proces w USA, gdzie jako dowód pokazano zdjęcie oskarżonego popełniającego przestępstwo, na drugi dzień obrona przyniosła identyczne zdjęcie, na którym zamiast oskarżonego był prokurator - najlepsi eksperci nie znaleźli na nim żadnych śladów fotomontażu). Seria zajęć na kole informatycznym w I LO będzie miała na celu przybliżenie tej dziedziny zdolnym uczniom - niestety, materiał informatyki w liceum pomija te zagadnienia całkowicie. Powodem jest prawdopodobnie usunięcie rachunku macierzowego z programu nauczania matematyki. Z drugiej strony jest to dziedzina bardzo skomplikowana i jej opanowanie wymaga dużego wysiłku.

Podstawowym narzędziem nauki będzie język C++ oraz biblioteki SDL i OpenGL. Na początku zrealizujemy podstawowe zagadnienia w czystym SDL z wykorzystaniem naszej biblioteki newgfx. Następnie przejdziemy do środowiska OpenGL i poznamy profesjonalne metody tworzenia grafiki 3D. Podstawowym warunkiem sukcesu jest opanowanie prezentowanego tutaj materiału i wykonanie proponowanych ćwiczeń. Jeśli czegoś nie rozumiesz, zapytaj nauczyciela lub przeczytaj ponownie materiał. Nie pozostawiaj białych plam w swojej wiedzy, gdyż bardzo szybko stracisz orientację w "przestrzeni 3D" :).

 

Współrzędne punktu w przestrzeni 3D

Punkt jest najprostszym obiektem w przestrzeni. Nie posiada on wymiarów, jedynie położenie, które definiujemy w przestrzennym układzie kartezjańskim. W tym celu ustalamy trzy proste, które są do siebie nawzajem prostopadłe i przecinają się w jednym punkcie, nazywanym początkiem układu współrzędnych. Prostym nadajmy odpowiednie zwroty i nazwijmy je osiami OX, OY i OZ:

 

obrazek

 

 

Oś OX jest pozioma i biegnie w prawo.

Oś OY jest pionowa i biegnie do góry.

Oś OZ jest prostopadła do płaszczyzny osi OX i OY, biegnie w kierunku obserwatora.

Na osiach ustalmy miary, które nazwiemy współrzędnymi odpowiednio x, y i z. Początek układu współrzędnych znajduje się w punkcie o mierze 0 każdej z osi. Oznaczamy go za pomocą trójki liczb (0,0,0).

 

obrazek

 

Umieśćmy w przestrzeni dowolny punkt P.

 

obrazek

 

Dokonajmy teraz rzutów prostokątnych punktu P na płaszczyzny zawierające osie OX-OY xy), OX-OZ xz) i OY-OZ yz):

 

obrazek

 

Współrzędne tych rzutów odpowiadają współrzędnym x,y,z punktu w przestrzeni trójwymiarowej. Zatem każdy punkt będziemy określać trójką liczb x,y,z. W pamięci komputera będzie to struktura zawierająca 3 liczby zmiennoprzecinkowe:

 

struct vertex3D
{
    double x,y,z;

};

 

Model szkieletowy

Wiemy jak określić punkty w przestrzeni trójwymiarowej. Obiekty szkieletowe są zbudowane z wierzchołków połączonych za pomocą linii. Do zdefiniowania takiego obiektu musimy określić zbiór wierzchołków. Zdefiniujmy sześcian, którego środek znajduje się w środku układu współrzędnych, a bok ma długość 200. Wierzchołki numerujemy kolejno od zera (przyda nam się to do późniejszej definicji linii):

 

obrazek

 

Współrzędne x, y, z tych wierzchołków są następujące:

 

wierzchołek x y z
0 -100 100 100
1 100 100 100
2 100 -100 100
3 -100 -100 100
4 -100 100 -100
5 100 100 -100
6 100 -100 -100
7 -100 -100 -100

 

Wierzchołki możemy umieścić w tablicy V, której elementami są struktury typu vertex3D. W języku C++ robimy to następująco:

 

// Definicja wierzchołków figury

const int VN = 8;  // Liczba wierzchołków

vertex3D V[] = {{-100, 100, 100},{ 100, 100, 100},{ 100,-100, 100},{-100,-100, 100},
                {-100, 100,-100},{ 100, 100,-100},{ 100,-100,-100},{-100,-100,-100}};

 

Krawędź jest linią, którą będziemy rysowali od wierzchołka początkowego v1 do końcowego v2. Jeśli znamy numery tych wierzchołków, to linię definiujemy następująco:

 

struct line3D
{
  int v1,v2;
};

 

Ponumerujmy kolejne krawędzie naszego sześcianu:

 

obrazek

 

W poniższej tabelce mamy definicje poszczególnych krawędzi sześcianu. Definicja określa kolejno numer wierzchołka startowego i numer wierzchołka końcowego krawędzi:

 

krawędź v1 v2
0 0 1
1 1 2
2 2 3
3 3 0
4 4 5
5 5 6
6 6 7
7 7 4
8 0 4
9 1 5
10 2 6
11 3 7

 

Podobnie jak wierzchołki, krawędzie również umieszczamy w tablicy, której elementami są struktury typu line3D:

 

// Definicja krawędzi figury

const int LN = 12;  // Liczba krawędzi

line3D L[] = {{0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}};

 

Obiekt jest zdefiniowany.

 

Odwzorowanie perspektywiczne na powierzchni graficznej

Teraz zajmiemy się sposobem wyświetlenia obrazu naszej figury, którą definiują:

Ustalmy położenie powierzchni graficznej SDL_Surface na płaszczyźnie Πxy - zawierającej osie OX i OY. Od razu umówmy się, że powierzchnia ta zostanie ułożona, tak aby środek układu współrzędnych pokrywał się ze środkiem powierzchni:

 

obrazek

 

Jeśli przyjrzysz się dokładnie powyższemu rysunkowi, to stwierdzisz, że układ współrzędnych powierzchni SDL_Surface nie zgadza się z układem współrzędnych przestrzennych:

  1. Powierzchnia SDL_Surface zbudowana jest z punktów obrazowych, czyli pikseli, które posiadają współrzędne całkowite. Współrzędne przestrzenne są współrzędnymi rzeczywistymi. Zatem położenie punktu obrazu 3D będzie musiało być zaokrąglane do położenia najbliższego piksela.
  2. Początek układu współrzędnych powierzchni SDL_Surface znajduje się w lewym górnym narożniku. Początek układu współrzędnych 3D jest na środku tej powierzchni.
  3. Oś y układu współrzędnych powierzchni SDL_Surface skierowana jest w dół, a w układzie przestrzennym do góry.

Wynika stąd konieczność przeliczenia współrzędnych przestrzennych na współrzędne powierzchni graficznej - nazwijmy je współrzędnymi ekranowymi. Jeśli tego nie zrobimy, otrzymamy obraz przesunięty i do góry nogami. Na szczęście transformacja jest bardzo prosta.

 

Niech x i y oznaczają współrzędne punktu leżącego na powierzchni SDL w układzie 3D, a xe i ye współrzędne tego samego punktu w układzie SDL:

 

obrazek

 

xe = w/2 + x

ye = h/2 - y

w - szerokość powierzchni SDL_Surface
h - wysokość powierzchni SDL_Surface

 

Teraz wyznaczymy obraz punktu P na płaszczyźnie SDL (pod uwagę będziemy brali tylko punkty, których współrzędna z jest nieujemna), najpierw we współrzędnych 3D, a później we współrzędnych samej płaszczyzny SDL_Surface. Istnieje kilka sposobów wyznaczania takiego obrazu - my zajmiemy się tzw. rzutem perspektywicznym. Zasada jest następująca:

 

Na ujemnej połówce osi OZ określamy punkt obserwatora. Punkt ten jest w odległości d od początku układu współrzędnych 3D. Z punktu obserwatora prowadzimy prostą do punktu P, którego obraz chcemy znaleźć na powierzchni SDL_Surface. Prosta ta przecina powierzchnię SDL_Surface w punkcie P'. Punkt ten jest obrazem punktu P w rzucie perspektywicznym.

 

obrazek

 

 

W celu wyznaczenia współrzędnych x i y punktu P rozważmy dwa rzuty prostokątne tej sytuacji:

 

obrazek
na płaszczyznę Πxz
  obrazek
na płaszczyznę Πyz

 

W obu tych przypadkach sytuacja jest podobna i możemy utworzyć proste proporcje (z twierdzenia Talesa lub z podobieństwa trójkątów prostokątnych):

 

obrazek

 

A stąd już prosto:

 

obrazek

 

Otrzymane współrzędne przestrzenne przeliczamy na współrzędne ekranowe:

 

obrazek

 

Współrzędne xe i ye są liczbami całkowitymi. Określone są dla x' > -d i y' > -d.

 

Ponieważ obserwator jest w punkcie d poza płaszczyzną graficzną SDL, to z tego punktu widzenia oś OZ biegnie w drugą stronę, czyli w głąb ekranu:

 

obrazek

 

Wynika z tego, iż do definicji obiektów możemy przyjąć układ przestrzeni 3D lub układ obserwatora, który różni się zwrotem osi OZ. Wzory w obu przypadkach pozostają identyczne.

 

Rysowanie figury szkieletowej w rzucie perspektywicznym

Zasada ryzowania figury szkieletowej będzie następująca:
 

Tworzymy tablicę VE współrzędnych ekranowych wierzchołków figury. Tablica ta składa się ze współrzędnych x,y na powierzchni graficznej SDL_Surface. Elementami tej tablicy będą struktury:

 

struct vertex2D
{
  Sint32 x,y;
};

 

Tablica ma tyle samo elementów co tablica V, czyli w przypadku naszego sześcianu - 8. Dokonujemy transformacji przestrzennej punktów V, a następnie wynik przekształcamy na współrzędne ekranowe i zapisujemy w tablicy VE (musimy sprawdzić, czy punkty x' i y' spełniają ostatni warunek - jeśli nie, to jako xe i ye zapisujemy jakąś dużą wartość). Teraz przetwarzamy tablicę krawędzi L. Dla każdej pary wierzchołków rysujemy linię pomiędzy współrzędnymi ekranowymi punktu startowego i końcowego. Jeśli współrzędne ekranowe są duże, to rezygnujemy z narysowania linii - jest to przypadek, gdy współrzędna z punktu P wyszła poza punkt obserwatora d.

 

Poniższy program animuje nasz sześcian, przesuwając go w przestrzeni do różnych punktów, ale tak dobranych, aby wszystkie punkty wierzchołkowe dało się zobrazować na powierzchni graficznej (z będzie zawsze nieujemne).

 

Program wykorzystuje bibliotekę newgfx.

 

Code::Blocks
// Obiekty szkieletowe 3D
// (C)2012 Koło Informatyczne
// I LO w Tarnowie
//-------------------------------

#include "newgfx.h"
#include <cmath>
#include <list>

const double PI=3.1415926535897;

// Definicja typów danych

struct vertex3D
{
    double x,y,z;
};

struct vertex2D
{
    Sint32 x,y;
};

struct line3D
{
    int v1,v2;
};

// Definicja wierzchołków figury

const int VN = 8;  // Liczba wierzchołków

vertex3D V[] = {{-100, 100, 100},{ 100, 100, 100},{ 100,-100, 100},{-100,-100, 100},
                {-100, 100,-100},{ 100, 100,-100},{ 100,-100,-100},{-100,-100,-100}};

// Definicja krawędzi figury

const int LN = 12;  // Liczba krawędzi

line3D L[] = {{0,1},{1,2},{2,3},{3,0},{4,5},{5,6},{6,7},{7,4},{0,4},{1,5},{2,6},{3,7}};

// Tablica współrzędnych ekranowych wierzchołków

vertex2D VE[VN];

int main(int argc, char *argv[])
{
    int waiting = 1;

    SDL_Surface * screen;

    SDL_Event event;

    double kx = PI, ky = 0, kz = 0;       // Kąty pozycji na elipsie

    double sx = 0, sy = 100, sz = 1000;   // Współrzędne środka elipsy

    double rx = 700, ry = 600, rz = 900;  // Promienie elipsy

    double tx,ty,tz;  // Wektor przesunięcia

    double d = 1000;  // Punkt obserwatora

    double m;         // Mnożnik

    SDL_Rect r;       // Prostokąt wymazywania

    if(!SDL_Init(SDL_INIT_VIDEO))
    {
        atexit(SDL_Quit);

        screen = SDL_SetVideoMode(1280, 1024, 32, SDL_HWSURFACE | SDL_FULLSCREEN);

        // Ustawiamy prostokąt wymazywania

        r.x = r.y = 0;
        r.w = screen->w;
        r.h = screen->h;

        do
        {
            // Obliczamy wektor przesunięcia

            tx = sx + rx * cos(kx);
            ty = sy + ry * sin(ky);
            tz = sz + rz * cos(kz);

            // modyfikujemy kąty

            kx += 0.005;   if(kx > PI+PI) kx = 0;
            ky += 0.0075;  if(ky > PI+PI) ky = 0;
            kz += 0.0125;  if(kz > PI+PI) kz = 0;

            // Obliczamy współrzędne ekranowe

            for(int i = 0; i < VN; i++)
            {
                m = d / (V[i].z + tz + d);  // obliczamy mnożnik

                VE[i].x = (screen->w >> 1) + m * (V[i].x + tx);
                VE[i].y = (screen->h >> 1) - m * (V[i].y + ty);
            }

            if(SDL_MUSTLOCK(screen)) SDL_LockSurface(screen);

            // Rysujemy szkielet figury w kolorze białym

            for(int i = 0; i < LN; i++)
            {
                gfxClipWuLine(screen,VE[L[i].v1].x,VE[L[i].v1].y,VE[L[i].v2].x,VE[L[i].v2].y,0xffffff);
                gfxClipWuLine(screen,VE[L[i].v1].x+1,VE[L[i].v1].y,VE[L[i].v2].x+1,VE[L[i].v2].y,0xffffff);
                gfxClipWuLine(screen,VE[L[i].v1].x,VE[L[i].v1].y+1,VE[L[i].v2].x,VE[L[i].v2].y+1,0xffffff);
                gfxClipWuLine(screen,VE[L[i].v1].x+1,VE[L[i].v1].y+1,VE[L[i].v2].x+1,VE[L[i].v2].y+1,0xffffff);
            }

            if(SDL_MUSTLOCK(screen)) SDL_UnlockSurface(screen);

            SDL_UpdateRect(screen, 0, 0, 0, 0); // Uaktualniamy ekran

            if(SDL_MUSTLOCK(screen)) SDL_LockSurface(screen);

            SDL_FillRect(screen, &r, 0);  // Wymazujemy szkielet figury

            if(SDL_MUSTLOCK(screen)) SDL_UnlockSurface(screen);

            // Czekamy na klawisz ESCAPE

            if (SDL_PollEvent(&event))
                if ((event.type == SDL_QUIT) ||
                   ((event.type == SDL_KEYDOWN) &&
                    (event.key.keysym.sym == SDLK_ESCAPE))) waiting = 0;
        } while(waiting);
    }

    return 0;
}

 


   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2024 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe