diff --git a/src/chapter3.0.md b/src/chapter3.0.md new file mode 100644 index 0000000..b7ddd87 --- /dev/null +++ b/src/chapter3.0.md @@ -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 + +``` + +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. diff --git a/src/chapter3.1.md b/src/chapter3.1.md new file mode 100644 index 0000000..7c7ce85 --- /dev/null +++ b/src/chapter3.1.md @@ -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. diff --git a/src/chapter3.2.md b/src/chapter3.2.md new file mode 100644 index 0000000..d149746 --- /dev/null +++ b/src/chapter3.2.md @@ -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. diff --git a/src/chapter3.3.md b/src/chapter3.3.md new file mode 100644 index 0000000..e391fba --- /dev/null +++ b/src/chapter3.3.md @@ -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. diff --git a/src/chapter3.4.md b/src/chapter3.4.md new file mode 100644 index 0000000..6df73ac --- /dev/null +++ b/src/chapter3.4.md @@ -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. diff --git a/src/chapter3.5.0.md b/src/chapter3.5.0.md new file mode 100644 index 0000000..179bee7 --- /dev/null +++ b/src/chapter3.5.0.md @@ -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. diff --git a/src/chapter3.5.1.md b/src/chapter3.5.1.md new file mode 100644 index 0000000..136d9e8 --- /dev/null +++ b/src/chapter3.5.1.md @@ -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 { + 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 { + 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 +) { + 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 +): 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`. diff --git a/src/chapter3.6.md b/src/chapter3.6.md new file mode 100644 index 0000000..fc73208 --- /dev/null +++ b/src/chapter3.6.md @@ -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 + +``` + +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 + +``` + +### 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. diff --git a/src/figures/c.png b/src/figures/c.png new file mode 100644 index 0000000..f6caff3 Binary files /dev/null and b/src/figures/c.png differ diff --git a/src/figures/ca1.png b/src/figures/ca1.png new file mode 100644 index 0000000..09f7ab3 Binary files /dev/null and b/src/figures/ca1.png differ diff --git a/src/figures/ca2.png b/src/figures/ca2.png new file mode 100644 index 0000000..2737037 Binary files /dev/null and b/src/figures/ca2.png differ diff --git a/src/figures/rbm.png b/src/figures/rbm.png new file mode 100644 index 0000000..dcd8d25 Binary files /dev/null and b/src/figures/rbm.png differ diff --git a/src/figures/rfm.png b/src/figures/rfm.png new file mode 100644 index 0000000..c093be6 Binary files /dev/null and b/src/figures/rfm.png differ diff --git a/src/figures/ss.png b/src/figures/ss.png new file mode 100644 index 0000000..9e0d8d4 Binary files /dev/null and b/src/figures/ss.png differ