Obiettivi della lezione
Adesso si apre il canvas. Una scena 3D in pagina vuole cinque cose e non una di più: un posto dove disegnare, una scena, una camera che guarda, qualcosa da guardare e una luce che lo illumini. Poi un giro che ridisegna sessanta volte al secondo. Tutto il resto del corso è roba che si appoggia a queste cinque.
Alla fine della lezione riesci a:
- Caricare Three.js nel modo che consiglia la sua documentazione, con gli import map;
- Montare scena, camera, renderer e il giro di disegno;
- Capire che cosa sono geometria, materiale e mesh, e perché sono tre cose separate;
- Far stare la scena dentro al suo riquadro e reagire al ridimensionamento;
- Non far bollire il telefono con il rapporto dei pixel.
Caricare Three.js senza sbagliare
Qui c'è una trappola, e sta in tutti i tutorial vecchi. Three.js si caricava con un <script src="three.min.js">; adesso quel file non esiste più e la loro documentazione dice esplicitamente di usare un import map. Questo, per intero, è il modo giusto in una pagina statica:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
</script>
L'import map è un dizionario: dice al browser che quando un modulo chiede 'three' deve andare a prendere quell'indirizzo. Serve perché nel browser import ... from 'three' da solo non vuol dire niente: i nomi corti li risolve un impacchettatore, e qui l'impacchettatore non c'è.
three/addons/ è la cartella degli accessori: i controlli del mouse, i caricatori dei modelli, i post effetti. Stanno fuori dal cuore della libreria apposta, per non farti scaricare quello che non usi.
Due versioni uguali, sempre
Il numero in three@0.186.0 deve essere lo stesso nelle due righe. Se il cuore è una versione e gli accessori un'altra, ti ritrovi con due copie di Three.js in memoria e con errori che non dicono niente, tipo un oggetto che non viene disegnato senza che nessuno protesti.
I cinque pezzi
Il canvas è il rettangolo della pagina dove si disegna. Lo scrivi in HTML e gli dai una misura col CSS, come a un'immagine.
La scena è l'elenco di quello che c'è nel mondo. Ci appendi oggetti e luci, e lei si ricorda dove stanno.
La camera è il punto di vista. Quella che userai quasi sempre è la prospettica, e vuole quattro numeri: l'angolo di campo in gradi, il rapporto fra larghezza e altezza, e le distanze minima e massima oltre le quali smette di disegnare.
Il renderer è chi disegna: prende la scena e la camera e sporca i pixel del canvas.
Il mesh è un oggetto, e nasce da due cose separate. La geometria è la forma, cioè i vertici. Il materiale è come reagisce alla luce: colore, lucidità, metallo. Stanno separati perché così la stessa forma la vesti in dieci modi, e lo stesso materiale lo metti su dieci forme.
La scena, per intero
Questa pagina funziona così com'è. Salvala come scena.html e aprila con un server locale, non col doppio clic: i moduli JavaScript da file:// il browser li rifiuta.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>La prima scena</title>
<style>
body { margin: 0; background: #0f172a; }
#vista { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="vista"></canvas>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/"
} }
</script>
<script type="module">
import * as THREE from 'three';
const canvas = document.querySelector('#vista');
// 1. la scena: l'elenco di quello che esiste
const scena = new THREE.Scene();
scena.background = new THREE.Color(0x0f172a);
// 2. la camera: angolo di campo, proporzione, vicino, lontano
const camera = new THREE.PerspectiveCamera(
45, canvas.clientWidth / canvas.clientHeight, 0.1, 100
);
camera.position.set(3, 2, 5);
camera.lookAt(0, 0, 0);
// 3. il renderer: chi disegna dentro al canvas
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
// 4. un oggetto: forma + materiale
const forma = new THREE.BoxGeometry(1.5, 1.5, 1.5);
const materiale = new THREE.MeshStandardMaterial({
color: 0x16a34a, roughness: 0.35, metalness: 0.1
});
const cubo = new THREE.Mesh(forma, materiale);
scena.add(cubo);
// 5. le luci: con MeshStandardMaterial, senza luci vedi nero
scena.add(new THREE.AmbientLight(0xffffff, 0.6));
const sole = new THREE.DirectionalLight(0xffffff, 2.5);
sole.position.set(4, 5, 3);
scena.add(sole);
// il canvas deve avere i pixel che ha davvero sullo schermo
function adatta() {
const l = canvas.clientWidth, h = canvas.clientHeight;
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(l, h, false); // false: le misure CSS non le tocca
camera.aspect = l / h;
camera.updateProjectionMatrix();
}
adatta();
window.addEventListener('resize', adatta);
// il giro: gira una volta per fotogramma
renderer.setAnimationLoop(() => {
cubo.rotation.y += 0.01;
cubo.rotation.x += 0.004;
renderer.render(scena, camera);
});
</script>
</body>
</html>
Sono una settantina di righe e dentro c'è tutto il mestiere. Da qui in avanti si aggiunge, non si cambia impianto.
Il giro di disegno
Una pagina normale la disegni una volta e resta. Una scena 3D va ridisegnata a ogni fotogramma, perché niente si aggiorna da sé: muovi il cubo di un centesimo di radiante e poi chiedi di ridisegnare.
renderer.setAnimationLoop chiama la tua funzione prima di ogni fotogramma, in genere sessanta volte al secondo. Fa la stessa cosa di requestAnimationFrame chiamato in ciclo, con due vantaggi: si ferma da solo quando la scheda è in secondo piano, e funziona anche nei visori.
Il tempo non è il fotogramma
Scrivere rotation.y += 0.01 lega la velocità al numero di fotogrammi: su uno schermo a 120 Hz il cubo gira il doppio. Per un cubo decorativo non importa; per qualcosa che deve durare un tempo preciso si usa il tempo vero, che è il primo argomento che setAnimationLoop passa alla tua funzione. Nella Lezione 6 il problema sparisce da sé, perché a comandare sarà lo scorrimento.
I pixel, cioè dove si brucia la batteria
setPixelRatio è la riga che decide quanto lavora la scheda grafica. Un telefono moderno dichiara un rapporto di 3: senza quella riga disegneresti nove volte i pixel di un canvas da 400x800, a ogni fotogramma, per sempre.
Math.min(window.devicePixelRatio, 2) è il compromesso che usano quasi tutti: a 2 i bordi sono già puliti e la differenza con 3 non si vede, mentre il lavoro cala di più della metà. È la singola riga che più spesso trasforma una scena che scatta in una scena fluida.
L'altro numero che conta è renderer.setSize(l, h, false). Quel false dice di non toccare le misure CSS del canvas: le decide il tuo foglio di stile, e il renderer si limita a dargli il numero giusto di pixel dentro. Senza, il canvas si mette una misura sua e il tuo impaginato salta.
Laboratorio: la tua prima scena
Obiettivo: una scena che gira nella pagina del tuo progetto, dentro al suo riquadro, senza far soffrire il telefono.
Consegna
- Copia
scena.htmle aprila con un server locale (python3 -m http.serverbasta e avanza). Deve girare un cubo verde. - Cambia forma: prova
SphereGeometry(1, 32, 16),TorusKnotGeometry(0.8, 0.25),CylinderGeometry(1, 1, 2, 32). Guarda il secondo e il terzo numero della sfera: sono quanti triangoli la compongono, e sotto un certo punto si vedono le sfaccettature. - Togli le luci e guarda: nero. Poi rimetti solo l'ambientale: piatto. Poi solo la direzionale: contrastato. È il modo più veloce di capire a che cosa serve ognuna.
- Cambia il materiale in
MeshBasicMaterial: torna visibile senza luci, e diventa piatto come un adesivo. Adesso sai la differenza fra i due. - Portala nel tuo riquadro: togli
100vw/100vhe mettila in un contenitore largo metà pagina. Ridimensiona la finestra e controlla che non si deformi. - Misura: apri il pannello delle prestazioni del browser e guarda i fotogrammi al secondo con
setPixelRatioa 1, a 2 e senza la riga. Segnati i tre numeri.
Risultato atteso
Un oggetto che gira dentro al suo riquadro, che si ridimensiona senza deformarsi, e tre numeri di fotogrammi al secondo che ti dicono quanto costa il rapporto dei pixel. Se il canvas si deforma, il colpevole è quasi sempre camera.aspect dimenticato dentro adatta().
Mini-test di autoverifica
Istruzioni
Sezione A: 6 domande a risposta multipla (1 punto). Sezione B: 2 domande aperte (3 punti). Totale 12. Soglia: 60% (8/12).
Sezione A: risposta multipla
- L'import map serve perché:
a) comprime i file b) nel browserimport 'three'da solo non ha un indirizzo c) è obbligatorio in HTML5 d) carica le texture - Geometria e materiale sono separati perché:
a) così vuole WebGL b) la stessa forma si veste in modi diversi e viceversa c) pesano meno d) uno sta sulla GPU e l'altro no - Con
MeshStandardMateriale nessuna luce vedi:
a) il colore pieno b) nero c) un errore d) il fondo renderer.setSize(l, h, false): quelfalse:
a) spegne l'antialias b) dice di non toccare le misure CSS del canvas c) disattiva il resize d) non fa nienteMath.min(window.devicePixelRatio, 2)serve a:
a) migliorare i colori b) non disegnare più pixel del necessario c) centrare la scena d) caricare prima- Una scena 3D va ridisegnata:
a) una volta sola b) a ogni fotogramma c) a ogni scroll d) quando cambia il DOM
Sezione B: risposta aperta
- (3 punti) Elenca i cinque pezzi di una scena e di' in una riga a che cosa serve ognuno.
- (3 punti) Riporta i tre numeri di fotogrammi al secondo misurati nel laboratorio e di' quale valore di
setPixelRatioterresti sul tuo progetto, e perché.
Mostra risposte corrette
Sezione A
- b: i nomi corti li risolve un impacchettatore, e in una pagina statica non c'è.
- b: una forma, dieci vestiti; un vestito, dieci forme.
- b: nero, perché quel materiale reagisce alla luce e luce non ce n'è.
- b: le misure CSS restano tue, il renderer mette solo i pixel dentro.
- b: taglia il lavoro della scheda grafica dove l'occhio non vede la differenza.
- b: a ogni fotogramma, perché niente si aggiorna da sé.
Sezione B, risposte modello
7. Canvas: il rettangolo dove si disegna. Scena: l'elenco di quello che esiste. Camera: il punto di vista, con angolo di campo e proporzione. Renderer: chi prende scena e camera e sporca i pixel. Mesh: l'oggetto, fatto di geometria (la forma) e materiale (come reagisce alla luce).
8. La risposta è personale e vale tre punti se contiene tre misure fatte davvero e una scelta motivata. Su un portatile tipico si vedono 60 fotogrammi al secondo a rapporto 1 e a 2, e un calo netto senza la riga su uno schermo ad alta densità: allora si tiene 2, che è pulito e non costa.
Valutazione (su 12)
- 11-12: ottimo. 8-10: sufficiente. < 8: rimonta la scena da zero senza guardare il codice.
Riassunto della lezione
Cosa hai imparato
- Three.js si carica con un import map, con la stessa versione sul cuore e sugli accessori.
- Cinque pezzi: canvas, scena, camera, renderer, mesh. Il mesh è geometria più materiale.
- Il giro ridisegna a ogni fotogramma, e
setAnimationLoopsi ferma da solo in secondo piano. setPixelRatiolimitato a 2 è la riga che più spesso salva la fluidità.setSize(l, h, false)lascia l'impaginazione al CSS.
Prossimi passi
Nella Lezione 5, Materiali, luci e texture smettiamo di guardare un cubo verde: luci che fanno volume, materiali che sembrano qualcosa, e il motivo per cui una scena appena montata sembra sempre finta.
Vuoi costruirlo sul tuo prodotto, con me accanto?
Porto questi percorsi in azienda e in presenza a Livorno: si arriva con il proprio caso e si esce con la pagina che gira.