Merge branch 'chapter3' into dev
This commit is contained in:
commit
b4b2df4be1
41
src/chapter3.0.md
Normal file
41
src/chapter3.0.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Progetti d'esempio
|
||||||
|
|
||||||
|
Per poter realizzare delle applicazioni mediante ARCore e Sceneform sono necessarie una serie di configurazioni iniziali.
|
||||||
|
|
||||||
|
Requisito necessario al funzionamento di ARCore è una versione di Android uguale o superiore ad Android 7.0 Nougat(API level 24).
|
||||||
|
Inoltre se si sta lavorando su un progetto con API level minore di 26 è necessario esplicitare il supporto a Java 8 andando a modificare file `app/build.gradle`.
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
android {
|
||||||
|
...
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sempre nel file per il build del progetto è necessario aggiungere la dipendenza di Sceneform.
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.6.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
Inoltre per sfruttare al massimo le potenzialità offerte da Sceneform e ridurre al minimo il lavoro extra per la gestione delle view, si deve aggiungere il fragment di Sceneform al file di layout dell'activity principale mediante il seguente codice xml.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/sceneform_fragment"
|
||||||
|
android:name="com.google.ar.sceneform.ux.ArFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
```
|
||||||
|
|
||||||
|
Infine nell'Android Manifest[^manifest] va dichiarato l'utilizzo del permesso della fotocamera[^camera] e l'utilizzo di ARCore[^arcore].
|
||||||
|
|
||||||
|
[^manifest]: File in cui vengono dichiarate tutte le caratteristiche di un'applicazione Android, tra cui anche i permessi.
|
||||||
|
|
||||||
|
[^camera]: Lo sviluppatore deve solo dichiarare l'utilizzo del permesso, la richiesta di concessione è gestita in automatico da Sceneform.
|
||||||
|
|
||||||
|
[^arcore]: L'utilizzo di ARCore deve essere dichiarata in quanto non tutti i dispositivi lo supportano.
|
133
src/chapter3.1.md
Normal file
133
src/chapter3.1.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
## Augmented images
|
||||||
|
|
||||||
|
Nel primo progetto d'esempio si è affrontato un classico problema di AR marker based, ovvero il riconoscimento di un'immagine preimpostata e il conseguente sovrapponimento di un oggetto virtuale.
|
||||||
|
Nel caso specifico si vuole riconoscere una foto del pianeta terra e sostituirvi un modello tridimensionale di essa.
|
||||||
|
|
||||||
|
### Aggiunta del modello
|
||||||
|
|
||||||
|
Il modello tridimensionale della terra è stato recuperato dal sito `poly.google.com`, che funge da repository per modelli 3D.
|
||||||
|
La scelta del modello è stata dettata sia del formato[^format] in cui era disponibile, sia dalla licenza con cui veniva distribuito.
|
||||||
|
Una volta ottenuto il modello è stato salvato nella cartella *"sampledata"*, il cui contenuto sarà usato solo in fase di progettazione.
|
||||||
|
|
||||||
|
L'importazione del modello all'interno del progetto di Android Studio è stato effettuato mediante l'utilizzo del plug-in *Google Sceneform Tools*, che si occupa sia di convertire il modello nel formato di Sceneform, sia di aggiornare il file `build.gradle` affinché sia incluso nell'APK[^apk] finale.
|
||||||
|
|
||||||
|
### Creazione del database
|
||||||
|
|
||||||
|
Il database contenente tutte le immagini che si desidera far riconosce all'applicazione, può essere creato sia a priori, sia a tempo di esecuzione.
|
||||||
|
Per la prima soluzione Google mette a disposizione *The arcoreimage tool*, un software a linea di comando, che oltre a creare il database, si occupa anche di valutare l'immagine.
|
||||||
|
|
||||||
|
Nel caso specifico si vuole far riconoscere un'unica immagine, quindi si è optato per la generazione del database a tempo di esecuzione.
|
||||||
|
In particolare quest'operazione avviene mediante la funzione `setupAugmentedImageDb`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun setupAugmentedImageDb (
|
||||||
|
config: Config
|
||||||
|
): Boolean {
|
||||||
|
val image = loadImage(IMAGE_FILE_NAME) ?: return false
|
||||||
|
|
||||||
|
val augmentedImageDb = AugmentedImageDatabase(session)
|
||||||
|
augmentedImageDb.addImage(IMAGE_NAME, image)
|
||||||
|
config.augmentedImageDatabase = augmentedImageDb
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Riconoscimento dell'immagine
|
||||||
|
|
||||||
|
Il riconoscimento dell'immagine non può avvenire mediate l'utilizzo di una callback in quanto ARCore non permette di registrare un listener all'evento.
|
||||||
|
Risulta dunque evidente che la verifica dell'avvenuto match sarà delegata allo sviluppatore.
|
||||||
|
|
||||||
|
Per fare ciò si è usato il metodo `addOnUpdateListener` dell'oggetto `Scene`, che permette di eseguire uno *snippet* di codice ogni qual volta la scena viene aggiornata.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// ...
|
||||||
|
arSceneView.scene.addOnUpdateListener(
|
||||||
|
this::detectAndPlaceAugmentedImage
|
||||||
|
)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove la funzione `detectAndPlaceAugmentedImage` si occupa di verificare la presenza di un match e nel caso di un riscontro positivo, dell'aggiunta dell'oggetto virtuale alla scena.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun detectAndPlaceAugmentedImage(frameTime: FrameTime) {
|
||||||
|
if (isModelAdded)
|
||||||
|
return
|
||||||
|
|
||||||
|
val augmentedImage = arSceneView.arFrame
|
||||||
|
.getUpdatedTrackables(AugmentedImage::class.java)
|
||||||
|
.filter { isTrackig(it) }
|
||||||
|
.find { it.name.contains(IMAGE_NAME) }
|
||||||
|
?: return
|
||||||
|
|
||||||
|
val augmentedImageAnchor = augmentedImage
|
||||||
|
.createAnchor(augmentedImage.centerPose)
|
||||||
|
|
||||||
|
buildRenderable(this, Uri.parse(MODEL_NAME)) {
|
||||||
|
addTransformableNodeToScene(
|
||||||
|
arFragment,
|
||||||
|
augmentedImageAnchor,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isModelAdded = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il settaggio del flag `isModelAdded` al valore booleano di vero, si rende necessario al fine di evitare l'aggiunta incontrollata di nuovi modelli alla medesima immagine.
|
||||||
|
|
||||||
|
### Rendering del modello
|
||||||
|
|
||||||
|
Il rendering del modello avviene attraverso il metodo `buildRenderable` che a sua volta chiama la funzione di libreria `ModelRenderable.builder`.
|
||||||
|
Poiché quest'ultima è un operazione onerosa viene restituito un `Future`[^future] che racchiude il `Renderable` vero e proprio.
|
||||||
|
L'interazione con l'oggetto concreto avviene mediante una callback che è possibile specificare attraverso il metodo `thenAccept`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun buildRenderable(
|
||||||
|
context: Context,
|
||||||
|
model: Uri,
|
||||||
|
onSuccess: (renderable: Renderable) -> Unit
|
||||||
|
) {
|
||||||
|
ModelRenderable.builder()
|
||||||
|
.setSource(context, model)
|
||||||
|
.build()
|
||||||
|
.thenAccept(onSuccess)
|
||||||
|
.exceptionally {
|
||||||
|
Log.e("SCENEFORM", "unable to load model", it)
|
||||||
|
return@exceptionally null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggiunta dell'oggetto virtuale nella scena
|
||||||
|
|
||||||
|
L'ultima operazione da compiere è l'aggiunta del modello renderizzato alla scena.
|
||||||
|
Questa operazione avviene attraverso la funzione `addTrasformableNodeToScene` che si occupa di creare un'ancora in corrispondenza del punto reale d'interesse.
|
||||||
|
A partire da quest'ancora viene creato un nodo che racchiude l'oggetto renderizzato.
|
||||||
|
Inoltre nel caso specifico si è usato un `TransformabelNode`, in modo da concedere all'utente la possibilità di ridimensionare o ruotare il modello.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun addTransformableNodeToScene(
|
||||||
|
arFragment: ArFragment,
|
||||||
|
anchor: Anchor,
|
||||||
|
renderable: Renderable
|
||||||
|
) {
|
||||||
|
val anchorNode = AnchorNode(anchor)
|
||||||
|
val transformableNode = TransformableNode(
|
||||||
|
arFragment.transformationSystem
|
||||||
|
)
|
||||||
|
transformableNode.renderable = renderable
|
||||||
|
transformableNode.setParent(anchorNode)
|
||||||
|
arFragment.arSceneView.scene.addChild(anchorNode)
|
||||||
|
transformableNode.select()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[^format]: Attualmente sono supportati solo modelli OBJ, FBX e gLTF.
|
||||||
|
|
||||||
|
[^apk]: Formato delle applicazioni Android.
|
||||||
|
|
||||||
|
[^future]: In informatica con il termine *future*, o *promise*, *delay* e *deferred*, si indica un tecnica che permette di sincronizzare l'esecuzione di un programma concorrente.
|
103
src/chapter3.2.md
Normal file
103
src/chapter3.2.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
## Runtime fetching models
|
||||||
|
|
||||||
|
Nella seconda applicazione d'esempio viene mostrato come sia possibile recuperare i modelli da renderizzare anche durante l'esecuzione dell'applicazione.
|
||||||
|
Questa funzione risulta particolarmente utile quando si deve rilasciare un'applicazione che sfrutta numerosi modelli e non si vuole appesantire eccessivamente il volume del file *APK*.
|
||||||
|
Inoltre concede maggiore libertà allo sviluppatore in quanto è possibile aggiungere nuovi modelli, o aggiornare quelli vecchi, senza dover operare sull'applicazione, ma lavorando esclusivamente lato server.
|
||||||
|
|
||||||
|
In questo caso specifico l'applicazione dovrà riconosce uno o più piani e in seguito ad un tocco dell'utente su di essi, mostrare un modello di *Andy*, la mascotte di Android(vedi fig. \ref{rfm}).
|
||||||
|
|
||||||
|
Per quest'applicazione oltre alle configurazioni già viste in precedenza è necessario aggiungere una nuova dipendenza che include le funzioni necessarie per il fetching del modello.
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
implementation 'com.google.ar.sceneform:assets:1.6.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
Inoltre nell'Android Manifest bisogna aggiungere il permesso per accedere alla rete.
|
||||||
|
|
||||||
|
### Interazione con l'utente
|
||||||
|
|
||||||
|
L'interazione con l'utente avviene mediante un tocco sul display in corrispondenza di un piano.
|
||||||
|
Sceneform ci permette di personalizzare il comportamento al verificarsi di questo evento tramite il metodo `setOnTapArPlaneListener`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// ...
|
||||||
|
arFragment.setOnTapArPlaneListener(
|
||||||
|
this::fetchAndPlaceModel
|
||||||
|
)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove la funzione `fetchAndPlaceModel` si occupa di recuperare il modello e renderizzarlo.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun fetchAndPlaceModel(
|
||||||
|
hitResult: HitResult,
|
||||||
|
plane: Plane,
|
||||||
|
motionEvent: MotionEvent
|
||||||
|
) {
|
||||||
|
val modelUri = Uri.parse(MODEL_SOURCE)
|
||||||
|
val fetchedModel = fetchModel(this, modelUri)
|
||||||
|
buildRenderable(this, fetchedModel, modelUri) {
|
||||||
|
addTransformableNodeToScene(
|
||||||
|
arFragment,
|
||||||
|
hitResult.createAnchor(),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetching del model
|
||||||
|
|
||||||
|
Il recupero del modello avviene attraverso la funzione `fetchModel`, che a sua volta chiama la funzione di libreria `RenderableSource.builder`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun fetchModel(
|
||||||
|
context: Context,
|
||||||
|
source: Uri
|
||||||
|
) : RenderableSource {
|
||||||
|
return RenderableSource.builder()
|
||||||
|
.setSource(
|
||||||
|
context,
|
||||||
|
source,
|
||||||
|
RenderableSource.SourceType.GLTF2
|
||||||
|
)
|
||||||
|
.setRecenterMode(
|
||||||
|
RenderableSource.RecenterMode.ROOT
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Attualmente[^sceneform-1.6] Sceneform supporta unicamente il fetching di modelli gLTF.
|
||||||
|
|
||||||
|
### Rendering e aggiunta del modello
|
||||||
|
|
||||||
|
Il rendering del modello avviene tramite la funzione `buildRenderable`, che riprende in buona parte quella vista precedentemente, con la differenza che in questo caso deve essere passato anche il modello recuperato.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun buildRenderable(
|
||||||
|
context: Context,
|
||||||
|
model: RenderableSource,
|
||||||
|
modelUri: Uri,
|
||||||
|
onSuccess: (renderable: Renderable) -> Unit
|
||||||
|
) {
|
||||||
|
ModelRenderable.builder()
|
||||||
|
.setRegistryId(modelUri)
|
||||||
|
.setSource(context, model)
|
||||||
|
.build()
|
||||||
|
.thenAccept(onSuccess)
|
||||||
|
.exceptionally {
|
||||||
|
Log.e("SCENEFORM", "unable to load model", it)
|
||||||
|
return@exceptionally null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Infine l'aggiunta del modello renderizzato alla scena avviene mediante la medesima funzione `addTransformableNodeToScene` vista in precedenza.
|
||||||
|
|
||||||
|
![Rendering di un modello recuperato a runtime](figures/rfm.png){#rfm width=225px height=400px}
|
||||||
|
|
||||||
|
[^sceneform-1.6]: Sceneform 1.6.0.
|
121
src/chapter3.3.md
Normal file
121
src/chapter3.3.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
## Runtime building models
|
||||||
|
|
||||||
|
Lo scopo di questo progetto è mostrare come sia possibile costruire dei semplici modelli tridimensionali senza dover ricorrere ad asset pre costituiti.
|
||||||
|
|
||||||
|
L'SDK di Sceneform fornisce due classi per adempiere a questo compito:
|
||||||
|
|
||||||
|
- `MaterialFactory`: consente di creare un *"materiale"*, partendo o da un colore o da una texture[^texture] definita precedentemente.
|
||||||
|
- `MaterialShape`: consente di creare delle semplici forme geometriche come cilindri, sfere e cuboidi.
|
||||||
|
|
||||||
|
Nel caso specifico è stata realizzata un'applicazione che in seguito al tocco dell'utente renderizza nella scena un oggetto dalla forma e dal colore *pseudo-casuali*(vedi fig. \ref{rbm}).
|
||||||
|
Inoltre è stato aggiunto un ulteriore elemento di interazione con l'utente, che gli consente di cliccare sull'oggetto renderizzato, al fine di cambiare la tinta di quest'ultimo.
|
||||||
|
|
||||||
|
![Rendering di modelli costruiti a runtime](figures/rbm.png){#rbm width=225px height=400px}
|
||||||
|
|
||||||
|
### Interazione con l'utente
|
||||||
|
|
||||||
|
Anche in questo caso l'interazione con l'utente è gestita mediante il metodo `setOnTapArPlaneListener`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// ...
|
||||||
|
arFragment.setOnTapArPlaneListener(this::addModel)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dove la funzione `addModel` si occupa della creazione del materiale e della forma e infine dell'aggiunta del modello alla scena.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun addModel(
|
||||||
|
hitResult: HitResult,
|
||||||
|
plane: Plane,
|
||||||
|
motionEvent: MotionEvent
|
||||||
|
) {
|
||||||
|
val color = generateColor()
|
||||||
|
|
||||||
|
buildMaterial(this, color) {
|
||||||
|
val node = addTransformableNodeToScene(
|
||||||
|
arFragment,
|
||||||
|
hitResult.createAnchor(),
|
||||||
|
buildShape(generateShape(), it)
|
||||||
|
)
|
||||||
|
|
||||||
|
node.setOnTapListener {_ , _ ->
|
||||||
|
changeColorOfMaterial(
|
||||||
|
this,
|
||||||
|
generateColor(),
|
||||||
|
node.renderable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creazione del materiale
|
||||||
|
|
||||||
|
La creazione del materiale avviene mediante la funzione `buildMaterial` che a sua volta richiama la funzione di libreria ` MaterialFactory .makeOpaqueWithColor`.
|
||||||
|
|
||||||
|
Come già visto in precedenza, la soluzione adottata da Sceneform per interagire con oggetti *pesanti* è una callback che nel caso specifico può essere specificata mediante il parametro `onSuccess`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun buildMaterial(
|
||||||
|
context: Context,
|
||||||
|
color: Color,
|
||||||
|
onSuccess: (material: Material) -> Unit
|
||||||
|
) {
|
||||||
|
MaterialFactory
|
||||||
|
.makeOpaqueWithColor(context, color)
|
||||||
|
.thenAccept(onSuccess)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creazione della forma
|
||||||
|
|
||||||
|
Per la costruzione della forma geometrica si è usata la funzione `buildShape` che si comporta da *facade* per le funzioni della classe di libreria `ShapeFactory`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun buildShape(
|
||||||
|
shape: Shape,
|
||||||
|
material: Material
|
||||||
|
): ModelRenderable {
|
||||||
|
val center = Vector3(0.0f, 0.0f, 0.0f)
|
||||||
|
return when (shape) {
|
||||||
|
Shape.CUBE -> ShapeFactory
|
||||||
|
.makeCube(Vector3(0.2f, 0.2f, 0.2f),
|
||||||
|
center, material)
|
||||||
|
Shape.CYLINDER -> ShapeFactory
|
||||||
|
.makeCylinder(0.1f, 0.2f, center, material)
|
||||||
|
Shape.SPHERE -> ShapeFactory
|
||||||
|
.makeSphere(0.1f, center, material)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Come è possibile notare a seconda della figura, vanno specificate le caratteristiche spaziali che la contraddistingue e il materiale creato precedentemente.
|
||||||
|
|
||||||
|
### Aggiunta del nodo alla scena
|
||||||
|
|
||||||
|
L'aggiunta di un nuovo nodo alla scena avviene mediante la funzione `addTransformableNodeToScene` che presenta il medesimo comportamento visto nei precedenti progetti, con l'unica differenza del valore di ritorno.
|
||||||
|
Infatti se prima veniva restituito un'`Unit`[^unit] in questo caso viene restituito un oggetto di tipo `Node`.
|
||||||
|
|
||||||
|
Questa modifica si rende necessaria per poter aggiungere al nodo un listener sull'evento di tocco.
|
||||||
|
Questa operazione avviene mediante il metodo `setOnTapListener`, al quale, mediante una *lambda expression*, viene passata la funzione `changeColorOfMaterial`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun changeColorOfMaterial(
|
||||||
|
context: Context,
|
||||||
|
color: Color,
|
||||||
|
renderable: Renderable
|
||||||
|
) {
|
||||||
|
buildMaterial(context, color) {
|
||||||
|
renderable.material = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Quest'ultima funzione si occupa di creare un nuovo materiale e sostituirlo a quello precedente.
|
||||||
|
|
||||||
|
[^texture]: In ambito grafico con il termine *texture* si è soliti indicare una qualità visiva che si ripete mediante un pattern ben definito.
|
||||||
|
|
||||||
|
[^unit]: Equivalente in Kotlin dell'oggetto `Void` di Java.
|
88
src/chapter3.4.md
Normal file
88
src/chapter3.4.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
## Collision
|
||||||
|
|
||||||
|
Quando si ha a che fare con più nodi presenti sulla scena può risultare utile verificare se due o più di questi si sovrappongono.
|
||||||
|
In questo progetto viene mostrato come eseguire questo controllo mediante l'API di ARCore.
|
||||||
|
|
||||||
|
Per questo progetto si è utilizzata una rivisitazione dell'applicazione vista nel progetto precedente, con la differenza che l'aggiunta di un oggetto non è consentita se questo va in collisione con un altro già presente nella scena(vedi fig. \ref{c}).
|
||||||
|
|
||||||
|
![Schermata di errore dovuta ad una collisione](figures/c.png){#c width=225px height=400px}
|
||||||
|
|
||||||
|
### Rilevamento della collisione
|
||||||
|
|
||||||
|
Attualmente ARCore e Sceneform non forniscono nessun listener o metodo che può essere sovrascritto per la gestione della collisione, quindi seguendo un approccio già esaminato precedentemente andiamo ad aggiungere un listener all'evento dell'aggiornamento della scena.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// ...
|
||||||
|
arScene.addOnUpdateListener(this::onUpdate)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La verifica di una collisione può essere effettuata o attraverso il metodo `overlapTest`, che dato un nodo in input restituisce il primo nodo che entra in collisione con questo, oppure mediante il metodo `overlapTestAll`, che dato un nodo in input restituisce una lista con tutti i nodi che collidono con esso.
|
||||||
|
Nel caso in cui non siano state riscontrate collisioni, i metodi restituiscono rispettivamente `null` e una lista vuota.
|
||||||
|
|
||||||
|
La funzione `onUpdate` si occupa di verificare la presenza di collisioni.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
|
||||||
|
private fun onUpdate(frameTime: FrameTime) {
|
||||||
|
val node = lastNode ?: return
|
||||||
|
val overlappedNodes = arScene.overlapTestAll(node)
|
||||||
|
if (overlappedNodes.isNotEmpty())
|
||||||
|
onCollision()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Mentre la funzione `onCollision` si occupa di notificare all'utente l'avvenuta collisione mediante un *Toast*[^toast] e di eliminare il nodo dalla scena.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun onCollision() {
|
||||||
|
Toast.makeText(this, "collision", Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
lastNode?.isEnabled = false
|
||||||
|
lastNode = null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il test di collisione non avviene direttamente sul `Renderable`, ma sulla `CollisionShape` ovvero una *"scatola"* invisibile che racchiude il modello renderizzato vero e proprio.
|
||||||
|
Di default ARCore utilizza `CollisionShape` o di forma rettangolare o sferica, ma può essere cambiata mediante il metodo `setCollisionShape` della classe `Node`.
|
||||||
|
In questo progetto si è usata la `CollisionShape` di default.
|
||||||
|
|
||||||
|
### Aggiunta del nodo alla scena
|
||||||
|
|
||||||
|
L'aggiunta del nodo alla scena avviene mediante l'aggiunta di un listener all'evento di tocco su un piano.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// ...
|
||||||
|
arFragment.setOnTapArPlaneListener(this::addShape)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La funzione `addShape`, utilizzando le funzioni `buildMaterial` e `buildShape` analizzate in precedenza, si occupa dell'effettiva aggiunta dell'oggetto alla scena.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun addShape(
|
||||||
|
hitResult: HitResult,
|
||||||
|
plane: Plane,
|
||||||
|
motionEvent: MotionEvent
|
||||||
|
) {
|
||||||
|
val red = Color(android.graphics.Color.RED)
|
||||||
|
|
||||||
|
buildMaterial(this, red) {
|
||||||
|
val cube = buildShape(Shape.CUBE, it)
|
||||||
|
|
||||||
|
lastNode = addNodeToScene(
|
||||||
|
arFragment,
|
||||||
|
hitResult.createAnchor(),
|
||||||
|
cube
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Risulta importante notare come attraverso questa strategia l'aggiunta del modello alla scena avvenga incondizionatamente, ed è solo all'aggiornamento di quest'ultima che si effettua il controllo di collisione sull'ultimo nodo aggiunto.
|
||||||
|
|
||||||
|
[^toast]: Oggetto nativo di Android mediante il quale è possibile informare l'utente in modo non invasivo.
|
29
src/chapter3.5.0.md
Normal file
29
src/chapter3.5.0.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
## Animazioni e movimento
|
||||||
|
|
||||||
|
Negli esempi discussi fino a questo momento sono stati usati unicamente asset tridimensionali statici.
|
||||||
|
Gli oggetti virtuali che di volta in volta sono stati integrati nel mondo reale non erano né dotati di animazioni, né erano in grado di muoversi all'interno dell'ambiente reale circostante.
|
||||||
|
La scelta di utilizzare modelli statici è stata dettata da un lato dalla filosofia *kiss*[^kiss], e quindi di concentrarsi unicamente sull'aspetto rilevante del progetto, dall'altro da un supporto quasi inesistente alle animazioni e al movimento.
|
||||||
|
|
||||||
|
Come abbiamo avuto modo di vedere nel secondo capitolo, queste lacune sono da imputare principalmente alla libreria grafica.
|
||||||
|
Infatti sia per le animazioni, sia per la gestione del movimento, Sceneform non offre alcun supporto nativo o diretto.
|
||||||
|
Nonostante ciò è possibile aggirare il problema tramite opportuni escamotage, anche se bisogna tenere in conto che i risultati sono generalmente modesti e macchinosi da raggiungere.
|
||||||
|
|
||||||
|
### Animazioni
|
||||||
|
|
||||||
|
L'utilizzo di oggetti animati oltre ad essere un orpello grafico diventa, in molti casi, un aspetto fondamentale nel processo di sviluppo.
|
||||||
|
Questo è ancora più vero nello sviluppo di applicazioni AR su smartphone che, per forza di cose, hanno nell'utenza consumers lo sbocco naturale.
|
||||||
|
Il mancato utilizzo di un'animazione potrebbe segnare in modo permanete l'esperienza utente e quindi determinare il fallimento del progetto.
|
||||||
|
|
||||||
|
Non a caso uno dei problemi più discussi nell'issues tracker di Sceneform su GitHub[@googlear:Animated3DObjects:2019], è proprio la totale mancanza di supporto alle animazioni.
|
||||||
|
Sebbene ci siano stati dei lavori in questo senso ed una prima contabilità con i modelli animati FBX sia stata aggiunta alla code base, ad oggi non è possibile utilizzare questa funzione, in quanto è in fase di testing per il solo personale interno.
|
||||||
|
|
||||||
|
In attesa di un rilascio al pubblico, l'unica via percorribile è quella presentata dalla stessa Google durante un codelab[@googlear:ChromaKey:2019].
|
||||||
|
La soluzione consiste nel renderizzare un schermo trasparente nel mondo reale e proiettare su di esso un video.
|
||||||
|
Affinché l'*illusione* riesca è necessario usare un video che sfrutti il *chroma key*[^chroma-key], in questo modo l'integrazione con il mondo reale risulta migliore.
|
||||||
|
Inoltre per impedire che l'utente possa guardare il retro dello schermo è consigliabile rendere quest'ultimo solidale con l'utente.
|
||||||
|
|
||||||
|
Non solo risultano evidenti le limitazioni di questo metodo, ma anche la natura più di *pezza* che soluzione al problema, che non lo rendono adatto per un utilizzo commerciale.
|
||||||
|
|
||||||
|
[^kiss]: Acronimo di *Keep It Simple, Stupid*, è un principio di programmazione che consiglia di concentrarsi su un problema alla volta.
|
||||||
|
|
||||||
|
[^chroma-key]: Conosciuto anche con il nome di *green screen*, è una tecnica cinematografica che permette di sovrapporre due sorgenti video. Nel caso specifico la prima fonte video è catturata in real time dal sensore fotografico del device, mentre la seconda è quella che vogliamo integrare nell'ambiente.
|
219
src/chapter3.5.1.md
Normal file
219
src/chapter3.5.1.md
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
### Movimento
|
||||||
|
|
||||||
|
Anche in questo caso Sceneform non ci fornisce un supporto diretto, ma a differenza di quanto visto con le animazioni, è possibile sopperire a questa mancanza abbastanza facilmente e con risultati soddisfacenti tramite gli `ObjectAnimator`.
|
||||||
|
|
||||||
|
L'`ObjectAnimator` non è una classe specifica di ARCore o Sceneform, ma dell'SDK di Android che può essere usata per gestire facilmente animazioni e transizioni all'interno delle applicazioni Android.
|
||||||
|
Grazie a questa classe e una serie di punti nello spazio, *collegati* tramite un interpolatore, saremo in grado di conferire il movimento ai nostri modelli.
|
||||||
|
|
||||||
|
Per mostrare il funzionamento degli animator è stato realizzato un progetto d'esempio in grado di renderizzare un modello del sistema solare in cui i pianeti realizzano sia il modo di rotazione su se stessi, sia quello di rivoluzione intorno al sole(vedi fig. \ref{ss}).
|
||||||
|
|
||||||
|
![Rendering del sistema solare](figures/ss.png){#ss width=400px height=225px}
|
||||||
|
|
||||||
|
#### Recupero e rendering dei modelli
|
||||||
|
|
||||||
|
Visto l'elevato numero di modelli con cui si deve operare si è scelto di recuperarli da un server a runtime.
|
||||||
|
Il procedimento è simile a quello visto precedentemente, con la differenza che in questo caso non si è optato per l'utilizzo delle callback, al fine di evitare il *callback hell*[^callback-hell], a favore delle *coroutines*, uno strumento messo a disposizione dal linguaggio Kotlin che permette di gestire codice asincrono come se fosse sequenziale.
|
||||||
|
Sempre attraverso le *coroutines* è stato possibile eseguire più rendering in parallelo e quindi ottimizzare il tempo di CPU dell'applicazione.
|
||||||
|
|
||||||
|
All'interno del metodo `onCreate` viene avviata una coroutine che richiama la funzione `loadPlanets`.
|
||||||
|
Inoltre viene conservato un riferimento al `Job` della coroutine.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// ...
|
||||||
|
loadPlanetsJob = GlobalScope.launch(Dispatchers.Main){
|
||||||
|
renderablePlanets = loadPlanets(this@MainActivity)
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
La funzione `loadPlanets` si occupa di caricare e restituire tramite una `Map` tutti i pianeti del sistema solare.
|
||||||
|
Mentre il caricamento del singolo pianeta avviene mediante la funzione `loadPlanet`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
suspend fun loadPlanets(
|
||||||
|
context: Contex
|
||||||
|
) : Map<Planet, ModelRenderable> {
|
||||||
|
val sun = loadPlanet(context, Planet.SUN)
|
||||||
|
/**
|
||||||
|
* ...
|
||||||
|
* caricamento degli altri pianeti
|
||||||
|
* ...
|
||||||
|
*/
|
||||||
|
val neptune = loadPlanet(context, Planet.NEPTUNE)
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
Pair(Planet.SUN, sun.await()),
|
||||||
|
// ...
|
||||||
|
Pair(Planet.NEPTUNE, neptune.await())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nella funzione `loadPlanet` viene da prima recuperato il modello tridimensionale dal server e successivamente se ne effettua il rendering attraverso la funzione `buildFutureRenderable`.
|
||||||
|
Quest'ultima, come abbiamo già visto, restituisce un `CompletableFuture` che per poter essere utilizzato tramite delle coroutines deve essere trasformarlo in un `Deferred`[^deferred].
|
||||||
|
Questa operazione avviene attraverso il costruttore di coroutines `async`.
|
||||||
|
Inoltre viene usato il dispatcher `IO` che ci consente di eseguire l'operazione in background.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun loadPlanet(
|
||||||
|
context: Context,
|
||||||
|
planet: Planet
|
||||||
|
): Deferred<ModelRenderable> {
|
||||||
|
val modelSource = fetchModel(
|
||||||
|
context,
|
||||||
|
Uri.parse(planet.value)
|
||||||
|
)
|
||||||
|
val futureRenderable = buildFutureRenderable(
|
||||||
|
context,
|
||||||
|
modelSource,
|
||||||
|
Uri.parse(planet.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
return GlobalScope.async(Dispatchers.IO) {
|
||||||
|
futureRenderable.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ultima operazione da dover effettuare prima di poter usare i modelli renderizzati è assicurarci che l'operazione di rendering sia stata completata.
|
||||||
|
Per fare ciò viene usato ancora una volta il costruttore di coroutines `launch` e si attende, in modo non bloccante, il completamento del job di rendering.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
loadPlanetsJob.join()
|
||||||
|
// operazioni con gli oggetti renderizzati
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Orbite e pianeti
|
||||||
|
|
||||||
|
Per realizzare le orbite e i pianeti è stata implementata la classe `RotationNode` che va ad estendere la classe di libreria `Node`.
|
||||||
|
|
||||||
|
Componente principale di questa è la funzione `createAnimator` che si occupa della creazione dell'`ObjectAnimator` che permette di muovere i modelli.
|
||||||
|
All'interno della funzione vengono definiti i punti da cui ottenere la rotazione attraverso l'interpolatore.
|
||||||
|
Infine viene impostato l'`ObjectAnimator` affinché riproduca in loop l'animazione.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun createAnimator(): ObjectAnimator {
|
||||||
|
val orientations = arrayOf(0f, 120f, 240f, 360f)
|
||||||
|
.map {
|
||||||
|
Quaternion
|
||||||
|
.axisAngle(Vector3(0.0f, 1.0f, 0.0f), it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val orbitAnimation = ObjectAnimator()
|
||||||
|
orbitAnimation.setObjectValues(*orientations.toTypedArray())
|
||||||
|
|
||||||
|
orbitAnimation.setEvaluator(QuaternionEvaluator())
|
||||||
|
|
||||||
|
orbitAnimation.repeatCount = ObjectAnimator.INFINITE
|
||||||
|
orbitAnimation.repeatMode = ObjectAnimator.RESTART
|
||||||
|
orbitAnimation.interpolator = LinearInterpolator()
|
||||||
|
orbitAnimation.setAutoCancel(true)
|
||||||
|
|
||||||
|
return orbitAnimation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Inoltre nella classe `RotationNode` vanno sovrascritti i metodi `OnActivate` e `OnDeactivate`, per gestire lo start e lo stop dell'animazione.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
override fun onActivate() {
|
||||||
|
startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeactivate() {
|
||||||
|
stopAnimation()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La creazione dei pianeti è gestita attraverso un ulteriore classe, `PlanetNode`, anch'essa estensione della classe `Node`.
|
||||||
|
Quest'ultima viene definita come un nodo dotato di un `Renderable` ancorato ad un `RotationNode`.
|
||||||
|
Come vedremo in seguito questa operazione si rende necessaria per garantire il moto di rivoluzione.
|
||||||
|
|
||||||
|
La creazione delle orbite e dei pianeti avviene mediante la funzione `createPlanetNode`.
|
||||||
|
L'orbita del pianeta viene ancorata al nodo principale, nel caso specifico il sole, e il pianeta viene ancorato alla sua orbita.
|
||||||
|
Inoltre viene assegnato anche il renderable al nodo del pianeta.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun createPlanetNode(
|
||||||
|
planet: Planet,
|
||||||
|
parent: Node,
|
||||||
|
auFromParent: Float,
|
||||||
|
orbitDegreesPerSecond: Float,
|
||||||
|
renderablePlanets: Map<Planet, ModelRenderable>
|
||||||
|
) {
|
||||||
|
val orbit = RotationNode()
|
||||||
|
orbit.degreesPerSecond = orbitDegreesPerSecond
|
||||||
|
orbit.setParent(parent)
|
||||||
|
|
||||||
|
val renderable = renderablePlanets[planet] ?: return
|
||||||
|
val planetNode = PlanetNode(renderable)
|
||||||
|
planetNode.setParent(orbit)
|
||||||
|
planetNode.localPosition = Vector3(
|
||||||
|
AU_TO_METERS * auFromParent,
|
||||||
|
0.0f,
|
||||||
|
0.0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
È importante notare che in questo modo non sono i pianeti a ruotare intorno al sole, ma sono le orbite a ruotare su se stesse e visto che i pianeti sono *"incollati"* ad esse si ha l'illusione del moto di rivoluzione.
|
||||||
|
|
||||||
|
#### Creazione e aggiunta del sistema solare
|
||||||
|
|
||||||
|
La creazione del nostro sistema solare avviene mediante la funzione `createSolarSystem` che riceve in ingresso la `Map` con tutti i modelli dei pianeti, li posiziona intorno al sole e infine restituisce quest'ultimo.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun createSolarSystem(
|
||||||
|
renderablePlanets: Map<Planet, ModelRenderable>
|
||||||
|
): Node {
|
||||||
|
val base = Node()
|
||||||
|
|
||||||
|
val sun = Node()
|
||||||
|
sun.setParent(base)
|
||||||
|
sun.localPosition = Vector3(0.0f, 0.5f, 0.0f)
|
||||||
|
|
||||||
|
val sunVisual = Node()
|
||||||
|
sunVisual.setParent(sun)
|
||||||
|
sunVisual.renderable = renderablePlanets[Planet.SUN]
|
||||||
|
sunVisual.localScale = Vector3(0.5f, 0.5f, 0.5f)
|
||||||
|
|
||||||
|
createPlanetNode(
|
||||||
|
Planet.MERCURY,
|
||||||
|
sun,
|
||||||
|
0.4f,
|
||||||
|
47f,
|
||||||
|
renderablePlanets
|
||||||
|
)
|
||||||
|
// ...
|
||||||
|
createPlanetNode(
|
||||||
|
Planet.NEPTUNE,
|
||||||
|
sun,
|
||||||
|
6.1f,
|
||||||
|
5f,
|
||||||
|
renderablePlanets
|
||||||
|
)
|
||||||
|
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
L'aggiunta dei modelli alla scene avviene mediante la ben nota funzione `addNodeToScene`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val solarSystem = createSolarSystem(renderablePlanets)
|
||||||
|
addNodeToScene(
|
||||||
|
arFragment,
|
||||||
|
hitResult.createAnchor(),
|
||||||
|
solarSystem
|
||||||
|
)
|
||||||
|
isModelAdded = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Anche in questo caso si rende necessario l'utilizzo di un flag booleano per evitare l'aggiunta di più sistemi solari.
|
||||||
|
|
||||||
|
[^callback-hell]: Con il termine *callback hell* si indica l'utilizzo eccessivo di callback all'interno di altre callback. Questo fenomeno comporta una diminuzione della leggibilità del codice e un aumento della complessità e di conseguenza della presenza di bug.
|
||||||
|
|
||||||
|
[^deferred]: In Kotlin i *future* sono gestiti mediante l'oggetto `Deferred<T>`.
|
221
src/chapter3.6.md
Normal file
221
src/chapter3.6.md
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
## Cloud anchors
|
||||||
|
|
||||||
|
Un'ulteriore funzionalità messa a disposizione da ARCore sono le *Cloud Anchors* che ci permette di salvare su un server remoto le ancore a cui sono agganciati i nodi.
|
||||||
|
Grazie a questa funzionalità è possibile salvare un'esperienza di realtà aumentata per un uso futuro[^futuro] o per condividerla con altri utenti.
|
||||||
|
|
||||||
|
In questo progetto verrà mostrato come sia possibile posizionare, tramite il device A, un vaso di fiori su una superficie piana, e vedere la stessa scena sul dispositivo B.
|
||||||
|
|
||||||
|
![Messaggio di avvenuto upload sul server](figures/ca1.png){#ca1 width=225px height=400px}
|
||||||
|
|
||||||
|
![Schermata di ripristino di un'ancora](figures/ca2.png){#ca2 width=225px height=400px}
|
||||||
|
|
||||||
|
### Configurazioni iniziali
|
||||||
|
|
||||||
|
Per poter sfruttare le cloud anchors è necessario richiedere un API KEY sul sito di Google \url{https://console.cloud.google.com/apis/library/arcorecloudanchor.googleapis.com}.
|
||||||
|
Una volta ottenuta la chiave è necessario dichiararla nell'Android Manifest mediante il seguente codice xml.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.ar.API_KEY"
|
||||||
|
android:value="API_KEY"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Inoltre per tenere traccia dello stato dell'applicazione si è definita una classe enumerativa con cinque possibili valori.
|
||||||
|
|
||||||
|
- `NONE`: non è presente alcuno oggetto nella scena né se ne sta recuperando uno dal server.
|
||||||
|
- `HOSTING`: si sta caricando l'ancora sul server.
|
||||||
|
- `HOSTED`: l'ancora è stata caricata sul server.
|
||||||
|
- `RESOLVING`: si sta recuperando l'ancora dal server.
|
||||||
|
- `RESOLVED`: l'ancora è stata recuperata del server.
|
||||||
|
|
||||||
|
### Attivazione delle cloud anchors
|
||||||
|
|
||||||
|
Le cloud anchors di default sono disattivate e la loro attivazione può avvenire in due modi.
|
||||||
|
|
||||||
|
- **Attivazione manuale**:
|
||||||
|
Con questa soluzione lo sviluppatore si occupa di creare una nuova configurazione della sessione di ARCore in cui le cloud anchors sono attivate e andare a sostituire questa nuova configurazione a quella di default.
|
||||||
|
- **Estensione dell'`ArFragment`**:
|
||||||
|
Viene creata una nuova classe che estende `ArFragment` in cui le cloud anchors sono attivate.
|
||||||
|
|
||||||
|
Sebbene la prima soluzione possa sembrare più immediata, nasconde una grande insidia.
|
||||||
|
Infatti sarà compito dello sviluppatore andare a sovrascrivere i vari metodi che gestiscono il ciclo di vita dell'activity affinché non vengano ripristinate le impostazioni iniziali.
|
||||||
|
Mentre con il secondo metodo sarà Sceneform a gestire il ciclo di vita al posto nostro.
|
||||||
|
|
||||||
|
Per questo motivo è stata creata la classe `CloudArFragment` in cui è stata sovrascritta la funzione `getSessionConfiguration` in modo da attivare le cloud anchors.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class CloudArFragment: ArFragment(){
|
||||||
|
override fun getSessionConfiguration(
|
||||||
|
session: Session?
|
||||||
|
): Config {
|
||||||
|
val config = super.getSessionConfiguration(session)
|
||||||
|
config.cloudAnchorMode = Config
|
||||||
|
.CloudAnchorMode.ENABLED
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Inoltre bisogna modificare anche il file di layout affinché non utilizzi più l'`ArFragment`, ma il `CloudArFragment`.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<fragment
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:name="it.norangeb.cloudanchors.CloudArFragment"
|
||||||
|
android:id="@+id/ar_fragment"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloud Anchor Helper
|
||||||
|
|
||||||
|
Quando viene caricata un'ancora sul server viene associata ad essa un valore alfanumerico che ci permette di identificarla univocamente.
|
||||||
|
Dato che il codice risulta essere molto lungo e quindi difficile da ricordare e ricopiare, si è scelto di appoggiarsi al servizio *firestore * di Firebase[@firebase:Firebase:2019] per creare una relazione uno a uno tra l'UUID e uno *short code* intero.
|
||||||
|
|
||||||
|
Queste operazioni avvengono tramite la classe `CloudAnchorHelper` che fornisce due metodi principali `getShortCode` e `getCloudAnchorId`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun getShortCode(cloudAnchorId: String): Int {
|
||||||
|
fireStoreDb.collection(COLLECTION)
|
||||||
|
.document(DOCUMENT)
|
||||||
|
.set(
|
||||||
|
mapOf(Pair(nextCode.toString(), cloudAnchorId)),
|
||||||
|
SetOptions.merge()
|
||||||
|
)
|
||||||
|
uploadNextCode(nextCode+1)
|
||||||
|
|
||||||
|
return nextCode++
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun getCloudAnchorId(
|
||||||
|
shortCode: Int,
|
||||||
|
onSuccess: (String) -> Unit
|
||||||
|
) {
|
||||||
|
fireStoreDb.collection(COLLECTION)
|
||||||
|
.document(DOCUMENT)
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener {
|
||||||
|
val uuid=it.data.get(shortCode.toString()) as String
|
||||||
|
onSuccess(uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il primo metodo riceve in ingresso l'UUID dell'ancora e lo aggiunge al database di Firebase usando come chiave un numero intero che viene restituito al chiamante.
|
||||||
|
Mentre il secondo metodo, dato il codice intero, recupera l'identificativo dell'ancora e svolge su di esso le operazioni specificate nella *lambda expression* `onSuccess`.
|
||||||
|
|
||||||
|
### Aggiunta del modello
|
||||||
|
|
||||||
|
L'aggiunta del modello avviene attraverso la funzione `addModel`, che opera in modo simile a quanto visto fin'ora, con l'unica differenza che l'aggiunta è consentita solo se l'applicazione si trova nello stato `NONE`.
|
||||||
|
Inoltre la creazione dell'ancora è delegata al metodo `hostCloudAnchors` che si occupa anche dell'upload di quest'ultima sul server.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun addModel(
|
||||||
|
hitResult: HitResult,
|
||||||
|
plane: Plane,
|
||||||
|
motionEvent: MotionEvent
|
||||||
|
) {
|
||||||
|
if (cloudAnchorState != CloudAnchorState.NONE)
|
||||||
|
return
|
||||||
|
|
||||||
|
cloudAnchor = arFragment.arSceneView.session
|
||||||
|
.hostCloudAnchor(hitResult.createAnchor())
|
||||||
|
|
||||||
|
cloudAnchorState = CloudAnchorState.HOSTING
|
||||||
|
|
||||||
|
buildRenderable(this, Uri.parse("model.sfb")) {
|
||||||
|
addTransformableNodeToScene(
|
||||||
|
arFragment,
|
||||||
|
cloudAnchor ?: return@buildRenderable,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Hosting
|
||||||
|
|
||||||
|
Il metodo `checkCloudAnchor` viene eseguito ogni qual volta viene aggiornata la scena e, in base allo stato dell'applicazione vengono eseguite determinate operazioni.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun checkCloudAnchor(frameTime: FrameTime) {
|
||||||
|
if (cloudAnchorState != CloudAnchorState.HOSTING
|
||||||
|
&& cloudAnchorState != CloudAnchorState.RESOLVING
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
val cloudState=cloudAnchor?.cloudAnchorState?:return
|
||||||
|
|
||||||
|
if (cloudState.isError) {
|
||||||
|
toastError()
|
||||||
|
cloudAnchorState = CloudAnchorState.NONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cloudState != Anchor.CloudAnchorState.SUCCESS)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (cloudAnchorState == CloudAnchorState.HOSTING)
|
||||||
|
checkHosting()
|
||||||
|
else
|
||||||
|
checkResolving()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nel caso specifico in cui il processo di caricamento sia stato completato con successo viene eseguita la funzione `checkHosting` che si occupa di notificare all'utente il codice numerico associato all'ancora(vedi fig. \ref{ca1}) e di cambiare lo stato dell'applicazione da `HOSTING` a `HOSTED`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun checkHosting() {
|
||||||
|
val cAnchor = cloudAnchor ?: return
|
||||||
|
|
||||||
|
val shortCode = cloudAnchorsHelper
|
||||||
|
.getShortCode(cAnchor.cloudAnchorId)
|
||||||
|
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
this,
|
||||||
|
"Anchor hosted with code $shortCode",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
cloudAnchorState = CloudAnchorState.HOSTED
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resolving dell'ancora
|
||||||
|
|
||||||
|
L'utente può ripristinare un'ancora premendo sul pulsante *resolve*.
|
||||||
|
Il listener associato a questo evento è racchiuso nella funzione `onResolve` che a sua volta mostra all'utente un dialog in cui può inserire il codice dell'ancora da ripristinare(vedi fig. \ref{ca2}).
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun onResolveClick(view: View) {
|
||||||
|
if (cloudAnchor != null)
|
||||||
|
return
|
||||||
|
|
||||||
|
val dialog = ResolveDialogFragment()
|
||||||
|
dialog.setOkListener(this::onResolveOkPressed)
|
||||||
|
dialog.show(supportFragmentManager, "Resolve")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Alla conferma dell'inserimento, da parte dell'utente, viene eseguito il metodo `onResolveOkPressed` che converte lo *short code* nell'UUID dell'ancora e da questo ripristina il nodo nella scena.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun onResolveOkPressed(dialogValue: String) {
|
||||||
|
val shortCode = dialogValue.toInt()
|
||||||
|
cloudAnchorsHelper.getCloudAnchorId(shortCode) {
|
||||||
|
cloudAnchor = arFragment.arSceneView.session
|
||||||
|
.resolveCloudAnchor(it)
|
||||||
|
|
||||||
|
buildRenderable(this, Uri.parse("model.sfb")) {
|
||||||
|
val anchor = cloudAnchor ?: return@buildRenderable
|
||||||
|
addTransformableNodeToScene(arFragment, anchor, it)
|
||||||
|
cloudAnchorState = CloudAnchorState.RESOLVING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[^futuro]: Il ripristino non può essere troppo dilazionato nel tempo in quanto le ancore vengono conservate sul server per massimo ventiquattro ore.
|
BIN
src/figures/c.png
Normal file
BIN
src/figures/c.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
BIN
src/figures/ca1.png
Normal file
BIN
src/figures/ca1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
src/figures/ca2.png
Normal file
BIN
src/figures/ca2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 954 KiB |
BIN
src/figures/rbm.png
Normal file
BIN
src/figures/rbm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
src/figures/rfm.png
Normal file
BIN
src/figures/rfm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
src/figures/ss.png
Normal file
BIN
src/figures/ss.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
Loading…
Reference in New Issue
Block a user