[#_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.