wtorek, 29 listopada 2011

AndroidPlay - część 4: Czworokąty i tekstury

Wstęp.

W poprzedniej części stworzyliśmy aplikację wyświetlającą trójkąt. Teraz przyszedł czas na teksturowanie - bo co by to była za gra, gdyby wyświetlała tylko różnokolorowe figury - dawno temu może by to przeszło, ale nie dziś.


Poprawki w szablonie naszej aplikacji.

Nie będę tutaj opisywał ponownie tego, co opisane zostało w poprzedniej lekcji - chodzi o ten kod, który pojawia się w każdej aplikacji - tworzący obiekt renderujący, płaszczyznę rysowania czy ustawiający rzutowanie itd. Dopiszemy tylko trzy zmienne. Dwie pozwolą nam na łatwe ustawianie rozdzielczości w jakiej będziemy rysować (można powiedzieć, że układu współrzędnych), trzecia umożliwia dostęp do zasobów aplikacji. W klasie AppRenderer dopisujemy:
private int resolutionX = 480, 
            resolutionY = 320;
private Context appContext;
Kontekst aplikacji dziedziczony jest przez klasę Activity, dlatego w klasie naszej aktywności, przekazujemy do konstruktora obiektu renderującego wskaźnik, na siebie (czyli naszą aktywność):
view.setRenderer(new AppRenderer(this));
Natomiast w klasie AppRenderer konstruktor przepisuje go do appContext:
public AppRenderer(Context context) {
    this.appContext = context;
}
Zmienne resolution wykorzystujemy przy ustawianiu rzutowania w onSurfaceChanged() modyfikując istniejące wywołanie funkcji, tak aby lewy dolny punkt ekranu wynosił (0,0), a prawy górny (resolutionX, resolutionY).
gl.glOrthof(0, resolutionX, 0, resolutionY, -1, 1);

Orientacja

Ustawienie orientacji ekranu także może okazać się przydatne. Orientację ekranu możemy ustawić np. w pliku manifest - odpowiada za to atrybut android:screenOrientation = "WARTOŚĆ" w tagu <activity>. Może on przyjmować jedne z następujących wartości:
  • unspecified - pozwalamy systemowi zdecydować za nas
  • landscape - orientacja pozioma, ignorujemy informacje o fizycznym położeniu urządzenia, których dostarcza nam czujnik.
  • portrait - orientacja pionowa, ignorujemy informacje o fizycznym położeniu urządzenia, których dostarcza nam czujnik.
  • user - używamy preferowanych ustawień użytkownika
Więcej informacji znajdziesz w dokumentacji.

Chcemy, aby nasza aplikacja zawsze była w orientacji poziomej, zaoszczędzi to nam kłopotów, a granie w takim trybie wydaje się najwygodniejsze. Dlatego w pliku manifest dodajemy we wspomnianym tagu <activity> atrybut:
<activity  (...) android:screenOrientation="landscape">

Rysowanie czworokątów

Zaczniemy od zdefiniowania (uwzględniając dalsze prace) trzech buforów. Pierwszym będzie bufor wierzchołków, drugim bufor współrzędnych tekstur, trzeci to bufor indeksów. W klasie AppRenderer dodajemy odpowiednie pola:
private FloatBuffer quadsVB,  // współrzędne wierzchołków
                    quadsTCB; // współrzędne tekstur
private ShortBuffer quadsIB;  // indeksy (potrzebne do używania glDrawElements)

Dodajemy także metodę initShapes(), którą wywołamy w onSurfaceCreated(). Tablica quadsCoords zawierać w sobie będzie współrzędne czworokąta.Dla pełnej jasności - przy alokowaniu pamięci dla ByteBuffer długość tablicy mnożymy przez 4, bo typ float reprezentowany jest przez 4 bajty. Współrzędne czworokąta są uzależnione od resolutionX i resolutionY tak, żeby narysować na środku ekranu kwadrat o boku 150.
private void initShapes(){
    float quadsCoords[] = {
        // Pierwszy czworokąt
        (float)resolutionX/2 - 75.0f, (float)resolutionY/2 - 75.0f, 0,
        (float)resolutionX/2 + 75.0f, (float)resolutionY/2 - 75.0f, 0,
        (float)resolutionX/2 + 75.0f, (float)resolutionY/2 + 75.0f, 0,
        (float)resolutionX/2 - 75.0f, (float)resolutionY/2 + 75.0f, 0,
    };

    // inicjalizujemy bufor wierzchołków dla czworokąta
    ByteBuffer vbb = ByteBuffer.allocateDirect(quadsCoords.length * 4); 
    vbb.order(ByteOrder.nativeOrder()); // użyj naturalnego porządku bajtów
    quadsVB = vbb.asFloatBuffer();      // utwórz z ByteBuffer FloatBuffer
    quadsVB.put(quadsCoords);           // dodaj współrzędne do bufora 
    quadsVB.position(0);                // ustaw pozycję początkową
}

Nie zapomnijmy o włączeniu tablic wierzchołków, w funkcji onSurfaceCreated dorzucamy
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // Włącz tablice wierzchołków

Do rysowania naszego czworokąta użyjemy funkcji glDrawElements(int mode, int count, int type, Buffer indices), która daje nam większe możliwości niż glDrawArrays - pozwala efektywniej wykorzystać wierzchołki, ponieważ możemy określić kolejność pobierania wierzchołków z bufora i użyć jednego wierzchołka wielokrotnie. Parametry są następujące:
  • mode określa jakie prymitywy chcemy renderować
  • count to ilość wierzchołków (elementów) jakie rysujemy
  • type - typ danych, które przechowujemy w buforze indices
  • indices czyli bufor, w którym przechowujemy indeksy rysowanych wierzchołków
Niestety w przeciwieństwie do normalnego OpenGL, w wersji ES nie ma możliwości rysowania czworokątów (GL_QUADS). Musimy sobie radzić sami - będziemy po prostu rysować dwa trójkąty (do czego tak na prawdę sprowadza się rysowanie czworokąta). W buforze wierzchołków trzymamy ich współrzędne, musimy utworzyć bufor indeksów, aby glDrawElements wiedziało jakie wierzchołki ma po kolei wykorzystywać. Na poniższym rysunku przedstawiłem sposób rysowania.


Tworzymy więc w funkcji initShapes, tak samo jak bufor wierzchołków, bufor indeksów.
short quadsIndices[] = {
     // Pierwszy
     0, 1, 3, 1, 2, 3
};

ByteBuffer ibb = ByteBuffer.allocateDirect(quadsIndices.length * 2); 
ibb.order(ByteOrder.nativeOrder());// użyj naturalnego porządku bajtów
quadsIB = ibb.asShortBuffer();      // utwórz z ByteBuffer FloatBuffer
quadsIB.put(quadsIndices);            // dodaj indeksy do bufora 
quadsIB.position(0);                // ustaw pozycję początkową

Teksturowanie

Przyszedł czas na poznanie, moim zdaniem, kluczowego zagadnienia, jakim jest teksturowanie. Zacznijmy od zwrócenia uwagi na to, że w przeciwieństwie do normalnego OpenGL, tutaj układ współrzędnych tekstur trochę się różni. Lewy-górny wierzchołek tekstury to punkt (0,0). Długo szukałem przyczyny błędnego nakładania tekstury na obiekt i jakież było moje zdziwienie, gdy po ciężkich bojach zauważyłem ten drobny szczegół. Poleganie na starych nawykach z normalnego OpenGL potrafi czasami być zgubne. Jeden piksel obrazu tekstury nazywamy tekselem.

Aby skorzystać z jego dobrodziejstw musimy utworzyć jednostkę tekstury, identyfikuje ona jednoznacznie teksturę. Aby przechowywać jednostki tekstur możemy użyć tablicy, w naszym przypadku o rozmiarze 1, bo chcemy trzymać tylko jedną teksturę.
private int[] texIDs = new int[1];
Muszę przyznać, że załadowanie obrazu z pliku i utworzenie tekstury mi się podoba. Cały myk polega na tym, że fabryka bitmap (BitmapFactory) wczytuje z zasobów aplikacji (pierwszy parametr funkcji decodeResource) określony zasób (drugi parametr) i go zwraca. R.drawable.texture to identyfikator pliku texture.png, który znajduje się w folderze res/drawable - zasoby, które można rysować, wspólne dla wszystkich wielkości ekranów. W folderze np. drawable-hdpi znajdują się obrazy wyższej roździelczości i zostaną one automatycznie załadowane, gdy będziemy pracować na urządzeniu, które posiada ekran o dużej rozdzielczości. Ale o tym możesz doczytać w dokumentacji, my dla potrzeb lekcji mamy jedną teksturę dla wszystkich. Przyjmuje się, że rozmiary obrazu tekstury powinny być potęgą dwójki. Funkcja glGenTextures generuje określoną ilość (pierwszy parametr) jednostek tekstur i zapisuje w tablicy (drugi parametr) uwzględniając offset (trzeci parametr).
Przenieśmy się do metody onSurfaceCreated i dodajmy odpowiedni kod.
Bitmap bmp = BitmapFactory.decodeResource(appContext.getResources(), R.drawable.texture);
gl.glGenTextures(1, texIDs, 0);

Aby wykorzystywać daną jednostkę tekstury musimy ją uczynić bieżącą, robi to glBindTexture(int target, int texture). Pierwszy parametr to rodzaj tekstury, drugi to jednostka tekstury. Tekstura musi zostać dopasowana do obiektu, który pokrywa - nazywamy to filtrowaniem tekstur. Wykorzystując funkcję glTexParameterf możemy ustawić odpowiednie filtry:
  • GL_TEXTURE_MAG_FILTER - gdy mamy do czynienia z powiększaniem tekstury.
  • GL_TEXTURE_MIN_FILTER - gdy mamy do czynienia z pomniejszaniem tekstury.

Podstawowe filtry (bez wykorzystania mipmap - tego nie używamy, więc jeśli chcesz wiedzieć więcej, doczytaj na własną rękę):
  • GL_NEAREST - Używa teksela położonego najbliżej środka rysowanego piksela.
  • GL_LINEAR - Stosuje średnią ważoną czterech tekseli położonych najbliżej środka rysowanego piksela.
Klasa GLUtils, która pomaga zintegrować OpenGL ES z Android API, dostarcza nam bardzo przydatną funkcję texImage2D(int target, int level, Bitmap bitmap, int border), która sama wykrywa wewnętrzny format i typ danych podanego obrazu i definiuje obraz tekstury.
  • target - rodzaj tekstury
  • level - używany do określenia poziomu mipmapy, wywołujemy tylko dla poziomu 0.
  • bitmap - obraz tekstury
  • border - szerokość obramowania tekstury

To co wyżej zostało napisane załatwiamy następującymi wywołaniami.
gl.glBindTexture(GL10.GL_TEXTURE_2D, texIDs[0]);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bmp, 0);

Pozostało nam jeszcze utworzyć tablicę współrzędnych tekstury, aby rysując wierzchołki OpenGL ES wiedział jak dopasować do nich teksturę. Każdemu wierzchołkowi musimy przyporządkować odpowiednie współrzędne tekstury z zakresu od 0 do 1. Jeżeli przekroczymy ten zakres, to tekstura będzie powtarzana w danym kierunku. Przyporządkowanie wierzchołkom współrzędnych tekstur przedstawiłem na prowizorycznym rysunku, pod rysunkiem widzimy odpowiadający temu kod.


float quadsTexCoords[] = {
    // Pierwszy
    0.0f, 1.0f,
    1.0f, 1.0f,
    1.0f, 0.0f,
    0.0f, 0.0f,
};

// inicjalizujemy bufor współrzędnych tekstur dla czworokąta
ByteBuffer tcbb = ByteBuffer.allocateDirect(quadsTexCoords.length * 4); 
tcbb.order(ByteOrder.nativeOrder());// użyj naturalnego porządku bajtów
quadsTCB = tcbb.asFloatBuffer();      // utwórz z ByteBuffer FloatBuffer
quadsTCB.put(quadsTexCoords);            // dodaj współrzędne do bufora 
quadsTCB.position(0);                // ustaw pozycję początkową

Nie zapomnijmy o włączeniu teksturowania oraz tablic współrzędnych tekstur w onSurfaceCreated():
gl.glEnable(GL10.GL_TEXTURE_2D);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); // Włącz tablice współrzędnych tekstur

Teraz możemy w końcu narysować nasz czworokąt wykorzystując te wszystkie bufory, które tworzyliśmy. W metodzie onDrawFrame() dodajemy poniższy kod. Omówienia wymagają już tylko dwie funkcje glVertexPointer(int size, int type, int stride, Buffer pointer) oraz glTexCoordPointer(int size, int type, int stride, Buffer pointer), które podają OpenGLowi odpowiednie bufory, z których ma korzystać przy rysowaniu elementów funkcją glDrawElements(), którą już opisałem. Posiadają takie same parametry:
size - ilość współrzędnych przypadających na wierzchołek. W przypadku współrzędnych wierzchołka używamy 3, a tekstury opisujemy dwoma współrzędnymi.
type - typ danych każdej współrzędnej w bufforze
stride - odstęp pomiędzy wierzchołkami w tablicy, u nas wszystko jest upakowane jedno obok drugiego, dlatego wynosi 0.
pointer - bufor z danymi
// Rysuj czworokąt
gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, quadsVB);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, quadsTCB);
gl.glDrawElements(GL10.GL_TRIANGLES, 6, GL10.GL_UNSIGNED_SHORT, quadsIB);

Gotowe, oto co powinniśmy ujrzeć na ekranie naszego urządzenia - kwadrat z nałożoną teksturą.



<< Poprzednia lekcja Następna lekcja >>
Materiały do pobrania:

Brak komentarzy:

Prześlij komentarz