Vizualizacija virtualnog 3D objekta korištenjem žiroskopa i senzora gibanja

Dario Filipaj 2014/2015

Uvod

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.


OpenGL ES

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:

  • OpenGL ES 1.0 i 1.1 podržavaju svi Android API leveli,
  • OpenGL ES 2.0, Android API 8+,
  • OpenGL ES 3.0, Android API 18+,
  • OpenGL ES 3.1, Android API 21+.


Senzori

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 Androida

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:

  • Acceleration sensor
  • Gravity sensor
  • Gyroscope
  • Linear acceleration sensor
  • Geomagnetic field senso



Određeni senzori (najčešće pozicijski) pokazuju položaj uređaja s obzirom na zemljin sjeverni pol, a s obzirom na to kako i ti senzori najčešće vračaju kut s obzirom na osi X,Y i Z potrebno je poznavati i koordinatni sustav zemaljske kugle.

U ovom radu korišteni su sljedeći senzori:
  • Orientation
  • Gyroscope
  • Accelerometer
  • Magnetic field
Od navedenih senzora orientation i magnetic filed su pozicijski senzori, dok su žiroskop i akcelerometar senzori pomaka.


Primjer #1 - kocka

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


Primjer #2 - šuplja piramida

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.


Primjer #3 - dva objekta, "kuća"

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();
	}
				


Izvorni kod

Izvorni kod je dostupan na GitHubu ili ovdje,a APK datoteku je moguće preuzeti ovdje.

Literatura