System Android posiada wsparcie dla OpenGL ES w wersji 1.0/1.1 oraz 2.0 (Od wersji Android 2.2 - API level 8). Mamy do niego dostęp albo poprzez androidowe API, albo przy użyciu Android NDK (Native Development Kit), gdzie odwołujemy się bezpośrednio do funkcji OpenGL ES. Będziemy korzystać z tego, co oferuje nam API. Na chwilę obecną emulator nie wspiera OpenGL ES, dlatego do testowania kodu potrzebujemy fizycznego urządzenia. Opis konfiguracji urządzenia i systemu operacyjnego znajduje się w tej części tego kursu. Będę starał się omawiać używane funkcje OpenGL, ale po szczegóły odsyłam do innych źródeł.
Wybór wersji OpenGL ES.
Zastanawiałem się nad wyborem wersji OpenGL - ostatecznie zdecydowałem, że wybieram OpenGL 1.0/1.1, miałem już styczność ze starym OpenGLem, natomiast wersja 2.0 wymusza korzystanie z shaderów. Myślę, że do pisania prostych gierek w zupełności wystarczy OpenGL 1.0.
OpenGL ES znajdziemy w pakietach:
- android.opengl - zapewnia interfejs do klas OpenGL ES 1.0/1.1, jest szybszy od pakietu poniżej.
- javax.microedition.khronos.opengles - zapewnia interfejs do standardowej implementacji OpenGL ES 1.0/1.1.
- android.opengl.GLES20 - zapewnia interfejs do klas OpenGL ES 2.0.
Do dzieła! - tworzymy aplikację wyświetlającą trójkąt
Podstawowymi elementami aplikacji korzystającej z OpenGL ES są dwa elementy:
- GLSurfaceView - płaszczyzna po której będziemy rysować
- GLSurfaceView.Renderer - interfejs, który umożliwia rysowanie po płaszczyźnie
package com.blogspot.pgoralik.androidplay; import java.nio.*; import android.app.Activity; import android.os.Bundle; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.*; import android.opengl.GLES10; import android.opengl.GLSurfaceView; public class TriangleActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); view = new GLSurfaceView(this); view.setRenderer(new AppRenderer()); setContentView(view); } private GLSurfaceView view; }
Do płaszczyzny podpinamy metodą setRenderer() nasz obiekt rysujący AppRenderer, którego klasa znajduje się poniżej:
class AppRenderer implements GLSurfaceView.Renderer{ @Override public void onDrawFrame(GL10 gl) { } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { } }
Klasa AppRenderer implementuje interfejs GLSurfaceView.Renderer, który dostarcza odpowiednie metody:
- onDrawFrame() - wywoływana przy każdym rysowaniu klatki na GLSurfaceView.
- onSurfaceChanged() - wywoływana gdy zmienią się rozmiary GLSurfaceView, np. podczas zmiany orientacji ekranu urządzenia.
- onSurfaceCreated() - wywoływana raz, aby ustawić środowisko GLSurfaceView.
W klasie AppRenderer tworzymy bufor triangleVB przechowujący współrzędne wierzchołków oraz funkcję initShapes(), która utworzy nam trójkąt.
private FloatBuffer triangleVB; private void initShapes(){ float triangleCoords[] = { // X, Y, Z 0.0f, 0.0f, 0, 1.0f, 0.0f, 0, 0.0f, 0.5f, 0 }; // inicjalizujemy bufor wierzchołków (ilość wsp. * rozmiar typu float) ByteBuffer vbb = ByteBuffer.allocateDirect(triangleCoords.length * 4); vbb.order(ByteOrder.nativeOrder());// użyj naturalnego porządku bajtów triangleVB = vbb.asFloatBuffer(); // utwórz z ByteBuffer FloatBuffer triangleVB.put(triangleCoords); // dodaj współrzędne do bufora triangleVB.position(0); // ustaw pozycję początkową }
Teraz dokonujemy ustawienia kilku rzeczy - robimy to tylko raz, dlatego w metodzie onSurfaceCreated() dodajemy:
GLES10.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // Ustaw kolor tła gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // Włącz tablice wierzchołków initShapes(); // tworzy trójkąt
Okno widoku to obszar, po którym rysuje OpenGL, to co wychodzi poza okno widoku jest obcinane- definiuje ono płaszczyznę przycinania. Domyślnie okno widoku rozciąga się na całą płaszczyznę rysowania. W onSurfaceChanged() dopasowujemy okno widoku do zmiany rozmiarów/orientacji płaszczyzny rysowania.
// Ustawiamy rozmiary okna widoku: x, y, szerokość, wysokość okna GLES10.glViewport(0, 0, width, height);
Teraz co rysowanie klatki w onDrawFrame() musimy czyścić bufor koloru (i dodatkowo głębi) oraz rysować nasz trójkąt:
// Wyczyść bufor głębi i koloru ustawioną wcześniej wartością GLES10.glClear(GLES10.GL_COLOR_BUFFER_BIT | GLES10.GL_DEPTH_BUFFER_BIT); gl.glColor4f(0.63671875f, 0.76953125f, 0.22265625f, 1.0f); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, triangleVB); gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
Funkcja glVertexPointer() definiuje tablicę wierzchołków. Pierwszy parametr określa ilość współrzędnych na jeden wierzchołek, drugi to typ danych w jakim są one zapisane, kolejne argumenty to odstęp pomiędzy wierzchołkami w tablicy oraz bufor z danymi.Funkcja glDrawArrays() w prosty sposób rysuje zawartość włączonej tablicy wierzchołków. Argumenty to kolejno: typ podstawowych elementów grafiki, indeks od którego zaczynamy rysowanie oraz ile wierzchołków chcemy narysować.
Uruchom i sprawdź swoją aplikację - widać trójkąt na szarym tle, jesteśmy na dobrej drodze. Ale widać, że trójkąt jest zniekształcony, spodziewaliśmy się, że jego przyprostokątne będą równe.
Rzutowanie i przekształcenia widoku.
Standardowo OpenGL przyjmuje taki układ współrzędnych, że lewy górny róg powierzchni, po której rysuje to punkt (-1, 1), a prawy dolny to (1, -1). Gdy ekran jest kwadratem jest w porządku, ale na obrazku z prawej zniekształcenia są fatalne. Urządzenia z systemem Android mają przeważnie prostokątne wyświetlacze, więc otrzymujemy rozciągnięty obraz:
Źródło: http://developer.android.com |
OpenGL umożliwia dwa typy rzutowania:
- rzutowanie ortograficzne - zachowuje rzeczywiste rozmiary obiektów niezależnie od ich odległości od obserwatora. Przydaje się w komputerowym wspomaganiu projektowania - wystarcza też do gier 2D.
- rzutowanie perspektywiczne - odwzorowuje widzenie rzeczywistego świata - obiekty znajdujące się dalej od obserwatora wydają się mniejsze, niż te, które są bliżej.
W metodzie onSurfaceChanged dodajemy:
gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); if(height > width) { // orientacja pionowa gl.glOrthof(-1, 1, -(float) height/width, (float) height/width, -1, 1); } else { // orientacja pozioma gl.glOrthof(-(float) width/height, (float) width/height, -1, 1, -1, 1); } gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity();
Dzięki powyższemu wybieramy macierz projekcji, następnie ta aktualna macierz jest resetowana - wczytujemy do niej macierz jednostkową. Funkcja glOrthof(float left, float right, float bottom, float top, float nearVal, float farVal) mnoży aktualną macierz przez macierz ortogonalną. Jej parametry określają sposób rzutowania sceny OpenGL - można powiedzieć, że tworzony jest nowy układ współrzędnych o odpowiednich wartościach.
- left, right - Określają współrzędne lewego i prawego krańca płaszczyzny obcinania poziomego.
- bottom, top - Określają współrzędne dolnego i górnego krańca płaszczyzny obcinania pionowego.
- nearVal, farVal - Określają dystans do bliższej i dalszej płaszczyzny obcinania głębi.
Osiągnęliśmy następujący efekt:
W tym wypadku celem było tylko pozbycie się zniekształceń, ale wydaje mi się, że takie ustalanie układu współrzędnych przy tworzeniu gry jest co najmniej dziwne i niekomfortowe. Na chwilę obecną nie wiem jak to się robi w profesjonalnych produkcjach na Androida.
To koniec tej części kursu. Zachęcam do komentowania, wytykania rzeczy które mogłem opisać lepiej, bardziej zrozumiale. Konstruktywna krytyka mile widziana.
<< Poprzednia lekcja | Następna lekcja >> |
Materiały do pobrania: |
Świetny kurs !
OdpowiedzUsuńByłoby miło poczytac kolejne kursy o OpenGL ES :)
pozdrawiam
Dzięki za miłe słowa :). Na chwilę obecną raczej się nie zanosi na rozbudowę (oprócz tego co jest dostępne w innych lekcjach).
UsuńRównież pozdrawiam.