From c3340866ea027083a92062c25473893d212ce3e8 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sun, 13 Jan 2019 22:22:30 +0100 Subject: [PATCH 01/13] init chapter3.3 --- src/chapter3.3.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/chapter3.3.md diff --git a/src/chapter3.3.md b/src/chapter3.3.md new file mode 100644 index 0000000..142858e --- /dev/null +++ b/src/chapter3.3.md @@ -0,0 +1,116 @@ +## Runtime building models + +Lo scopo di questo progetto è mostrare come sia possibile costruire dei semplici modelli tridimensionali senza dover ricorrere da asset pre costruiti. + +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 di un utente renderizza nella scena un oggetto dalla forma e colore *pseudo-casuali*. +Inoltre è stato aggiunto un ulteriore elemento di interazione con l'utente, che potrà cliccare anche sull'oggetto virtuale, evento che comporterà un cambiamento nella tinta di quest'ultimo. + +### Interazione con l'utente + +Anche in questo caso l'interazione con l'utente avviene mediante un tocco sul display in corrispondenza di un piano. + + +```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 + +Il materiale dell'oggetto tridimensionale viene creato a partire da un colore. +La creazione 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 dimension = Vector3(0.1f, 0.1f, 0.1f) + return when (shape) { + Shape.CUBE -> ShapeFactory + .makeCube(dimension, Vector3(0.0f, 0.0f, 0.0f), material) + Shape.CYLINDER -> ShapeFactory + .makeCylinder(0.1f, 0.3f, Vector3(0.0f, 0.0f, 0.0f), material) + Shape.SPHERE -> ShapeFactory + .makeSphere(0.1f, dimension, 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 del 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 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. From 9e589dc734db6baf2d506d0ffd7f3107dc4cc6ce Mon Sep 17 00:00:00 2001 From: norangebit Date: Mon, 14 Jan 2019 16:10:19 +0100 Subject: [PATCH 02/13] correzione errori --- src/chapter3.3.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/chapter3.3.md b/src/chapter3.3.md index 142858e..37865fd 100644 --- a/src/chapter3.3.md +++ b/src/chapter3.3.md @@ -1,19 +1,18 @@ ## Runtime building models -Lo scopo di questo progetto è mostrare come sia possibile costruire dei semplici modelli tridimensionali senza dover ricorrere da asset pre costruiti. +Lo scopo di questo progetto è mostrare come sia possibile costruire dei semplici modelli tridimensionali senza dover ricorrere ad asset pre costruiti. 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 di un utente renderizza nella scena un oggetto dalla forma e colore *pseudo-casuali*. -Inoltre è stato aggiunto un ulteriore elemento di interazione con l'utente, che potrà cliccare anche sull'oggetto virtuale, evento che comporterà un cambiamento nella tinta di quest'ultimo. +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*. +Inoltre è stato aggiunto un ulteriore elemento di interazione con l'utente, che gli consente di cliccare anche sull'oggetto renderizzato, al fine di cambiare la tinta di quest'ultimo. ### Interazione con l'utente -Anche in questo caso l'interazione con l'utente avviene mediante un tocco sul display in corrispondenza di un piano. - +Anche in questo caso l'interazione con l'utente è gestita mediante il metodo `setOnTapArPlaneListener`. ```kotlin override fun onCreate(savedInstanceState: Bundle?) { @@ -49,10 +48,9 @@ private fun addModel(hitResult: HitResult, plane: Plane, motionEvent: MotionEven ### Creazione del materiale -Il materiale dell'oggetto tridimensionale viene creato a partire da un colore. -La creazione avviene mediante la funzione `buildMaterial` che a sua volta richiama la funzione di libreria ` MaterialFactory.makeOpaqueWithColor`. +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`. +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( @@ -75,14 +73,14 @@ fun buildShape( shape: Shape, material: Material ): ModelRenderable { - val dimension = Vector3(0.1f, 0.1f, 0.1f) + val center = Vector3(0.0f, 0.0f, 0.0f) return when (shape) { Shape.CUBE -> ShapeFactory - .makeCube(dimension, Vector3(0.0f, 0.0f, 0.0f), material) + .makeCube(Vector3(0.2f, 0.2f, 0.2f), center, material) Shape.CYLINDER -> ShapeFactory - .makeCylinder(0.1f, 0.3f, Vector3(0.0f, 0.0f, 0.0f), material) + .makeCylinder(0.1f, 0.2f, center, material) Shape.SPHERE -> ShapeFactory - .makeSphere(0.1f, dimension, material) + .makeSphere(0.1f, center, material) } } ``` @@ -111,6 +109,6 @@ fun changeColorOfMaterial( Quest'ultima 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. +[^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. From 471e296ef30b0563381414e567583f24b17af49d Mon Sep 17 00:00:00 2001 From: norangebit Date: Wed, 16 Jan 2019 21:33:09 +0100 Subject: [PATCH 03/13] init chapter3.4 --- src/chapter3.4.0.md | 31 ++++++++ src/chapter3.4.1.md | 185 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 src/chapter3.4.0.md create mode 100644 src/chapter3.4.1.md diff --git a/src/chapter3.4.0.md b/src/chapter3.4.0.md new file mode 100644 index 0000000..d2cc83f --- /dev/null +++ b/src/chapter3.4.0.md @@ -0,0 +1,31 @@ +## 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 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 concentrassi 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 e diretto. +Nonostante ciò è possibile aggirare il problema tramite opportuni escamotage, anche se bisogna tenere in conto che i risultati sono modesti e macchinosi da raggiungere. + +Nelle sezioni seguenti verrano analizzate in dettaglio le possibili soluzioni. + +### Animazioni + +L'utilizzo di oggetti animati oltre ad essere un orpello grafico diventa, in molti casi, un punto principale 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], sia 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 si 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. + +Risultano evidenti non solo le limitazioni, ma anche la natura più di *pezza* che soluzione al problema. + +[^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 secondo è quella che vogliamo integrare nell'ambiente. diff --git a/src/chapter3.4.1.md b/src/chapter3.4.1.md new file mode 100644 index 0000000..86437ff --- /dev/null +++ b/src/chapter3.4.1.md @@ -0,0 +1,185 @@ +### Movimento + +Anche in questo caso Sceneform non mette a disposizione 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 nelle applicazioni. +Inoltre per poter simulare il movimento avremo bisogno di settare dei punti nello spazio e *collegarli* tramite un interpolatore. + +Per mostrare l'applicazione degli animator è stato realizzato un progetto d'esempio che in seguito al tocco dell'utente renderizzerà un modello del sistema solare in cui i pianeti realizzano sia il modo di rotazione, sia quello di rivoluzione. + +#### Recupero e rendering dei modelli + +Visto l'elevato numero di modelli con cui si deve operare si è scelto di recuperarli a runtime. +Il procedimento è grossomodo identico a quello visto precedentemente, ma in questo caso si è scelto di evitare l'uso 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. +Inoltre sempre attraverso le *coroutines* è stato possibile eseguire più rendering in parallelo e quindi velocizzare l'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 con una `Map` tutti i pianeti del sistema solare. + +```kotlin +suspend fun loadPlanets(context: Context): 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()) + ) +} +``` + +Il caricamento del singolo pianeta avviene mediante la funzione `loadPlanet`. +Nella funzione da prima viene 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 future che in questo caso viene ottenuto in modo asincrono attraverso il costruttore di coroutines `async`. +Com'è possibile notare 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 fare 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 `launch` e si attende finché il job di rendering non sia concluso. + +```kotlin +GlobalScope.launch(Dispatchers.Main) { + loadPlanetsJob.join() + // operazioni con gli oggetti renderizzati +} +``` + +#### Orbite e pianeti + +Per realizzare le orbite e i pianeti è stata creata la classe `RotationNode` che va ad estendere la classe di libreria `Node`. + +Componente principale di questa classe è la funzione `createAnimator` che si occupo 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. +Inoltre 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.propertyName = "localRotation" + + orbitAnimation.setEvaluator(QuaternionEvaluator()) + + orbitAnimation.repeatCount = ObjectAnimator.INFINITE + orbitAnimation.repeatMode = ObjectAnimator.RESTART + orbitAnimation.interpolator = LinearInterpolator() + orbitAnimation.setAutoCancel(true) + + return orbitAnimation +} +``` + +Sempre 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() +} +``` + +Inoltre per la creazione dei pianeti si è reso necessario creare un ulteriore classe, `PlanetNode`, anch'essa estensione della classe `Node`. +Questa classe altro non è che un nodo con all'interno un `RotationNode`. + +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 alla scena + +La creazione del sistema solare avviene mediante la funzione `createSolarSystem` che riceve in ingresso la `Map` con tutti i modelli dei pianeti e li posizionare intorno al sole restituendo infine 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 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. From a41ac0665816b20b987d74e718bc3c8199674149 Mon Sep 17 00:00:00 2001 From: norangebit Date: Thu, 17 Jan 2019 15:36:23 +0100 Subject: [PATCH 04/13] impaginazione - impaginazione del codice sorgente - correzioni generali --- src/chapter3.3.md | 69 +++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/chapter3.3.md b/src/chapter3.3.md index 37865fd..5477485 100644 --- a/src/chapter3.3.md +++ b/src/chapter3.3.md @@ -1,6 +1,6 @@ ## Runtime building models -Lo scopo di questo progetto è mostrare come sia possibile costruire dei semplici modelli tridimensionali senza dover ricorrere ad asset pre costruiti. +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: @@ -8,7 +8,7 @@ L'SDK di Sceneform fornisce due classi per adempiere a questo compito: - `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*. -Inoltre è stato aggiunto un ulteriore elemento di interazione con l'utente, che gli consente di cliccare anche sull'oggetto renderizzato, al fine di cambiare la tinta di quest'ultimo. +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. ### Interazione con l'utente @@ -16,16 +16,20 @@ Anche in questo caso l'interazione con l'utente è gestita mediante il metodo `s ```kotlin override fun onCreate(savedInstanceState: Bundle?) { - //... - arFragment.setOnTapArPlaneListener(this::addModel) - //... + // ... + 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) { +private fun addModel( + hitResult: HitResult, + plane: Plane, + motionEvent: MotionEvent +) { val color = generateColor() buildMaterial(this, color) { @@ -48,19 +52,19 @@ private fun addModel(hitResult: HitResult, plane: Plane, motionEvent: MotionEven ### Creazione del materiale -La creazione del materiale avviene mediante la funzione `buildMaterial` che a sua volta richiama la funzione di libreria ` MaterialFactory.makeOpaqueWithColor`. +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 + context: Context, + color: Color, + onSuccess: (material: Material) -> Unit ) { - MaterialFactory - .makeOpaqueWithColor(context, color) - .thenAccept(onSuccess) + MaterialFactory + .makeOpaqueWithColor(context, color) + .thenAccept(onSuccess) } ``` @@ -70,18 +74,19 @@ Per la costruzione della forma geometrica si è usata la funzione `buildShape` c ```kotlin fun buildShape( - shape: Shape, - material: Material + 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) - } + 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) + } } ``` @@ -89,7 +94,7 @@ Come è possibile notare a seconda della figura, vanno specificate le caratteris ### Aggiunta del nodo alla scena -L'aggiunta del 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. +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. @@ -97,17 +102,17 @@ Questa operazione avviene mediante il metodo `setOnTapListener`, al quale, media ```kotlin fun changeColorOfMaterial( - context: Context, - color: Color, - renderable: Renderable + context: Context, + color: Color, + renderable: Renderable ) { - buildMaterial(context, color) { - renderable.material = it - } + buildMaterial(context, color) { + renderable.material = it + } } ``` -Quest'ultima si occupa di creare un nuovo materiale e sostituirlo a quello precedente. +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. From f3ed17dba70c97da04e5736ab10ad4e2f7e2f1b2 Mon Sep 17 00:00:00 2001 From: norangebit Date: Thu, 17 Jan 2019 22:04:36 +0100 Subject: [PATCH 05/13] impaginazione --- src/chapter3.4.0.md | 16 +++---- src/chapter3.4.1.md | 111 ++++++++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 49 deletions(-) diff --git a/src/chapter3.4.0.md b/src/chapter3.4.0.md index d2cc83f..53eafa7 100644 --- a/src/chapter3.4.0.md +++ b/src/chapter3.4.0.md @@ -1,22 +1,20 @@ ## 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 dotati di animazioni, né erano in grado di muoversi all'interno dell'ambiente reale circostante. +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 concentrassi 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 e diretto. -Nonostante ciò è possibile aggirare il problema tramite opportuni escamotage, anche se bisogna tenere in conto che i risultati sono modesti e macchinosi da raggiungere. - -Nelle sezioni seguenti verrano analizzate in dettaglio le possibili soluzioni. +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 punto principale nel processo di sviluppo. +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], sia proprio la totale mancanza di supporto alle animazioni. +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 si 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]. @@ -24,8 +22,8 @@ La soluzione consiste nel renderizzare un schermo trasparente nel mondo reale e 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. -Risultano evidenti non solo le limitazioni, ma anche la natura più di *pezza* che soluzione al problema. +Non solo risultano evidenti le limitazioni di questo metodo, ma anche la natura più di *pezza* che soluzione al problema. [^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 secondo è quella che vogliamo integrare nell'ambiente. +[^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.4.1.md b/src/chapter3.4.1.md index 86437ff..1d9010b 100644 --- a/src/chapter3.4.1.md +++ b/src/chapter3.4.1.md @@ -1,17 +1,17 @@ ### Movimento -Anche in questo caso Sceneform non mette a disposizione 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`. +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 nelle applicazioni. -Inoltre per poter simulare il movimento avremo bisogno di settare dei punti nello spazio e *collegarli* tramite un interpolatore. +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 l'applicazione degli animator è stato realizzato un progetto d'esempio che in seguito al tocco dell'utente renderizzerà un modello del sistema solare in cui i pianeti realizzano sia il modo di rotazione, sia quello di rivoluzione. +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. #### Recupero e rendering dei modelli -Visto l'elevato numero di modelli con cui si deve operare si è scelto di recuperarli a runtime. -Il procedimento è grossomodo identico a quello visto precedentemente, ma in questo caso si è scelto di evitare l'uso 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. -Inoltre sempre attraverso le *coroutines* è stato possibile eseguire più rendering in parallelo e quindi velocizzare l'applicazione. +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 si è scelto di non usare le 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. +Inoltre 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. @@ -19,16 +19,19 @@ Inoltre viene conservato un riferimento al `Job` della coroutine. ```kotlin override fun onCreate(savedInstanceState: Bundle?) { // ... - loadPlanetsJob = GlobalScope.launch(Dispatchers.Main) { + loadPlanetsJob = GlobalScope.launch(Dispatchers.Main){ renderablePlanets = loadPlanets(this@MainActivity) } // ... } ``` -La funzione `loadPlanets` si occupa di caricare e restituire con una `Map` tutti i pianeti del sistema solare. +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: Context): Map { +suspend fun loadPlanets( + context: Contex +) : Map { val sun = loadPlanet(context, Planet.SUN) /** * ... @@ -45,14 +48,20 @@ suspend fun loadPlanets(context: Context): Map { } ``` -Il caricamento del singolo pianeta avviene mediante la funzione `loadPlanet`. -Nella funzione da prima viene 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 future che in questo caso viene ottenuto in modo asincrono attraverso il costruttore di coroutines `async`. -Com'è possibile notare viene usato il dispatcher `IO` che ci consente di eseguire l'operazione in background. +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 utilizzarlo 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)) +fun loadPlanet( + context: Context, + planet: Planet +): Deferred { + val modelSource = fetchModel( + context, + Uri.parse(planet.value) + ) val futureRenderable = buildFutureRenderable( context, modelSource, @@ -65,8 +74,8 @@ fun loadPlanet(context: Context, planet: Planet): Deferred { } ``` -L'ultima operazione da dover fare 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 `launch` e si attende finché il job di rendering non sia concluso. +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) { @@ -77,37 +86,35 @@ GlobalScope.launch(Dispatchers.Main) { #### Orbite e pianeti -Per realizzare le orbite e i pianeti è stata creata la classe `RotationNode` che va ad estendere la classe di libreria `Node`. +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 classe è la funzione `createAnimator` che si occupo della creazione dell'`ObjectAnimator` che permette di muovere i modelli. +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. Inoltre 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 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.propertyName = "localRotation" + orbitAnimation.setObjectValues(*orientations.toTypedArray()) orbitAnimation.setEvaluator(QuaternionEvaluator()) - orbitAnimation.repeatCount = ObjectAnimator.INFINITE + orbitAnimation.repeatCount = ObjectAnimator.INFINITE orbitAnimation.repeatMode = ObjectAnimator.RESTART orbitAnimation.interpolator = LinearInterpolator() orbitAnimation.setAutoCancel(true) - return orbitAnimation + return orbitAnimation } ``` -Sempre nella classe `RotationNode` vanno sovrascritti i metodi `OnActivate` e `OnDeactivate`, per gestire lo start e lo stop dell'animazione. +Inoltre nella classe `RotationNode` vanno sovrascritti i metodi `OnActivate` e `OnDeactivate`, per gestire lo start e lo stop dell'animazione. ```kotlin override fun onActivate() { @@ -119,8 +126,8 @@ override fun onDeactivate() { } ``` -Inoltre per la creazione dei pianeti si è reso necessario creare un ulteriore classe, `PlanetNode`, anch'essa estensione della classe `Node`. -Questa classe altro non è che un nodo con all'interno un `RotationNode`. +La creazione dei pianeti è gestita attraverso un ulteriore classe, `PlanetNode`, anch'essa estensione della classe `Node`. +Questa classe altro non è che un nodo che come attributo ha un `RotationNode`. 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. @@ -141,7 +148,11 @@ private fun createPlanetNode( val renderable = renderablePlanets[planet] ?: return val planetNode = PlanetNode(renderable) planetNode.setParent(orbit) - planetNode.localPosition = Vector3(AU_TO_METERS * auFromParent, 0.0f, 0.0f) + planetNode.localPosition = Vector3( + AU_TO_METERS * auFromParent, + 0.0f, + 0.0f + ) } ``` @@ -149,10 +160,12 @@ private fun createPlanetNode( #### Creazione e aggiunta del sistema solare alla scena -La creazione del sistema solare avviene mediante la funzione `createSolarSystem` che riceve in ingresso la `Map` con tutti i modelli dei pianeti e li posizionare intorno al sole restituendo infine quest'ultimo. +La creazione del nostro sistema solare avviene mediante la funzione `createSolarSystem` che riceve in ingresso la `Map` con tutti i modelli dei pianeti, li posizionare intorno al sole e infine restituisce quest'ultimo. ```kotlin -private fun createSolarSystem(renderablePlanets: Map): Node { +private fun createSolarSystem( + renderablePlanets: Map +): Node { val base = Node() val sun = Node() @@ -164,22 +177,40 @@ private fun createSolarSystem(renderablePlanets: Map): sunVisual.renderable = renderablePlanets[Planet.SUN] sunVisual.localScale = Vector3(0.5f, 0.5f, 0.5f) - createPlanetNode(Planet.MERCURY, sun, 0.4f, 47f, renderablePlanets) + createPlanetNode( + Planet.MERCURY, + sun, + 0.4f, + 47f, + renderablePlanets + ) // ... - createPlanetNode(Planet.NEPTUNE, sun, 6.1f, 5f, renderablePlanets) + createPlanetNode( + Planet.NEPTUNE, + sun, + 6.1f, + 5f, + renderablePlanets +) return base } ``` -L'aggiunta alla scene avviene mediante la ben nota funzione `addNodeToScene`. +L'aggiunta dei modelli alla scene avviene mediante la ben nota funzione `addNodeToScene`. ```kotlin val solarSystem = createSolarSystem(renderablePlanets) -addNodeToScene(arFragment, hitResult.createAnchor(), solarSystem) +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`. From fc14dcdf8d6656cb3efe384446c1974981ede875 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sat, 19 Jan 2019 12:39:13 +0100 Subject: [PATCH 06/13] Impaginazione snippet --- src/chapter3.0.md | 2 +- src/chapter3.1.md | 31 ++++++++++++++++++------------- src/chapter3.2.md | 38 +++++++++++++++++++++++--------------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/chapter3.0.md b/src/chapter3.0.md index 32b2184..a532ef3 100644 --- a/src/chapter3.0.md +++ b/src/chapter3.0.md @@ -19,7 +19,7 @@ android { 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" +implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.6.0' ``` Inoltre nell'Android Manifest[^manifest] va dichiarato l'utilizzo del permesso della fotocamera[^camera] e l'utilizzo di ARCore[^arcore]. diff --git a/src/chapter3.1.md b/src/chapter3.1.md index cc89b41..7c7ce85 100644 --- a/src/chapter3.1.md +++ b/src/chapter3.1.md @@ -14,13 +14,15 @@ L'importazione del modello all'interno del progetto di Android Studio è stato e ### 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 arcoreimag tool*, un software a linea di comando, che oltre a creare il database, si occupa anche di valutare l'immagine. +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. -Dato che nel caso specifico si vuole far riconoscere un'unica immagine, si è optato per la generazione del database a runtime. +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 { +private fun setupAugmentedImageDb ( + config: Config +): Boolean { val image = loadImage(IMAGE_FILE_NAME) ?: return false val augmentedImageDb = AugmentedImageDatabase(session) @@ -35,22 +37,22 @@ private fun setupAugmentedImageDb (config: Config): Boolean { 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 un pezzo di codice ogni qual volta la scena viene aggiornata. +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) - - //... + // ... + 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 -private fun detectAndPlaceAugmentedImage(frameTime: FrameTime) { +fun detectAndPlaceAugmentedImage(frameTime: FrameTime) { if (isModelAdded) return @@ -60,7 +62,8 @@ private fun detectAndPlaceAugmentedImage(frameTime: FrameTime) { .find { it.name.contains(IMAGE_NAME) } ?: return - val augmentedImageAnchor = augmentedImage.createAnchor(augmentedImage.centerPose) + val augmentedImageAnchor = augmentedImage + .createAnchor(augmentedImage.centerPose) buildRenderable(this, Uri.parse(MODEL_NAME)) { addTransformableNodeToScene( @@ -78,7 +81,7 @@ Il settaggio del flag `isModelAdded` al valore booleano di vero, si rende necess ### Rendering del modello -Il rendering del modello avviene attraverso la funzione `buildRenderable` che a sua volta chiama la funzione di libreria `ModelRenderable.builder()`. +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`. @@ -113,7 +116,9 @@ fun addTransformableNodeToScene( renderable: Renderable ) { val anchorNode = AnchorNode(anchor) - val transformableNode = TransformableNode(arFragment.transformationSystem) + val transformableNode = TransformableNode( + arFragment.transformationSystem + ) transformableNode.renderable = renderable transformableNode.setParent(anchorNode) arFragment.arSceneView.scene.addChild(anchorNode) diff --git a/src/chapter3.2.md b/src/chapter3.2.md index 822af0c..d6d83ea 100644 --- a/src/chapter3.2.md +++ b/src/chapter3.2.md @@ -21,9 +21,11 @@ Sceneform ci permette di personalizzare il comportamento al verificarsi di quest ```kotlin override fun onCreate(savedInstanceState: Bundle?) { - //... - arFragment.setOnTapArPlaneListener(this::fetchAndPlaceModel) - //... + // ... + arFragment.setOnTapArPlaneListener( + this::fetchAndPlaceModel + ) + // ... } ``` @@ -49,7 +51,7 @@ private fun fetchAndPlaceModel( ### Fetching del model -Il recupero del modello avviene attraverso la funzione `fetchModel`, che a sua volta chiama la funzione di libreria `RenderableSource.builder()`. +Il recupero del modello avviene attraverso la funzione `fetchModel`, che a sua volta chiama la funzione di libreria `RenderableSource.builder`. ```kotlin fun fetchModel( @@ -57,8 +59,14 @@ fun fetchModel( source: Uri ) : RenderableSource { return RenderableSource.builder() - .setSource(context, source, RenderableSource.SourceType.GLTF2) - .setRecenterMode(RenderableSource.RecenterMode.ROOT) + .setSource( + context, + source, + RenderableSource.SourceType.GLTF2 + ) + .setRecenterMode( + RenderableSource.RecenterMode.ROOT + ) .build() } ``` @@ -76,15 +84,15 @@ fun buildRenderable( 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 - } + ModelRenderable.builder() + .setRegistryId(modelUri) + .setSource(context, model) + .build() + .thenAccept(onSuccess) + .exceptionally { + Log.e("SCENEFORM", "unable to load model", it) + return@exceptionally null + } } ``` From 76d1e976c23f59bfdc83ffafbb4387ecc955da02 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sun, 20 Jan 2019 15:57:10 +0100 Subject: [PATCH 07/13] rename chapter3.4 to chapter3.5 --- src/{chapter3.4.0.md => chapter3.5.0.md} | 0 src/{chapter3.4.1.md => chapter3.5.1.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{chapter3.4.0.md => chapter3.5.0.md} (100%) rename src/{chapter3.4.1.md => chapter3.5.1.md} (100%) diff --git a/src/chapter3.4.0.md b/src/chapter3.5.0.md similarity index 100% rename from src/chapter3.4.0.md rename to src/chapter3.5.0.md diff --git a/src/chapter3.4.1.md b/src/chapter3.5.1.md similarity index 100% rename from src/chapter3.4.1.md rename to src/chapter3.5.1.md From 700899f257e0c9135239805d64693d86f8d1ff91 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sun, 20 Jan 2019 16:10:03 +0100 Subject: [PATCH 08/13] fix paragraph title --- src/chapter3.5.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chapter3.5.1.md b/src/chapter3.5.1.md index 1d9010b..b5438ce 100644 --- a/src/chapter3.5.1.md +++ b/src/chapter3.5.1.md @@ -158,7 +158,7 @@ private fun createPlanetNode( È 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 alla scena +#### 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 posizionare intorno al sole e infine restituisce quest'ultimo. From c9bf508a8355d4bfc226c9a4f8b7f0e9db6f7889 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sun, 20 Jan 2019 17:23:44 +0100 Subject: [PATCH 09/13] init collision --- src/chapter3.4.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/chapter3.4.md diff --git a/src/chapter3.4.md b/src/chapter3.4.md new file mode 100644 index 0000000..f21bd11 --- /dev/null +++ b/src/chapter3.4.md @@ -0,0 +1,87 @@ +## 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. + +### 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 sono 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) + // ... +} +``` + +Con la funzione `addShape` che, utilizzando le funzioni `buildMaterial` e `buildShape` analizzate in precedenza, si occupa dell'effettiva aggiunta dell'aggetto 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 che 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. From 1576f7414feabe6c3a34143b57b3faf2c921fb56 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sun, 20 Jan 2019 17:25:19 +0100 Subject: [PATCH 10/13] add sceneform fragment to intro --- src/chapter3.0.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/chapter3.0.md b/src/chapter3.0.md index a532ef3..3bf4fd2 100644 --- a/src/chapter3.0.md +++ b/src/chapter3.0.md @@ -22,7 +22,17 @@ Sempre nel file per il build del progetto è necessario aggiungere la dipendenza implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.6.0' ``` -Inoltre nell'Android Manifest[^manifest] va dichiarato l'utilizzo del permesso della fotocamera[^camera] e l'utilizzo di ARCore[^arcore]. +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 caratteristiche di un'applicazione Android, tra cui anche i permessi. From 283d10568e2de0ca7849069ae6ab6a250860ab0f Mon Sep 17 00:00:00 2001 From: norangebit Date: Mon, 21 Jan 2019 19:18:58 +0100 Subject: [PATCH 11/13] correzioni errori --- src/chapter3.0.md | 4 ++-- src/chapter3.4.md | 9 ++++----- src/chapter3.5.0.md | 6 +++--- src/chapter3.5.1.md | 13 +++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/chapter3.0.md b/src/chapter3.0.md index 3bf4fd2..b7ddd87 100644 --- a/src/chapter3.0.md +++ b/src/chapter3.0.md @@ -22,7 +22,7 @@ Sempre nel file per il build del progetto è necessario aggiungere la dipendenza 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. +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 Date: Mon, 21 Jan 2019 21:52:49 +0100 Subject: [PATCH 12/13] init cloud anchor --- src/chapter3.6.md | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/chapter3.6.md diff --git a/src/chapter3.6.md b/src/chapter3.6.md new file mode 100644 index 0000000..1ef748b --- /dev/null +++ b/src/chapter3.6.md @@ -0,0 +1,207 @@ +## 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 o per condividere 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. + +### 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 ne 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 anchor 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`. + +### 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 ricopiare, si è scelto di appoggiarsi al servizio *firestore * di Firebase per creare una relazione uno tra l'UUID e un valore numerico intero. + +Queste operazioni avvengono mediante 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 usando come chiave un numero intero. +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() +} +``` + +La funzione `checkHosting` si occupa di notificare all'utente il codice numerico associato all'ancora e di cambiare lo stato dell'applicazione. + +```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. + +```kotlin +fun onResolveClick(view: View) { + if (cloudAnchor != null) + return + + val dialog = ResolveDialogFragment() + dialog.setOkListener(this::onResolveOkPressed) + dialog.show(supportFragmentManager, "Resolve") +} +``` + +Alla conferma da parte dell'utente viene eseguito il metodo `onResolveOkPressed` che converte il codice numerico 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 + } + } +} +``` From 0d3c187c529ba3be836184c6508541fc52a570ea Mon Sep 17 00:00:00 2001 From: norangebit Date: Tue, 22 Jan 2019 11:08:57 +0100 Subject: [PATCH 13/13] correzioni errori - correzioni - aggiunta snippet xml --- src/chapter3.6.md | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/chapter3.6.md b/src/chapter3.6.md index 1ef748b..50aa472 100644 --- a/src/chapter3.6.md +++ b/src/chapter3.6.md @@ -1,7 +1,7 @@ ## 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 o per condividere con altri utenti. +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 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. @@ -18,7 +18,7 @@ Una volta ottenuta la chiave è necessario dichiararla nell'Android Manifest med Inoltre per tenere traccia dello stato dell'applicazione si è definita una classe enumerativa con cinque possibili valori. -- `NONE`: non è presente alcuno oggetto nella scena ne se ne sta recuperando uno dal server. +- `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. @@ -26,7 +26,7 @@ Inoltre per tenere traccia dello stato dell'applicazione si è definita una clas ### Attivazione delle cloud anchors -Le cloud anchor di default sono disattivate e la loro attivazione può avvenire in due modi. +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. @@ -54,12 +54,20 @@ class CloudArFragment: ArFragment(){ 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 ricopiare, si è scelto di appoggiarsi al servizio *firestore * di Firebase per creare una relazione uno tra l'UUID e un valore numerico intero. +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 mediante la classe `CloudAnchorHelper` che fornisce due metodi principali `getShortCode` e `getCloudAnchorId`. +Queste operazioni avvengono tramite la classe `CloudAnchorHelper` che fornisce due metodi principali `getShortCode` e `getCloudAnchorId`. ```kotlin fun getShortCode(cloudAnchorId: String): Int { @@ -90,8 +98,8 @@ fun getCloudAnchorId( } ``` -Il primo metodo riceve in ingresso l'UUID dell'ancora e lo aggiunge al database usando come chiave un numero intero. -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`. +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 @@ -124,7 +132,7 @@ private fun addModel( ### 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. +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) { @@ -133,7 +141,7 @@ private fun checkCloudAnchor(frameTime: FrameTime) { ) return - val cloudState = cloudAnchor?.cloudAnchorState?:return + val cloudState=cloudAnchor?.cloudAnchorState?:return if (cloudState.isError) { toastError() @@ -151,7 +159,7 @@ private fun checkCloudAnchor(frameTime: FrameTime) { } ``` -La funzione `checkHosting` si occupa di notificare all'utente il codice numerico associato all'ancora e di cambiare lo stato dell'applicazione. +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 e di cambiare lo stato dell'applicazione da `HOSTING` a `HOSTED`. ```kotlin private fun checkHosting() { @@ -174,7 +182,7 @@ private fun checkHosting() { ### Resolving dell'ancora -L'utente può ripristinare un ancora premendo sul pulsante *resolve*. +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. ```kotlin @@ -188,7 +196,7 @@ fun onResolveClick(view: View) { } ``` -Alla conferma da parte dell'utente viene eseguito il metodo `onResolveOkPressed` che converte il codice numerico nell'UUID dell'ancora e da questo ripristina il nodo nella scena. +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) {