init chapter3.4
This commit is contained in:
parent
9e589dc734
commit
471e296ef3
31
src/chapter3.4.0.md
Normal file
31
src/chapter3.4.0.md
Normal 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
185
src/chapter3.4.1.md
Normal 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.
|
Loading…
Reference in New Issue
Block a user