Three.js è un API utilizzata per creare ed esporre animazioni e modelli 3D su un browser.
Uno dei formati file supportati più usati per gli oggetti 3d in Three.js è glTF(GL Transmission Format), un buon authoring tool per farne uno è Blender.
WebGL 3D Model Viewer è il tool utilizzato per vedere i modelli 3D sul browser usando Three.js.
Il mio compito era quello di fare in modo che su un modello 3D si potessero vedere degli hotspot interagibili, in modo tale da far si che ogni hotspot possa avere funzioni diverse, come mandare ad un’link, far apparire un pop-up oppure avviare un audio.
Per fare questa task ho deciso di usare CSS2DRenderer che permette di combinare oggetti 3D con labels 2D fatti in HTML, CSS2DRenderer però è un addon, quindi da solo non funziona perciò ho dovuto installare prima il resto delle cose.
Ogni progetto in Three.js deve avere almeno una pagina HTML per definire la webpage ed un file javascipt per il codice del progetto che faranno da struttura base per il progetto.
Per iniziare ho installato Node cosi da poter fare un’applicazione server-side e per poter utilizzare il build tool, dopo ho installato Three.js ed il build tool Vite utilizzando il terminale con i seguenti comandi:
npm install –save three
npm install –save-dev vite
Poi ho usato il comando “npx vite” per ricevere un link che porta alla pagina con il progetto, che per ora è vuoto.
Il prossimo passaggio per poter visualizzare qualcosa con Three.js è creare una scena, una camera ed un renderer, in questo caso si farà un cubo che rotea, il codice qui sotto presenta come crearli:
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
Ci sono diversi tipi di camera, in questo caso si utilizza una Perspecive Camera, il primo attributo è il Field Of View che indica quanto si veda della scene sul display, il secondo attributo è aspect ratio che sarà sempre la width divisa per l’height, gli ultimi due attributi sono near e far che servono a limitare il rendering dell’oggetto, tutte le cose oltre far non vengono renderizzate e tutte le cose prima di near neanche.
Per il renderer oltre a crearne l’istanza bisogna anche configurarne la dimensione a cui deve renderizzare l’app, in questo caso prende le dimensione della pagina web, dopo ho aggiunto il renderer all’ file HTML così da poter vedere la scene.
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
camera.position.z = 5;
Per creare il cubo ho usato BoxGeometry, un Object che contiene tutti i vertici e facce del cubo, poi serve il materiale del cubo per dargli un colore, Three.js ha parecchi materiali, io ho utilizzato MashBasicMaterial egli ho dato l’attributo del colore verde, bisogna poi aggiungere un Mesh, un oggetto che prende una forma geometrica e ci applica il materiale, così da visualizzarlo e spostarlo a volere, infine si aggiunge il cubo alla scena e si sposta la camera così da non avere camera e cubo compenetrati insieme.
Per renderizzare la scena bisogna fare un render animation loop che ricarica la scena ogni volta che lo schermo si refresha, questa è la funzione usata:
function animate() {
renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );
Per animare il cubo basta usare le seguenti linee di codice:
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
Questo fa si che il cubo ruoti sia nell’asse x che nel asse y.
Dopo aver fatto ciò ho cambiato il cubo in un Icosaedro, quindi al posto di
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
ho fatto
const geometry = new THREE.IcosahedronGeometry( 1, 2);
ed ho cambiato il nome del cubo in “mesh” per darli un nome usabile con qualunque forma.
Dopo ho cambiato il materiale del oggetto con uno che funziona con le luci, quindi ho sostituito
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
con
const material = new THREE.MeshStandardMaterial( {
color: 0xffffff,
flatShading: true
} );
date che non si è ancora aggiunta un’illuminazione l’oggetto non sarà visibile su schermo, per aggiungere una luce ho usato il seguente codice:
const hemiLight= new THREE.HemisphereLight(0x228B22, 0xCCCC00);
scene.add(hemiLight);
I due attributi sono il colore della luce, il primo è la luce sopra ed il secondo quella sotto, dopodiché l’ho aggiunto alla scena.
Ho aggiunto anche un “Wire” in modo tale da evidenziare i vari lati dell’oggetto per poterlo vedere in modo più chiaro, per farlo ho usato il seguente codice:
const wireMat= new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true
})
const wireMesh= new THREE.Mesh(geometry, wireMat);
wireMesh.scale.setScalar(1.001);
mesh.add(wireMesh);
Ho creato un material ed un mesh di colore bianco, e l’ho leggermente scalato rendendolo più grande per non avere compenetrazioni e per vederlo in modo più chiaro, dopo l’ho aggiunto come figlio del mesh.
Dopo ho aggiunto un modo per poter controllare la forma in modo da girarla con il mouse, per farlo ho aggiunto l’addon di OrbitControls, importandolo con questo codice:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
Per implementarlo ho usato il seguente codice:
const controls= new OrbitControls(camera, renderer.domElement);
controls.enableDamping=true;
controls.dampingFactor=0.03;
il damping serve a dare un senso di peso al movimento dell’oggetto così che sembri più fluido/naturale.
Infine ho cambiato l’animazione dell’oggetto in questo modo:
function animate() {
requestAnimationFrame(animate);
mesh.rotation.y += 0.001;
renderer.render( scene, camera );
controls.update();
}
animate();
Ho prima di tutto fatto in modo che l’animazione si ripetesse dentro alla funzione, poi ho rimosso la rotazione dell’asse x e ho reso la rotazione dell’asse y leggermente più lenta, ed infino ho fatto in modo che i controlli si aggiornino ogni volta.
Dopo dovevo aggiungerci degli hotspot sulla forma che facessero diversi tipi di azioni, per fare ciò come ho detto in precedenza bisogna importare gli addon di CSS2DRenderer così da poter visualizzare testi in modo dinamico, qui sotto è indicato il setup:
import {CSS2DRenderer, CSS2DObject} from 'three/addons/renderers/CSS2DRenderer';
//setup
const labelRenderer= new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight); labelRenderer.domElement.style.position='absolute';
labelRenderer.domElement.style.top='0px'; labelRenderer.domElement.style.pointerEvents='none'; document.body.appendChild(labelRenderer.domElement);
const group= new THREE.Group();
PointerEvents=’none’ serve a far in modo che il container non catturi eventi del mouse così da funzionare con Orbit controls.
Group servirà a raggruppare insieme tutti i label creati, per crearli però serve una funzione:
function createCpointMesh(name, x, y , z){
const geo= new THREE.SphereGeometry(0.1);
const mat= new THREE.MeshBasicMaterial({color:0xFF0000});
const mesh= new THREE.Mesh(geo, mat);
mesh.position.set(x,y,z);
mesh.name=name;
return mesh;
}
const sphereMesh1 =createCpointMesh('sphereMesh1', 1, 0.6, 1.646);
const sphereMesh2 =createCpointMesh('sphereMesh2', -1.4, 0.6, -1.3);
group.add(sphereMesh1);
group.add(sphereMesh2);
mesh.add(group);
Nella funzione bisogna inserire il nome e la posizione(coordinate) del label, dopo aver creato la funzione l’ho usata per creare due labels che poi ho aggiunto a group per poi aggiungere group come figlio di mesh.
Nella funzione animate bisogna anche aggiungere la seguente linea di codice per aggiornare il render dei label:
labelRenderer.setSize(this.window.innerWidth, this.window.innerHeight);
Il seguente event listener aggiorna la dimensione dello schermo per adattarsi alla dimensione della pagina:
window.addEventListener('resize', function(){
camera.aspect=this.window.innerWidth/this.window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.setSize(this.window.innerWidth, this.window.innerHeight);
})
Ora dovevo far si che i labels fossero visibili come dei pop-up, ho scelto i pop-up ma si possono fare diversi eventi non solo questo, per renderlo visibile ho fatto così:
const p=document.createElement('p');
p.className='tooltip';
const pContainer=document.createElement('div')
pContainer.appendChild(p);
const cPointLabel= new CSS2DObject(pContainer);
mesh.add(cPointLabel);
const mousePos= new THREE.Vector2();
const raycaster= new THREE.Raycaster();
window.addEventListener('mousemove', function(e){
mousePos.x=(e.clientX/this.window.innerWidth)*2-1;
mousePos.y=-(e.clientY/this.window.innerWidth)*2+1;
raycaster.setFromCamera(mousePos, camera);
const intersects=raycaster.intersectObject(group);
if (intersects.length>0) {
switch (intersects[0].object.name) {
case 'sphereMesh1':
p.className= 'tooltip show';
cPointLabel.position.set(1, 0.6, 1.646)
p.textContent= 'Hotspot 1 (1, 0.6, 1.646)'
break;
case 'sphereMesh2':
p.className= 'tooltip show';
cPointLabel.position.set(-1.4, 0.6, -1.3)
p.textContent= 'Hotspot 2 (-1.4, 0.6, -1.3)'
break;
default:
break;
}
}
else{
p.className = 'tooltip hide'
}
})
Ciò permette di conoscere sempre la posizione del mouse e di usare le funzione tooltip show quando il mouse è sopra alle coordinate prestabilite.
Poi per rendere i label più belli da vedere e per dargli un effetto di “fading” ho creato un file CSS (che poi ho linkato all’HTML) in cui ho messo i seguenti comandi:
body {
margin: 0;
}
.tooltip{
background-color: white;
color: black;
padding: 10px;
position: relative;
transform: translateY(-10px);
opacity: 0;
transition-duration: 2s;
transition-property: opacity, transform;
}
.tooltip::after{
position: absolute;
content: '';
width: 20px;
height: 20px;
background-color: white;
top: 90%;
left: 50%;
transform: rotateZ(45deg) translateX(-50%);
z-index: -1;
}
.hide{
opacity: 0;
transform: translateY(-10px);
}
.show{
opacity: 1;
transform: translateY(0px);
}
Per aggiungere una texture alla forma basta installare un immagine e metterlo in una cartella, dopo bisogna scrivere:
const texture = new THREE.TextureLoader().load("src/Img/terra.jpg")
(“src/Img/terra.jpg”) è il path dell’immagine che poi verrà applicata all’oggetto cambiando il material così:
const material = new THREE.MeshStandardMaterial( {
map: texture,
side: THREE.DoubleSide,
//flatShading: true
} );
Ho commentato flatShading per dar l’impressione che l’oggetto sia sferico, ho anche aumentato il numero di triangoli utilizzati a 10 nell’icosahedron, poi ho cambiato i colori della luce a bianco e nero per avere una luce più naturale.