Skrypt dla Xamarin przygotowany przez Kevina Springera na podstawie skryptu dla Android Studio (http://fizyka.umk.pl/~jacek/dydaktyka/mobilne/android_studio/OpenGL_ES.txt) Grafika 3D w systemie Android w technologii Xamarin (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) 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ądzeniu powinny mieć rozmiary będące potęgami 2. Projekt dla OpenGL ES 1. Tworzymy nowy projekt typu Android Application Project o nazwie 2. Przechodzimy do metody MainActivity.onCreate i następnie: a. komentujemy SetContentView(Resource.Layout.activity_main); b. przygotowujemy pola prywatne glView oraz renderer b. I dodajemy następujące linie kodu: GLSurfaceView view = new GLSurfaceView(this); view.setRenderer(new OpenGLRenderer()); SetContentView(view); c. Wynik: public class MainActivity : Activity { private GLSurfaceView glView; private OpenGLRenderer renderer; protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); glView = new GLSurfaceView(this); renderer = new OpenGLRenderer(ApplicationContext, true); // zamiast getApplicationContext -> ApplicationContext glView.SetRenderer(renderer); // Set our view from the "main" layout resource SetContentView(glView); } .... } 3. Klasy OpenGLRenderer jeszcze nie ma i trzeba je zdefiniować (musi implementować klasę Java.Lang.Object i interfejs GLSurfaceView.IRenderer) class OpenGLRenderer : Java.Lang.Object, GLSurfaceView.IRenderer { public void OnSurfaceCreated(IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config) { } public void OnDrawFrame(IGL10 gl) { } public void OnSurfaceChanged(IGL10 gl, int width, int height) { } } 4. Warto dodać dwie metody sterujące do klasy aktywności: public class MainActivity : Activity { ... protected override void OnPause() { base.OnPause(); glView.OnPause(); } protected override void OnResume() { base.OnResume(); glView.OnResume(); } } Notka: 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). 5. W klasie OpenGLRenderer będzie cały dalszy kod. Można projekt uruchomić - będzie czarny ekran z tytułem. Następnie konfiguracja 6. Po uruchomieniu aplikacji uruchamiana jest metoda GLSurfaceView.IRenderer.OnSurfaceCreated. W niej umieszczamy ogólne ustawienia (kolor do czyszczenia bufora, włączenie testu głębi itd.) // W funkcji OnSurfaceCreated w klasie OpenGLRenderer public void OnSurfaceCreated(IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config) { gl.GlShadeModel(GL10.GlSmooth); //ustawienia testu glebii (takie, jak domylne) gl.GlClearDepthf(1.0f); gl.GlEnable(GL10.GlDepthTest); gl.GlDepthFunc(GL10.GlLequal); gl.GlHint(GL10.GlPerspectiveCorrectionHint, GL10.GlNicest); gl.GlDisable(GL10.GlDither); } 7. 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(IGL10 gl, int szer, int wys) { gl.GlViewport(0, 0, szer, wys); gl.GlMatrixMode(GL10.GlProjection); //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.GlModelview); //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 } 8. W OnSurfaceChanged public void OnSurfaceChanged(IGL10 gl, int width, int height) { if (height == 0) height = 1; UstawienieSceny(gl,width,height); } 9. 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(IGL10 gl) { gl.GlClear(GL10.GlColorBufferBit | GL10.GlDepthBufferBit); } 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. 10. 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 const 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 }; 11. Zdefiniujmy bufor werteksów i zainicjujmy go w metodzie pomocniczej: // dodajemy Java.Nio 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 } 12. Metodę tę wywołajmy z metody OnSurfaceCreated: public void OnSurfaceCreated(IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config) { ... InicjujBuforWerteksow(); } 13. Do rysowania 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(IGL10 gl, float krawedz, bool kolor) { gl.GlFrontFace(GL10.GlCcw); //przednie sciany wskazane przez nawijanie przeciwne do ruchu wskazówek zegara gl.GlEnable(GL10.GlCullFaceCapability); //usuwanie tylnich powierzchni gl.GlCullFace(GL10.GlBack); if (krawedz != 1.0f) gl.GlPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) gl.GlEnableClientState(GL10.GlVertexArray); gl.GlVertexPointer(3, GL10.GlFloat, 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.GlTriangleStrip, i * 4, 4); } gl.GlDisableClientState(GL10.GlTextureCoordArray); gl.GlDisableClientState(GL10.GlVertexArray); gl.GlDisable(GL10.GlCullFaceCapability); } 14. I wreszcie narysujemy sześcian: public void OnDrawFrame(IGL10 gl) { gl.GlClear(GL10.GlColorBufferBit | GL10.GlDepthBufferBit); 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). Animacja 15. 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(IGL10 gl) { gl.GlClear(GL10.GlColorBufferBit | GL10.GlDepthBufferBit); gl.GlRotatef(1, 1, 3, 0); // <--- dodajemy to gl.GlPushMatrix(); //zapamietaj macierz model-widok (wloz na stos macierzy) ... } Oświetlenie 15. Wyłączmy kolorowanie ścian sześcianu (ostatni argument metody RysujSzescian zmianiamy na false). Bryła straci przestrzenność. 16. Przestrzenność przywróci światło rozproszone. To wymaga jednak wzbogacenie werteksów o definicje normalnych. Zacznijmy od tabeli normalnych: private float[,] normalne = new float[,] { {0f,0f,-1f}, //tylnia {0f,0f,1f}, //przednia {1f,0f,0f}, //prawa {-1f,0f,0f}, //lewa {0f,1f,0f}, //gorna {0f,-1f,0f} //dolna }; 17.Uzupełnijmy metodę RysujSzescian: private void RysujSzescian(IGL10 gl, float krawedz, bool 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]); // Dodajemy tę linię if (kolor) gl.GlColor4f(kolory[i,0], kolory[i,1], kolory[i,2], kolory[i,3]); gl.GlDrawArrays(GL10.GlTriangleStrip, i * 4, 4); } ... } 18. Włączamy pod-system oświetlenia i definiujemy dwa źródła światła: private void Oswietlenie(IGL10 gl) { gl.GlEnable(GL10.GlLighting); //wlaczenie systemu oswietlania //nie ma color-material, wiec kolory w werteksach sa ignorowane (tylko tekstury) //źródła światła MlecznaZarowka(gl, GL10.GlLight1); Reflektor(gl, GL10.GlLight2); } private void MlecznaZarowka(IGL10 gl, int zrodloSwiatla) { float[] kolor1_rozproszone = { 0.5f, 0.5f, 0.5f, 1.0f }; gl.GlLightfv(zrodloSwiatla, GL10.GlDiffuse, kolor1_rozproszone, 0); gl.GlEnable(zrodloSwiatla); } private void Reflektor(IGL10 gl, int zrodloSwiatla) { float[] kolor_rozproszone = { 0.3f, 0.3f, 0.3f, 1.0f }; float[] kolor_reflektora = { 1.0f, 1.0f, 1.0f, 1.0f }; float[] pozycja = { 0.0f, -10.0f, 10.0f, 1.0f }; float szerokosc_wiazki = 60.0f; //w stopniach gl.GlLightfv(zrodloSwiatla, GL10.GlPosition, pozycja, 0); gl.GlLightfv(zrodloSwiatla, GL10.GlDiffuse, kolor_rozproszone, 0); gl.GlLightfv(zrodloSwiatla, GL10.GlSpecular, kolor_reflektora, 0); gl.GlLightf(zrodloSwiatla, GL10.GlSpotCutoff, szerokosc_wiazki); gl.GlEnable(zrodloSwiatla); } 19. Metodę oswietlenie nalezy uruchomic z OnSurfaceCreated: public void OnSurfaceCreated(IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config) { ... InicjujBuforWerteksow(); Oswietlenie(gl); } Uwaga: Po włączeniu oświetlenia emulator może działac mniej wydajnie. Teksturowanie 20. 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: private const bool teksturowanie = true; private 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; 21. Do metody InicjujBuforWerteksow dodajmy inicjację bufora współrzędnych teksturowania private void InicjujBuforWerteksow() { ... 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); } } 22. Tekstury należy przypiąć do ścian sześcianu (my z każdą ścianyą zwiążemy tą samą teksturę) private void RysujSzescian(IGL10 gl, float krawedz, bool kolor) { ... if (teksturowanie) { gl.GlEnableClientState(GL10.GlTextureCoordArray); gl.GlTexCoordPointer(2, GL10.GlFloat, 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]); // Dodajemy tę linię if (kolor) gl.GlColor4f(kolory[i,0], kolory[i,1], kolory[i,2], kolory[i,3]); gl.GlDrawArrays(GL10.GlTriangleStrip, i * 4, 4); } ... } 23. 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 public void LoadTexture(IGL10 gl, Context context) { gl.GlGenTextures(1, textureIDs, 0); //twórz tablicę 1D na tekstury gl.GlBindTexture(GL10.GlTexture2d, textureIDs[0]); //Wiązanie tekstur z identyfikatorami z tablicy gl.GlTexParameterf(GL10.GlTexture2d, GL10.GlTextureMinFilter, GL10.GlNearest); //ustawienie filtrów tekstur gl.GlTexParameterf(GL10.GlTexture2d, GL10.GlTextureMagFilter, GL10.GlLinear); //wczytywanie obrazu z res\drawable-hdpi\tejstura.png System.IO.Stream istream = context.Resources.OpenRawResource(Resource.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.GlTexture2d, 0, bitmap, 0); bitmap.Recycle(); } 24. 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: public class OpenGLRenderer : Java.Lang.Object, GLSurfaceView.IRenderer { private Context context; public OpenGLRenderer(Context context) { this.context = context; } .... } 25. W konsekwencji należy zmienić polecenie tworzące powierzchnię GL w metodzie MainActivity.OnCreate protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); glView = new GLSurfaceView(this); renderer = new OpenGLRenderer(this); // odwołujemy się tutaj to this glView.SetRenderer(renderer); SetContentView(glView); } 26. Wreszcie należy wywołać metodę wczytującą teksturę w metodzie OnSurfaceCreated: public void OnSurfaceCreated(IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config) { ... Oswietlenie(gl); if (teksturowanie) { LoadTexture(gl, context); gl.GlEnable(GL10.GlTexture2d); } else gl.GlDisable(GL10.GlTexture2d); } 27. 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) public void OnDrawFrame(IGL10 gl) { gl.GlClear(GL10.GlColorBufferBit | GL10.GlDepthBufferBit); //gl.GlRotatef(1, 1, 3, 0); komentujemy bo niepotrzebne 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) ... } 28. Wykrywanie wciśnięcia klawiszy najłatwiej będzie zrobić z poziomu aktywności w metodzie MainActivity.onKeyDown: public override bool OnKeyDown(Keycode keyCode, KeyEvent e) { switch (keyCode) { case Keycode.W: case Keycode.DpadUp: renderer.katX -= 1; break; case Keycode.S: case Keycode.DpadDown: renderer.katX += 1; break; case Keycode.A: case Keycode.DpadLeft: renderer.katY -= 1; break; case Keycode.D: case Keycode.DpadRight: renderer.katY += 1; break; } return base.OnKeyDown(keyCode, e); } Obsługujemy klawisze strzałek oraz WSAD. 29. 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: protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); glView = new GLSurfaceView(this); renderer = new OpenGLRenderer(ApplicationContext, true); // dodajemy flage na tekstury oraz AppContext 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. 30. Zdefiniujmy wobec tego macierz m zbierającą przekształcenia i użyjmy jej: dodajemy pole do klasy OpenGlRenderer: private float[] m = new float[] { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; public void OnDrawFrame(IGL10 gl) { gl.GlClear(GL10.GlColorBufferBit | GL10.GlDepthBufferBit); //gl.GlRotatef(1, 1, 3, 0); 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 }; Android.Opengl.Matrix.RotateM(yx, 0, katY, 0, 1, 0); Android.Opengl.Matrix.RotateM(yx, 0, katX, 1, 0, 0); Android.Opengl.Matrix.MultiplyMM(m, 0, m, 0, yx, 0); //osie lokalne //Android.Opengl.Matrix.MultiplyMM(m, 0, yx, 0, m, 0); //osie sceny //bez tego klawisze wplywaja na przyspieszenie katX = 0; katY = 0; ... } Android.Opengl.Matrix.MultiplyMM(m, 0, m, 0, yx, 0); //osie lokalne //Android.Opengl.Matrix.MultiplyMM(m, 0, yx, 0, m, 0); //osie sceny 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. 31. 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. private bool poprzednie = false; private float poprzednieX = 0; private float poprzednieY = 0; private const float skalowanieZmianyKataDotyk = 0.002f; private float skalowanieX; private float skalowanieY; public override bool OnTouchEvent(MotionEvent e) { var akcja = e.Action; switch (akcja) { case MotionEventActions.Down: case MotionEventActions.Up: poprzednie = false; break; case MotionEventActions.Move: float X = e.GetX(); float Y = e.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 = WindowManager.DefaultDisplay; skalowanieX = wyswietlacz.Width / 2.0f * skalowanieZmianyKataDotyk; skalowanieY = wyswietlacz.Height / 2.0f * skalowanieZmianyKataDotyk; } poprzednieX = X; poprzednieY = Y; break; } return base.OnTouchEvent(e); } 32. Opcjonalne: kontrolowanie obrotów sześcianu za pomocą orientacji urządzenia w klasie public class OpenGLRenderer : Java.Lang.Object, GLSurfaceView.IRenderer, ISensorEventListener private SensorManager sensorManager = null; Sensor orientacja = null; private float oz_azymut = 0; //stopnie private float ox_pochylenie = 0; private float oy_nachylenie = 0; private float o_dlugosc = 0; private bool o_0 = true; private float ox_0 = 0; private float oy_0 = 0; private float skalowaniePrzechylenia = 1; private long czas; public void OnSensorChanged(SensorEvent e) { if(e.Sensor.Type == SensorType.Orientation) { oz_azymut = e.Values[0]; ox_pochylenie = e.Values[1]; oy_nachylenie = e.Values[2]; if (o_0) { czas = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; 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) < prog) ox_pochylenie = 0; if (Math.Abs(oy_nachylenie) < prog) oy_nachylenie = 0; katX -= skalowaniePrzechylenia * ox_pochylenie; katY -= skalowaniePrzechylenia * oy_nachylenie; long dt = (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond) - czas; czas = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; if(dt > 0) { katX /= dt; katY /= dt; } } } Różnice względem wcześniej opisanego rozwiązania: - kalibracja do położenia w momencie uruchamiania - próg 33. W metodzie onSurfaceCreated należy zarejestrować czujnik: public void OnSurfaceCreated(IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config) { ... if (teksturowanie) { LoadTexture(gl, context); gl.GlEnable(GL10.GlTexture2d); } else gl.GlDisable(GL10.GlTexture2d); //czujniki if (sensorManager == null) { // konieczne użycie JavaCast(); sensorManager = context.GetSystemService(Context.SensorService).JavaCast(); orientacja = sensorManager.GetDefaultSensor(SensorType.Orientation); sensorManager.RegisterListener(this, orientacja, SensorDelay.Game); } } Zwróćmy uwagę, że rejestrując nasłuchiwacz użyłem mniejszego opóźnienia tj. zamiast SENSOR_DELAY_NORMAL wartość SENSOR_DELAY_GAME