Grafika 3D w systemie Android (OpenGL ES) Cel: narysować oświetlony sześcian obłożony teksturą Dwie ważne klasy: GLSurfaceView (klasa widoku) i GLSurfaceView.Renderer (interfejs, który wymusza zdefiniowanie trzech metod: onSurfaceCreated - inicjacja onDrawFrame - odświeżanie viewportu onSurfaceChanged - zmiana parametrów viewportu (np. obrót urządzenia) Android >= 1.0 - OpenGL ES 1.0 Android >= 2.2 - OpenGL ES 2.0 Użyjemy najniższej wersji 1.0. Tą wersję wspiera emulator. Tutorial OpenGL ES 2.0: http://www.learnopengles.com/android-lesson-one-getting-started/ 1. Projekt dla OpenGL ES a. Tworzymy nowy projekt typu Android Application Project o nazwie Grafika3D, pakiet pl.umk.fizyka.grafika3D b. Build SDK (ja wybrałem Android 1.6 (API 4), ale może być dowolna wersja) c. Minimum SDK - to samo d. Create Activity - BlankActivity o nazwie MainActivity e. Przechodzimy do pliku MainActivity.java: import android.opengl.GLSurfaceView; oraz do metody MainActivity.onCreate dodajemy: public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); GLSurfaceView view = new GLSurfaceView(this); view.setRenderer(new OpenGLRenderer()); setContentView(view); } f. Klasy OpenGLRenderer jeszcze nie ma - trzeba ją zdefiniować (musi implementować interface android.opengl.GLSurfaceView.Renderer). Oto szkielet z wymaganymi metodami: class OpenGLRenderer implements android.opengl.GLSurfaceView.Renderer { public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub } public void onDrawFrame(GL10 gl) { // TODO Auto-generated method stub } public void onSurfaceChanged(GL10 gl, int width, int height) { // TODO Auto-generated method stub } } g. Warto dodać dwie metody sterujące do klasy aktywności: @Override protected void onPause() { super.onPause(); glView.onPause(); } @Override protected void onResume() { super.onResume(); glView.onResume(); } h. Po zastąpieniu domyślnego widoku aktywnosci budowanego na bazie pliku res/layout/activity_main.xml przez widok OpenGL, plik XML można usunąć z projektu (nie pomyslić z plikiem res/menu/activity_main.xml). W klasie OpenGLRenderer będzie cały dalszy kod. Można projekt uruchomić - będzie czarny ekran z tytułem. 2. Konfiguracja a. Po uruchomieniu aplikacji uruchamiana jest metoda GLSurfaceView.Renderer.onSurfaceCreated. W niej umieszczamy ogólne ustawienia (kolor do czyszczenia bufora, włączenie testu głębi itd.) public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glShadeModel(GL10.GL_SMOOTH); //ustawienia testu glebii (takie, jak domylne) gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); gl.glDisable(GL10.GL_DITHER); } b. Po niej, a także za każdym razem gdy zmienią się parametry wyświetlania (np. w wyniku obrotu urządzenia), wywoływana jest metoda onSurfaceChanged. Dlatego tam umieszczamy polecenia konfigurujące wyświetlanie, w szczególności macierz perspektywy i model-widok OpenGL. Tu polecenia te "wyjęte" zosały do metody pomocniczej ustawieniaSceny: private void ustawienieSceny(GL10 gl,int szer,int wys) { gl.glViewport(0, 0, szer, wys); gl.glMatrixMode(GL10.GL_PROJECTION); //przełączenie na macierz projekcji gl.glLoadIdentity(); //left,right,bottom,top,znear,zfar (clipping) //float wsp=wys/(float)szer; //gl.glFrustumf(-0.1f, 0.1f, wsp*-0.1f, wsp*0.1f, 0.3f, 100.0f); GLU.gluPerspective(gl, 45.0f, (float)szer/(float)wys, 0.1f, 100.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); //powrót do macierzy widoku modelu gl.glLoadIdentity(); GLU.gluLookAt( gl, 0, 0, 7.5f, //polozenie kamery 0, 0, 0, //cel 0, 1, 0); //polaryzacja } public void onSurfaceChanged(GL10 gl, int width, int height) { if (height == 0) height = 1; ustawienieSceny(gl,width,height); } c. Wreszcie w metodzie onDrawFrame, która uruchamiana jest każdorazowo, gdy odświeżony ma być widok, umieszczamy polecenie bezpośrednio związane z rysowaniem. Na razie będzie to tylko czyszczenie ramki: public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); } 3. Rysowanie sześcianu (bryła 3D) W OpenGL na Androidzie nie ma możliwości przesyłania werteksów "w locie" do karty graficznej. Należy je obowiązkowo umieścić wcześniej w buforze werteksów. Nie ma więc polecenia glVertex3f. a. Zacznijmy od zdefiniowania tablicy werteksów, którą potem umieścimy w buforze. Kolejność werteksów w tablicy nie jest przypadkowa - związana jest z nawijaniem, które wyznacza front i tył trójkątów, z których zbudowana będzie bryła. //pola klasy OpenGLRenderer private final float a = 1f; private float[] tablicaWerteksow = { //tylnia a,-a,-a, -a,-a,-a, a,a,-a, -a,a,-a, //przednia -a,-a,a, a,-a,a, -a,a,a, a,a,a, //prawa a,-a,a, a,-a,-a, a,a,a, a,a,-a, //lewa -a,-a,-a, -a,-a,a, -a,a,-a, -a,a,a, //gorna -a,a,a, a,a,a, -a,a,-a, a,a,-a, //dolna -a,-a,-a, a,-a,-a, -a,-a,a, a,-a,a }; b. Zdefiniujmy bufor werteksów i zainicjujmy go w metodzie pomocniczej: private FloatBuffer buforWerteksow_Polozenia; private void inicjujBuforWerteksow() { ByteBuffer vbb = ByteBuffer.allocateDirect(tablicaWerteksow.length * 4); //float = 4 bajty vbb.order(ByteOrder.nativeOrder()); buforWerteksow_Polozenia = vbb.asFloatBuffer(); //konwersja z bajtów do float buforWerteksow_Polozenia.put(tablicaWerteksow); //kopiowanie danych do bufora buforWerteksow_Polozenia.position(0); //rewind } c. metodę tę wywołajmy z metody onSurfaceCreated: public void onSurfaceCreated(GL10 gl, EGLConfig config) { ... inicjujBuforWerteksow(); } d. Do rysowanie sześcianu użyjemy kolejnej funkcji pomocniczej: float kolory[][] = { {1f,0f,0f,1f}, //tylnia {1f,0f,0f,1f}, //przednia {0f,1f,0f,1f}, //prawa {0f,1f,0f,1f}, //lewa {0f,0f,1f,1f}, //gorna {0f,0f,1f,1f} //dolna }; private void rysujSzescian(GL10 gl,float krawedz,boolean kolor) { gl.glFrontFace(GL10.GL_CCW); //przednie sciany wskazane przez nawijanie przeciwne do ruchu wskazówek zegara gl.glEnable(GL10.GL_CULL_FACE); //usuwanie tylnich powierzchni gl.glCullFace(GL10.GL_BACK); if(krawedz!=1.0f) gl.glPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buforWerteksow_Polozenia); if(!kolor) gl.glColor4f(1f, 1f, 1f, 1f); for(int i=0;i<6;++i) { if(kolor) gl.glColor4f(kolory[i][0], kolory[i][1], kolory[i][2], kolory[i][3]); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, i*4, 4); } gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisable(GL10.GL_CULL_FACE); } e. I wreszcie narysujmy sześcian: public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) rysujSzescian(gl,1.0f,true); gl.glPopMatrix(); //zdejmij ze stosu macierzy = odtworz zapamietany stan } Teraz wreszcie po uruchomieniu powinniśmy coś zobaczyć. Będzie to niestety tylko przednia ściana sześcianu - czerwony kwadrat. Kilka uwag: Dzięki funkcji glScale można uzyskać dowolny prostopadłościan. Metody glPushMatrix i glPopMatrix - zapobiegają kumulacji skalowania (i ew. innych przekształceń na macierzy model-widok). f. Animacja. Aby zobaczyć nie tylko jedną ścianę, dodajmy obroty. W każdej klatce niech to będzie obrót o jeden stopień wokół osi wyznaczonej przez wektor (1,3,0). public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glRotatef(1, 1, 3, 0); gl.glPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) rysujSzescian(gl,1.0f,true); gl.glPopMatrix(); //zdejmij ze stosu macierzy = odtworz zapamietany stan } 4. Oświetlenie a. Wyłączmy kolorowanie ścian sześcianu (ostatni argument metody rysujSzescian zmianiamy na false). Bryła straci przestrzenność. b. Przestrzenność przywróci światło rozproszone. To wymaga jednak wzbogacenie werteksów o definicje normalnych. Zacznijmy od tabeli normalnych: float normalne[][] = { {0f,0f,-1f}, //tylnia {0f,0f,1f}, //przednia {1f,0f,0f}, //prawa {-1f,0f,0f}, //lewa {0f,1f,0f}, //gorna {0f,-1f,0f} //dolna }; c. Uzupełnijmy metodę rysujSzescian: private void rysujSzescian(GL10 gl,float krawedz,boolean kolor) { ... if(!kolor) gl.glColor4f(1f, 1f, 1f, 1f); for(int i=0;i<6;++i) { gl.glNormal3f(normalne[i][0], normalne[i][1], normalne[i][2]); if(kolor) gl.glColor4f(kolory[i][0], kolory[i][1], kolory[i][2], kolory[i][3]); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, i*4, 4); } ... } d. Włączamy pod-system oświetlenia i definiujemy dwa źródła światła: private void oswietlenie(GL10 gl) { gl.glEnable(GL10.GL_LIGHTING); //wlaczenie systemu oswietlania //nie ma color-material, wiec kolory w werteksach sa ignorowane (tylko tekstury) //źródła światła mlecznaZarowka(gl,GL10.GL_LIGHT1); reflektor(gl,GL10.GL_LIGHT2); } private void mlecznaZarowka(GL10 gl,int zrodloSwiatla) { final float kolor1_rozproszone[]={0.5f,0.5f,0.5f,1.0f}; gl.glLightfv(zrodloSwiatla,GL10.GL_DIFFUSE,kolor1_rozproszone,0); gl.glEnable(zrodloSwiatla); } private void reflektor(GL10 gl,int zrodloSwiatla) { final float kolor_rozproszone[]={0.3f,0.3f,0.3f,1.0f}; final float kolor_reflektora[]={1.0f,1.0f,1.0f,1.0f}; final float pozycja[]={0.0f,-10.0f,10.0f,1.0f}; final float szerokosc_wiazki=60.0f; //w stopniach gl.glLightfv(zrodloSwiatla,GL10.GL_POSITION,pozycja,0); gl.glLightfv(zrodloSwiatla,GL10.GL_DIFFUSE,kolor_rozproszone,0); gl.glLightfv(zrodloSwiatla,GL10.GL_SPECULAR,kolor_reflektora,0); gl.glLightf(zrodloSwiatla,GL10.GL_SPOT_CUTOFF,szerokosc_wiazki); gl.glEnable(zrodloSwiatla); } e. Metodę oswietlenie nalezy uruchomic z onSurfaceCreated: public void onSurfaceCreated(GL10 gl, EGLConfig config) { ... inicjujBuforWerteksow(); oswietlenie(gl); } Po włączeniu oświetlenia emulator może działac mniej wydajnie. 5. Teksturowanie a. Zdefiniujmy współrzędne teksturowania dla każdej ściany. Będziemy je musieli później obowiązkowo umieścić w buforze. Oprócz tego definiujemy pole reprezentujące bufor oraz flagę, która umożliwi włączanie i wyłączanie teksturowania: final boolean teksturowanie = true; float[] texCoords = { //tylnia 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, //przednia 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, //prawa 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, //lewa 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, //gorna 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, //dolna 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; private FloatBuffer buforWerteksow_WspTeksturowania; b. Do metody inicjujBuforWerteksow dodajmy inicjację bufora współrzędnych teksturowania private void inicjujBuforWerteksow() { ByteBuffer vbb = ByteBuffer.allocateDirect(tablicaWerteksow.length * 4); //float = 4 bajty vbb.order(ByteOrder.nativeOrder()); // Use native byte order buforWerteksow_Polozenia = vbb.asFloatBuffer(); // Convert from byte to float buforWerteksow_Polozenia.put(tablicaWerteksow); // Copy data into buffer buforWerteksow_Polozenia.position(0); // Rewind if(teksturowanie) { ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length * 4); tbb.order(ByteOrder.nativeOrder()); buforWerteksow_WspTeksturowania = tbb.asFloatBuffer(); buforWerteksow_WspTeksturowania.put(texCoords); buforWerteksow_WspTeksturowania.position(0); } } c. Tekstury należy przypiąć do ścian sześcianu (my z każdą ścianyą zwiążemy tą samą teksturę) private void rysujSzescian(GL10 gl,float krawedz,boolean kolor) { ... if(teksturowanie) { gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, buforWerteksow_WspTeksturowania); } if(!kolor) gl.glColor4f(1f, 1f, 1f, 1f); for(int i=0;i<6;++i) { gl.glNormal3f(normalne[i][0], normalne[i][1], normalne[i][2]); if(kolor) gl.glColor4f(kolory[i][0], kolory[i][1], kolory[i][2], kolory[i][3]); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, i*4, 4); } ...l } e. I wreszcie należy wczytać tekstury z pliku dołączonego do zasobów. Plik ten należy dodać do katalogu res/drawable-* np. res/drawable-hdpi Uwaga! Plik tekstury (np. PNG) powinien mieć rozmiary będące potęgami dwójki np. 256x512 int[] textureIDs = new int[1]; //tablica identyfikatorów //Uwaga! Oba rozmiary tekstury powinny być potęgami dwójki np. 256x256 public void loadTexture(GL10 gl, Context context) { gl.glGenTextures(1, textureIDs, 0); //twórz tablicę 1D na tekstury gl.glBindTexture(GL10.GL_TEXTURE_2D, textureIDs[0]); //Wiązanie tekstur z identyfikatorami z tablicy gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); //ustawienie filtrów tekstur gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); //wczytywanie obrazu z res\drawable-hdpi\tejstura.png InputStream istream = context.getResources().openRawResource(R.drawable.tekstura); Bitmap bitmap; try { //dekoduj do obrazu bitmap = BitmapFactory.decodeStream(istream); } finally { try { istream.close(); } catch(IOException e) { } } //buduj tejsturę z odczytanej bitmapy GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0); bitmap.recycle(); } f. Metoda pozwalająca na dostęp do zasobów wymaga odwołania do kontekstu. Musimy go wobec tego przekazać do klasy OpenGLRender. Zrobimy to definiując konstruktor: class OpenGLRenderer implements android.opengl.GLSurfaceView.Renderer { Context context; public OpenGLRenderer(Context context) { this.context=context; } g. W konsekwencji należy zmienić polecenie tworzące powierzchnię GL w metodzie MainActivity.onCreate public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); //plik XML jest niepotrzebny!!! glView = new GLSurfaceView(this); glView.setRenderer(new OpenGLRenderer(getApplicationContext())); setContentView(glView); } h. Wreszcie należy wywołać metodę wczytującą teksturę w metodzie onSurfaceCreated: public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glShadeModel(GL10.GL_SMOOTH); //ustawienia testu glebii (takie, jak domylne) gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); gl.glDisable(GL10.GL_DITHER); inicjujBuforWerteksow(); oswietlenie(gl); if(teksturowanie) { loadTexture(gl, context); gl.glEnable(GL10.GL_TEXTURE_2D); } else gl.glDisable(GL10.GL_TEXTURE_2D); } 6. Sterowanie obrotami sześcianu za pomocą klawiszy i dotyku a. Przygotujmy projekt na sterownaie klawiszami - wciśnięckie klawiszy kierunkowych spowoduje obrót sześcianu wokół osi X i Y (obrót będzie trwał dopóki, dopóty klawisz jest wciśnięty) float katX=0; float katY=0; public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glRotatef(katY, 0, 1, 0); gl.glRotatef(katX, 1, 0, 0); //bez tego klawisze wplywaja na przyspieszenie katX=0; katY=0; gl.glPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) rysujSzescian(gl,1.0f,false); gl.glPopMatrix(); //zdejmij ze stosu macierzy = odtworz zapamietany stan } b. Wykrywanie wciśnięcia klawiszy najłatwiej będzie zrobić z poziomu aktywności w metodzie MainActivity.onKeyDown: final float zmianaKataKlawiatura = 5; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_W: case KeyEvent.KEYCODE_DPAD_UP: renderer.katX-=1; break; case KeyEvent.KEYCODE_S: case KeyEvent.KEYCODE_DPAD_DOWN: renderer.katX+=1; break; case KeyEvent.KEYCODE_A: case KeyEvent.KEYCODE_DPAD_LEFT: renderer.katY-=1; break; case KeyEvent.KEYCODE_D: case KeyEvent.KEYCODE_DPAD_RIGHT: renderer.katY+=1; break; } return super.onKeyDown(keyCode, event); } Obsługujemy klawisze strzałek oraz WSAD. c. Obiekt renderer użyty w powyższym kodzie to instancja klasy OpenGLRenderer. Aby mieć do niej dostęp, należy dodać pole do aktywności: public class MainActivity extends Activity { GLSurfaceView glView; OpenGLRenderer renderer; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); //plik XML jest niepotrzebny!!! glView = new GLSurfaceView(this); renderer = new OpenGLRenderer(getApplicationContext()); glView.setRenderer(renderer); setContentView(glView); } Możemy teraz uruchomić aplikację. Zwróćmy uwagę, że obroty następują wokół osi lokalnego układu współrzędnych sześcianu. Tak sterowalibyśmy samolotem, ale w tym przypadku wolałbym, aby sześcian obracał się wokół osi układu odniesienia związanego ze sceną. Warto też zauważyć, że analogiczne polecenia w DirectX lub XNA powodowałyby obroty wokół osi układu związanego ze sceną. Powodem jest to, że w OpenGL mnożenie macierzy jest post-multiplication, a w DirectX/XNA - pre-multiplication. Jeżeli chcemy uzyskać obroty wokół osi związanych ze sceną, powinniśmy zmienić kolejność mnożenia macierzy. d. Zdefiniujmy wobec tego macierz m zbierającą przekształcenia i użyjmy jej: float katX=0; float katY=0; float m[]=new float[] {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); //gl.glRotatef(katY, 0, 1, 0); //gl.glRotatef(katX, 1, 0, 0); float yx[]=new float[] {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; Matrix.rotateM(yx, 0, katY, 0, 1, 0); Matrix.rotateM(yx, 0, katX, 1, 0, 0); Matrix.multiplyMM(m, 0, m, 0, yx, 0); //osie lokalne //Matrix.multiplyMM(m, 0, yx, 0, m, 0); //osie sceny //bez tego klawisze wplywaja na przyspieszenie katX=0; katY=0; gl.glPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) gl.glMultMatrixf(m, 0); rysujSzescian(gl,1.0f,false); gl.glPopMatrix(); //zdejmij ze stosu macierzy = odtworz zapamietany stan } Te polecenia odtwarzają wcześniejszy stan. Macierz bieżąca m określająca orientację bryły jest bowiem mnożona z prawej strony (post-multiplication) przez macierz przekształcenia yx. Wystarczy jednak zmienić czynniki w tym iloczynie, aby obracać sześcian wokół osi nieruchomych. //Matrix.multiplyMM(m, 0, m, 0, yx, 0); //osie lokalne Matrix.multiplyMM(m, 0, yx, 0, m, 0); //osie sceny c. Do sterowania obrotami via pola katX i katY można podpiąć również dotyk. W tym celu w klasie aktywnści należy nadpisać metodę onTouchEvent. Uwaga! Najlepszym rozwiązaniem byłby jednak ArcBall. boolean poprzednie = false; float poprzednieX = 0; float poprzednieY = 0; final float skalowanieZmianyKataDotyk = 0.002f; float skalowanieX; float skalowanieY; @Override public boolean onTouchEvent(MotionEvent event) { int akcja = event.getAction(); switch(akcja) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: poprzednie = false; break; case MotionEvent.ACTION_MOVE: float X = event.getX(); float Y = event.getY(); if(poprzednie) { float dX=X-poprzednieX; float dY=Y-poprzednieY; renderer.katX+=skalowanieX*dY; //zamiana współrzędnych, bo oś/kierunek renderer.katY+=skalowanieY*dX; } else { poprzednie=true; Display wyswietlacz=getWindowManager().getDefaultDisplay(); skalowanieX=wyswietlacz.getWidth()/2.0f*skalowanieZmianyKataDotyk; skalowanieY=wyswietlacz.getHeight()/2.0f*skalowanieZmianyKataDotyk; } poprzednieX=X; poprzednieY=Y; break; } return super.onTouchEvent(event); } 7. Opcjonalne: kontrolowanie obrotów sześcianu za pomocą orientacji urządzenia a. Klasa OpenGLRenderer powinna implementować interfejs SensorEventListener class OpenGLRenderer implements android.opengl.GLSurfaceView.Renderer, SensorEventListener { b. Dwie metody wymagane przez ten interfejs metody i dodatkowe pola: public void onAccuracyChanged(Sensor sensor, int accuracy) { } private SensorManager sensorManager = null; Sensor orientacja = null; float oz_azymut = 0; //stopnie float ox_pochylenie = 0; float oy_nachylenie = 0; float o_dlugosc = 0; boolean o_0 = true; float ox_0 = 0; float oy_0 = 0; float skalowaniePrzechylenia = 1; long czas; public void onSensorChanged(SensorEvent event) { if(event.sensor.getType()==Sensor.TYPE_ORIENTATION) { oz_azymut = event.values[0]; ox_pochylenie = event.values[1]; oy_nachylenie = event.values[2]; if(o_0) { czas=System.currentTimeMillis(); ox_0=ox_pochylenie; oy_0=oy_nachylenie; o_0=false; } ox_pochylenie-=ox_0; oy_nachylenie-=oy_0; float prog = 1; //stopnie if(Math.abs(ox_pochylenie)0) { katX/=dt; katY/=dt; } } } Różnice względem wcześniej opisanego rozwiązania: - kalibracja do położenia w momencie uruchamiania - próg c. W metodzie onSurfaceCreated należy zarejestrować czujnik: public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glShadeModel(GL10.GL_SMOOTH); //ustawienia testu glebii (takie, jak domylne) gl.glClearDepthf(1.0f); gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); gl.glDisable(GL10.GL_DITHER); inicjujBuforWerteksow(); oswietlenie(gl); if(teksturowanie) { loadTexture(gl, context); gl.glEnable(GL10.GL_TEXTURE_2D); } else gl.glDisable(GL10.GL_TEXTURE_2D); //czujniki if(sensorManager==null) { sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); orientacja = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); sensorManager.registerListener(this,orientacja,SensorManager.SENSOR_DELAY_GAME); } } Zwróćmy uwagę, że rejestrując nasłuchiwacz użyłem mniejszego opóźnienia tj. zamiast SENSOR_DELAY_NORMAL wartość SENSOR_DELAY_GAME -------- Kilka różnic między pełnym OpenGL i OpenGL ES 1) buforowanie werteksów jest obowiązkowe, 2) nie ma mechanizmu Color-Material, 3) buforowanie współrzędnych teksturowania jest obowiązkowe, 4) tekstury nawet na dobrym urządzeniach powinny mieć rozmiary będące potęgami 2.