### 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. #### 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 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. ```kotlin override fun onCreate(savedInstanceState: Bundle?) { // ... loadPlanetsJob = GlobalScope.launch(Dispatchers.Main){ renderablePlanets = loadPlanets(this@MainActivity) } // ... } ``` La funzione `loadPlanets` si occupa di caricare e restituire tramite una `Map` tutti i pianeti del sistema solare. Mentre il caricamento del singolo pianeta avviene mediante la funzione `loadPlanet`. ```kotlin suspend fun loadPlanets( context: Contex ) : Map { val sun = loadPlanet(context, Planet.SUN) /** * ... * caricamento degli altri pianeti * ... */ val neptune = loadPlanet(context, Planet.NEPTUNE) return mapOf( Pair(Planet.SUN, sun.await()), // ... Pair(Planet.NEPTUNE, neptune.await()) ) } ``` Nella funzione `loadPlanet` viene da prima recuperato il modello tridimensionale dal server e successivamente se ne effettua il rendering attraverso la funzione `buildFutureRenderable`. Quest'ultima, come abbiamo già visto, restituisce un `CompletableFuture` che per poter essere 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) ) 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. 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.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`. 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. 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 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 { 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`.