Titelbild: evtl. Androide mit Cardboard-Brille?

Papp-App
Android-Programmierung für Google Cardboard mit OpenGL

Google hat mit seinem 3D-Brillen-Bastelkit Cardboard das Eintauchen in virtuelle Realitäten für Smartphone-Besitzer einfach und erschwinglich gemacht. Mit dem gleichnamigen SDK lassen sich nun auch eigene 3D-Apps für die Pappbrille schreiben.
 

Zahlreiche Apps sind bereits für Google-Cardboard erschienen, einige davon durchaus beeindruckend [1]. Dabei sind die ersten Schritte zu einer eigenen 3D-App gar nicht so schwer. Wir werfen einen Blick auf das von Google kostenlos zur Verfügung gestellte Cardboard-SDK und die zugrunde liegende OpenGL-Programmierung auf Android-Geräten.

Google bietet das Cardboard-SDK in zwei Varianten an: einmal für die Verwendung mit dem Spiele-Framework Unity (allerdings in der kostenpflichtigen Pro-Version), und einmal in einer Variante, die direkt auf das 3D-Render-Framework OpenGL aufsetzt. Wir zeigen im folgenden die Programmierung mit OpenGL.

Leider ist die von Google mitgelieferte Beispiel-App bereits ziemlich komplex und nur schwach dokumentiert. Zum Verständnis des Codings sind detaillierte Kenntnisse in der OpenGL-Programmierung vonnöten. Wir haben daher für diesen Artikel versucht, ein möglichst einfaches Beispiel zu implementieren. Der Wow-Faktor ist zugegeben nicht besonders groß, die Reduktion auf das wesentliche hilft aber, zunächst einmal die Grundideen zu verstehen. 

Google Cardboard setzt auf OpenGL für die dreidimensionale Darstellung von Objekten auf. Android unterstützte OpenGL ES (Open Graphics Library for Embedded Systems) schon immer; die für Cardboard benötigte Version OpenGL ES20 ist seit Android 2.2 verfügbar.  

Hallo Dreieck

Jedes noch so komplexe OpenGL-Objekt wird aus einer Menge von Dreiecken zusammengesetzt. Das Pendant zu "Hello World" in OpenGL ist daher ein Programm, das ein einzelnes Dreieck zeichnet. Bis dieses Dreieck auf dem Schirm erscheint, sind allerdings erstmal ein paar Vorarbeiten zu leisten.

Zunächst sind die Raum-Koordinaten des Objektes, in OpenGL Vertex (Plural Vertices) genannt, festzulegen. OpenGL verwendet ein rechtshändiges Koordinatensystem: dabei gibt der Daumen der rechten Hand die x-Achse an und zeigt nach rechts, der Zeigefinger (y-Achse) zeigt nach oben, und der Mittelfinger (z-Achse) nach vorn auf den Betrachter. Drei Vertices mit jeweils drei Parametern x, y und z beschreiben ein Dreieck eindeutig im Raum. Dieses Dreieck wird schließlich als eine Menge von Pixeln, in OpenGL Fragments genannt, auf dem Schirm dargestellt.

Um das Objekt in der richtigen Größe und Lage auf den Bildschirm zu bringen, werden die Vertex-Koordinaten mit einer Reihe von Matrizen multipliziert. Die sogenannte Model-Matrix verschiebt, dreht und skaliert das Objekt im Raum, die View-Matrix beschreibt die Kamera-Perspektive und die aus der Multiplikation beider resultierende Model-View-Perspective-Matrix beschreibt schließlich die Abbildung auf das  Display des Betrachters. Zum Glück muss man die Matrizen nicht alle selbst ausrechnen, diverse Hilfsmethoden vereinfachen die Arbeit. So erzeugt etwa die folgende Methode

	float cameraMatrix[]=new float[16];
	Matrix.setLookAtM(cameraMatrix,0,eyex,eyey,eyez,centerx,centery,centerz,upx,upy,upz);

eine Transformationsmatrix, die einem Kamera-Blick von Position (eyex,eyey,eyez) nach (centerx,centery,centerz) entspricht, wobei der Vektor (upx,upy,upz) nach oben zeigt. Der zweite Parameter gibt bei den meisten OpenGL-Methoden den Startindex des Ergebnis-Arrays an und ist normalerweise 0. Mit

    Matrix.multiplyMM(result,0,matrix1,0,matrix2,0);

multipliziert man zwei Matrizen.

OpenGL stammt aus der C/C++-Welt und kann mit der in Java verwendeten dynamischen Speicherverwaltung nichts anfangen. Java-Arrays mit Objektkoordinaten oder andere Daten wie z.B. Farben müssen daher mit speziellen Befehlen jeweils vor der Verwendung in ein OpenGL-kompatibles Format umgewandelt werden. Das folgende Coding

     float triangleCoords[] = {0,1,0, ...};

	vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();
	vertexBuffer.put(triangleCoords);
	vertexBuffer.put(triangleCoords);

vermittelt zwischen den Welten.

Die oben erwähnten Matrix-Operationen werden nicht in Java, sondern als Maschinencode ausgeführt. Jedes OpenGL-Programm enthält dazu mindestens zwei sogenannte Shader. Das sind Mini-C++-Programme, die zur Laufzeit der App vor der ersten Verwendung  kompiliert werden. Ein Shader vom Typ GL_VERTEX_SHADER berechnet die finale Position aller Vertices, während ein Shader vom Typ GL_FRAGMENT_SHADER während der sogenannten Rasterung aufgerufen wird und die Farbe jedes einzelnen Pixels festlegt. Wann und für welche Werte die Shader tatsächlich aufgerufen werden, bestimmt das OpenGL-Framework. Durch die Ausführung in Maschinencode ist die Darstellung auch für komplexe Objekte unglaublich schnell. 

Ein einfacher Vertex-Shader sieht wie folgt aus:

    private final String VERTEX_SHADER =
		"uniform mat4 uMVPMatrix;" + // Model-View-Projektions-Matrix
		"attribute vec4 vPosition;" + // Position
		"void main() {" +
		"  gl_Position = uMVPMatrix * vPosition;" + // Position auf dem Bildschirm
		"}";
		
Er berechnet die jeweilige Position gl_Position auf dem Bildschirm  durch Multiplikation der übergebenen Model-View-Projektionsmatrix uMVPMatrix mit dem Vertex vPosition.

Im einfachsten Fall weist der Fragment-Shader jedem Pixel dieselbe Farbe vColor zu

    private final String FRAGMENT_SHADER =
		"uniform vec4 vColor;" + // Farbe
		"void main() {" +
		"  gl_FragColor = vColor;" +
		"}";

Wichtig: der Vertex-Shader wird für jeden Vertex (Eckpunkt) des Objekts aufgerufen, der Fragment-Shader dagegen für jedes Pixel auf dem Schirm.

Das folgende Coding kompiliert und linkt die beiden Shader zusammen.

	mProgram = GLES20.glCreateProgram();             
	GLES20.glAttachShader(mProgram, vertexShader);   
	GLES20.glAttachShader(mProgram, fragmentShader); 
	GLES20.glLinkProgram(mProgram);    
			
Mit diesen Vorarbeiten kann man schließlich mittels
 
	GLES20.glUseProgram(mProgram);

	GLES20.glVertexAttribPointer(
			vPosition, COORDS_PER_VERTEX,
			GLES20.GL_FLOAT, false,
			0 , vertexBuffer)

	GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);

ein einzelnes Dreieck auf den Bildschirm zeichnen. Der zweite Parameter gibt wie üblich den Start-Index im Vertex-Array an, der letzte Parameter bestimmte die Anzahl der zu zeichnenden Vertices.

In Stereo

Nach diesem kleinen Exkurs in die OpenGL-Programmierung soll es nun an die stereoskopische Darstellung mit dem Cardboard-SDK gehen.

Das SDK besteht aus der Bibliothek cardboard.jar und einer Hilfsbibliothek libprotobuf-java-2.3-nano.jar. Beide liegen im Verzeichnis libs des Google-Beispielprojekts (siehe Soft-Link). Cardboard benötigt mindestens Android-Version 16 (Jelly Bean). 

Im AndroidManifest der App sind einige Einstellungen vorzunehmen. Nicht jedes Android-Gerät hat die nötige Rechen-Power, um OpenGL-Programme auszuführen, daher erlaubt die Zeile 

	<uses-feature android:glEsVersion="0x00020000" android:required="true" />

die Installation nur auf entsprechend kompatiblen Geräten. Das Cardboard-SDK benötigt die Berechtigungen

    <uses-permission android:name="android.permission.NFC"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

für das automatische Starten der Google Cardboard-App wenn ein NFC-Tag gefunden wird, sowie für benutzerspezifische Anpassungen der Stereo-Darstellung.

Ein Intent-Filter

    <category android:name="com.google.intent.category.CARDBOARD" />

sorgt dafür, dass die offizielle Google Cardboard App die eigene App findet und in ihren Katalog aufnimmt.

Da man bei aufgesetzter 3D-Brille schlecht aufs Display tippen kann, enthalten Cardboard-Apps keine Buttons oder andere interaktive Elemente auf dem Display. Neben der Bewegung der Brille bzw. des Kopfes im Raum ist nur eine einzige, über einen Magneten an der Brille ausgelöste Interaktion möglich. Cardboard-Apps sind immer Vollbildschirm-Apps, die im Querformat laufen, und bestehen normalerweise aus nur einer Activity. Diese ist von der Google-Klasse CardboardActivity abgeleitet, welche automatisch für das Ausblenden von Status- und Action-Bar sorgt. Das Layout enthält einen einzelnen View, den CardboardView, der von GLSurfaceView abgeleitet ist. Er wird mit   
      
	setCardboardView(cardboardView);

der Activity bekannt gemacht und mit

    cardboardView.setRenderer(StereoRenderer renderer)

wird ein Renderer festgelegt, der für das Zeichnen des Screens mittels OpenGL zuständig ist. Der Einfachheit halber implementiert unsere Activity direkt das StereoRenderer-Interface. Von dessen verschiedenen Methoden sind insbesondere onSurfaceCreated und onDrawEye wichtig.

    public void onSurfaceCreated(EGLConfig eglConfig) 

wird aufgerufen, wenn das OpenGL-System initialisiert wurde. Hier ist der richtige Ort, um OpenGL-Initialisierungen wie das Laden von Vertices und das Kompilieren der Shader zu starten.
	
Die Hauptarbeit geschieht in

    public void onDrawEye(Eye eye) 

An dieser Stelle wird der OpenGL-Frame gezeichnet und zwar für jedes Auge aus einer entsprechend leicht versetzten Perspektive. Die Klasse Eye enthält dazu in eye.getEyeView() die Transformationsmatrix für das entsprechende Auge. Angewendet darauf ergibt sich die Model-View-Projektionsmatrix für den OpenGL-Shader:

	float camera[]=new float[16];
	Matrix.setLookAtM(camera,0,
			0, 0.04f, 0.0f,// eye x,y,z
			0, 0,0, // center x,y,z
			0, 0,1); // up x,y,z
	float view[]=new float[16];
	Matrix.multiplyMM(view,0,eye.getEyeView(),0,camera,0);

Betätigt der Anwender den Magnetschalter an der Brille, ruft die CardboardActivity den Callback

public void onCardboardTrigger()

auf. In unserem einfachen Beispiel ändern wir bei jedem Auslösen des Triggers die Farbe des Dreiecks.

Ausbau

Ein flaches Dreieck ist natürlich nicht so richtig spannend für eine 3D-Brille. In einer Ausbaustufe haben wir daher das c’t-Logo in die OpenGL-Cardboard-Welt übertragen. Am einfachsten kommt man an die benötigten Vertices, wenn man ein geeignetes Blender- oder SketchUp-Modell, z.B. aus [2], als obj-Datei exportiert. Anders als der Name suggeriert sind Dateien im obj-Format einfache Text-Dateien, die sich mit einem beliebigen Text-Editor anschauen und bearbeiten lassen. Für uns interessant sind die mit v beginnenden Zeilen, die jeweils die Koordinaten eines einzelnen Vertices enthalten, sowie die mit vn beginnenden Normalen (Senkrechten) der aus den Vertices gebildeten Dreiecke. 

Mit einem kleinen Script wie z.B. obj2opengl.pl von Heiko Behrens (siehe Soft-Link) lassen sich aus der obj-Datei Konstanten für C bzw. Java generieren. Kleiner Schönheitsfehler: Das Skript erzeugt Konstanten im Double-Format (wie etwa 0.1) - OpenGL für Java erwartet aber Floats (0.1f). Mit einem globalen Suchen/Ersetzen ist die Umwandlung jedoch schnell erledigt. Die Klassen CTLogoVertices und CTLogoNormals enthalten die generierten Arrays. Eine Java-Methode, und dazu gehört auch die Initialisierung eines Arrays, darf nur maximal 64 kB groß werden. Für das c’t-Logo reicht das gerade so aus, bei umfangreicheren Objekten kommt man aber um das Einlesen und Umwandlung der obj-Datei zur Laufzeit nicht herum.

Da das Objekt einfarbig sehr flach wirkt, fügen wir noch einen Beleuchtungs-Shader hinzu. Dieser berechnet den Helligkeitswert jedes einzelnen Pixels durch Multiplikation der Normalen mit der Ausrichtung zur Lichtquelle - Einzelheiten dazu finden Sie z.B. in einem der OpenGL-Tutorials auf www.learnopengles.com.

Fazit

Die dreidimensionale Darstellung von Objekten mit der Google-Cardboard-Brille ist dank des zugehörigen SDKs gar nicht so schwer. Die Darstellung eines einfachen geometrischen Objekts setzt nur eine geringe Einarbeitung in OpenGL voraus. Wer allerdings richtige 3D-Spiele oder andere anspruchsvollere Programme für die CardBoard-Virtual-Reality schreiben will, kommt  um die eingehende Beschäftigung mit Unity oder einem anderen auf OpenGL basierenden Framework nicht herum.

Literatur

@lit:[1]~~Jan-Keno Janssen, Mittendrin statt nur 3D, Das Smartphone wird zur Virtual-Reality-Brille, c't**7/15, S.**88
@lit:[2]~~Peter König, Mischmasch-Mesh, 3D-Modelle aus Fundstücken collagieren, c't**14/13, S.**82

Bilder

dreieck-cardboard.png
Unser einfaches Beispielprogramm zeichnet lediglich ein im Raum schwebendes flaches Dreieck - das man aber dank Cardboard von allen Seiten betrachten kann.

ctlogo-cardboard.png
Das c’t Logo als dreidimensionales Objekt in Google Cardboard.

blender-export.png
In Blender exportiert man das Object als *.obj-Datei und wählt "Write Normals" und "Triangulate Faces" aus.

google-cardboard-app.png
Mit dem richtigen Intent-Filter erscheint die eigene App auch in der Google-Cardboard-Übersicht.


Links
Google-Cardboard-Doku
https://developers.google.com/cardboard/android/

Cardboard-Bibliothek und Beispielprojekt
https://github.com/googlesamples/cardboard-java/tree/master/CardboardSample/

Offizielle Google Cardboard-App
https://play.google.com/store/apps/details?id=com.google.samples.apps.cardboarddemo

Script zur Generierung von Vertex-Konstanten aus einer .obj Datei. 
http://heikobehrens.net/2009/08/27/obj2opengl/

OpenGL ES Dokumentation und Tutorials
https://www.khronos.org/opengles/sdk/docs/man/
http://www.learnopengles.com/
http://www.jayway.com/2009/12/03/opengl-es-tutorial-for-android-part-i/

