init chapter3.4

This commit is contained in:
Raffaele Mignone 2019-01-16 21:33:09 +01:00
parent 9e589dc734
commit 471e296ef3
Signed by: norangebit
GPG Key ID: 4B9DF72AB9508845
2 changed files with 216 additions and 0 deletions

31
src/chapter3.4.0.md Normal file
View File

@ -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.

185
src/chapter3.4.1.md Normal file
View File

@ -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<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())
)
}
```
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<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 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<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 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<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 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.