Dario Filipaj 2014/2015
Cilj ovog rada je izraditi mobilnu aplikaciju na Android mobilnom operativnom sustavu pomoću OpenGL ES tehnologije. Aplikacija će simulirati prikaz virtualnog 3D objekta s obzirom na pomicanje samog mobilnog uređaja. Pomicanje će se očitavati pomoću senzora mobilnog uređaja.
Aplikacije je kreirana za minimalni Android API level 14 (Android 4.0), a ciljana je najnovija verzija tj. Android API 21 (Android 5.0). Za kreiranje 3D objekata korištena je OpenGL ES verzija 1.0. Verzija OpenGL ES tehnologije je odabrana kako bi bila što sličnija materijalima i primjerima s kolegija „Računalna grafika“ te kako bi se obuhvatio što veći broj mobilnih uređaja. Novije verzije OpenGL ES zahtijevaju i novije verzije Android OS-a. Specifikacije su kako slijedi:
Android senzori podijeljeni su na senzore pomaka (eng. motion sensors) i senzore pozicije (eng. position sensors).
Kod senzora pomaka dva senzora su uvijek bazirana na hardveru, a to su akcelerometar i žiroskop. Ostali senzori mogu biti bazirani na softveru ili
hardveru. Senzori pomaka se koriste za detektiranje pomaka uređaja, kao što s nagib, trzaj, rotacija, njihanje.
Pozicijski senzori određuju fizički položaj uređaja u okolini. Npr. mogu odrediti relativni položaj uređaja u usporedbi sa sjevernim polom. Većina
ovih senzora je bazirana na hardveru.
Koordinatni sustav je definiran relativno s obzirom na ekran uređaja. Kada je uređaj postavljen uspravno X os pokazuje u desnu stranu, Y os prema gore, a Z os izlazi iz ekrana prema vlasniku uređaja. Koordinatni sustav koriste sljedeći senzori:
Prvi primjer realiziran je pomoću pozicijskog senzora TYPE_ORIENTATION. Riječ je o vrlo jednostavnom,
ali korisnom softverskom senzoru koji je u novijim verzijama zamijenjen modernijim pristupom i skupom senzora (sljedeći primjeri).
Kako bi se koristi senzori i OpenGL funkcije potrebno je na klasu kreirati na sljedeći način:
public class DisplayPyramid extends GLSurfaceView implements Renderer, SensorEventListener{}
Također korištenje određenih senzora potrebno je omogućiti unutar Android manifesta. Preporučuje se sljedeće deklaracije
uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" uses-feature android:name="android.hardware.sensor.gyroscope"Za početak rada sa senzorima potrebno je definirati upravitelj senzora te sam senzor:
private final SensorManager SensorM; private final Sensor Orientation; public DisplayKocka(Context context) { super(context); SensorM = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); Orientation = SensorM.getDefaultSensor(Sensor.TYPE_ORIENTATION); ... }Sljedeći korak je dohvaćanje vrijednosti koji šalje senzor. Svaki senzor ima nekoliko (najčešće tri) eventa tj. tri vrijednosti koje šalje. Kod korištenog senzora to su azimut, pitch i roll, odnosno kutevi na osi x,y i z.
//Rotacija objekta s obzirom na promjene senzora @Override public void onSensorChanged(SensorEvent event) { float azimut = event.values[0]; float pitch = event.values[1]; float roll = event.values[2]; if ( null == bazaAzimut ) { bazaAzimut = azimut; } if ( null == bazaPitch ) { bazaPitch = pitch; } if ( null == bazaRoll ) { bazaRoll = roll; } float azimuthDifference = azimut - bazaAzimut; float pitchDifference = pitch - bazaPitch; float rollDifference = roll - bazaRoll; ykut += rollDifference; xkut += pitchDifference; bazaRoll = roll; bazaPitch = pitch; }Varijable xkut i ykut određuju rotaciju 3D objekta s obzirom na pomicanje uređaja. Potrebno je baze korištenih vrijednosti postaviti na nove vrijednosti kako bi se izbjegao privid neprekidnog rotiranja. Crtanje kocke:
gl.glTranslatef(0.0f, 0.0f, zos); gl.glScalef(0.8f, 0.8f, 0.8f); gl.glRotatef(xkut, 1.0f, 0.0f, 0.0f); gl.glRotatef(ykut, 0.0f, 1.0f, 0.0f); kocka.init(gl);Za kraj je potrebno definirati ponašanje senzora prilikom pauziranja i ponovnog pokretanja aplikacije.
@Override public void onPause() { SensorM.unregisterListener(this); super.onPause(); } @Override public void onResume() { SensorM.registerListener(this, Orientation, SensorManager.SENSOR_DELAY_NORMAL); super.onResume(); }Odgoda (eng. delay) senzora utječe i na njegovu osjetljivost pa je s obzirom na slučaj korištenja potrebno mijenjati ovu vrijednost
U drugom primjeru korišten je jedan senzor pomaka, a to je žiroskop koji se definira kao TYPE_GYROSCOPE.
Za razliku od prethodnog senzora koji je vračao vrijednosti u stupnjevima, žiroskop vrača vrijednosti u rad/s pa je potrebno preračunavanje.
//inicijalizacija senzora private final SensorManager SensorM; private final Sensor orientation; public DisplayPyramid(Context context) { super(context); SensorM = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); orientation = SensorM.getDefaultSensor(Sensor.TYPE_GYROSCOPE); ... }Za preračunavanje koristi se proteklo vrijeme između dva eventa te konstanta NS2S
private static final float NS2S = 1.0f / 1000000000.0f; private final float[] deltaRotationVector = new float[4]; private float timestamp; //Rotacija objekta s obzirom na promjene senzora @Override public void onSensorChanged(SensorEvent event) { if (timestamp != 0) { final float dT = (event.timestamp - timestamp) * NS2S; float axisX = event.values[0]; float axisY = event.values[1]; float axisZ = event.values[2]; // angular speed float omegaMagnitude = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ); // normalizacija if (omegaMagnitude > 50) { axisX /= omegaMagnitude; axisY /= omegaMagnitude; axisZ /= omegaMagnitude; } float thetaOverTwo = omegaMagnitude * dT / 2.0f; float sinThetaOverTwo = (float)Math.sin(thetaOverTwo); float cosThetaOverTwo = (float)Math.cos(thetaOverTwo); deltaRotationVector[0] = sinThetaOverTwo * axisX; deltaRotationVector[1] = sinThetaOverTwo * axisY; deltaRotationVector[2] = sinThetaOverTwo * axisZ; deltaRotationVector[3] = cosThetaOverTwo; } timestamp = event.timestamp; if ( null == bazax ) { bazax = (float)Math.toDegrees(deltaRotationVector[0]); } if ( null == bazay ) { bazay = (float)Math.toDegrees(deltaRotationVector[1]); } if ( null == bazaz ) { bazaz = (float)Math.toDegrees(deltaRotationVector[2]); } float diffx = (float)Math.toDegrees(deltaRotationVector[0]) - bazax; float diffy = (float)Math.toDegrees(deltaRotationVector[1]) - bazay; float diffz = (float)Math.toDegrees(deltaRotationVector[2]) - bazaz; xkut -= diffx; ykut -= diffy; bazax = (float)Math.toDegrees(deltaRotationVector[0]); bazay = (float)Math.toDegrees(deltaRotationVector[1]); bazaz = (float)Math.toDegrees(deltaRotationVector[2]); }Za bolje rezultate trebalo bi ukloniti "šum" i "izgladiti" nagle pomake tj. malo smanjiti osjetljivost.
Treći primjer koristi dva objekta (kocku i piramidu iz prethodnih primjera) i dva senzora (TYPE_ACCELEROMETER i TYPE_MAGNETIC_FIELD). Takđer u trećem primjeru je aktivirana i z os prilikom pomicanja uređaja, odnosno 3D objekta. Accelerometer vraća vrijednosti u metrima po sekundi na kvadrat, dok "magnetsko polje" uređaja vraća vrijednosti u μT (mikroTesla). Koristi se ova kombinacija senzora zbog veće točnosti.
//inicijalizacija senzora private final SensorManager SensorM; private final Sensor accelerometer; private final Sensor magnetometer; public DisplayKuca(Context context) { super(context); SensorM = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); accelerometer = SensorM.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); magnetometer = SensorM.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); ... }S obzirom na to kako su oba senzora jako osjetljiva, dobro je prilikom dohvaćanja vrijednosti smanjiti osjetljivost. Eventi s dva senzora se dohvaćaju naizmjence u jako malim vremenskim intervalima.
public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) mGravity = event.values; if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) mGeomagnetic = event.values; if (mGravity != null && mGeomagnetic != null) { float R[] = new float[9]; float I[] = new float[9]; boolean success = SensorManager.getRotationMatrix(R, I, mGravity, mGeomagnetic); if (success) { float orientation[] = new float[3]; SensorManager.getOrientation(R, orientation); float azimut = (float)Math.toDegrees(orientation[0])*0.1f; float pitch = (float)Math.toDegrees(orientation[1])*0.8f; float roll = (float)Math.toDegrees(orientation[2])*0.8f; if ( null == bazaAzimut ) { bazaAzimut = azimut; } if ( null == bazaPitch ) { bazaPitch = pitch; } if ( null == bazaRoll ) { bazaRoll = roll; } float azimuthDifference = azimut - bazaAzimut; float pitchDifference = pitch - bazaPitch; float rollDifference = roll - bazaRoll; ykut -= rollDifference; xkut += pitchDifference; zos += azimuthDifference; bazaRoll = roll; bazaPitch = pitch; bazaAzimut = azimut; } } }
@Override public void onPause() { SensorM.unregisterListener(this); super.onPause(); } @Override public void onResume() { SensorM.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL); SensorM.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL); super.onResume(); }