singe/docs/it/lessons/23-models.adoc

456 lines
19 KiB
Text

[#_lesson_23_models_and_light]
== Lezione 23: Modelli e luce
image::learn/23-models.png[La lezione finita, 480]
Cubi e sfere ti porteranno lontano, ma prima o poi il gioco vuole un drago. Un
*modello* è una forma che qualcun altro ha costruito in un programma di
modellazione: centinaia di triangoli, i suoi colori e le sue superfici, a volte
uno scheletro e dei movimenti che vanno con esso, tutto in un solo file.
Caricarne uno sono due righe. Fargli avere un aspetto decente è questione di
luce, e la luce è gran parte di questa lezione.
Non ti servono grafica né download. La primissima volta che hai eseguito Singe,
il motore ha estratto una cartella chiamata `Singe` nella tua cartella di
lavoro, e lì dentro ci sono due modelli: `DragonModel.glb`, il drago della
schermata del titolo del motore, e `SingeText.glb`, la scritta che c'è sotto.
Questa lezione usa il drago.
[#_the_scene_around_it]
=== La scena attorno
Un modello ha bisogno di un posto dove stare. Crea una cartella, crea
`dragon.singe` al suo interno, e comincia con un pavimento e una telecamera.
Se hai fatto la ventiduesima lezione qui non c'è niente di nuovo; se non l'hai
fatta, è una scena accesa, una lastra piatta su cui stare e un punto da cui
guardare.
[source,lua]
----
sceneEnable(true)
sceneSetBackground(10, 12, 20)
sceneSetAmbient(28, 30, 38)
local stone = materialNew()
materialSetColor(stone, 105, 105, 115)
materialSetRoughness(stone, 0.9)
local floor = nodeNew()
nodeSetMesh(floor, meshBox(24, 0.4, 24), stone)
nodeSetPosition(floor, 0, -0.2, 0)
local camera = nodeNew()
nodeSetPosition(camera, 0, 2.6, 9)
nodeLookAt(camera, 0, 1.7, 0)
cameraSet(camera)
cameraSetPerspective(55, 0.1, 200)
function onOverlayUpdate()
return OVERLAY_UPDATED
end
----
Eseguilo con `Singe -R dragon`. Una lastra grigia, vista leggermente
dall'alto, su uno sfondo blu scuro. La lastra è larga ventiquattro unità e
spessa poco meno di mezza unità, e appoggiarla a `y = -0.2` mette la sua
superficie superiore esattamente a `y = 0`, il che rende facile ragionare su
tutto quello che metterai dopo: il pavimento è zero.
[#_loading_the_dragon]
=== Caricare il drago
Due righe.
[source,lua]
----
local dragonModel = modelLoad("Singe/DragonModel.glb")
local dragon = modelInstance(dragonModel)
----
Mettile dopo il pavimento e salva. Compare un drago, ed è decisamente troppo
grande per l'inquadratura -- stai guardando la sua parte centrale.
[#_what_just_happened_19]
=== Cos'è appena successo
Le due righe fanno lavori diversi, e la differenza conta.
`modelLoad` legge il file, passa le forme e le loro superfici alla scheda
grafica, e tiene tutto pronto. È la chiamata lenta. Falla una volta, quando il
gioco parte, e mai nel mezzo della partita.
`modelInstance` mette una copia nella scena e ti restituisce un nodo -- un nodo
normale, dello stesso tipo che ti dà `nodeNew`, che posizioni, ruoti, scali ed
elimini con le chiamate che già conosci. Creare un'istanza è la chiamata
economica. Carica un albero, creane quaranta istanze e hai un bosco, con una
sola copia delle forme sulla scheda grafica e quaranta posti in cui vengono
disegnate.
Con il modello arriva tutto ciò che il file conteneva: le forme, le superfici e
i loro colori, il modo in cui sono disposte le parti, l'eventuale scheletro e
gli eventuali movimenti. Per un modello caricato non costruisci i materiali; si
è portato i suoi.
[#_scale_and_the_first_thing_that_goes_wrong]
=== La scala, e la prima cosa che va storta
Il drago è alto circa sette unità e mezza. La tua telecamera è nove unità più
indietro. Ecco perché riempie la finestra.
[source,lua]
----
nodeSetScale(dragon, 0.5)
----
Aggiungila sotto la riga `modelInstance`. Adesso è alto meno di quattro unità,
sta sul pavimento ed entra nell'inquadratura.
Non è un dettaglio; è il primo problema più comune con un modello, e ha due
facce. Un modello costruito in centimetri arriva cento volte troppo grande e
vedi un'unica superficie enorme che riempie lo schermo, cosa che di solito
sembra «la mia scena è rotta» invece di «sono dentro il collo del drago». Un
modello costruito in qualche altra unità arriva così piccolo da essere un
puntino, o invisibile contro lo sfondo, e pensi che il file non si sia
caricato.
La soluzione è sempre la stessa: una scala sul nodo dell'istanza. Non sulle
parti, non nel programma di modellazione se puoi evitarlo, ma un solo
`nodeSetScale` sul nodo che ti ha dato `modelInstance`, scelto provando un
numero e guardando. `0.01` e `100` sono risposte perfettamente normali.
Quando non riesci a capire quanto è grande una cosa, datti un righello. Mettile
accanto un cubo di un'unità:
[source,lua]
----
local ruler = nodeNew()
nodeSetMesh(ruler, meshBox(1, 1, 1), stone)
nodeSetPosition(ruler, 3, 0.5, 0)
----
Un oggetto grande quanto una cassa accanto al modello ti dice con un'occhiata
se il modello è un drago o una montagna. Togli il righello quando hai finito.
Mentre lo sistemi, giralo verso di te:
[source,lua]
----
nodeSetRotation(dragon, 0, -60, 0)
----
Il drago è stato modellato rivolto lungo la X positiva, cioè verso destra,
quindi parte di profilo. Girarlo attorno a Y lo porta verso la telecamera. Il
«davanti» di un modello è la direzione che ha scelto chi l'ha costruito, quale
che sia, e per scoprirla devi girarlo e guardare.
[#_light]
=== La luce
Esegui quello che hai adesso. Il drago c'è, ed è deprimente: una forma piatta e
fioca, senza riflessi e senza ombra. È perché l'unica luce nella tua scena è
quella ambiente, il debole chiarore da ogni direzione che imposta
`sceneSetAmbient`.
È la delusione più comune in assoluto nel 3D, e capita a tutti: il modello si è
caricato perfettamente e la scena è nera, o quasi, perché niente lo illumina.
Prima di sospettare del file, metti una luce.
Ce ne sono di tre tipi, e ognuna è un nodo, quindi le posizioni e le punti con
`nodeSetPosition` e `nodeLookAt` come tutto il resto.
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, -6, 9, 7)
nodeLookAt(sun, 0, 1.5, 0)
lightSetColor(sun, 255, 244, 224)
lightSetIntensity(sun, 1.8)
lightSetShadow(sun, true)
----
`LIGHT_DIRECTIONAL` è un sole. Viene trattato come se fosse infinitamente
lontano, quindi conta solo la direzione in cui splende e tutto nella scena ne
riceve la stessa quantità. La sua posizione serve a `nodeLookAt` per puntarlo.
È la luce che dà a una scena la sua forma principale, e la maggior parte delle
scene ne vuole esattamente una.
`lightSetIntensity` è la luminosità. Per una luce direzionale, `1` porta fino
al bianco una superficie bianca rivolta dritta verso di lei, quindi `1.8` è una
giornata luminosa. `lightSetColor` la tinge: un bianco un po' caldo per il sole,
un blu freddo per la luce della luna, e per il resto la sezione sulle luci del
riferimento.
`lightSetShadow(sun, true)` è quello che fa stare il drago sul pavimento invece
di farlo fluttuare sopra. Le ombre sono spente per impostazione predefinita
perché ogni luce che proietta ombre costa un altro passaggio su tutta la scena a
ogni fotogramma. Accendile per l'unica luce che conta e lasciale spente per le
altre; una macchina piccola ti ringrazierà.
[source,lua]
----
local torch = lightNew(LIGHT_POINT)
nodeSetPosition(torch, 3.5, 1.2, 3.0)
lightSetColor(torch, 255, 150, 70)
lightSetIntensity(torch, 14)
lightSetRange(torch, 12)
----
`LIGHT_POINT` è una lampadina. Sta in un punto, splende in tutte le direzioni e
si affievolisce con la distanza. Quell'affievolirsi è il motivo per cui il
numero è `14` e non `1`: l'intensità di una luce puntiforme è la sua luminosità
nel punto in cui si trova la luce, e dopo poche unità non ne resta molta. Una
lampadina che illumina una stanza di solito vuole qualcosa nell'ordine delle
decine. Se una luce puntiforme sembra non fare niente, il motivo è quasi sempre
questo.
`lightSetRange` dice fin dove arriva prima di spegnersi del tutto, il che evita
che una lampada in una stanza illumini debolmente quella accanto, e impostarlo
non costa niente.
Il terzo tipo è `LIGHT_SPOT`, un cono che punta lungo la Z negativa del nodo
stesso, con `lightSetCone` per l'angolo del centro luminoso e del bordo
sfumato: una torcia elettrica, un faro d'auto, un riflettore. Possono splendere
fino a otto luci contemporaneamente, più di quante ne servano alla maggior parte
delle scene. Se ne vuoi di più, in realtà ne vuoi di meno.
Eseguilo adesso. Un sole quasi bianco da sinistra, un caldo bagliore arancione
da destra, un'ombra sulla lastra, e un drago con un davanti e un dietro.
[#_materials_at_the_level_you_need_today]
=== I materiali, quanto ti basta per oggi
Il drago si è portato i suoi materiali, e sono tre: un metallo scuro, un metallo
più chiaro color champagne, e un vetro chiaro in cui lo sguardo entra un poco.
La maggior parte delle sue parti è ricavata da più di uno. Non devi toccare
niente di tutto questo, e la maggior parte delle volte non dovresti.
Quando vuoi davvero cambiare l'aspetto di una parte di un modello, crei un
materiale come nella ventiduesima lezione e lo metti sulla parte:
[source,lua]
----
local red = materialNew()
materialSetColor(red, 220, 50, 40)
materialSetRoughness(red, 0.4)
nodeSetMaterial(nodeFind("crest", dragon), red)
----
`nodeFind(name, root)` cerca in un'istanza di un modello una parte con il nome
che aveva nel file e ne restituisce il nodo, oppure `nil` quando quel nome non
c'è. È così che arrivi dentro un modello: una torretta, una ruota, una mano a
cui appendere una torcia, o la cresta del drago.
La cresta è un pezzo solo di un solo materiale, quindi il suo nodo porta la
forma stessa e la riga qui sopra la ridipinge. Una parte costruita con più
materiali viene tenuta come un nodo con un figlio per ogni pezzo, e mettere un
materiale sul genitore non cambia niente che tu possa vedere. Quando un
`nodeSetMaterial` sembra non fare niente, il motivo è questo, e
`nodeGetChildren` ti mostrerà che cosa c'è sotto.
Quattro numeri descrivono la maggior parte delle superfici. Il colore, da
`materialSetColor`. `materialSetRoughness`, da `0` per uno specchio a `1` per
il gesso. `materialSetMetallic`, che è `0` per quasi tutto -- vernice, legno,
plastica, pelle -- e `1` per il metallo vero, e i valori intermedi servono a
poco. E `materialSetEmissive`, che fa brillare una superficie del suo colore,
per schermi, lampade e ferro rovente, senza illuminare niente attorno.
Una cosa da sapere sul metallo: una superficie metallica ti mostra i suoi
riflessi invece di un colore suo, quindi in una scena senza niente da riflettere
sembra scura e spenta. È per questo che il metallo scuro del drago ha bisogno
del sole per prendere vita. La soluzione vera è un cielo (`sceneSetSky`), che
avvolge la scena in un panorama che si vede dietro tutto e allo stesso tempo la
illumina; ne parla «L'aspetto del fotogramma» nel riferimento.
[#_movement_the_model_brings_with_it]
=== I movimenti che il modello si porta dietro
Molti modelli contengono animazioni: una camminata, una corsa, una posa da
fermo, una porta che si apre. Sono salvate nel file, e riprodurne una è una sola
chiamata sul nodo radice dell'istanza.
[source,lua]
----
animationPlay(dragon, "Walk", true)
----
Il secondo argomento è il nome della clip come lo salva il file, oppure il suo
numero. `true` vuol dire ripeterla all'infinito. Altri due numeri facoltativi
impostano la velocità e la dissolvenza incrociata:
`animationPlay(hero, "Run", true, 1.2, 0.4)` riproduce la corsa un quinto più
veloce e ci sfuma dentro in quattro decimi di secondo, così una camminata
diventa una corsa senza scatti. `animationStop`, `animationPause` e
`animationIsPlaying` fanno quello che dice il loro nome.
Quella riga non puoi ancora eseguirla, ed ecco il motivo onesto: il drago nella
tua cartella `Singe` non ha animazioni. È un modello, non uno spettacolo.
Chiedere una clip che non c'è ferma il gioco con un errore, quindi non dare mai
niente per scontato -- chiedi:
[source,lua]
----
local clips = modelGetAnimations(dragonModel)
if #clips > 0 then
animationPlay(dragon, 1, true)
else
debugPrint("DragonModel.glb carries no animation clips. The wings are turned by hand.")
end
----
`modelGetAnimations` restituisce una tabella con i nomi delle clip nel file, in
ordine, quindi `clips[1]` è la prima e `#clips` è quante ce ne sono -- gli
strumenti per le tabelle della sesta lezione, su una tabella che ha riempito il
motore. Eseguilo e guarda la console: il messaggio compare, perché quel
conteggio è zero.
Prendi l'abitudine di fare quel controllo con ogni modello che non hai fatto tu.
Costa tre righe e trasforma «il gioco è morto all'avvio» in una frase nel log.
[#_moving_the_parts_yourself]
=== Muovere le parti da te
Un modello senza animazioni non è una statua. Le sue parti sono nodi, e i nodi
girano.
[source,lua]
----
local wingLeft = nodeFind("wingL", dragon)
local wingRight = nodeFind("wingR", dragon)
----
[source,lua]
----
function onOverlayUpdate()
local flap = math.sin(singeGetTicks() / 260) * 22
nodeSetRotation(wingLeft, flap, 0, 0)
nodeSetRotation(wingRight, -flap, 0, 0)
nodeRotate(dragon, 0, 0.25, 0)
overlayClear()
overlayPrint(2, 2, "Singe/DragonModel.glb")
overlayPrint(2, 4, "animation clips in the file: " .. #clips)
return OVERLAY_UPDATED
end
----
Salvalo. Le ali battono, il drago gira lentamente su se stesso, e l'ombra sul
pavimento batte con loro.
`singeGetTicks` è il numero di millisecondi da quando il motore è partito,
quindi sale regolarmente per sempre. `math.sin` prende quel numero che sale e
restituisce un valore che scivola dolcemente da `-1` a `1` e indietro, un
battito completo circa ogni secondo e mezzo con questo divisore. Moltiplicare
per `22` lo trasforma in un angolo di ventidue gradi da ogni parte. Dividere per
un numero più grande rallenta il battito; moltiplicare per un numero più grande
lo allarga. Quella riga da sola è l'animazione più economica che esista, e vale
la pena tenerla in tasca.
Le ali girano attorno al proprio asse X, e girano attorno alla spalla invece che
attorno al centro dell'animale, perché chi ha costruito il file ha messo il nodo
di ogni parte dove va l'articolazione. Un modello che non è stato costruito così
farà girare le sue parti attorno al centro del modello, e la soluzione è
spostare il nodo della parte, di cui parla la sezione sui nodi del riferimento.
Nota che il girare di tutto il drago non disturba il girare delle ali. Le parti
sono appese sotto la radice dell'istanza, quindi ruotare la radice si porta
dietro tutto, e la rotazione propria di ogni ala si aggiunge sopra. Quella
disposizione -- una cosa dentro una cosa dentro una cosa, ognuna che si muove
nel mondo del suo genitore -- è ciò a cui serve un albero della scena.
[#_your_own_models]
=== I tuoi modelli
Quando ne fai di tuoi, esportali da Blender -- o da Maya, o da 3ds Max, o da
qualsiasi altro programma moderno -- come *glTF 2.0 binario*, cioè un file
`.glb`. È la scelta predefinita di Blender ed è l'unico formato di modelli che
Singe legge.
L'unica regola è che il file deve essere autosufficiente: le forme, le immagini
sopra di esse e i movimenti, tutti dentro un solo `.glb`. Un file che fa
riferimento a un `.bin` o a un `.png` che gli sta accanto viene rifiutato, con
un messaggio che nomina quello che voleva. L'esportatore di Blender fa la cosa
giusta con le sue impostazioni predefinite.
Non tutto quello che puoi costruire sopravvive al viaggio. La sezione «Modelli
e animazione» del Riferimento, nel capitolo «Scene 3D», è l'elenco: che cosa viene
letto, che cosa viene ignorato senza dire niente, e i limiti (uno scheletro può
avere fino a centoventotto giunti, per esempio). Leggila una volta prima di
passare un fine settimana su un modello, non dopo.
[#_try_it_23]
=== Prova tu
. *Spegni le luci.* Commenta tutto il blocco `sun` mettendo `--` all'inizio di
ogni riga, ed eseguilo. Poi fai lo stesso con `sceneSetAmbient`. Ricordati
che aspetto ha, perché un giorno succederà senza che tu lo voglia.
. *Sposta il sole.* Cambia `nodeLookAt(sun, 0, 1.5, 0)` in
`nodeLookAt(sun, 0, 20, 0)` così il sole punta sopra la testa del drago.
Guarda l'ombra e i riflessi sparire. Puntare una luce non è decorazione.
. *Scalda la torcia.* Alza l'intensità della luce puntiforme da `14` a `60` e
abbassa la sua portata a `4`. Poi spostala in `(0, 6, 0)`, sopra il drago.
Una luce puntiforme è una lampadina, e stai scegliendo dove appenderla.
. *Crea due istanze.* Chiama `modelInstance(dragonModel)` una seconda volta,
tienila in un'altra variabile, scalala e mettila in `(-4, 0, -3)`. Un
caricamento, due draghi. Poi prova invece a caricare il modello due volte e
capisci perché non dovresti.
. *Trova un'altra parte.* Le parti del drago si chiamano `head`, `jaw`, `neck`,
`tail`, `body`, `crest`, `hornL`, `hornR`, `wingL`, `wingR`, `foreLegL`,
`foreLegR`, `hindLegL` e `hindLegR`. Fai ondeggiare la coda con un suo `math.sin` a una
velocità diversa da quella delle ali. Poi prova `nodeFind("wings", dragon)` e
guarda che cosa fa un nome sbagliato quando passi quel `nil` dritto a
`nodeSetRotation`.
[#_break_it_on_purpose_22]
=== Rompilo apposta
Cambia il nome del file in `Singe/Dragonmodel.glb` -- una sola maiuscola di
differenza -- ed eseguilo. Singe si ferma prima che il gioco parta:
----
13:modelLoad: Unable to read Singe/Dragonmodel.glb.
----
La riga, la chiamata e la lamentela, in quest'ordine. «Unable to read» vuol
dire che il motore ha cercato e non ha trovato niente, il che è quasi sempre una
di tre cose: il nome è scritto male, le maiuscole sono sbagliate, o il file non
è dove hai detto. Su Windows le maiuscole sarebbero state perdonate e poi il tuo
gioco non avrebbe funzionato per nessuno su Linux, che è un bug peggiore di
questo. Scrivi i nomi esattamente.
Lo stesso messaggio con un nome di file giusto vuol dire che il file non è
dove l'ha cercato il motore. La cartella `Singe` viene creata nella cartella *da cui*
esegui il motore, quindi se hai avviato Singe da un'altra parte, anche
`Singe/DragonModel.glb` punta da un'altra parte.
[#_what_you_learned_23]
=== Cosa hai imparato
* `modelLoad` legge un `.glb` una volta; `modelInstance` ne mette delle copie
tutte le volte che vuoi.
* La radice di un'istanza è un nodo normale: posizionala, ruotala, scalala ed
eliminala come qualsiasi altro.
* Un modello si porta dietro le sue forme e i suoi materiali.
* Un modello arriva alla dimensione che ha usato il suo autore, ed è per questo
che un `nodeSetScale` sull'istanza fa parte del collocarlo.
* Una scena senza luce è nera o quasi, ed è la prima cosa da controllare quando
un modello ha un aspetto sbagliato.
* Una luce direzionale è un sole, una luce puntiforme è una lampadina che si
affievolisce con la distanza e vuole un'intensità molto più grande, e un
faretto è un cono.
* Le ombre sono spente finché `lightSetShadow` non le accende, e ognuna ha un
costo.
* `nodeFind` raggiunge una parte di un modello con il nome che ha nel file.
* `modelGetAnimations` dice che cosa può riprodurre un modello, e chiedere una
clip che non c'è è un errore.
* Un modello senza animazioni si può comunque muovere una parte alla volta.
[#_next_time_23]
=== La prossima volta
Tutto in questa scena si muove perché gliel'hai detto tu, fotogramma per
fotogramma. La prossima lezione passa quel lavoro a un motore fisico: casse che
cadono, rotolano, si impilano e si fanno cadere a vicenda senza che tu scriva
una riga di aritmetica -- e la domanda più difficile di quando dovresti
lasciarglielo fare. È la ventiquattresima lezione.