Toolbox Interactiondesign


Emerging Technologies

Sensoren für Virtual Reality

Virtual Reality

Benjamin Hatscher, Emerging Technologies bei Sascha Reinhold

Ein handelsübliches Android-Smartphone kann mit einer einfachen Konstruktion zur Virtual Reality-Brille umfunktioniert werden. Die Firma Durovis hat eine solche Halterung entwickelt und die Baupläne direkt zum kostenlosen Download für den eigenen 3D-Drucker veröffentlich, Open Dive genannt. Parallel existiert eine Kauf-Version namens Durovis Dive. Beide funktionieren nach dem gleichen Prinzip:

Ein Smartphone wird durch das Gestell direkt vor den Augen am Kopf fixiert (HeadMounted Display). Es befinden sich zwei Linsen im Gestell, die trotz den kurzen Abstandes zwischen Augen und Bildschirm scharfes sehen ermöglichen. Zugehörige Software teilt den Bildschirm in zwei Bereiche, so dass jedes Auge ein leicht versetztes Bild zu sehen bekommt. Dadurch entsteht der dreidimensionale Eindruck.

Zugehörige Software erfasst nun die Kopfbewegungen mittels der Bewegungssensoren des Smartphones. Das 3D-Bild wird damit immer auf die Blickrichtung des Kopfes angepasst.

Mitgeliefert werden bisher einige wenige Anwendungen. Der große Vorteil dieses Systems ist jedoch die Verwendung der weit verbreiteten, kostenlosen Spiele-Entwicklungsumgebung „Unity“. Dadurch lassen sich eigene virtuelle Realitäten erschaffen – sei es zu Forschungszwecken oder zur Unterhaltung.

geeignete Eingabemöglichkeiten fehlen

Durovis Dive ermöglicht zwar Virtual Reality-Umgebungen für wenig Geld und mit einfachen Mitteln in das eigene Wohnzimmer zu holen, neue Eingabemöglichkeiten dafür sind jedoch noch nicht zu haben. Maus und Tastatur fühlen sich in einer VR-Umgebung nicht nur falsch an, viele Interaktionen im Raum lassen sich damit auch gar nicht abbilden.

 Sensoren die als interessante Eingabemöglichkeiten in Frage kommen gibt es viele. Auch Fachwissen in Elektrotechnik ist dank Arduino nicht mehr unbedingt nötig. Einzig die Anbindung solcher Sensordaten an eine Unity-Szene wie sie die Dive benutzt fehlt bisher.

Anbindung auf Umwegen

Dabei gibt es jedoch einige Unwägbarkeiten zu meistern:

Sensordaten lassen sich am einfachsten über eine Bluetoothverbindung an Smartphones und somit auch an Unity-Apps übertragen. OpenDive bietet einzig Unity als Entwicklungsumgebung an. Unity kann zwar Applikationen als Android-Apps erstellen, jedoch auf keine weiteren Android-Schnittstellen wie z.B. die Bluetooth-Verbindung zugreifen.

Um Sensordaten über eine Bluetoothverbindung in einer Unity-App verwenden zu können, muss abgefragt werden, welche Daten über die Bluetooth-Schnittstellen Empfangen werden. Dies gelingt nur, wenn eine Unity Andwendung als Android-Projekt exportiert wird. Nachträglich lässt sich das Unity-Projekt um native Android-Funktionen erweitern, und ermöglicht somit das Durchreichen von Bluetooth-Daten an Unity. Dort können z.B. Objektpositionen oder sonstige Eigenschaften von Sensordaten abhängig gemacht werden.

Anmerkung

Einen kompletten Einstieg in alle nötigen Technolgien kann hier leider nicht geboten werden.

Arduino bietet hier einen Einstieg: arduino.cc/en/Guide/HomePage

Für Android empfehle ich die Trainings-Sektion unter developer.android.com/training/index.html

Auch Unity bietet eine große Anzahl einsteigerfreundlicher Tutorials: unity3d.com/learn/tutorials/modules

Die oben beschriebe Smartphone-Halterung kann mittels eines 3D-Druckers selbst gedruckt (Open Dive), oder vom Entwickler Durovis erworben werden: durovis.com

1. Schritt – Sensordaten erfassen und über Bluetooth verschicken

String btBuffer;
char btChar;

int valX = 0;
int valY = 0;
int valZ = 0;

int correctX[5];
int correctY[5];
int correctZ[5];

int corCounter = 0;

long commandDelay = 0;
long calib = 500;

int treshold = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial.println("Bluetooth On please wait....");
  //pinMode(ledpin,OUTPUT);
  
  //commandCall("V92");
}

void loop() {
  if (millis() >= commandDelay) {
    analogWrite(3, 0);
  }
   
  
  // ======================= BESCHLEUNIGUNGSSENSOR
    
    corCounter++;
    if (corCounter > 4) corCounter = 0;
    
    correctX[corCounter] = valX;
    correctY[corCounter] = valY;
    correctZ[corCounter] = valZ;
    
    float corX = 0;
    float corY = 0;
    float corZ = 0;
    
    for (int i=0;i<5;i++) {
      corX+=correctX[i];
      corY+=correctY[i];
      corZ+=correctZ[i];
    }
    corX/=5;
    corY/=5;
    corZ/=5;
    
    valX = analogRead(0);
    valY = analogRead(1);
    valZ = analogRead(2);
    
    Serial.print("X");
    Serial.print(valX - corX);
    Serial.print(":Y");
    Serial.print(valY - corY);
    Serial.print(":Z");
    Serial.println(valZ - corZ);
    delay(500);
  
  // ======================= BLUETOOTH-KOMMUNIKATION
  
  if (Serial.available()) {
    btChar = Serial.read();
    if (btChar != '\r\n') {
      btBuffer += btChar;
    } else {
      commandCall(btBuffer);
      btBuffer = "";
    }
  }
}
  
void commandCall(String command) {
  switch (command[0]) {
    case 'V':
      if (millis() > commandDelay) {
          unsigned long strength = charToLong(command[1]);
          unsigned long duration = charToLong(command[2]);
          if (strength > 9) strength = 9;
          if (strength < 3) strength = 3;
          
          analogWrite(3, strength*15);
          commandDelay = millis()+(duration*100);
      }      
    break;
    case 'T':
        treshold = charToLong(command[1]);
    break;
  }
}

unsigned long charToLong(char input) {
  unsigned long num = 0;
  num = num * 10 + (input - '0');
  return num;
}

Bei dem Eingabegerät handelt es sich in meinem Fall um einen Arduino-Nano-Nachbau mit einem 3-Achsen-Beschleunigungssensor sowie einem Bluetooth-Modul, versorgt über eine 9V-Batterie. Das Bluetooth-Modul wird über die serielle Schnittstelle angesprochen.

Das Arduino-Script ließt ständig den Sensor aus und gibt einen String mit allen drei Werten an das Bluetooth-Modul weiter. Die Werte des Sensors werden vor der Ausgabe über eine Durchschnittsberechnung der letzten 5 Werte gelättet um Sprünge in den Werten zu mindern.

2. Schritt – Im Unity-Projekt Sensordaten von Android entgegen nehmen

Die Reihenfolge klingt zwar verwirrend, macht aber Sinn: zuerst wird das Unity-Projekt angelegt und das Empfangen der Sensordaten vorbereitet. Erst danach wird das Unity-Projekt als Android-Projekt gespeichert und die Daten von der Bluetooth-Schnittstelle durchgegeben. Dies ist nötig, da Unity nur den kompletten Export als Android-Projekt erlaubt. Alternativ könnte ein Unity-Plugin geschrieben werden um die nachfolgenden Modifikationen im Android-Projekt vor zu nehmen, dies hätte nur unverhältnismäßigen Mehraufwand bedeutet. Für eine beispielhafte Vorführung reicht die folgende Methode aus.

using UnityEngine;
using System.Collections;

public class movement : MonoBehaviour {
	
	private readonly AndroidJavaClass _ActivityClass;
	private readonly AndroidJavaObject _ActivityObject;
	private readonly AndroidJavaClass _MyActivityClass;
	
	string newPos = "0:0:0";
	
	void Start () {
		
	}
	
	void Update () {
		
		AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
		AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
		
		newPos = activity.Call<String>("getBTValues");
		
		string[] arr = newPos.Split(':');
		float newX = float.Parse (arr[0])/2000;
		float newY = float.Parse (arr[1])/2000;
		float newZ = float.Parse (arr[2])/2000;
		Vector3 temp = new Vector3(newX,newY,newZ);
		transform.Translate(temp);
		
	}
}

In Unity stellt sich das auslesen der Sesordaten verhältnismäßig einfach dar. In der Update-Funktion eine beliebigen Game-Objektes muss zuerst eine Referenz auf die Klasse „AndroidJavaClass“ erzeugt werden. Diese Instanz namens „unityPlayer“ umschließt das Unity-Programm und macht es als Android-App lauffähig. Mit dem Aufruf „Call“ können über diese Instanz dann beliebige Methoden aufgerufen werden. So kann eine Kommunikation von Unity mit nativen Android-Funktionen realisiert werden. In unserem Fall rufen wir mit „Call“ die später in Android implementierte Funktion „getBTValues“ auf. Diese Funktion wird einen String der Sensorwerte zurückliefern. Deshalb folgt gleich darauf ein parsen des Rückgabewertes zu drei Werten, die in den drei Variablen „newX“, „newY“ und „newZ“ abgelegt werden. Um eine Auswirkung in unserer Testanwendung feststellen zu können, verändern sich die Positionsdaten des betroffenen GameObjects in Unity abhängig von den Sensordaten.

Dieses Script muss natürlich zuletzt noch einem Unity-GameObject zugewiesen werden.

3. Schritt – Sensordaten in Android für Unity bereitstellen

    public String getBTValues(){
    	String tempVals = mService.getBTValue();
    	Log.d("BTCon", tempVals);
  		return tempVals;
    }

Das oben beschriebene Unity-Projekt kann jetzt als Android-Projekt expoertiert werden. Heraus kommt ein Android-Projekt, dessen interessanteste Klasse für uns „UnityPlayerNativeActivityim „src“-Ordner ist. Hier muss die Funktion, welche Unity im vorran gegangenen Abschnitt aufruft angelegt werden.

Wie zu erkennen ist, wird der Aufruf mit dem Rückgabewert der Methode „getBTValue“ der Instanz „mService“ beatwortet. Bei „mService“ handelt es sich um einen Service, der die Bluetooth-Kommunikation übernimmt. Dieser kann auch bei geschlossener App im Hintergrund weiter laufen und die Kommunikation aufrecht erhalten.

package com.interactiondesign_hs_magdeburg;

import android.app.Service;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.widget.Toast;
import android.os.Process;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.UUID;

import com.interactiondesign_hs_magdeburg.BTCon.ConnectedThread;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Service;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import android.bluetooth.*;
import android.content.Intent;

@SuppressLint("HandlerLeak")
public class BTService extends Service {
	
	private final IBinder mBinder = new LocalBinder();
	
	  private Looper mServiceLooper;
	  private ServiceHandler mServiceHandler;
	  
	  private static final String TAG = "BTCon";
	  
	  final static String MY_ACTION = "MY_ACTION";
	  
    public String output = "xxx";
    private static Activity mActivity;
    BluetoothAdapter mBluetoothAdapter;
    Handler h;
    
    final int RECIEVE_MESSAGE = 1;
    private BluetoothAdapter btAdapter = null;
    private BluetoothSocket btSocket = null;
    private StringBuilder sb = new StringBuilder();
    
    private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    
    // MAC-address of Bluetooth module (you must edit this line)
    private static String address = "xx:xx:xx:xx:xx:xx";
    
    private ConnectedThread mConnectedThread;

	  // Handler that receives messages from the thread
	  private final class ServiceHandler extends Handler {
	      public ServiceHandler(Looper looper) {
	          super(looper);
	    	  //Log.i(TAG, "...Handler receiving...");
	      }
	  }

	  @Override
	  public void onCreate() {
	    HandlerThread thread = new HandlerThread("ServiceStartArguments",
	            Process.THREAD_PRIORITY_BACKGROUND);
	    thread.start();
	    mServiceLooper = thread.getLooper();
	    mServiceHandler = new ServiceHandler(mServiceLooper);
	    
	    h = new Handler() {
	      	public void handleMessage(android.os.Message msg) {
	      		switch (msg.what) {
	              case RECIEVE_MESSAGE:			
	              	byte[] readBuf = (byte[]) msg.obj;
	              	String strIncom = new String(readBuf, 0, msg.arg1);				
	              	sb.append(strIncom);			
	              	int endOfLineIndex = sb.indexOf("\r\n");	
	              	if (endOfLineIndex > 0) { 			
	              		output = sb.substring(0, endOfLineIndex);	
	                      sb.delete(0, sb.length());									// and clear
	                  }
	              	
	                String string = output;
	                Intent intent = new Intent();
	                intent.putExtra("string", string);
	                sendBroadcast(intent);
	              	
	              	break;
	      		 }
	          };
	  	  };
	  	  onResume();
	  }
	  
	  private void checkBTState() {
	      if(btAdapter==null) { 
	      } else {
	        if (btAdapter.isEnabled()) {
	        } else {
	          Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
	        }
	      }
	    }

	  @Override
	  public int onStartCommand(Intent intent, int flags, int startId) {
	      Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

	      Message msg = mServiceHandler.obtainMessage();
	      msg.arg1 = startId;
	      mServiceHandler.sendMessage(msg);
	      return START_STICKY;
	  }
	  
	  public class LocalBinder extends Binder {
	        BTService getService() {
	            return BTService.this;
	        }
	    }

	  @Override
	  public IBinder onBind(Intent intent) {
	      return mBinder;
	  }
	    public String getBTValue() {
	      return output;
	    }
	  
	  @Override
	  public void onDestroy() {
	        Log.d(TAG, "...kill it...");
	    Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show(); 
	  }

	  public void onResume() {
	     
	        Log.d(TAG, "...onResume - try connect...");
	       
	        btAdapter = BluetoothAdapter.getDefaultAdapter();

	        BluetoothDevice device = btAdapter.getRemoteDevice(address);
	        
	    	try {
	    		btSocket = createBluetoothSocket(device);
	    	} catch (IOException e) {
		        Log.d(TAG, "...no Device... "+e.getMessage());
	    	}
	        btAdapter.cancelDiscovery();
	        try {
	          btSocket.connect();
	        } catch (IOException e) {
	          try {
	            btSocket.close();
	          } catch (IOException e2) {
	          }
	        }
	         
	        mConnectedThread = new ConnectedThread(btSocket);
	        mConnectedThread.start();
	      }
	    
	    private BluetoothSocket createBluetoothSocket(BluetoothDevice device) throws IOException {
	        if(Build.VERSION.SDK_INT >= 10){
	            try {
	                final Method  m = device.getClass().getMethod("createInsecureRfcommSocketToServiceRecord", new Class[] { UUID.class });
	                return (BluetoothSocket) m.invoke(device, MY_UUID);
	            } catch (Exception e) {
	            }
	        }
	        return  device.createRfcommSocketToServiceRecord(MY_UUID);
	    }
	  private class ConnectedThread extends Thread {
	  	    private final InputStream mmInStream;
	  	    public ConnectedThread(BluetoothSocket socket) {
	  	        InputStream tmpIn = null;
	  	        try {
	  	            tmpIn = socket.getInputStream();
	  	        } catch (IOException e) { }
	  	 
	  	        mmInStream = tmpIn;
	  	    }
	  	 
	  	    public void run() {
	  	        byte[] buffer = new byte[255]; 
	  	        int bytes;

	  	        while (true) {
	  	        	try {
	  	                bytes = mmInStream.read(buffer);	
	                      h.obtainMessage(RECIEVE_MESSAGE, bytes, -1, buffer).sendToTarget();	 message queue Handler
	  	            } catch (IOException e) {
	  	                break;
	  	            }
	  	        }
	  	    }
	  	 
	  	}
	}

Bluetooth-Service-Tutorials lassen sich leicht auf unterschiedlichen Plattformen finden. der Vollständigkeit halber hier der Code des Services.

Die MAC-Adresse des Bluetooth-Moduls muss jedoch nachgetragen werden.

  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Zuletzt muss der Android-App noch das Recht eingeräumt werden, auf die Bluetooth-funktionalität des Smartphones zugreifen zu dürfen. Dazu werden die nebenstehenden Zeilen an die bereits eingetragenen uses-permissions in der Datei „AndroidManifest.xml“ angefügt.

 

fertig – bei erfolgreichem Bluetooth-Pairing sollte die Unity-Anwendung auf dem Android-Device jetzt auf Änderungen des Sensors – in meinem Fall ein 3-Achsen-Beschleinigungssensor – regieren. Bei korrekter Konfiguration verändert das GameObject, dem das Script zugewiesen wurde die Position, abhängig von der Sensorbewegung.

Fazit

Dieses auf Unity und Android basierende System bietet durch seine Offenheit perfekte Eignung als Werkzeug zur Erprobung virtueller Umgebungen. Nicht nur dass der Inhalt an sich selbst generierbar ist, auch Eingabemöglichkeiten und Schnittstellen jeglicher Art lassen sich anbinden und erproben.

06.06.2014 | Benjamin Hatscher |