Lekcja 9. Poruszanie bitmap w przestrzeni 3D
Autor: Marcin 'Moriturius' Radoszewski
Oryginał: Moving Bitmaps In 3D Space (Jeff 'NeHe' Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson09.zip

Witaj w 9. tutorialu. W tej chwili powinieneś już bardzo dobrze rozumieć OpenGL. Nauczyłeś się wszystkiego od tworzenia okna dla OpenGL po mapowanie tekstur na obracającym się obiekcie podczas używania oświetllenia i przezroczystości (alpha blendingu). To będzie pierwszy zaawansowany tutorial, w którym nauczysz się jak poruszać bitmapy po ekranie w 3D, usuwać czarne piksele wokół bitmapy (używając blendingu), dodawać kolor do czarnobiałej tekstury i na końcu, nauczysz się jak stworzyć fantazyjne kolory i prostą animację przez mieszanie różnokolorowych tekstur.

W tym turorialu będziemy modyfikować kod z pierwszej lekcji. Na początek dodamy kilka zmiennych na początku programu. Przepiszę całą sekcję kodu aby było łatwiej zobaczyć wprowadzane zmiany.

#include    <windows.h>         // Plik nagłówkowy Windowsa
#include    <stdio.h>         // Plik nagłówkowy standardowego wejscia/wyjscia
#include    <gl\gl.h>         // Plik nagłówkowy OpenGL'a
#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;         // Kontekst rysujący
HWND        hWnd=NULL;         // Uchwyt do okna
HINSTANCE    hInstance;         // Instancja aplikacji
bool        keys[256];         // Tablica klawiszy - wcisniety czy nie
bool        active=TRUE;         // Flaga aktywnosci okna
bool        fullscreen=TRUE;         // Flaga pełnego ekranu (tak/nie)

Następujące linie są nowe. twinkle i tp są zmiennymi typu BOOL co oznacza że mogą przyjmować tylko wartości TRUE albo FALSE (prawda albo fałsz). twinkle będzie informować czy aktywny jest efekt migotania (twinkle effect). tp będzie zawierać informację czy klawisz 'T' został wciśnięty albo puszczony (wciśnięty tp=TRUE, puszczony [itp[/i]=FALSE).

BOOL twinkle;         // Czy gwiazdy migoczą?
BOOL tp;         // Czy klawisz 'T' jest wciśnięty?

num będzie zawierać ilość gwiazd, które narysujemy na ekranie. Jest zdefiniowane jako stała, co oznacza ze nie zostanie zmienione w żadnym miejscu w kodzie. Powodem dla którego definiujemy ją jako stałą jest to że nie mozna zdefiniować ponownie tablicy więc jeśli ustawiliśmy tablicę na 50 i zdecydujemy na zwiększenie ilości gwiazd do 51 to pojawi się błąd, bo tablica nie może zwiększyć się do 51. Jeśli chcesz, możesz zmienić liczbę gwiazd TYLKO w tej linii. Nie próbuj zmieniać wartości num później jeśli nie chcesz doprowadzić do katastrofy.

const num = 50;         // Ilość gwiazdek do narysowania

Teraz stworzymy strukturę. Słowo struktura brzmi może troche strasznie ale nie jest takie naprawdę. Struktura to grupa prostych danych (zmiennych, itp.) reprezentujących większą grupę. Zobaczysz, że 7. linia kodu poniżej zawiera 'stars;'. Wiemy że każda gwiazda będzie miała 3 wartości dla koloru i wszystkie te wartości będą typu całkowitego (integer). Trzecia linia 'int r, g, b' tworzy 3 zmienne typu całkowitego. Jedną dla składowej czerwonej (r), jedną dla zielonej (g) i jedną dla niebieskiej (b). Wiemy że każda gwiazda będzie w różnej ogległości od środka ekranu i może być umieszczona pod kątem od 0 do 360 stopni. W 4. lini kodu niżej tworzymy wartość zmiennoprzecinkową nazwaną dist. Ta zmienna będzie przechowywała odległość od śreodka ekranu. Piąta linia tworzy zmienną zmiennoprzecinkową angle, w której będzie przechowywany kąt.

typedef struct         // tworzenie struktury dla gwiazdy
{
    int r, g, b;         // Kolor gwiazdy
    GLfloat dist;         // Odgległość od środka
    GLfloat angle;         // Aktualny kąt
}
stars;         // nazwa struktury to 'stars'
stars star[num];         // Stwórz 'num'-elementową tablice 'star' używając informacji struktury 'stars'

Następnie ustawiamy zmienne definiujące jak daleko od gwiazd stoi oglądający (zbliżenie [zoom]), i pod jakim kątem oglądamy gwiazdy (nachylenie [tilt]). Tworzymy zmienną spin, która będzie obracać migające gwiazdy wokół osi Z dzieki czemu będzie to wyglądać jakby obracały się na ich aktualnych pozycjach.

loop to zmienna, którą będziemy używać w programie aby narysować wszystkie 50 giwazd, a texture[1] będzie użyte w celu przechowania czarnobiałej tekstury którą załadujemy. Jeśli chcesz więcej tekstur to zmien ilość tekstur z 1 na tyle ile chcesz uzyć.

GLfloat    zoom=-15.0f;         // Odległość obserwatora
GLfloat tilt=90.0f;         // Nachylenie widoku
GLfloat    spin;         // Obrót migoczących gwiazd
GLuint    loop;         // Główna zmienna pętli
GLuint    texture[1];         // Zmienna przechowująca jedną teksturę
LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // Deklaracja procedury okna

Zaraz po powyższym dodamy kod, który załaduje naszą teksturę. Nie muszę tłumaczyć tego kodu z wielkimi detalami. Jest to ten sam kawałek kodu który użyliśmy aby załadować tekstury w lekcji 6, 7 i 8. Bitmapa którą tym razem załadujemy nazywa się star.bmp. Wygenerujemy tylko jedną teksturę używając glGenTextures(1, &texture[0]). Tekstura będzie filtrowana liniowo.

AUX_RGBImageRec *LoadBMP(char *Filename)         // Wczytuje bitmapę
{
    FILE *File=NULL;         // Uchwyt pliku
    if (!Filename)         // Upewnij się że podano nazwę pliku
    {
        return NULL;         // Jeśli nie to zwróć NULL
    }
    File=fopen(Filename,"r");         // Sprawdź czy plik istnieje
    if (File)         // Czy plik istnieje?
    {
        fclose(File);         // Zamknij uchwyt
        return auxDIBImageLoad(Filename);         // Załaduj bitmapę i zwróć do niej uchwyt
    }
    return NULL;         // Jeśli ładowanie się nie powiodło zwróć NULL
}

To jest sekcja, która ładuje bitmapę (używając powyższego kodu) i konwertuje ją na tekstury. Status jest uzyte do okreslenia czy tekstura została załadowana.

int LoadGLTextures()         // Załaduj bitmapy i przekonwertuj je na tekstury
{
    int Status=FALSE;         // Wskaźnik statusu
    AUX_RGBImageRec *TextureImage[1];         // Tworzymy miejsce na teksturę
    memset(TextureImage,0,sizeof(void *)*1);         // Ustawiamy wskaźnik na NULL
        // Załaduj bitmapę, sprawdź czy nie ma błędów i jeśli bitmapa nie została załadowana - wyjdź
    if (TextureImage[0]=LoadBMP("Data/Star.bmp"))
    {
        Status=TRUE;         // Ustaw Status na TRUE
        glGenTextures(1, &texture[0]);         // Stwórz jedną teksturę
        // Stwórz teksturę filtrowaną liniowo
        glBindTexture(GL_TEXTURE_2D, texture[0]);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
        glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
    }
    if (TextureImage[0])         // Jeśli tekstura istnieje
    {
        if (TextureImage[0]->data)         // Jeśli obrazek tekstury istnieje
        {
            free(TextureImage[0]->data);         // Uwolnij pamięć obrazka
        }
        free(TextureImage[0]);         // Uwolnij strukturę obrazka
    }
    return Status;         // Zwróć Status
}

Teraz ustawimy OpenGL aby renderował tak jak chcemy. Nie zamierzemy używać Testowania Głębokości (Depth Test) w tym projekcie, więc upewnij się że używasz kodu z lekcji pierwszej z usuniętym glDepthFunc(GL_LEQUAL); i glEnagle(GL_DEPTH_TEST); w innym przypadku efekty nie będą przyjemne dla oka. Używamy mapowania tekstur wiec włączymy je razem z mieszaniem (blending).

int InitGL(GLvoid)         // Ustawia środowisko OpenGL
{
    if (!LoadGLTextures())         // Załaduj tekstury
    {
        return FALSE;         // Jeśli nie załadowano tekstur zwróć FALSE
    }
    glEnable(GL_TEXTURE_2D);         // Włącz mapowanie tekstur
    glShadeModel(GL_SMOOTH);         // Włącz gładkie cieniowanie
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);         // Ustaw czarne tło
    glClearDepth(1.0f);         // Ustawienie bufora głębokości
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);         // Najlepsze obliczania perspektywy
    glBlendFunc(GL_SRC_ALPHA,GL_ONE);         // Ustaw funkcje mieszania
    glEnable(GL_BLEND);         // Włącz mieszanie

Następujący kod jest nowy. Ustawia początkowy kąt, odległość i kolor dla każdej gwiazdy. Zauważ jak łatwo jest zmieniać informacje w strukturze. Pętla przerobi wszystkie 50 gwiazd. Aby zmienić kąt gwiazdy star[1] wszystko co musimy zrobić to napisać star[1].angle={jakiś numer}. To takie proste!

    for (loop=0; loop<num; loop++)         // Stwórz pętle przerabiającą wszystkie gwiazdy
    {
        star[loop].angle=0.0f;         // Wszystkie gwiazdy startują pod kątem 0

Obliczam odległość przez wzięcie aktualnej gwiazdy (ktrórej indeks to loop) i podzielenie jej przez maksymalną ilość gwiazd. Wtedy mnożę wynik przez 5.0f. W ten sposób każda gwiazda jest lekko przesunięta w stosunku do poprzedniej . Kiedy loop osiągnie wartość 50 (ostatnia gwiazda), loop podzielone przez num da wynik 1.0f. Powodem dla którego mnożę przez 5.0f jest to że 1.0f*5.0f = 5.0f, a 5.0f jest odległością brzegu od śdodka. Nie chcę żeby gwiazdy były poza ekranem wiec 5.0f jest dobre. Jeśli ustawisz zoom bardziej w kierunku ekranu możesz użyć wiekszej liczby niż 5.0f, ale Twoje gwiazy będą znacznie mniejsze (przez perspektywę).

Zauważysz że kolor każdej z gwiazd stworzony jest z losowych wartości od 0 do 255. Pewnie się zastanawiasz jak możemy użyć tak duże wartości kiedy normalnie kolory są z przedziału od 0.0f do 1.0f. Kiedy będziemy ustawiać kolor użyjemy glColor4ub zamiast glColor4f. ub znaczy Unsigned Byte (nieznakowany bajt). Bajt może przyjmować wartości tylko od 0 do 255. W tym programie łatwiej jest używać bajtów niż tworzyć losowe wartości zmiennoprzecinkowe.

        star[loop].dist=(float(loop)/num)*5.0f;         // Oblicz odległość od środka
        star[loop].r=rand()%256;         // Wylosuj wartość czerwonego star[loop]
        star[loop].g=rand()%256;         // Wylosuj wartość zielonego star[loop]
        star[loop].b=rand()%256;         // Wylosuj wartość niebieskiego star[loop]
    }
    return TRUE;         // Inicjalizacja poszła OK
}

Kod zmieniania rozmiaru jest taki sam więc go ominiemy i przeskoczymy od razu do kodu rysującego. Jeśli używasz kodu z pierwszej lekcji, usuń funkcję DrawGLScene i wklej tą która jest poniżej. W lekcji pierwszej są tylko 2 linijki kodu więc nie ma specjalnie dużo do usuwania.

int DrawGLScene(GLvoid)         // Tutaj wszystko rysujemy
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Wyczyść ekran i bufor głębokości
    glBindTexture(GL_TEXTURE_2D, texture[0]);         // Wybierz naszą teksture
    for (loop=0; loop<num; loop++)         // Dla kazdej gwiazdy
    {
        glLoadIdentity();         // Zresetuj widok zanim narysujemy gwiazdę
        glTranslatef(0.0f,0.0f,zoom);         // Ustaw obserwatora (używając 'zoom')
        glRotatef(tilt,1.0f,0.0f,0.0f);         // Ustaw nachylenie (używając 'tilt')

Teraz poruszymy gwiazdę. Gwiazda znajduje się teraz w środku ekranu. Pierwszą rzeczą jaką zrobimy jest obrót sceny wokół osi Y o 90. Jeśli obrócimy o 90 stopni, to oś X nie będzie już biegła od lewej do prawej tylko od ekranu wgłąb sceny. Jako przykład aby pomóc zrozumieć. Wyobraź sobie że jestes w centrum jakiegoś pokoju. Teraz wyobraź sobie że na lewej ścianie jest napisane -x, na ścianie przed Tobą jest -z, na prawej ścianie jest +x, a na ścianie za Tobą jest +z. Jeśli pokój obróci się o 90 stopni w prawo, ale bez Ciebie, na ścianie przed Tobą będzie teraz napis -x. Wszystkie ściany się przemieściły. -z będzie po prawej, +z po lewej, a +x za Tobą. Ma sens? Przez obrót sceny, zmieniamy kierunek płaszczyzn x i z.

Następna linijka przemieszcza gwiazdę w dodatnią stronę osi X. Normalnie dodatnia wartość na osi X przesunęłaby gwiazdę w prawą stronę ekranu (gdzie znajduje sie domyslnie +x), ale obrócilismy układ i teraz +x moze być wszędzie. Jeśli obrócimy o 180 stopni to +x będzie po lewej stronie ekranu zamiast po prawej. Kiedy przesuniemy gwiazdę na stronę dodatnią osi X moze ona przemieszczać się w którąkolwiek stronę.

        glRotatef(star[loop].angle,0.0f,1.0f,0.0f);         // Obróć układ o kąt aktualnej gwiazdy
        glTranslatef(star[loop].dist,0.0f,0.0f);         // Przesuń o odległośc giwzdy po osi X

Teraz bedzie chytry kod. Gwiazda jest właściwie tylko płaską teksturą. Jeśli narysujesz czworokąt w środku ekranu i oteksturujesz ją to będzie wyglądała dobrze. Będzie zwrócona przodem do Ciebie tak jak powinna, ale jeśli obrócisz układ o 90 stopni wokół osi Y, tekstura bedzie zwrócona w prawo, a wszystko co zobaczysz to będize cienka lnia. Nie chcemy aby tak się stało. Chcemy aby gwiazdy były zwrócone w naszą strone przez cały czas niezależnie od tego o ile obrócimy układ i jak zmienimy nachylenie.

Nasz cel osiągniemy przez anulowanie rotacji które zrobiliśmy tuż przed narysowaniem gwiazdy. Musisz anulować rotacje w odwrodtnej kolejności. Najpierw nachyliliśmy ekran, a potem obracaliśmy układ o kąt zawarty w strukturze gwiazdy. W odwrotnej kolejności najpierw anulujemy obrót, a potem nachylenie. Aby to zrobić użyjemy wartości przeciwnych danych kątków i obrócimy o te kąty układ. Jeśli obrócilismy gwiazdę o 10 stopni to obrót o -10 stopni sprawi, że gwiazda będzie zwrócona do nas przodem na danej osi. Pierwsza linia poniższego kodu anuluje rotacje na osi Y. W nastepnej linii musimy anulować nachylenie na osi X. Aby to zrobić musimy obrócić układ o -tilt. Po tym jak anulujemy te obroty gwiazda będzie zwrócona w naszą stronę całkowicie.

        glRotatef(-star[loop].angle,0.0f,1.0f,0.0f);         // Anuluj obrót gwiazdy
        glRotatef(-tilt,1.0f,0.0f,0.0f);         // Anuluj nachylenie

Jeżeli twinkle jest ustawione na TRUE, to bedziemy rysować nieobracające się gwiazdy na ekranie. Aby osiągnąć inny kolor weźmiemy maksymalną ilośc gwiazd(num) i odejmiemy numer aktualnej gwiazdy (loop), a nastepnie odejmiemy jeszcze 1 ponieważ loop w pętli przyjmuje wartości od 0 do num-1. Jeśli wynik byłby 10 to użylibyśmy koloru z gwiazdy numer 10. W ten sposób kolor dwóch gwiazd jest zazwyczaj inny. Nie jest to najlepszy sposób na zrobienie tego ale za to jest efektywny. Ostatnia wartość to wartość alpha. Im niższa wartość, tym ciemniejsza będzie gwiazda.

Jeśli miganie jest włączone to kazda gwiazda będzie rysowana podwójnie. To spowolni troche komputer w zależności od tego jakim komputerem dysponujesz. Jeśli miganie będzie włączone to kolory dwóch nakładających się gwiazd będą mieszane ze sobą tworząc naprawde fajne kolory.

Zauważ jak łatwo jest dodać kolor do tekstury. Nawet jeśli tekstura jest czarnobiała to po wyswietleniu na ekranie bedzie miala wybrany kolor. Zwróć też uwagę na to że używamy kolorów z przeciałów 0-255 niz 0.0f-1.0f.

        if (twinkle)         // Miganie gwiazd włączone
        {
        // Przypisz kolor uzywając Bajtów (0-255)
            glColor4ub(star[(num-loop)-1].r,star[(num-loop)-1].g,star[(num-loop)-1].b,255);
            glBegin(GL_QUADS);         // Zacznij rysowanie czworokąta
                glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
                glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
                glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
                glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
            glEnd();         // Zakończ rysowanie czworokąta
        }

Teraz narysujemy główną gwiazdę. Jedyna różnica od kodu powyżej jest taka, że ta gwiazda jest rysowana zawsze i kręci się wokół osi Z.

        glRotatef(spin,0.0f,0.0f,1.0f);         // Obróć wokół osi Z
        // Przypisz kolor uzywajnąc Bajtów (0-255)
        glColor4ub(star[loop].r,star[loop].g,star[loop].b,255);
        glBegin(GL_QUADS);         // Zacznij rysowanie czworokąta
            glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,-1.0f, 0.0f);
            glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,-1.0f, 0.0f);
            glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 0.0f);
            glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f);
        glEnd();         // Zakończ rysowanie czworokąta

Teraz napiszemy cały ruch. Obracamy normalne gwiazdy poprzez zwiększanie wartości spin. Następnie zmieniamy kąt kazdej gwiazdy. Kąt każdej z nich jest zwiększany o loop/num, co powoduje ze gwiazdy będące dalej od środka kręcą się szybciej, a te które są blisko - wolniej. Na końcu zmniejszamy odległość każdej gwiazdy od środka ekranu. To sprawia ze wygladaja jakby srodek ekranu je przyciągał.

        spin+=0.01f;         // Uzyte aby obracac gwiazdy
        star[loop].angle+=float(loop)/num;         // Zmienia kąt gwiazdy
        star[loop].dist-=0.01f;         // Zmienia odległość gwiazdy

Linie ponizej sprawdzają czy któraś z gwiazd nie osiągnęła środka ekranu. Jeśli tak to dostaje ona nowy kolor i znów odsuwana jest o 5 jednostek od środka tak aby mogła zacząć swoją podróż jako nowa gwiazda.

        if (star[loop].dist<0.0f)         // Czy gwiazda jest już w środku
        {
            star[loop].dist+=5.0f;         // Przesuń ją o 5 jednostek od srodka
            star[loop].r=rand()%256;         // Wylosuj jej nową składową (czerwony)
            star[loop].g=rand()%256;         // Wylosuj jej nową składową (zielony)
            star[loop].b=rand()%256;         // Wylosuj jej nową składową (niebieski)
        }
    }
    return TRUE;         // Wszystko OK
}

Teraz dodamy kod do sprawdzenia czy został wciśnięty jakiś klawisz. Znajdź funkcję WinMain(), a w niej linijkę SwapBuffers(hDC). Dodamy nasz kod zaraz pod tą linią.

Kod poniżej sprawdza czy klawisz 'T' nie został wciśnięty. Jeśli tak i nie był wcisnięty wczesniej to włączy lub wyłączy miganie gwiazd oraz ustawia tp na TRUE co zapobiega powtarzaniu wykonywania kodu bez przerwy dopoki wcisniety jest klawisz 'T'.

        SwapBuffers(hDC);         // Zmień bufory (Double Buffering)
        if (keys['T'] && !tp)         // Czy 'T' jest wcisniety i czy 'tp' = FALSE
        {
            tp=TRUE;         // Jeśli tak to 'tp' = TRUE
            twinkle=!twinkle;         // Zmień wartość 'twinkle' na jej negację
        }

Poniższy kod sprawdza czy klawisz 'T' został puszczony i jeśli tak to ustawia tp na FALSE. Naciskanie klawisza 'T' nic nie da dopóki tp jest równe FALSE, więc ta sekcja kodu jest bardzo ważna.

        if (!keys['T'])         // Klawisz został puszczony
        {
            tp=FALSE;         // Jeśli tak to ustaw 'tp' na FALSE
        }

Reszta kodu sprawdza czy któraś ze strzałek (w górę, w dół, w prawo, w lewo) nie została wciśnięta.

        if (keys[VK_UP])         // Czy strzałka w górę została wciśnięta?
        {
            tilt-=0.5f;         // Nachyl ekran w górę
        }
        if (keys[VK_DOWN])         // Czy strzałka w dół została wciśnięta?
        {
            tilt+=0.5f;         // Nachyl ekran w dół
        }
        if (keys[VK_PRIOR])         // Czy PageUp zostało wciśnięte?
        {
            zoom-=0.2f;         // Przybliż obserwatora
        }
        if (keys[VK_NEXT])         // Czy PageDown zostało wciśnięte?
        {
            zoom+=0.2f;         // Oddal obserwatora
        }

Tak jak we wszystkich poprzednich tutorialach, upewnij się że tytuł głównego okienka jest prawidłowy.

        if (keys[VK_F1])         // Czy F1 jest wciśnięte?
        {
            keys[VK_F1]=FALSE;         // Jesli tak to ustaw klawisz na FALSE
            KillGLWindow();         // Zamknij nasze okno OpenGL
            fullscreen=!fullscreen;         // Przełącz pełny ekran/okno
        // Stwórz ponownie okno OpenGL
            if (!CreateGLWindow("NeHe's Textures, Lighting & Keyboard Tutorial",640,480,16,fullscreen))
            {
                return 0;         // Zwróć 0 jeśli nie zostało stworzone
            }
        }
    }
}

W tym tutorialu próbowałem porządnie wytłumaczyć jak załadować bitmapę w skali szarości, usunąć czarny obszar dookoła niej (używając mieszania [blendingu]), dodać kolor do obrazka i poruszać obrazek po ekranie w świecie 3D. Pokazałem Ci także jak stworzyć piękne kolory i animacje poprzez nakładanie kopii bitmapy na oryginalną bitmapę. Jeśli raz zrozumiesz to czego Cię nauczyłem do tej pory, nie powinieneś mieć żadnych problemów z tworzeniem dem (przyp. tłumacza: animacji) w 3D na własną rękę. Wszystkie podstawy zostały przedstawione!