impaginazione

This commit is contained in:
Raffaele Mignone 2019-01-17 22:04:36 +01:00
parent a41ac06658
commit f3ed17dba7
Signed by: norangebit
GPG Key ID: 4B9DF72AB9508845
2 changed files with 78 additions and 49 deletions

View File

@ -1,22 +1,20 @@
## 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.
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.
Infatti sia per le animazioni, sia per la gestione del movimento, Sceneform non offre alcun supporto nativo o diretto.
Nonostante ciò è possibile aggirare il problema tramite opportuni escamotage, anche se bisogna tenere in conto che i risultati sono generalmente modesti e macchinosi da raggiungere.
### Animazioni
L'utilizzo di oggetti animati oltre ad essere un orpello grafico diventa, in molti casi, un punto principale nel processo di sviluppo.
L'utilizzo di oggetti animati oltre ad essere un orpello grafico diventa, in molti casi, un aspetto fondamentale 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.
Non a caso uno dei problemi più discussi nell'issues tracker di Sceneform su GitHub[@googlear:Animated3DObjects:2019], è 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].
@ -24,8 +22,8 @@ La soluzione consiste nel renderizzare un schermo trasparente nel mondo reale e
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.
Non solo risultano evidenti le limitazioni di questo metodo, 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.
[^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 seconda è quella che vogliamo integrare nell'ambiente.

View File

@ -1,17 +1,17 @@
### 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`.
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 nelle applicazioni.
Inoltre per poter simulare il movimento avremo bisogno di settare dei punti nello spazio e *collegarli* tramite un interpolatore.
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 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.
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 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.
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.
@ -19,16 +19,19 @@ Inoltre viene conservato un riferimento al `Job` della coroutine.
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
loadPlanetsJob = GlobalScope.launch(Dispatchers.Main) {
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.
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: Context): Map<Planet, ModelRenderable> {
suspend fun loadPlanets(
context: Contex
) : Map<Planet, ModelRenderable> {
val sun = loadPlanet(context, Planet.SUN)
/**
* ...
@ -45,14 +48,20 @@ suspend fun loadPlanets(context: Context): Map<Planet, ModelRenderable> {
}
```
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.
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<ModelRenderable> {
val modelSource = fetchModel(context, Uri.parse(planet.value))
fun loadPlanet(
context: Context,
planet: Planet
): Deferred<ModelRenderable> {
val modelSource = fetchModel(
context,
Uri.parse(planet.value)
)
val futureRenderable = buildFutureRenderable(
context,
modelSource,
@ -65,8 +74,8 @@ fun loadPlanet(context: Context, planet: Planet): Deferred<ModelRenderable> {
}
```
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.
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) {
@ -77,37 +86,35 @@ GlobalScope.launch(Dispatchers.Main) {
#### Orbite e pianeti
Per realizzare le orbite e i pianeti è stata creata la classe `RotationNode` che va ad estendere la classe di libreria `Node`.
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 classe è la funzione `createAnimator` che si occupo della creazione dell'`ObjectAnimator` che permette di muovere i modelli.
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 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.setObjectValues(*orientations.toTypedArray())
orbitAnimation.setEvaluator(QuaternionEvaluator())
orbitAnimation.repeatCount = ObjectAnimator.INFINITE
orbitAnimation.repeatCount = ObjectAnimator.INFINITE
orbitAnimation.repeatMode = ObjectAnimator.RESTART
orbitAnimation.interpolator = LinearInterpolator()
orbitAnimation.setAutoCancel(true)
return orbitAnimation
return orbitAnimation
}
```
Sempre nella classe `RotationNode` vanno sovrascritti i metodi `OnActivate` e `OnDeactivate`, per gestire lo start e lo stop dell'animazione.
Inoltre nella classe `RotationNode` vanno sovrascritti i metodi `OnActivate` e `OnDeactivate`, per gestire lo start e lo stop dell'animazione.
```kotlin
override fun onActivate() {
@ -119,8 +126,8 @@ override fun onDeactivate() {
}
```
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 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.
@ -141,7 +148,11 @@ private fun createPlanetNode(
val renderable = renderablePlanets[planet] ?: return
val planetNode = PlanetNode(renderable)
planetNode.setParent(orbit)
planetNode.localPosition = Vector3(AU_TO_METERS * auFromParent, 0.0f, 0.0f)
planetNode.localPosition = Vector3(
AU_TO_METERS * auFromParent,
0.0f,
0.0f
)
}
```
@ -149,10 +160,12 @@ private fun createPlanetNode(
#### 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.
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<Planet, ModelRenderable>): Node {
private fun createSolarSystem(
renderablePlanets: Map<Planet, ModelRenderable>
): Node {
val base = Node()
val sun = Node()
@ -164,22 +177,40 @@ private fun createSolarSystem(renderablePlanets: Map<Planet, ModelRenderable>):
sunVisual.renderable = renderablePlanets[Planet.SUN]
sunVisual.localScale = Vector3(0.5f, 0.5f, 0.5f)
createPlanetNode(Planet.MERCURY, sun, 0.4f, 47f, renderablePlanets)
createPlanetNode(
Planet.MERCURY,
sun,
0.4f,
47f,
renderablePlanets
)
// ...
createPlanetNode(Planet.NEPTUNE, sun, 6.1f, 5f, renderablePlanets)
createPlanetNode(
Planet.NEPTUNE,
sun,
6.1f,
5f,
renderablePlanets
)
return base
}
```
L'aggiunta alla scene avviene mediante la ben nota funzione `addNodeToScene`.
L'aggiunta dei modelli alla scene avviene mediante la ben nota funzione `addNodeToScene`.
```kotlin
val solarSystem = createSolarSystem(renderablePlanets)
addNodeToScene(arFragment, hitResult.createAnchor(), solarSystem)
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>`.