impaginazione
This commit is contained in:
parent
a41ac06658
commit
f3ed17dba7
@ -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 né 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.
|
||||
|
@ -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>`.
|
||||
|
Loading…
Reference in New Issue
Block a user