Ansätze für AR auf Mobilgeräten sind kein neuer Trend, in den letzten Jahren hat sich jedoch einiges getan: Durch technische Innovationen wie bessere Kameras und leistungsfähigere Hardware sind die Grenzen des machbaren weiter verschoben worden. Auch Spiele wie Pokémon GO haben dem Thema eine medial Aufmerksamkeit beschert, in deren Zug weitere Hersteller AR Apps entwickeln. Um diesem Trend gerecht zu werden, hat Google im März 2018 ARCore veröffentlicht. Mit diesem SDK lassen sich Augmented Reality Apps für Android und iOS erstellen, die auf einer definierten Liste von unterstützen Geräten lauffähig sind. ARCore ist dabei nicht der einzige Ansatz für AR auf Mobilgeräten: Bereits im Vorjahr hatte Apple, gleichzeitig mit der Ankündigung von iOS 11, das ARKit für iOS vorgestellt.
Im Rahmen meines Praxissemesters habe ich mit ARCore eine Android-Anwendung entwickelt, mit der 3D-Modelle im Raum platziert und dargestellt werden können. Zusätzlich kann der User auf verschiedenen Wegen mit den Modellen interagieren. Die 3D-Modelle kommen dabei von Poly, Googles Datenbank für 360-Grad-Fotos, VR-Szenen und 3D-Modelle.
Poly API
Zu Poly gehören neben einer Website und einer RESTful API auch verschiedene Plugins und Toolkits. Mit diesen können 3D-Modelle entweder direkt aus Blender, Maya, Cinema 4D oder 3Ds Max exportiert und zu der Poly-Datenbank hinzugefügt werden oder in Unity oder Unreal importiert werden. In der Beispielanwendung nutze ich die RESTful API, um die Datenbank zu durchsuchen und Modelle herunterzuladen.
Über die API können diverse Informationen zu öffentlichen Assets abgefragt werden. Der Server antwortet dabei im JSON-Format. Außerdem kann bis ins Detail differenziert werden, welche Felder der Assets in den JSON-Objekten enthalten sein sollen. Die Strukturierung der JSON-Objekte kann in der Dokumentation nachgelesen werden und ist meiner Meinung nach übersichtlich und nachvollziehbar. Neben einzelnen Assets kann auch eine Liste von Assets angefordert werden. Auch hierbei kann präzise definiert werden, welche Informationen in der Antwort enthalten sind und vor allem, wonach bei der Auswahl der Assets gefiltert werden sollen. Beispiele für mögliche Filter sind: Kategorie, Dateiformat, Lizenz, Komplexität der 3D-Modelle, Keywörter. Eine komplette Liste der Filterkriterien kann der Dokumentation entnommen werden. Bei jeder Abfrage muss zusätzlich ein gültiger API-Key enthalten sein, der auf der Poly-Webeite generiert wird.
Ein nützliches Tool beim Zusammenbasteln der GET-Request ist Googles apis-explorer. Dort findet man eine Übersicht von allen von der API bereitgestellten Services und kann Feld für Feld seine eigenen Kriterien angeben, die fertige GET-Request und die entsprechende Antwort einsehen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
interface PolyApi { companion object Factory { private const val BASE_URL = "https://poly.googleapis.com/v1/" fun create(): PolyApi { val retrofit = retrofit2.Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() return retrofit.create(PolyApi::class.java) } } @GET("assets") fun getAssetList(@QueryMap(encoded = true) options: Map<String, String>): Call<Assets> @Streaming @GET fun get3DModel(@Url fileUrl:String): Call<ResponseBody> } |
Da die Query aus teils statischen und teils dynamischen Feldern zusammengefügt werden soll, verpacke ich den Funktionsaufruf von oben in einer Wrapper-Klasse UseCasePolyApi. In dieser sind alle statischen Teile der Query definiert. Beim Funktionsaufruf von getAssetList() werden die vom User verwendeten Keywörter und/oder die Kategorie zu der QueryMap hinzugefügt und anschließend an die Implementierung des Interface durchgereicht.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class UseCasePolyApi(private val polyApi: PolyApi) { companion object { private const val FORMAT = "GLTF2" private const val FIELDS = "assets(authorName%2Cdescription%2CdisplayName%2Cformats%2Cname%2Cthumbnail)" // determine the necessary fields private const val API_KEY = "Insert key here" private const val PAGESIZE = "50" } private val queryMap = mutableMapOf( "format" to FORMAT, "fields" to FIELDS, "key" to API_KEY, "pageSize" to PAGESIZE ) fun getAssetList( category: AssetRepository.AssetCategory = AssetRepository.AssetCategory.none, keywords: String? = null ): Call<Assets> { if (category.name != "none") { queryMap["category"] = category.name } if (keywords != null) { queryMap["keywords"] = keywords } return polyApi.getAssetList(queryMap) } ... |
Nach Erhalt der Antwort können die Ergebnisse nach Bedarf entsprechend weiterverarbeitet werden. In meinem Fall verwende ich von jedem Asset Titel, Autor und Thumbnail, um dem User verfügbare 3D-Modelle in einer Liste zu präsentieren.

ARCore
Das wichtigste Feature von ARCore ist, dass das Telefon seine Position im Raum versteht und verfolgt. Durch die Kombination der optischen Daten der Kamera und den Daten der Lage- und Beschleunigungssensoren gelingt dies bei entsprechenden Umgebungsverhältnissen erstaunlich gut. Je mehr optisch-markante Punkte in der Umgebung sind, desto besser funktioniert das Positions-Tracking. In einem leeren Raum mit weißen Wänden und einfarbigem Teppich sollte man also nicht zu viel erwarten. Während der Vorgänger Tango noch einen Tiefensensor benötigte, braucht ARCore diesen nicht.
Weitere Features von ARCore sind Oberflächenerkennung (egal ob horizontal, vertikal oder schräg), Lichtverhältnisse abschätzen und Augmented Images. ARCore kann bis zu 20 Bilder gleichzeitig erkennen und Feedback geben, dass Bilder erkannt wurden und wo diese sich befinden. Was der Entwickler aus diesen Informationen macht, liegt bei ihm. Welche Bilder erkannt werden sollen, muss vorher definiert werden. Dabei gibt es bestimmte Kriterien, die die Bilder erfüllen müssen. Diese Kriterien werden ausführlich in der Dokumentation erläutert.
Wer vor allem dieses Feature nutzen möchte, sollte klären, ob er dazu ARCore einsetzen will. Denn der Schwerpunkt von ARCore liegt nicht auf Augmented Images. Bei einer App, die ausschließlich dieses Feature verwendet, lohnt es sich einen Blick auf zum Beispiel vuforia zu werfen. Vuforia kann in Bezug auf Augmented Images deutlich mehr und dies vor allem besser als ARCore.
Mit ARCore ist es möglich seine Augmented-Reality-Erfahrungen auch mit anderen zu teilen, selbst zwischen Android- und iOS-Geräten. Bei sogenannten CloudAnchorn werden visuell-markante Punkte in die Cloud hochgeladen und über einen Key zugänglich gemacht. Jeder mit Zugang zu diesem Key kann den Ankerpunkt herunterladen. Wenn ARCore diesen Punkt lokal wiedererkennt, wird dieser zur Szene hinzugefügt. CloudAnchor liefert dabei nur die Position des Punktes. Um eine Szene auf mehreren Geräten aufzubauen ist es daher notwendig, Informationen über 3D-Asset an dieser Position getrennt zu kommunizieren.
Da nicht jeder, der eine Augmented Reality App schreiben will, auch gleichzeitig hervorragende Kenntnisse im Bereich OpenGL mitbringt, hat Google zusätzlich Sceneform veröffentlicht. Denn mit dem Rendern von 3DModellen hat ARCore zunächst nichts zu tun. Sceneform enthält eine high-level Scenen-Graph-API, einen physikbasierten Renderer sowie ein Plugin für Android Studio zum importieren und ansehen von 3D-Modellen. Die gängigsten 3D-Formate wie OBJ, FBX und GLTF2 werden von Sceneform unterstützt. Mit dem Plugin wird für jedes importierte Modell eine SFA- und eine SFB-Datei erzeugt. Diese beiden Dateien sind speziell für Mobile optimiert und werden von Sceneform verarbeitet.
Der eigentliche Arbeitsablauf mit Sceneform ist folgender: Vor dem Erstellen der APK ist bekannt, welche 3D-Modelle in der Applikation gerendert werden sollen. Entsprechende Modelle werden in Android Studio importiert und SFA- und SFB-Dateien werden erzeugt. Die Applikation wird gebaut und benötigte Dateien werden in die APK integriert.
Meine Anwendung hat jedoch folgende Prämisse: Der User wählt zur Laufzeit aus, welche Modelle er darstellen möchte. Anschließend werden die entsprechenden Modelle heruntergeladen und zuletzt gerendert. Dieser Ansatz führt dazu, dass ausschließlich 3D-Modelle im GLTF2-Format für die Applikation interessant sind, da Sceneform diese als einzige auch zur Laufzeit verarbeiten kann.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
... private fun makeModelFromUrl(key: Int, path: String) { val uri = Uri.parse(path) ModelRenderable.builder() .setSource( this.context, RenderableSource.builder().setSource( this.context, uri, RenderableSource.SourceType.GLTF2 ).build() ) .setRegistryId(uri) .build() .thenAccept { val model = it if (model != null) { modelBlueprintsMap[key] = model } } .exceptionally { Log.e(TAG, it.message) val toast = Toast.makeText(this.context, "Unable to load renderable", Toast.LENGTH_LONG) toast.setGravity(Gravity.CENTER, 0, 0) toast.show() null } } ... |
Die abgebildete Methode erzeugt ein ModelRenderable und fügt es einer Map an. Das Objekt kann dann zu beliebig vielen Knoten hinzugefügt werden und wird an diesen gerendert.
Das Platzieren von Modellen in der AR-Szene ist dank Sceneform sehr einfach. Ein von Sceneform bereitgestelltes ARFragment enthält alles notwendige, um Oberflächen zu erkennen und Ankerpunkte auf diesen zu platzieren. An diese Ankerpunkte können dann die ModelRenderables gehängt werden. Außerdem liefert Sceneform die Klasse TransformableNode. Objekte dieser Klasse können über verschiedene Touch-Gesten verschoben, skaliert oder rotiert werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mainActivity = activity as MainActivity activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) walletViewModel.isArActive = true // to disable the remove button and to enable selection in wallet this.setOnTapArPlaneListener { hitResult: HitResult, _, _ -> val anchor = hitResult.createAnchor() val anchorNode = AnchorNode(anchor) anchorNode.setParent(this.arSceneView.scene) placeAndSelectModel(anchorNode) } } ... private fun placeAndSelectModel(anchorNode: AnchorNode) { val transformableNode = AdvancedTransformableNode(this.transformationSystem, this) transformableNode.renderable = modelBlueprintsMap[walletViewModel.currentEntry] transformableNode.resize() transformableNode.setParent(anchorNode) nodesList.add(transformableNode) transformableNode.select() onTabSelect(transformableNode) } ... |
Der Codeausschnitt stammt aus meiner von ARFragment erbenden Klasse. In der onCreate-Methode setzte ich einen OnTabArPlaneListener. Dieser sorgt dafür, dass ein neuer Knoten erzeugt wird, wenn auf eine gefundene Oberfläche getippt wird. An diesem Knoten wird dann ein AdvancedTransformableNode erzeugt. Dieser bekommt ein ModelRenderable zugewiesen, wird neu skaliert und dann an den ganz am Anfang erzeugten Ankerpunkt gehängt.
Um ein paar mögliche Interaktionen mit TransformableNode zu zeigen, habe ich die Klasse AdvancedTransformableNode erstellt. Diese erbt von TransformableNode. Jeder Knoten speichert seine Position und Rotation. Diese Felder können nach Belieben geändert werden, woraufhin Sceneform das gerenderte Model dementsprechend anpasst. Mit ein bisschen linearer Algebra (Position repräsentiert durch 3-dimensionalen Vektor und Rotationen repräsentiert durch Quaternion) lassen sich allerlei Spielerein mit den 3D-Modellen anstellen.
Jeder Knoten besitzt eine onUpdate-Methode, die jeden Frame aufgerufen wird. Diese kann überschrieben werden, um zum Beispiel zu überprüfen wie weit ein anderer Knoten entfernt ist, das Model zu verschieben oder zu rotieren.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
... private val rotationSpeed = 1f private val interactionRangeCam = 1.1f override fun onUpdate(frameTime: FrameTime) { if(isLocked) { lookAtCamera() } else if (cameraInRange()) { rotateAroundY() } } ... private fun cameraInRange(): Boolean { return (worldPosition.distance(scene.camera.worldPosition) <= interactionRangeCam) } private fun lookAtCamera() { val cameraPosition = scene.camera.worldPosition val direction = Vector3.subtract(worldPosition,cameraPosition ) val lookRotation = Quaternion.lookRotation(direction, Vector3.up()) worldRotation = lookRotation } private fun rotateAroundY() { val rotation = Quaternion.axisAngle(Vector3.up(), rotationSpeed) worldRotation = Quaternion.multiply(worldRotation, rotation) } fun Vector3.distance(v2: Vector3): Float { val x = (v2.x - this.x) * (v2.x - this.x) val y = (v2.y - this.y) * (v2.y - this.y) val z = (v2.z - this.z) * (v2.z - this.z) return Math.sqrt((x + y + z).toDouble()).toFloat() } ... |
Der Codeabschnitt zeigt einen Auszug aus AdvancedTransformableNode. Wenn zum Beispiel ein bestimmter Schwellenwert bei der Entfernung zwischen Kamera und Knoten unterschritten wird, beginnt das Model sich um die eigene Y-Achse zu drehen. Oder wenn ein Boolean entsprechend gesetzt wird, dreht sich das Model kontinuierlich mit seiner Vorderseite zur Kamera.
Fazit
Meine Erfahrungen bei der Arbeit mit der Poly API sind insgesamt gut. Die Dokumentation ist sehr ausführlich und beinhaltet anschauliche Beispiele, die den Einstieg erleichtern. Zusätzlich hat Googles APIs Explorer beim Erstellen der Querys geholfen. Jedoch ist die Poly API noch nicht optimal umgesetzt. So bin ich auf einen Bug in den GLTF2-Dateien von Poly gestoßen, der dazu führt, dass Dateien mit einem Leerzeichen im Namen der Ressource-Datei nicht von Sceneform verarbeitet werden können. Hier scheitert es daran, dass die Poly API URIs innerhalb dieser Datei nicht korrekt enkodiert. Nachdem dieser Bug von mir gemeldet wurde, gab es bisher noch keine Rückmeldung. Es ist zu hoffen, dass Poly diesen und andere Bugs angeht und eine stabile API anbietet.
Auch für ARCore ist die Dokumentation sehr ausführlich. Man kann außerdem verschiedene Beispielprojekte herunterladen und die Features des SDK damit erkunden. Dieses Vorgehen habe ich adaptiert, was sich als sehr nützlich erwiesen hat. Das Positions-Tracking funktioniert in meinen Tests gut. Selbst wenn das Smartphone zwischendurch die Position verliert, zum Beispiel wenn der Finger die Kamera verdeckt, schafft es ARCore meistens, sich wieder zu orientieren.
Sceneform stehe ich mit gemischten Gefühlen gegenüber. Die Dokumentation enthält kaum nützliche Beispiele und Erläuterungen zu wichtigen Komponenten fehlen oft ganz. Bei dem Github Repository von Sceneform sind jede Menge offene Issues und es gibt kaum Feedback von den Entwicklern. Auch ist der Feature-Umfang von Sceneform sehr begrenzt. Es kann keine Animationen abspielen und beim Debuggen ist es nicht möglich einen Schritt zurückzutreten und die Szene von außerhalb der Kamera zu betrachten. Solche nützlichen Features bieten andere Programme wie zum Beispiel Unity. Auch gibt es keine Möglichkeit, seine Szene außerhalb der Laufzeit zu betrachten.
Für einfache Anwendungsfälle von Augmented Reality reicht Sceneform aber auf jeden Fall aus. Man benötigt keine zusätzlichen Technologien (sofern man Zugang zu 3D-Modellen hat) und die Entwicklung kann komplett in Android Studio erfolgen. Will man jedoch komplexere Anwendungsfälle von Augmented Reality umsetzen, so empfehle ich auf Unity/Unreal zurückzugreifen. Beide Umgebungen sind dafür gemacht, in 3D-Szenen zu arbeiten und bieten unzählige Features und Debug-Mechanismen für 3D-Welten. Auch Animationen sind hier im Gegensatz zu Sceneform möglich.
Um dies zu verdeutlichen habe ich testweise eines der Beispielprojekte von ARCore in Unity importiert. In diesem Projekt habe ich das zu rendernde Modell durch eine Figur mit mehreren Animationen ausgetauscht und konnte auf Anhieb eine tanzende Figuren bei mir im Büro platzieren. Dass dies nur etwa 20 Minuten Zeit gekostet hat zeigt, dass Unity hier in kurzer Zeit Ergebnisse erzeugen kann, die so in Sceneform gar nicht möglich sind. Selbst den OpenGL-Code dafür zu schreiben dauert wahrscheinlich deutlich länger.
Meine App ist auf Github zu finden. Der interessierte Tester sei jedoch gewarnt: Gelegentlich stürzt Sceneform ohne Vorwarnung oder Fehlermeldung ab. Wie oben erwähnt, gibt es hier noch einiges zu tun in der Bibliothek. Auch der Beispielcode ist noch nicht ganz fehlerfrei. An der ein oder anderen Stelle findet man, wenn man genauer hinsieht, sicherlich noch einen Bug. Als Proof of Concept funktioniert die App meiner Meinung nach jedoch sehr gut.
Viel Spaß beim Ausprobieren!
Mitmachen!
Wer noch mehr Infos über unser Mobile-Portfolio möchte, findet dazu alles auf unserer Website. Darüber hinaus beschäftigen wir uns mit Smart Devices & Robotics. Wer schon Erfahrung mit Augmented Reality hat, findet vielleicht unter unseren Stellenangeboten etwas passendes.