From 471e296ef30b0563381414e567583f24b17af49d Mon Sep 17 00:00:00 2001 From: norangebit Date: Wed, 16 Jan 2019 21:33:09 +0100 Subject: [PATCH] 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.