220 lines
8.0 KiB
Markdown
220 lines
8.0 KiB
Markdown
### 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>`.
|