454 lines
18 KiB
Text
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.
|