marzo 5, 2021 · Javascript Three.js

Primi passi con Three.js

Three.js è una libreria Javascript che fa da wrapper attorno a WebGL, a sua volta una libreria per lo sviluppo di applicazioni grafiche 3D nel browser. Facciamo un esperimento.

Non è una gif animata! Si può trascinare con il mouse, o zoomare con la rotella del mouse stesso. Cool, eh?

Per prima cosa bisogna creare una nuova cartella. Dentro questa cartella creiamo un file index.html rappresentante una pagina vuota contenente semplicemente un canvas (su cui poi andremo a disegnare).

<!DOCTYPE html>
<html>
<head>
  <meta charset='utf-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <title>Orbits</title>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <link rel='stylesheet' type='text/css' media='screen' href='main.css'>
  <script type="module" src='main.js'></script>
</head>
<body>
  <canvas id="c"></canvas>
</body>
</html>

Nel file html facciamo riferimento a un CSS; ci servirà semplicemente per eliminare i margini e riempire l'intera pagina con il canvas. Creiamolo (main.css):

html, body {
  margin: 0;
  height: 100%;
}
#c {
  width: 100%;
  height: 100%;
  display: block;
}

Prima di passare oltre dobbiamo scaricare three,js. Una volta scaricato l'archivio compresso, possiamo prendere le cartelle build e examples/jsm e copiarle nella root del nostro progetto. Per concludere, creiamo il file javascript vuoto main.js. Ora il nostro progetto dovrebbe avere questo aspetto:

Adesso possiamo lavorare sul file main.js. Per prima cosa importiamo three.js (e orbitcontrols, che ci servirà per muovere la scena con il mouse):

import * as THREE from './build/three.module.js'
import { OrbitControls } from './examples/jsm/controls/OrbitControls.js'

Creiamo una telecamera, che è l'oggetto virtuale che pilota la rappresentazione su schermo della scena 3D. Usiamo una telecamera prospettica (una buona rappresentazione di come l'occhio umano percepisce una scena) con un campo visivo verticale di 45 gradi, un rapporto altezza/larghezza di 1, una profondità minima di 1 e massima di 100000. La mettiamo in una posizione iniziale spostata dall'origine di 50 unità lungo l'asse X, e la facciamo puntare verso l'origine delle coordinate (dove andremo a mettere il nostro pianeta).

var camera = new THREE.PerspectiveCamera(45, 1, 1, 100000);
camera.position.x = 50;
camera.lookAt(0,0,0)

Creiamo il renderer sul canvas html:

const canvas = document.querySelector('#c');
var renderer = new THREE.WebGLRenderer({canvas});

Già che ci siamo possiamo inizializzare l'OrbitController. Si tratta di un controller che reagisce agli input del mouse e sposta la telecamera; lo dobbiamo solo inizializzare, e poi ce ne possiamo dimenticare:

const controls = new OrbitControls(camera, canvas);
controls.target.set(0, 0, 0);
controls.update();

Creiamo la scena; la scena è il contenitore di più alto livello, che a sua volta contiene tutti gli oggetti che vorremo mostrare.

var scene = new THREE.Scene();

Aggiungiamo alla scena delle luci (altrimenti non si vedrebbe nulla). Per dare un buon effetto prospettico, tipicamente si una sua una luce globale (omnidirezionale) sia una luce direzionale. La luce direzionale, ovviamente, ha una posizione (che rappresenta la direzione da cui proviene la luce).

// Luce ambientale
var light = new THREE.AmbientLight( 0x888888 )
scene.add(light)

// Luce direzionale
var light = new THREE.DirectionalLight( 0xfdfcf0, 1 )
light.position.set(20,10,20)
scene.add(light)

A questo punto abbiamo il pezzo forte: aggiungiamo il nostro pianeta. Come tutti gli oggetti fisici, è composto da tre componenti:

Per creare la Terra, creiamo una sfera (c'è una geometria preconfezionata ad hoc) di raggio 10 e composta da 50 segmenti in verticale e 50 in orizzontale (sono parecchi, ma almeno assomiglia a una sfera anche da vicino). Come materiale carichiamo una texture, ovvero una immagine che verrà "spalmata" sulla superficie. Cerchiamone una qualunque (ad esempio qui), salviamola nella root del progetto e richiamiamola nel codice di seguito. Creiamo poi la mesh (unendo geometria e materiale) e aggiungiamola alla scena.

let earthGeometry = new THREE.SphereGeometry(10, 50, 50);

let earthMaterial = new THREE.MeshPhongMaterial({
  map: new THREE.ImageUtils.loadTexture("./2k_earth_daymap.jpg"),
  color: 0xaaaaaa,
  specular: 0x333333,
  shininess: 5
});

let earthMesh = new THREE.Mesh(earthGeometry, earthMaterial);

scene.add(earthMesh);

A questo punto non ci resta che renderizzare la scena: ordinare periodicamente al renderer di disegnarla sul canvas. Prima di renderizzare, però dobbiamo considerare una cosa: le proporzioni (larghezza/altezza) della nostra telecamera sono state impostate con un valore fisso 1:1, ma il browser ha un rapporto libero (dipende dalla finestra, e dalla forma del monitor). Rischiamo di trovarci una immagine deformata. Creiamo quindi una piccola funzione di supporto che verrà richiamata nel ciclo di rendering e si occuperò di verificare se le dimensioni del canvas sono congruenti, e se non lo sono modificherà il rapporto della camera:

function resizeRendererToDisplaySizeIfNeeded(renderer) {
  
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  
  if (needResize) {
    renderer.setSize(width, height, false);
    const canvas = renderer.domElement;
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();    
  }

}

Per concludere creiamo una funzione che effettua il rendering e poi richiama se stessa, in un ciclo infinito; per richiamare se stessa usa la funzione del browser "requestanimationframe", che viene usata per dettare i tempi delle animazioni (è il browser che decide a che frequenza chiamarla).

E, visto che ci siamo, ad ogni rendering aggiungiamo un pochino di rotazione della Terra lungo l'asse Y, tanto per ravvivare la scena.

var render = function (actions) {

  // Rotate earth
  earthMesh.rotation.y += 2 * Math.PI / 1000;
    
  resizeRendererToDisplaySizeIfNeeded(renderer);
  renderer.render(scene, camera);
  requestAnimationFrame( render );
  
};

render(); // Prima chiamata

Non ci resta che provare. Per far si che tutto funzioni, però, non è sufficiente aprire il file html con un browser, è necessario che venga servito da un server web. Si può fare in diversi modi, ma se abbiamo NodeJS installato nel nostro sistema possiamo richiamare una utility fatta apposta, e che non richiede installazione: npx serve.

Se tutto è andato per il verso giusto, puntando il browser a localhost:5000 dovremmo trovarci davanti una animazione simile a quella visibile in cima alla pagina. Congratulazioni!