Home/ Corsi/ Siti 3D con GSAP e WebGL/ Lezione 4: La prima scena

Lezione 4: La prima scena WebGL con Three.js

Si apre il canvas. Scena, camera, renderer, mesh e il giro di disegno: cinque pezzi e una pagina che gira davvero, scritta come dice la documentazione di Three.js e non come i tutorial vecchi.

La prima scena 3D in una pagina web con Three.js
Indice dei contenuti
Lezione 4 di 7 · WebGL Teoria + Laboratorio + Mini-test

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

  1. Copia scena.html e aprila con un server locale (python3 -m http.server basta e avanza). Deve girare un cubo verde.
  2. 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.
  3. 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.
  4. Cambia il materiale in MeshBasicMaterial: torna visibile senza luci, e diventa piatto come un adesivo. Adesso sai la differenza fra i due.
  5. Portala nel tuo riquadro: togli 100vw/100vh e mettila in un contenitore largo metà pagina. Ridimensiona la finestra e controlla che non si deformi.
  6. Misura: apri il pannello delle prestazioni del browser e guarda i fotogrammi al secondo con setPixelRatio a 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

  1. L'import map serve perché:
    a) comprime i file   b) nel browser import 'three' da solo non ha un indirizzo   c) è obbligatorio in HTML5   d) carica le texture
  2. 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
  3. Con MeshStandardMaterial e nessuna luce vedi:
    a) il colore pieno   b) nero   c) un errore   d) il fondo
  4. renderer.setSize(l, h, false): quel false:
    a) spegne l'antialias   b) dice di non toccare le misure CSS del canvas   c) disattiva il resize   d) non fa niente
  5. Math.min(window.devicePixelRatio, 2) serve a:
    a) migliorare i colori   b) non disegnare più pixel del necessario   c) centrare la scena   d) caricare prima
  6. 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

  1. (3 punti) Elenca i cinque pezzi di una scena e di' in una riga a che cosa serve ognuno.
  2. (3 punti) Riporta i tre numeri di fotogrammi al secondo misurati nel laboratorio e di' quale valore di setPixelRatio terresti sul tuo progetto, e perché.
Mostra risposte corrette

Sezione A

  1. b: i nomi corti li risolve un impacchettatore, e in una pagina statica non c'è.
  2. b: una forma, dieci vestiti; un vestito, dieci forme.
  3. b: nero, perché quel materiale reagisce alla luce e luce non ce n'è.
  4. b: le misure CSS restano tue, il renderer mette solo i pixel dentro.
  5. b: taglia il lavoro della scheda grafica dove l'occhio non vede la differenza.
  6. 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 setAnimationLoop si ferma da solo in secondo piano.
  • setPixelRatio limitato 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.

Vedi il workshop
Federico BoggiaRispondo io, in giornata

Ciao! Dimmi che ti serve: un corso online, un workshop di AI in aula a Livorno o la formazione per la tua azienda.

Scrivimi su WhatsApp