Merge branch 'chapter3' into dev

This commit is contained in:
Raffaele Mignone 2019-01-30 10:40:15 +01:00
commit b4b2df4be1
Signed by: norangebit
GPG Key ID: 4B9DF72AB9508845
14 changed files with 955 additions and 0 deletions

41
src/chapter3.0.md Normal file
View File

@ -0,0 +1,41 @@
# Progetti d'esempio
Per poter realizzare delle applicazioni mediante ARCore e Sceneform sono necessarie una serie di configurazioni iniziali.
Requisito necessario al funzionamento di ARCore è una versione di Android uguale o superiore ad Android 7.0 Nougat(API level 24).
Inoltre se si sta lavorando su un progetto con API level minore di 26 è necessario esplicitare il supporto a Java 8 andando a modificare file `app/build.gradle`.
```gradle
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
...
}
```
Sempre nel file per il build del progetto è necessario aggiungere la dipendenza di Sceneform.
```gradle
implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.6.0'
```
Inoltre per sfruttare al massimo le potenzialità offerte da Sceneform e ridurre al minimo il lavoro extra per la gestione delle view, si deve aggiungere il fragment di Sceneform al file di layout dell'activity principale mediante il seguente codice xml.
```xml
<fragment
android:id="@+id/sceneform_fragment"
android:name="com.google.ar.sceneform.ux.ArFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
```
Infine nell'Android Manifest[^manifest] va dichiarato l'utilizzo del permesso della fotocamera[^camera] e l'utilizzo di ARCore[^arcore].
[^manifest]: File in cui vengono dichiarate tutte le caratteristiche di un'applicazione Android, tra cui anche i permessi.
[^camera]: Lo sviluppatore deve solo dichiarare l'utilizzo del permesso, la richiesta di concessione è gestita in automatico da Sceneform.
[^arcore]: L'utilizzo di ARCore deve essere dichiarata in quanto non tutti i dispositivi lo supportano.

133
src/chapter3.1.md Normal file
View File

@ -0,0 +1,133 @@
## Augmented images
Nel primo progetto d'esempio si è affrontato un classico problema di AR marker based, ovvero il riconoscimento di un'immagine preimpostata e il conseguente sovrapponimento di un oggetto virtuale.
Nel caso specifico si vuole riconoscere una foto del pianeta terra e sostituirvi un modello tridimensionale di essa.
### Aggiunta del modello
Il modello tridimensionale della terra è stato recuperato dal sito `poly.google.com`, che funge da repository per modelli 3D.
La scelta del modello è stata dettata sia del formato[^format] in cui era disponibile, sia dalla licenza con cui veniva distribuito.
Una volta ottenuto il modello è stato salvato nella cartella *"sampledata"*, il cui contenuto sarà usato solo in fase di progettazione.
L'importazione del modello all'interno del progetto di Android Studio è stato effettuato mediante l'utilizzo del plug-in *Google Sceneform Tools*, che si occupa sia di convertire il modello nel formato di Sceneform, sia di aggiornare il file `build.gradle` affinché sia incluso nell'APK[^apk] finale.
### Creazione del database
Il database contenente tutte le immagini che si desidera far riconosce all'applicazione, può essere creato sia a priori, sia a tempo di esecuzione.
Per la prima soluzione Google mette a disposizione *The arcoreimage tool*, un software a linea di comando, che oltre a creare il database, si occupa anche di valutare l'immagine.
Nel caso specifico si vuole far riconoscere un'unica immagine, quindi si è optato per la generazione del database a tempo di esecuzione.
In particolare quest'operazione avviene mediante la funzione `setupAugmentedImageDb`.
```kotlin
private fun setupAugmentedImageDb (
config: Config
): Boolean {
val image = loadImage(IMAGE_FILE_NAME) ?: return false
val augmentedImageDb = AugmentedImageDatabase(session)
augmentedImageDb.addImage(IMAGE_NAME, image)
config.augmentedImageDatabase = augmentedImageDb
return true
}
```
### Riconoscimento dell'immagine
Il riconoscimento dell'immagine non può avvenire mediate l'utilizzo di una callback in quanto ARCore non permette di registrare un listener all'evento.
Risulta dunque evidente che la verifica dell'avvenuto match sarà delegata allo sviluppatore.
Per fare ciò si è usato il metodo `addOnUpdateListener` dell'oggetto `Scene`, che permette di eseguire uno *snippet* di codice ogni qual volta la scena viene aggiornata.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
arSceneView.scene.addOnUpdateListener(
this::detectAndPlaceAugmentedImage
)
// ...
}
```
Dove la funzione `detectAndPlaceAugmentedImage` si occupa di verificare la presenza di un match e nel caso di un riscontro positivo, dell'aggiunta dell'oggetto virtuale alla scena.
```kotlin
fun detectAndPlaceAugmentedImage(frameTime: FrameTime) {
if (isModelAdded)
return
val augmentedImage = arSceneView.arFrame
.getUpdatedTrackables(AugmentedImage::class.java)
.filter { isTrackig(it) }
.find { it.name.contains(IMAGE_NAME) }
?: return
val augmentedImageAnchor = augmentedImage
.createAnchor(augmentedImage.centerPose)
buildRenderable(this, Uri.parse(MODEL_NAME)) {
addTransformableNodeToScene(
arFragment,
augmentedImageAnchor,
it
)
}
isModelAdded = true
}
```
Il settaggio del flag `isModelAdded` al valore booleano di vero, si rende necessario al fine di evitare l'aggiunta incontrollata di nuovi modelli alla medesima immagine.
### Rendering del modello
Il rendering del modello avviene attraverso il metodo `buildRenderable` che a sua volta chiama la funzione di libreria `ModelRenderable.builder`.
Poiché quest'ultima è un operazione onerosa viene restituito un `Future`[^future] che racchiude il `Renderable` vero e proprio.
L'interazione con l'oggetto concreto avviene mediante una callback che è possibile specificare attraverso il metodo `thenAccept`.
```kotlin
fun buildRenderable(
context: Context,
model: Uri,
onSuccess: (renderable: Renderable) -> Unit
) {
ModelRenderable.builder()
.setSource(context, model)
.build()
.thenAccept(onSuccess)
.exceptionally {
Log.e("SCENEFORM", "unable to load model", it)
return@exceptionally null
}
}
```
### Aggiunta dell'oggetto virtuale nella scena
L'ultima operazione da compiere è l'aggiunta del modello renderizzato alla scena.
Questa operazione avviene attraverso la funzione `addTrasformableNodeToScene` che si occupa di creare un'ancora in corrispondenza del punto reale d'interesse.
A partire da quest'ancora viene creato un nodo che racchiude l'oggetto renderizzato.
Inoltre nel caso specifico si è usato un `TransformabelNode`, in modo da concedere all'utente la possibilità di ridimensionare o ruotare il modello.
```kotlin
fun addTransformableNodeToScene(
arFragment: ArFragment,
anchor: Anchor,
renderable: Renderable
) {
val anchorNode = AnchorNode(anchor)
val transformableNode = TransformableNode(
arFragment.transformationSystem
)
transformableNode.renderable = renderable
transformableNode.setParent(anchorNode)
arFragment.arSceneView.scene.addChild(anchorNode)
transformableNode.select()
}
```
[^format]: Attualmente sono supportati solo modelli OBJ, FBX e gLTF.
[^apk]: Formato delle applicazioni Android.
[^future]: In informatica con il termine *future*, o *promise*, *delay* e *deferred*, si indica un tecnica che permette di sincronizzare l'esecuzione di un programma concorrente.

103
src/chapter3.2.md Normal file
View File

@ -0,0 +1,103 @@
## Runtime fetching models
Nella seconda applicazione d'esempio viene mostrato come sia possibile recuperare i modelli da renderizzare anche durante l'esecuzione dell'applicazione.
Questa funzione risulta particolarmente utile quando si deve rilasciare un'applicazione che sfrutta numerosi modelli e non si vuole appesantire eccessivamente il volume del file *APK*.
Inoltre concede maggiore libertà allo sviluppatore in quanto è possibile aggiungere nuovi modelli, o aggiornare quelli vecchi, senza dover operare sull'applicazione, ma lavorando esclusivamente lato server.
In questo caso specifico l'applicazione dovrà riconosce uno o più piani e in seguito ad un tocco dell'utente su di essi, mostrare un modello di *Andy*, la mascotte di Android(vedi fig. \ref{rfm}).
Per quest'applicazione oltre alle configurazioni già viste in precedenza è necessario aggiungere una nuova dipendenza che include le funzioni necessarie per il fetching del modello.
```gradle
implementation 'com.google.ar.sceneform:assets:1.6.0'
```
Inoltre nell'Android Manifest bisogna aggiungere il permesso per accedere alla rete.
### Interazione con l'utente
L'interazione con l'utente avviene mediante un tocco sul display in corrispondenza di un piano.
Sceneform ci permette di personalizzare il comportamento al verificarsi di questo evento tramite il metodo `setOnTapArPlaneListener`.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
arFragment.setOnTapArPlaneListener(
this::fetchAndPlaceModel
)
// ...
}
```
Dove la funzione `fetchAndPlaceModel` si occupa di recuperare il modello e renderizzarlo.
```kotlin
private fun fetchAndPlaceModel(
hitResult: HitResult,
plane: Plane,
motionEvent: MotionEvent
) {
val modelUri = Uri.parse(MODEL_SOURCE)
val fetchedModel = fetchModel(this, modelUri)
buildRenderable(this, fetchedModel, modelUri) {
addTransformableNodeToScene(
arFragment,
hitResult.createAnchor(),
it
)
}
}
```
### Fetching del model
Il recupero del modello avviene attraverso la funzione `fetchModel`, che a sua volta chiama la funzione di libreria `RenderableSource.builder`.
```kotlin
fun fetchModel(
context: Context,
source: Uri
) : RenderableSource {
return RenderableSource.builder()
.setSource(
context,
source,
RenderableSource.SourceType.GLTF2
)
.setRecenterMode(
RenderableSource.RecenterMode.ROOT
)
.build()
}
```
Attualmente[^sceneform-1.6] Sceneform supporta unicamente il fetching di modelli gLTF.
### Rendering e aggiunta del modello
Il rendering del modello avviene tramite la funzione `buildRenderable`, che riprende in buona parte quella vista precedentemente, con la differenza che in questo caso deve essere passato anche il modello recuperato.
```kotlin
fun buildRenderable(
context: Context,
model: RenderableSource,
modelUri: Uri,
onSuccess: (renderable: Renderable) -> Unit
) {
ModelRenderable.builder()
.setRegistryId(modelUri)
.setSource(context, model)
.build()
.thenAccept(onSuccess)
.exceptionally {
Log.e("SCENEFORM", "unable to load model", it)
return@exceptionally null
}
}
```
Infine l'aggiunta del modello renderizzato alla scena avviene mediante la medesima funzione `addTransformableNodeToScene` vista in precedenza.
![Rendering di un modello recuperato a runtime](figures/rfm.png){#rfm width=225px height=400px}
[^sceneform-1.6]: Sceneform 1.6.0.

121
src/chapter3.3.md Normal file
View File

@ -0,0 +1,121 @@
## Runtime building models
Lo scopo di questo progetto è mostrare come sia possibile costruire dei semplici modelli tridimensionali senza dover ricorrere ad asset pre costituiti.
L'SDK di Sceneform fornisce due classi per adempiere a questo compito:
- `MaterialFactory`: consente di creare un *"materiale"*, partendo o da un colore o da una texture[^texture] definita precedentemente.
- `MaterialShape`: consente di creare delle semplici forme geometriche come cilindri, sfere e cuboidi.
Nel caso specifico è stata realizzata un'applicazione che in seguito al tocco dell'utente renderizza nella scena un oggetto dalla forma e dal colore *pseudo-casuali*(vedi fig. \ref{rbm}).
Inoltre è stato aggiunto un ulteriore elemento di interazione con l'utente, che gli consente di cliccare sull'oggetto renderizzato, al fine di cambiare la tinta di quest'ultimo.
![Rendering di modelli costruiti a runtime](figures/rbm.png){#rbm width=225px height=400px}
### Interazione con l'utente
Anche in questo caso l'interazione con l'utente è gestita mediante il metodo `setOnTapArPlaneListener`.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
arFragment.setOnTapArPlaneListener(this::addModel)
// ...
}
```
Dove la funzione `addModel` si occupa della creazione del materiale e della forma e infine dell'aggiunta del modello alla scena.
```kotlin
private fun addModel(
hitResult: HitResult,
plane: Plane,
motionEvent: MotionEvent
) {
val color = generateColor()
buildMaterial(this, color) {
val node = addTransformableNodeToScene(
arFragment,
hitResult.createAnchor(),
buildShape(generateShape(), it)
)
node.setOnTapListener {_ , _ ->
changeColorOfMaterial(
this,
generateColor(),
node.renderable
)
}
}
}
```
### Creazione del materiale
La creazione del materiale avviene mediante la funzione `buildMaterial` che a sua volta richiama la funzione di libreria ` MaterialFactory .makeOpaqueWithColor`.
Come già visto in precedenza, la soluzione adottata da Sceneform per interagire con oggetti *pesanti* è una callback che nel caso specifico può essere specificata mediante il parametro `onSuccess`.
```kotlin
fun buildMaterial(
context: Context,
color: Color,
onSuccess: (material: Material) -> Unit
) {
MaterialFactory
.makeOpaqueWithColor(context, color)
.thenAccept(onSuccess)
}
```
### Creazione della forma
Per la costruzione della forma geometrica si è usata la funzione `buildShape` che si comporta da *facade* per le funzioni della classe di libreria `ShapeFactory`.
```kotlin
fun buildShape(
shape: Shape,
material: Material
): ModelRenderable {
val center = Vector3(0.0f, 0.0f, 0.0f)
return when (shape) {
Shape.CUBE -> ShapeFactory
.makeCube(Vector3(0.2f, 0.2f, 0.2f),
center, material)
Shape.CYLINDER -> ShapeFactory
.makeCylinder(0.1f, 0.2f, center, material)
Shape.SPHERE -> ShapeFactory
.makeSphere(0.1f, center, material)
}
}
```
Come è possibile notare a seconda della figura, vanno specificate le caratteristiche spaziali che la contraddistingue e il materiale creato precedentemente.
### Aggiunta del nodo alla scena
L'aggiunta di un nuovo nodo alla scena avviene mediante la funzione `addTransformableNodeToScene` che presenta il medesimo comportamento visto nei precedenti progetti, con l'unica differenza del valore di ritorno.
Infatti se prima veniva restituito un'`Unit`[^unit] in questo caso viene restituito un oggetto di tipo `Node`.
Questa modifica si rende necessaria per poter aggiungere al nodo un listener sull'evento di tocco.
Questa operazione avviene mediante il metodo `setOnTapListener`, al quale, mediante una *lambda expression*, viene passata la funzione `changeColorOfMaterial`.
```kotlin
fun changeColorOfMaterial(
context: Context,
color: Color,
renderable: Renderable
) {
buildMaterial(context, color) {
renderable.material = it
}
}
```
Quest'ultima funzione si occupa di creare un nuovo materiale e sostituirlo a quello precedente.
[^texture]: In ambito grafico con il termine *texture* si è soliti indicare una qualità visiva che si ripete mediante un pattern ben definito.
[^unit]: Equivalente in Kotlin dell'oggetto `Void` di Java.

88
src/chapter3.4.md Normal file
View File

@ -0,0 +1,88 @@
## Collision
Quando si ha a che fare con più nodi presenti sulla scena può risultare utile verificare se due o più di questi si sovrappongono.
In questo progetto viene mostrato come eseguire questo controllo mediante l'API di ARCore.
Per questo progetto si è utilizzata una rivisitazione dell'applicazione vista nel progetto precedente, con la differenza che l'aggiunta di un oggetto non è consentita se questo va in collisione con un altro già presente nella scena(vedi fig. \ref{c}).
![Schermata di errore dovuta ad una collisione](figures/c.png){#c width=225px height=400px}
### Rilevamento della collisione
Attualmente ARCore e Sceneform non forniscono nessun listener o metodo che può essere sovrascritto per la gestione della collisione, quindi seguendo un approccio già esaminato precedentemente andiamo ad aggiungere un listener all'evento dell'aggiornamento della scena.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
arScene.addOnUpdateListener(this::onUpdate)
// ...
}
```
La verifica di una collisione può essere effettuata o attraverso il metodo `overlapTest`, che dato un nodo in input restituisce il primo nodo che entra in collisione con questo, oppure mediante il metodo `overlapTestAll`, che dato un nodo in input restituisce una lista con tutti i nodi che collidono con esso.
Nel caso in cui non siano state riscontrate collisioni, i metodi restituiscono rispettivamente `null` e una lista vuota.
La funzione `onUpdate` si occupa di verificare la presenza di collisioni.
```kotlin
private fun onUpdate(frameTime: FrameTime) {
val node = lastNode ?: return
val overlappedNodes = arScene.overlapTestAll(node)
if (overlappedNodes.isNotEmpty())
onCollision()
}
```
Mentre la funzione `onCollision` si occupa di notificare all'utente l'avvenuta collisione mediante un *Toast*[^toast] e di eliminare il nodo dalla scena.
```kotlin
private fun onCollision() {
Toast.makeText(this, "collision", Toast.LENGTH_LONG)
.show()
lastNode?.isEnabled = false
lastNode = null
}
```
Il test di collisione non avviene direttamente sul `Renderable`, ma sulla `CollisionShape` ovvero una *"scatola"* invisibile che racchiude il modello renderizzato vero e proprio.
Di default ARCore utilizza `CollisionShape` o di forma rettangolare o sferica, ma può essere cambiata mediante il metodo `setCollisionShape` della classe `Node`.
In questo progetto si è usata la `CollisionShape` di default.
### Aggiunta del nodo alla scena
L'aggiunta del nodo alla scena avviene mediante l'aggiunta di un listener all'evento di tocco su un piano.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
arFragment.setOnTapArPlaneListener(this::addShape)
// ...
}
```
La funzione `addShape`, utilizzando le funzioni `buildMaterial` e `buildShape` analizzate in precedenza, si occupa dell'effettiva aggiunta dell'oggetto alla scena.
```kotlin
private fun addShape(
hitResult: HitResult,
plane: Plane,
motionEvent: MotionEvent
) {
val red = Color(android.graphics.Color.RED)
buildMaterial(this, red) {
val cube = buildShape(Shape.CUBE, it)
lastNode = addNodeToScene(
arFragment,
hitResult.createAnchor(),
cube
)
}
}
```
Risulta importante notare come attraverso questa strategia l'aggiunta del modello alla scena avvenga incondizionatamente, ed è solo all'aggiornamento di quest'ultima che si effettua il controllo di collisione sull'ultimo nodo aggiunto.
[^toast]: Oggetto nativo di Android mediante il quale è possibile informare l'utente in modo non invasivo.

29
src/chapter3.5.0.md Normal file
View File

@ -0,0 +1,29 @@
## Animazioni e movimento
Negli esempi discussi fino a questo momento sono stati usati unicamente asset tridimensionali statici.
Gli oggetti virtuali che di volta in volta sono stati integrati nel mondo reale non erano né dotati di animazioni, né erano in grado di muoversi all'interno dell'ambiente reale circostante.
La scelta di utilizzare modelli statici è stata dettata da un lato dalla filosofia *kiss*[^kiss], e quindi di concentrarsi unicamente sull'aspetto rilevante del progetto, dall'altro da un supporto quasi inesistente alle animazioni e al movimento.
Come abbiamo avuto modo di vedere nel secondo capitolo, queste lacune sono da imputare principalmente alla libreria grafica.
Infatti sia per le animazioni, sia per la gestione del movimento, Sceneform non offre alcun supporto nativo o diretto.
Nonostante ciò è possibile aggirare il problema tramite opportuni escamotage, anche se bisogna tenere in conto che i risultati sono generalmente modesti e macchinosi da raggiungere.
### Animazioni
L'utilizzo di oggetti animati oltre ad essere un orpello grafico diventa, in molti casi, un aspetto fondamentale nel processo di sviluppo.
Questo è ancora più vero nello sviluppo di applicazioni AR su smartphone che, per forza di cose, hanno nell'utenza consumers lo sbocco naturale.
Il mancato utilizzo di un'animazione potrebbe segnare in modo permanete l'esperienza utente e quindi determinare il fallimento del progetto.
Non a caso uno dei problemi più discussi nell'issues tracker di Sceneform su GitHub[@googlear:Animated3DObjects:2019], è proprio la totale mancanza di supporto alle animazioni.
Sebbene ci siano stati dei lavori in questo senso ed una prima contabilità con i modelli animati FBX sia stata aggiunta alla code base, ad oggi non è possibile utilizzare questa funzione, in quanto è in fase di testing per il solo personale interno.
In attesa di un rilascio al pubblico, l'unica via percorribile è quella presentata dalla stessa Google durante un codelab[@googlear:ChromaKey:2019].
La soluzione consiste nel renderizzare un schermo trasparente nel mondo reale e proiettare su di esso un video.
Affinché l'*illusione* riesca è necessario usare un video che sfrutti il *chroma key*[^chroma-key], in questo modo l'integrazione con il mondo reale risulta migliore.
Inoltre per impedire che l'utente possa guardare il retro dello schermo è consigliabile rendere quest'ultimo solidale con l'utente.
Non solo risultano evidenti le limitazioni di questo metodo, ma anche la natura più di *pezza* che soluzione al problema, che non lo rendono adatto per un utilizzo commerciale.
[^kiss]: Acronimo di *Keep It Simple, Stupid*, è un principio di programmazione che consiglia di concentrarsi su un problema alla volta.
[^chroma-key]: Conosciuto anche con il nome di *green screen*, è una tecnica cinematografica che permette di sovrapporre due sorgenti video. Nel caso specifico la prima fonte video è catturata in real time dal sensore fotografico del device, mentre la seconda è quella che vogliamo integrare nell'ambiente.

219
src/chapter3.5.1.md Normal file
View File

@ -0,0 +1,219 @@
### Movimento
Anche in questo caso Sceneform non ci fornisce un supporto diretto, ma a differenza di quanto visto con le animazioni, è possibile sopperire a questa mancanza abbastanza facilmente e con risultati soddisfacenti tramite gli `ObjectAnimator`.
L'`ObjectAnimator` non è una classe specifica di ARCore o Sceneform, ma dell'SDK di Android che può essere usata per gestire facilmente animazioni e transizioni all'interno delle applicazioni Android.
Grazie a questa classe e una serie di punti nello spazio, *collegati* tramite un interpolatore, saremo in grado di conferire il movimento ai nostri modelli.
Per mostrare il funzionamento degli animator è stato realizzato un progetto d'esempio in grado di renderizzare un modello del sistema solare in cui i pianeti realizzano sia il modo di rotazione su se stessi, sia quello di rivoluzione intorno al sole(vedi fig. \ref{ss}).
![Rendering del sistema solare](figures/ss.png){#ss width=400px height=225px}
#### Recupero e rendering dei modelli
Visto l'elevato numero di modelli con cui si deve operare si è scelto di recuperarli da un server a runtime.
Il procedimento è simile a quello visto precedentemente, con la differenza che in questo caso non si è optato per l'utilizzo delle callback, al fine di evitare il *callback hell*[^callback-hell], a favore delle *coroutines*, uno strumento messo a disposizione dal linguaggio Kotlin che permette di gestire codice asincrono come se fosse sequenziale.
Sempre attraverso le *coroutines* è stato possibile eseguire più rendering in parallelo e quindi ottimizzare il tempo di CPU dell'applicazione.
All'interno del metodo `onCreate` viene avviata una coroutine che richiama la funzione `loadPlanets`.
Inoltre viene conservato un riferimento al `Job` della coroutine.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
loadPlanetsJob = GlobalScope.launch(Dispatchers.Main){
renderablePlanets = loadPlanets(this@MainActivity)
}
// ...
}
```
La funzione `loadPlanets` si occupa di caricare e restituire tramite una `Map` tutti i pianeti del sistema solare.
Mentre il caricamento del singolo pianeta avviene mediante la funzione `loadPlanet`.
```kotlin
suspend fun loadPlanets(
context: Contex
) : Map<Planet, ModelRenderable> {
val sun = loadPlanet(context, Planet.SUN)
/**
* ...
* caricamento degli altri pianeti
* ...
*/
val neptune = loadPlanet(context, Planet.NEPTUNE)
return mapOf(
Pair(Planet.SUN, sun.await()),
// ...
Pair(Planet.NEPTUNE, neptune.await())
)
}
```
Nella funzione `loadPlanet` viene da prima recuperato il modello tridimensionale dal server e successivamente se ne effettua il rendering attraverso la funzione `buildFutureRenderable`.
Quest'ultima, come abbiamo già visto, restituisce un `CompletableFuture` che per poter essere utilizzato tramite delle coroutines deve essere trasformarlo in un `Deferred`[^deferred].
Questa operazione avviene attraverso il costruttore di coroutines `async`.
Inoltre viene usato il dispatcher `IO` che ci consente di eseguire l'operazione in background.
```kotlin
fun loadPlanet(
context: Context,
planet: Planet
): Deferred<ModelRenderable> {
val modelSource = fetchModel(
context,
Uri.parse(planet.value)
)
val futureRenderable = buildFutureRenderable(
context,
modelSource,
Uri.parse(planet.value)
)
return GlobalScope.async(Dispatchers.IO) {
futureRenderable.get()
}
}
```
L'ultima operazione da dover effettuare prima di poter usare i modelli renderizzati è assicurarci che l'operazione di rendering sia stata completata.
Per fare ciò viene usato ancora una volta il costruttore di coroutines `launch` e si attende, in modo non bloccante, il completamento del job di rendering.
```kotlin
GlobalScope.launch(Dispatchers.Main) {
loadPlanetsJob.join()
// operazioni con gli oggetti renderizzati
}
```
#### Orbite e pianeti
Per realizzare le orbite e i pianeti è stata implementata la classe `RotationNode` che va ad estendere la classe di libreria `Node`.
Componente principale di questa è la funzione `createAnimator` che si occupa della creazione dell'`ObjectAnimator` che permette di muovere i modelli.
All'interno della funzione vengono definiti i punti da cui ottenere la rotazione attraverso l'interpolatore.
Infine viene impostato l'`ObjectAnimator` affinché riproduca in loop l'animazione.
```kotlin
private fun createAnimator(): ObjectAnimator {
val orientations = arrayOf(0f, 120f, 240f, 360f)
.map {
Quaternion
.axisAngle(Vector3(0.0f, 1.0f, 0.0f), it)
}
val orbitAnimation = ObjectAnimator()
orbitAnimation.setObjectValues(*orientations.toTypedArray())
orbitAnimation.setEvaluator(QuaternionEvaluator())
orbitAnimation.repeatCount = ObjectAnimator.INFINITE
orbitAnimation.repeatMode = ObjectAnimator.RESTART
orbitAnimation.interpolator = LinearInterpolator()
orbitAnimation.setAutoCancel(true)
return orbitAnimation
}
```
Inoltre nella classe `RotationNode` vanno sovrascritti i metodi `OnActivate` e `OnDeactivate`, per gestire lo start e lo stop dell'animazione.
```kotlin
override fun onActivate() {
startAnimation()
}
override fun onDeactivate() {
stopAnimation()
}
```
La creazione dei pianeti è gestita attraverso un ulteriore classe, `PlanetNode`, anch'essa estensione della classe `Node`.
Quest'ultima viene definita come un nodo dotato di un `Renderable` ancorato ad un `RotationNode`.
Come vedremo in seguito questa operazione si rende necessaria per garantire il moto di rivoluzione.
La creazione delle orbite e dei pianeti avviene mediante la funzione `createPlanetNode`.
L'orbita del pianeta viene ancorata al nodo principale, nel caso specifico il sole, e il pianeta viene ancorato alla sua orbita.
Inoltre viene assegnato anche il renderable al nodo del pianeta.
```kotlin
private fun createPlanetNode(
planet: Planet,
parent: Node,
auFromParent: Float,
orbitDegreesPerSecond: Float,
renderablePlanets: Map<Planet, ModelRenderable>
) {
val orbit = RotationNode()
orbit.degreesPerSecond = orbitDegreesPerSecond
orbit.setParent(parent)
val renderable = renderablePlanets[planet] ?: return
val planetNode = PlanetNode(renderable)
planetNode.setParent(orbit)
planetNode.localPosition = Vector3(
AU_TO_METERS * auFromParent,
0.0f,
0.0f
)
}
```
È importante notare che in questo modo non sono i pianeti a ruotare intorno al sole, ma sono le orbite a ruotare su se stesse e visto che i pianeti sono *"incollati"* ad esse si ha l'illusione del moto di rivoluzione.
#### Creazione e aggiunta del sistema solare
La creazione del nostro sistema solare avviene mediante la funzione `createSolarSystem` che riceve in ingresso la `Map` con tutti i modelli dei pianeti, li posiziona intorno al sole e infine restituisce quest'ultimo.
```kotlin
private fun createSolarSystem(
renderablePlanets: Map<Planet, ModelRenderable>
): Node {
val base = Node()
val sun = Node()
sun.setParent(base)
sun.localPosition = Vector3(0.0f, 0.5f, 0.0f)
val sunVisual = Node()
sunVisual.setParent(sun)
sunVisual.renderable = renderablePlanets[Planet.SUN]
sunVisual.localScale = Vector3(0.5f, 0.5f, 0.5f)
createPlanetNode(
Planet.MERCURY,
sun,
0.4f,
47f,
renderablePlanets
)
// ...
createPlanetNode(
Planet.NEPTUNE,
sun,
6.1f,
5f,
renderablePlanets
)
return base
}
```
L'aggiunta dei modelli alla scene avviene mediante la ben nota funzione `addNodeToScene`.
```kotlin
val solarSystem = createSolarSystem(renderablePlanets)
addNodeToScene(
arFragment,
hitResult.createAnchor(),
solarSystem
)
isModelAdded = true
```
Anche in questo caso si rende necessario l'utilizzo di un flag booleano per evitare l'aggiunta di più sistemi solari.
[^callback-hell]: Con il termine *callback hell* si indica l'utilizzo eccessivo di callback all'interno di altre callback. Questo fenomeno comporta una diminuzione della leggibilità del codice e un aumento della complessità e di conseguenza della presenza di bug.
[^deferred]: In Kotlin i *future* sono gestiti mediante l'oggetto `Deferred<T>`.

221
src/chapter3.6.md Normal file
View File

@ -0,0 +1,221 @@
## Cloud anchors
Un'ulteriore funzionalità messa a disposizione da ARCore sono le *Cloud Anchors* che ci permette di salvare su un server remoto le ancore a cui sono agganciati i nodi.
Grazie a questa funzionalità è possibile salvare un'esperienza di realtà aumentata per un uso futuro[^futuro] o per condividerla con altri utenti.
In questo progetto verrà mostrato come sia possibile posizionare, tramite il device A, un vaso di fiori su una superficie piana, e vedere la stessa scena sul dispositivo B.
![Messaggio di avvenuto upload sul server](figures/ca1.png){#ca1 width=225px height=400px}
![Schermata di ripristino di un'ancora](figures/ca2.png){#ca2 width=225px height=400px}
### Configurazioni iniziali
Per poter sfruttare le cloud anchors è necessario richiedere un API KEY sul sito di Google \url{https://console.cloud.google.com/apis/library/arcorecloudanchor.googleapis.com}.
Una volta ottenuta la chiave è necessario dichiararla nell'Android Manifest mediante il seguente codice xml.
```xml
<meta-data
android:name="com.google.android.ar.API_KEY"
android:value="API_KEY"/>
```
Inoltre per tenere traccia dello stato dell'applicazione si è definita una classe enumerativa con cinque possibili valori.
- `NONE`: non è presente alcuno oggetto nella scena né se ne sta recuperando uno dal server.
- `HOSTING`: si sta caricando l'ancora sul server.
- `HOSTED`: l'ancora è stata caricata sul server.
- `RESOLVING`: si sta recuperando l'ancora dal server.
- `RESOLVED`: l'ancora è stata recuperata del server.
### Attivazione delle cloud anchors
Le cloud anchors di default sono disattivate e la loro attivazione può avvenire in due modi.
- **Attivazione manuale**:
Con questa soluzione lo sviluppatore si occupa di creare una nuova configurazione della sessione di ARCore in cui le cloud anchors sono attivate e andare a sostituire questa nuova configurazione a quella di default.
- **Estensione dell'`ArFragment`**:
Viene creata una nuova classe che estende `ArFragment` in cui le cloud anchors sono attivate.
Sebbene la prima soluzione possa sembrare più immediata, nasconde una grande insidia.
Infatti sarà compito dello sviluppatore andare a sovrascrivere i vari metodi che gestiscono il ciclo di vita dell'activity affinché non vengano ripristinate le impostazioni iniziali.
Mentre con il secondo metodo sarà Sceneform a gestire il ciclo di vita al posto nostro.
Per questo motivo è stata creata la classe `CloudArFragment` in cui è stata sovrascritta la funzione `getSessionConfiguration` in modo da attivare le cloud anchors.
```kotlin
class CloudArFragment: ArFragment(){
override fun getSessionConfiguration(
session: Session?
): Config {
val config = super.getSessionConfiguration(session)
config.cloudAnchorMode = Config
.CloudAnchorMode.ENABLED
return config
}
}
```
Inoltre bisogna modificare anche il file di layout affinché non utilizzi più l'`ArFragment`, ma il `CloudArFragment`.
```xml
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="it.norangeb.cloudanchors.CloudArFragment"
android:id="@+id/ar_fragment"/>
```
### Cloud Anchor Helper
Quando viene caricata un'ancora sul server viene associata ad essa un valore alfanumerico che ci permette di identificarla univocamente.
Dato che il codice risulta essere molto lungo e quindi difficile da ricordare e ricopiare, si è scelto di appoggiarsi al servizio *firestore * di Firebase[@firebase:Firebase:2019] per creare una relazione uno a uno tra l'UUID e uno *short code* intero.
Queste operazioni avvengono tramite la classe `CloudAnchorHelper` che fornisce due metodi principali `getShortCode` e `getCloudAnchorId`.
```kotlin
fun getShortCode(cloudAnchorId: String): Int {
fireStoreDb.collection(COLLECTION)
.document(DOCUMENT)
.set(
mapOf(Pair(nextCode.toString(), cloudAnchorId)),
SetOptions.merge()
)
uploadNextCode(nextCode+1)
return nextCode++
}
```
```kotlin
fun getCloudAnchorId(
shortCode: Int,
onSuccess: (String) -> Unit
) {
fireStoreDb.collection(COLLECTION)
.document(DOCUMENT)
.get()
.addOnSuccessListener {
val uuid=it.data.get(shortCode.toString()) as String
onSuccess(uuid)
}
}
```
Il primo metodo riceve in ingresso l'UUID dell'ancora e lo aggiunge al database di Firebase usando come chiave un numero intero che viene restituito al chiamante.
Mentre il secondo metodo, dato il codice intero, recupera l'identificativo dell'ancora e svolge su di esso le operazioni specificate nella *lambda expression* `onSuccess`.
### Aggiunta del modello
L'aggiunta del modello avviene attraverso la funzione `addModel`, che opera in modo simile a quanto visto fin'ora, con l'unica differenza che l'aggiunta è consentita solo se l'applicazione si trova nello stato `NONE`.
Inoltre la creazione dell'ancora è delegata al metodo `hostCloudAnchors` che si occupa anche dell'upload di quest'ultima sul server.
```kotlin
private fun addModel(
hitResult: HitResult,
plane: Plane,
motionEvent: MotionEvent
) {
if (cloudAnchorState != CloudAnchorState.NONE)
return
cloudAnchor = arFragment.arSceneView.session
.hostCloudAnchor(hitResult.createAnchor())
cloudAnchorState = CloudAnchorState.HOSTING
buildRenderable(this, Uri.parse("model.sfb")) {
addTransformableNodeToScene(
arFragment,
cloudAnchor ?: return@buildRenderable,
it
)
}
}
```
### Check Hosting
Il metodo `checkCloudAnchor` viene eseguito ogni qual volta viene aggiornata la scena e, in base allo stato dell'applicazione vengono eseguite determinate operazioni.
```kotlin
private fun checkCloudAnchor(frameTime: FrameTime) {
if (cloudAnchorState != CloudAnchorState.HOSTING
&& cloudAnchorState != CloudAnchorState.RESOLVING
)
return
val cloudState=cloudAnchor?.cloudAnchorState?:return
if (cloudState.isError) {
toastError()
cloudAnchorState = CloudAnchorState.NONE
return
}
if (cloudState != Anchor.CloudAnchorState.SUCCESS)
return
if (cloudAnchorState == CloudAnchorState.HOSTING)
checkHosting()
else
checkResolving()
}
```
Nel caso specifico in cui il processo di caricamento sia stato completato con successo viene eseguita la funzione `checkHosting` che si occupa di notificare all'utente il codice numerico associato all'ancora(vedi fig. \ref{ca1}) e di cambiare lo stato dell'applicazione da `HOSTING` a `HOSTED`.
```kotlin
private fun checkHosting() {
val cAnchor = cloudAnchor ?: return
val shortCode = cloudAnchorsHelper
.getShortCode(cAnchor.cloudAnchorId)
Toast
.makeText(
this,
"Anchor hosted with code $shortCode",
Toast.LENGTH_LONG
)
.show()
cloudAnchorState = CloudAnchorState.HOSTED
}
```
### Resolving dell'ancora
L'utente può ripristinare un'ancora premendo sul pulsante *resolve*.
Il listener associato a questo evento è racchiuso nella funzione `onResolve` che a sua volta mostra all'utente un dialog in cui può inserire il codice dell'ancora da ripristinare(vedi fig. \ref{ca2}).
```kotlin
fun onResolveClick(view: View) {
if (cloudAnchor != null)
return
val dialog = ResolveDialogFragment()
dialog.setOkListener(this::onResolveOkPressed)
dialog.show(supportFragmentManager, "Resolve")
}
```
Alla conferma dell'inserimento, da parte dell'utente, viene eseguito il metodo `onResolveOkPressed` che converte lo *short code* nell'UUID dell'ancora e da questo ripristina il nodo nella scena.
```kotlin
private fun onResolveOkPressed(dialogValue: String) {
val shortCode = dialogValue.toInt()
cloudAnchorsHelper.getCloudAnchorId(shortCode) {
cloudAnchor = arFragment.arSceneView.session
.resolveCloudAnchor(it)
buildRenderable(this, Uri.parse("model.sfb")) {
val anchor = cloudAnchor ?: return@buildRenderable
addTransformableNodeToScene(arFragment, anchor, it)
cloudAnchorState = CloudAnchorState.RESOLVING
}
}
}
```
[^futuro]: Il ripristino non può essere troppo dilazionato nel tempo in quanto le ancore vengono conservate sul server per massimo ventiquattro ore.

BIN
src/figures/c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
src/figures/ca1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/figures/ca2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

BIN
src/figures/rbm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/figures/rfm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
src/figures/ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB