singe/docs/it/lessons/22-3d.adoc

454 lines
18 KiB
Text

[#_lesson_22_into_3d]
== Lezione 22: Nel 3D
image::learn/22-3d.png[La lezione finita, 480]
Tutto quello che hai disegnato finora era piatto. Sprite, testo, riquadri e
linee vivono tutti sull'overlay, che è una lastra di vetro sul davanti della
finestra, con le stesse coordinate che aveva nella prima lezione. Questa lezione
mette un mondo dietro quel vetro: un posto con la profondità, dove una cosa può
essere più vicina o più lontana, e dove una telecamera decide che cosa ne vedi.
Alla fine avrai un cubo che galleggia nello spazio e che puoi girare con i tasti
freccia, e i numeri dei suoi angoli scritti in un angolo dello schermo dallo
stesso `overlayPrint` che hai usato il tuo primissimo giorno. Quest'ultima parte
conta più di quanto sembri, e ha una sezione tutta sua.
[#_a_scene_a_box_and_a_light]
=== Una scena, un cubo e una luce
Crea una cartella, mettici dentro un file chiamato `spin.singe` e scrivi
questo.
[source,lua]
----
sceneEnable(true)
sceneSetBackground(18, 20, 34)
sceneSetAmbient(35, 35, 45)
local paint = materialNew()
materialSetColor(paint, 215, 95, 60)
materialSetRoughness(paint, 0.6)
local box = nodeNew()
nodeSetMesh(box, meshBox(2, 2, 2), paint)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 4, 6, 5)
nodeLookAt(sun, 0, 0, 0)
function onOverlayUpdate()
nodeRotate(box, 0, 0.5, 0)
return OVERLAY_UPDATED
end
----
Eseguilo con `Singe -R spin`. Un cubo arancione gira lentamente su uno sfondo
blu scuro, illuminato dall'alto e da destra, con una faccia chiara e le altre
che sfumano nell'ombra.
In quello script non c'è nessuna telecamera. Singe ti ha messo in `(0, 0, 5)` a
guardare il centro del mondo, perché una scena senza telecamera deve pur essere
vista da qualche parte. Tra un momento la sostituirai con una telecamera tua.
Se la finestra resta nera, leggi la console. Su una macchina la cui grafica non
sa disegnare una scena 3D, `sceneEnable` ferma il gioco con un messaggio che lo
dice, e non c'è altro da fare che eseguire la lezione da un'altra parte. Su una
macchina del genere i giochi 2D continuano a funzionare; il 3D no.
[#_what_just_happened_18]
=== Cos'è appena successo
Le tre chiamate in cima preparano il mondo nel suo insieme.
[source,lua]
----
sceneEnable(true)
sceneSetBackground(18, 20, 34)
sceneSetAmbient(35, 35, 45)
----
`sceneEnable(true)` accende il livello 3D. È l'unica chiamata che fa ogni gioco
3D, e finché non la fai niente di quello che costruisci nella scena viene
disegnato.
`sceneSetBackground` è il colore con cui la scena viene ripulita a ogni
fotogramma, rosso, verde e blu da `0` a `255`, come funzionano i colori dalla
nona lezione. C'è un quarto numero che puoi darle per la trasparenza, ed è così
che un gioco che riproduce video mette un oggetto 3D davanti al filmato. Se non
lo dai, lo sfondo è pieno.
`sceneSetAmbient` è un po' di luce che arriva da tutte le direzioni insieme. Il
suo compito è evitare che i lati che nessuna lampada raggiunge siano nero pieno.
Più avanti abbassala a `0, 0, 0` e guarda quanto diventa dura l'immagine.
[source,lua]
----
local paint = materialNew()
materialSetColor(paint, 215, 95, 60)
materialSetRoughness(paint, 0.6)
----
Un *materiale* è l'aspetto di una superficie: il suo colore, quanto è lucida,
se ha sopra un'immagine. `materialNew` ne crea uno e restituisce un numero che
lo rappresenta, lo stesso tipo di handle che ti ha dato `spriteLoad` nella nona
lezione. Tienilo in una variabile, perché tutte le chiamate successive ne hanno
bisogno.
`materialSetRoughness` va da `0`, uno specchio con un riflesso piccolo e netto,
a `1`, una superficie opaca come il gesso, senza nessun riflesso. `0.6` è
vernice sul legno. Ci sono un'altra dozzina di chiamate `materialSet`, e la
prossima lezione ne usa alcune; la sezione sui materiali del riferimento le
elenca tutte.
[source,lua]
----
local box = nodeNew()
nodeSetMesh(box, meshBox(2, 2, 2), paint)
----
Questa è quella importante. Un *nodo* è un posto nel mondo. Ha una posizione,
una rotazione e una scala, e da solo non disegna proprio niente: è un punto a
cui si possono attaccare delle cose. In una scena 3D tutto è un nodo, comprese
le luci e la telecamera.
`nodeNew` ne crea uno vuoto al centro del mondo. `nodeSetMesh` ci appende due
cose: una *mesh*, che è la forma, e il materiale, che è l'aspetto di quella
forma. `meshBox(2, 2, 2)` costruisce un cubo largo due unità, alto due e
profondo due, e ne restituisce un handle come ha fatto `materialNew`.
Vale la pena condividere mesh e materiali. Un solo handle di `meshBox` può
essere usato da cento nodi, ed è così che disegni cento casse spendendo poco.
Creare cento cubi separati che per caso hanno la stessa dimensione è il modo da
principiante di rendere lenta una scena.
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 4, 6, 5)
nodeLookAt(sun, 0, 0, 0)
----
Anche `lightNew` crea un nodo -- uno che porta una luce -- e ne restituisce
l'handle, così lo posizioni e lo punti con le stesse chiamate che usi per tutto
il resto. `LIGHT_DIRECTIONAL` è un sole: è così lontano che conta solo la
direzione in cui splende, ed è per questo che qui la posizione conta solo
perché `nodeLookAt` la usa per ricavare la direzione.
Togli quelle tre righe ed eseguilo di nuovo. Il cubo diventa quasi nero, perché
l'unica luce rimasta è quella ambiente, debole. Alla luce è dedicata tutta la
ventitreesima lezione, e questo è il primo assaggio della delusione più comune
nel 3D: il modello si è caricato benissimo, e la scena è nera perché niente lo
illumina.
[source,lua]
----
nodeRotate(box, 0, 0.5, 0)
----
`nodeRotate` gira un nodo di un certo numero di gradi attorno a ciascuno dei
suoi tre assi, in aggiunta alla rotazione che aveva già. Mezzo grado per
fotogramma, a circa sessanta fotogrammi al secondo, fa un giro ogni dodici
secondi. È lo stesso trucco della seconda lezione: un piccolo cambiamento,
applicato a ogni fotogramma, si legge come movimento.
[#_which_way_is_which]
=== Orientarsi
Tre numeri descrivono un posto nella scena, e devi sapere che cosa vuol dire
ciascuno prima di poter mettere qualcosa da qualche parte.
* *X* va verso destra. Una X più grande è più a destra.
* *Y* va verso l'alto. Una Y più grande è più in alto. Il pavimento di una
scena di solito è `y = 0`.
* *Z* va verso di te, fuori dallo schermo. Una Z più grande è più vicina alla
telecamera predefinita, e *la Z negativa è la direzione in cui guarda la
telecamera*.
Quest'ultima trae in inganno tutti. Le cose che vuoi davanti alla telecamera
vanno a Z negativa. Non è una stranezza di Singe: glTF e Blender usano la
stessa disposizione, quindi un modello esportato da Blender arriva rivolto dalla
parte che Singe si aspetta.
Non credermi sulla parola. Aggiungi questa riga dopo la riga `nodeSetMesh`,
salva e guarda:
[source,lua]
----
nodeSetPosition(box, 3, 1, 0)
----
Il cubo salta a destra e in alto. `nodeSetPosition` mette un nodo in un punto
preciso, e sostituisce qualunque posizione il nodo avesse. C'è una
`nodeMove(node, dx, dy, dz)` corrispondente che invece sposta un nodo di una
certa quantità, lungo i suoi assi, quindi `nodeMove(ship, 0, 0, -0.1)` fa
avanzare un'astronave da qualunque parte stia puntando.
Prova poi `nodeSetPosition(box, 0, 0, -20)`. Il cubo si rimpicciolisce in
lontananza, ed è la Z negativa che fa il suo lavoro. Poi prova `(0, 0, 20)`: il
cubo sparisce, perché l'hai messo dietro la telecamera.
Altre due chiamate completano la serie.
[source,lua]
----
nodeSetRotation(box, 0, 45, 0)
nodeSetScale(box, 0.5)
----
`nodeSetRotation` imposta la rotazione direttamente in gradi, mentre
`nodeRotate` la aggiunge. I tre numeri sono attorno a X, attorno a Y e attorno
a Z: girare attorno a Y è quello che fa una persona in piedi sul pavimento
quando si gira per guardare da un'altra parte, ed è quella che userai di più.
`nodeSetScale` rende il nodo più grande o più piccolo, con un solo numero per
tutti e tre gli assi oppure con tre, uno per asse.
Togli di nuovo tutti e due gli esperimenti prima di andare avanti. Il resto
della lezione parte dallo script come l'hai scritto la prima volta.
[#_your_own_camera]
=== Una telecamera tua
La vista predefinita è una cortesia, non una funzionalità. Metti una
telecamera.
[source,lua]
----
local camera = nodeNew()
nodeSetPosition(camera, 0, 2, 7)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
cameraSetPerspective(60, 0.1, 100)
----
Metti queste righe sotto la luce, prima del tuo `onOverlayUpdate`, ed eseguilo.
Adesso guardi il cubo leggermente dall'alto, da due unità più su e sette più
indietro.
Una telecamera è un nodo come un altro. `cameraSet` dice al motore da quale
nodo guardare, e la scena viene disegnata dalla posizione di quel nodo
guardando lungo la Z negativa del nodo stesso. Puntare una telecamera a mano
con `nodeSetRotation` sarebbe una tortura, quindi lo fa per te `nodeLookAt`:
dagli un punto del mondo e lui gira il nodo verso quel punto.
`cameraSetPerspective` è l'obiettivo. Il primo numero è il campo visivo in
gradi, in verticale: quanta parte del mondo viene schiacciata nell'altezza della
finestra. Un numero piccolo come `35` è un teleobiettivo che appiattisce tutto;
uno grande come `90` è un grandangolo che fa sembrare enorme la stanza e
gonfiare gli angoli. `60` è un valore predefinito comodo. Gli altri due numeri
sono la distanza più vicina e quella più lontana che vengono disegnate, e le
cose fuori da quell'intervallo non vengono disegnate affatto.
Siccome la telecamera è un nodo normale, tutto quello che hai imparato sui nodi
funziona anche su di lei. Più avanti appenderai una telecamera sotto un'auto in
movimento e avrai gratis una telecamera d'inseguimento.
[#_turning_it_with_the_arrow_keys]
=== Giralo con i tasti freccia
Leggere il sistema di coordinate su una pagina è un pessimo modo per impararlo.
Muoverci dentro qualcosa è un buon modo. Sostituisci il tuo `onOverlayUpdate`
con questo, e aggiungi sopra di esso le due nuove callback e le quattro
variabili.
[source,lua]
----
local turnLeft = false
local turnRight = false
local tiltUp = false
local tiltDown = false
----
Queste vanno proprio in cima al file, sopra tutto il resto.
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_LEFT then
turnLeft = true
elseif what == SWITCH_RIGHT then
turnRight = true
elseif what == SWITCH_UP then
tiltUp = true
elseif what == SWITCH_DOWN then
tiltDown = true
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
turnLeft = false
elseif what == SWITCH_RIGHT then
turnRight = false
elseif what == SWITCH_UP then
tiltUp = false
elseif what == SWITCH_DOWN then
tiltDown = false
end
end
function onOverlayUpdate()
if turnLeft then
nodeRotate(box, 0, -1.5, 0)
end
if turnRight then
nodeRotate(box, 0, 1.5, 0)
end
if tiltUp then
nodeRotate(box, -1.5, 0, 0)
end
if tiltDown then
nodeRotate(box, 1.5, 0, 0)
end
return OVERLAY_UPDATED
end
----
`onInputPressed` e `onInputReleased` sono callback, come `onOverlayUpdate`: le
scrivi tu, e Singe le chiama quando un comando viene premuto o rilasciato. Una
pressione arriva una volta sola, non di continuo mentre il tasto è tenuto
premuto, ed è per questo che ognuna imposta una variabile e il lavoro lo fa la
callback di disegno. Quello schema -- la pressione alza un indicatore, il
fotogramma legge l'indicatore -- è il modo in cui ottieni «mentre il tasto è
tenuto premuto» da un motore che ti dice solo i cambiamenti.
I nomi `SWITCH_` vengono dal motore stesso, quindi non c'è niente da caricare
per usarli. E non sono nemmeno tasti. Sono
quello che un comando *significa*, e `controls.cfg` decide quale tasto, pulsante
o direzione della levetta produce ciascuno. Oggi i tuoi tasti freccia mandano
`SWITCH_LEFT` e compagnia; la levetta di un controller manda la stessa cosa, e
il tuo script non si accorge mai della differenza.
I segni sono scelti in modo che ogni freccia sposti la faccia che stai guardando
nella direzione che hai premuto. Girare attorno a Y di un angolo positivo sposta
quella faccia vicina verso destra, quindi la freccia sinistra chiede un angolo
negativo. Se ti sembra al contrario, scambia i segni; sono i tuoi comandi, non
quelli del motore.
[#_the_overlay_is_still_on_top]
=== L'overlay sta ancora sopra
Aggiungi queste righe a `onOverlayUpdate`, sopra il `return`.
[source,lua]
----
local pitch, yaw = nodeGetRotation(box)
overlayClear()
overlayPrint(2, 2, "Arrow keys turn the box.")
overlayPrint(2, 4, "yaw " .. math.floor(yaw) .. " pitch " .. math.floor(pitch))
----
Qui non c'è niente di nuovo, tranne quello sopra cui viene scritto.
`overlayPrint` è la chiamata della prima lezione, che conta in celle di
carattere e non vuole nessun font caricato, e sta disegnando sopra una scena 3D
senza che nessuno le abbia detto niente.
Questa è la disposizione, e vale la pena dirla chiaramente: Singe disegna prima
il video, poi sopra di esso la scena 3D, poi sopra ancora l'overlay. Il tuo
punteggio, le tue vite, il tuo timer, il tuo mirino e i tuoi sottotitoli sono
tutti lavoro dell'overlay, esattamente come nella seconda parte. Un gioco 3D
tiene il suo punteggio nell'angolo con le stesse cinque righe che usa un gioco
2D.
`nodeGetRotation` restituisce tre numeri, gli angoli attorno a X, Y e Z, e
questa riga tiene i primi due in `pitch` e `yaw`. Sono i nomi che si usano di
solito: il pitch (beccheggio) è il muso che va su e giù, lo yaw (imbardata) è
girare a sinistra e a destra. `math.floor` butta via la parte decimale, così i
numeri smettono di sfarfallare.
Guarda lo yaw mentre tieni premuta la freccia sinistra. Scende oltre `-180` e
ricomincia da `180`, perché gli angoli vengono riportati come la rotazione più
piccola che ti porta lì e non come un totale progressivo. Se il tuo gioco ha
bisogno di sapere quante volte il giocatore ha girato su se stesso, contalo tu.
[#_try_it_22]
=== Prova tu
. *Sposta la telecamera.* Cambia `nodeSetPosition(camera, 0, 2, 7)` in
`(0, 8, 7)`, poi `(7, 2, 7)`, poi `(0, 0.2, 7)`. Ogni volta `nodeLookAt`
tiene il cubo al centro, quindi cambiando un solo numero ottieni
un'angolazione diversa della stessa scena.
. *Cambia l'obiettivo.* Prova `cameraSetPerspective(30, 0.1, 100)`, poi `(90,
0.1, 100)`, senza spostare la telecamera. Un gioco sembra veloce con un campo
visivo ampio e solenne con uno stretto, e questa è la manopola dell'atmosfera
più economica che esista.
. *Metti un secondo cubo.* Copia le due righe che hanno creato il cubo, rinomina
la variabile in `mark`, e dagli `nodeSetPosition(mark, 3, 0, 0)` e
`nodeSetScale(mark, 0.3)`. Adesso hai un punto di riferimento con cui
confrontare il primo cubo. Spostalo a turno in `(0, 3, 0)` e `(0, 0, -3)` e
di' ad alta voce quale asse è quale.
. *Condividi la mesh.* Invece di chiamare `meshBox` due volte, tieni la prima in
una variabile e passa lo stesso handle a tutti e due i nodi. All'aspetto non
cambia niente. È proprio questo il punto: una forma, due posti.
. *Appendi la telecamera al cubo.* Tieni il secondo cubo del passo precedente,
altrimenti non resterà niente rispetto a cui vedere il movimento. Poi cerca
`nodeSetParent` nel riferimento e appendi la telecamera sotto il primo cubo.
Tieni premuta una freccia: la telecamera segue la rotazione del cubo, quindi
il cubo sta fermo e il resto della scena ti gira attorno. Sei a una riga da
una visuale in prima persona, ed è anche così che scopri che una telecamera
d'inseguimento vuole un genitore e non dei conti.
[#_break_it_on_purpose_21]
=== Rompilo apposta
Sbaglia a scrivere il nome del materiale. Cambia la riga `nodeSetMesh` in modo
che usi `pain` invece di `paint`:
[source,lua]
----
nodeSetMesh(box, meshBox(2, 2, 2), pain)
----
Singe si ferma prima che il gioco parta e scrive qualcosa del genere:
----
15:nodeSetMesh: Argument 3 must be a number.
----
È un errore di forma diversa da quello della prima lezione, e viene dal motore
invece che da Lua. Il numero davanti è la riga. Il nome che lo segue è la
chiamata che si è lamentata. La lamentela vera e propria è che la terza cosa che
le hai passato non era un numero.
Vale la pena seguire il motivo, perché spiega un'intera famiglia di errori.
`pain` è una variabile a cui non è mai stato dato un valore, e in Lua una
variabile vuota è `nil` -- il valore che vuol dire «qui non c'è niente».
Passare `nil` a una chiamata che vuole l'handle di un materiale dà proprio
questo. Quando una chiamata del motore ti dice «must be a number», cerca su
quella riga il nome di una variabile scritto male, o un handle che ti sei
dimenticato di tenere.
[#_what_you_learned_22]
=== Cosa hai imparato
* `sceneEnable(true)` accende il livello 3D, e niente viene disegnato in 3D
finché non la chiami.
* Un nodo è un posto nel mondo con una posizione, una rotazione e una scala.
Mesh, luci e telecamere sono tutte appese ai nodi.
* Una mesh è la forma e un materiale è l'aspetto, e una di ciascuna può essere
condivisa da quanti nodi vuoi.
* X è a destra, Y è in alto, e la Z negativa è la direzione in cui guarda la
telecamera.
* `nodeSetPosition` e `nodeSetRotation` impostano un valore direttamente;
`nodeMove` e `nodeRotate` lo cambiano di una certa quantità.
* Qualsiasi nodo può essere la telecamera. `nodeLookAt` la punta e `cameraSet`
la sceglie.
* Il campo visivo in `cameraSetPerspective` cambia la sensazione della scena
senza spostare niente.
* Una scena senza luce è nera, ed è la prima cosa da controllare quando non
compare niente.
* L'overlay c'è ancora, sta ancora sopra, ed è ancora il posto del tuo
punteggio.
[#_next_time_22]
=== La prossima volta
Un cubo è un drago piuttosto scarso. La prossima lezione carica un vero
modello -- uno che il motore ha già estratto nella tua cartella di lavoro,
quindi non c'è niente da scaricare e niente da disegnare -- lo mette sul
pavimento e lo illumina come si deve. È la ventitreesima lezione.