Come personalizzare una scena 3D con luci, ombre e altro con ThreeJS (4/4)

Ci siamo lasciati nel precedente articolo Come inserire oggetti 3D sul browser con cubi e simpatiche paperelle sullo schermo dopo aver visto come è composta una mesh e come applicare texture, e dunque materials, sulle nostre geometrie.

Eppure, se ricordate, ci siamo limitati ad usare un solo material: il MeshBasicMaterial. In realtà Three.js ne ha a disposizione diversi che si comportano in maniera differente l’uno dall’altro. Vediamo quali sono i più popolari e quando usarli.

 

Materials

I materials sono usati per inserire colore in ogni parte visibile della geometria e gli algoritmi che calcolano questa funzionalità sono implementati nei programmi chiamati shaders. Noi, per fortuna, non abbiamo bisogno di scrivere questi programmi poiché possiamo usare quelli già build-in in Three.js e, in caso di esigenze particolari l’ampia community sarà pronta ad aiutarci.

 

image5.png

 

In Three.js possiamo quindi applicare diversi tipi di materials e ognuno ha diverse proprietà. Per approfondire tutte le caratteristiche si può consultare la documentazione ma i più importanti comunque sono:

  • MeshBasicMaterial: il materiale più basico da applicare alle geometrie, senza interferenze di luci o altro. Proprietà importanti:
    • Map: applica la texture sulla superficie della geometria;
    • Color: applica un colore uniformemente alla superficie;
    • Wireframe: mostra i triangoli che compongono la geometria;
    • Opacity: controlla la trasparenza, ma serve mettere trasparent: true;
    • Side: imposta come visibile una o entrambe le facce della geometria (THREE.DoubleSide)
  • MeshNormalMaterial: material che contiene informazioni sulla direzione esterna della faccia;
  • MeshLambertMaterial: material che reagisce alla luce (servono delle luci in scena per vederlo);
  • MeshToonMaterial: aggiunge un effetto cartoon alla mesh;
  • MeshStandardMaterial: il material più utilizzato, physics based, ed è quello usato in tutti gli engine 3D dato che mette a disposizione numerosi parametri per la personalizzazione. Supporta le luci ma con algoritmi più realistici. Inoltre, supporta anche metalness e roughtness.

Copertine eventi sito (2).png

Utilizzeremo da ora in poi il MeshStandardMaterial, proprio perché include calcoli per come interagire con la luce e la fisica della scena.

 

Luci

In Three.js, le luci sono oggetti (che ereditano da Object3D Transform) utilizzati per simulare l'illuminazione di una scena tridimensionale. Consentono di aggiungere effetti di luce realistici e influenzano l'aspetto degli oggetti renderizzati. Ci sono diversi tipi di luci disponibili in Three.js:

  • Ambient Light: applica luce omnidirezionale alla scena (i raggi provengono da ogni punto e colpiscono in maniera uniforme tutti gli oggetti), si può personalizzare colore e intensità;
  • Directional Light: in questo effetto, i raggi viaggiano in parallelo alla scena;
  • Point Light: piccola fonte di luce che si diffonde uniformemente in tutte le direzioni, come un accendino;
  • Spot Light: fonte di luce direzionale, come un faretto o una torcia elettrica;

Le luci in Three.js possono essere posizionate, orientate e configurate con parametri come il colore, l'intensità e l'attenuazione. Possono essere aggiunte alla scena e quindi influiscono sul modo in cui gli oggetti sono visualizzati e interagiscono con l'illuminazione virtuale.

 

image8.png

 

Vediamo come implementare una scena con un cubo avente il MeshStandardMaterial e delle luci in scena con il codice:

 

1import * as THREE from "three";
2import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
3import BaseColor from "./textures/gravel/Gravel_001_BaseColor.jpg";
4import Normal from "./textures/gravel/Gravel_001_Normal.jpg";
5import AmbientOcclusion from "./textures/gravel/Gravel_001_AmbientOcclusion.jpg";
6import Height from "./textures/gravel/Gravel_001_Height.png";
7import Roughness from "./textures/gravel/Gravel_001_Roughness.jpg";
8
9const threeCanvas = document.querySelector("#threeCanvas");
10const scene = new THREE.Scene();
11
12// Texture Loader
13const loadingManager = new THREE.LoadingManager();
14loadingManager.onLoad = () => console.log("loaded all textures");
15const textureLoader = new THREE.TextureLoader(loadingManager);
16const baseColorTexture = textureLoader.load(BaseColor);
17const normalTexture = textureLoader.load(Normal);
18const ambientOcclusionTexture = textureLoader.load(AmbientOcclusion);
19const displacementTexture = textureLoader.load(Height);
20const roughnessTexture = textureLoader.load(Roughness);
21
22// Lights
23const ambientLight = new THREE.AmbientLight("white", 1);
24scene.add(ambientLight);
25const pointLight = new THREE.PointLight("white", 0.5);
26pointLight.position.set(2, 3, 4);
27scene.add(pointLight);
28
29// Box
30const geometry = new THREE.BoxGeometry(5, 5, 5, 100, 100, 100);
31const material = new THREE.MeshStandardMaterial({
32  map: baseColorTexture,
33  aoMap: ambientOcclusionTexture,
34  aoMapIntensity: 1,
35  displacementMap: displacementTexture,
36  displacementScale: 0.05,
37  roughnessMap: roughnessTexture,
38  normalMap: normalTexture,
39  normalScale: { x: 10, y: 10 },
40});
41
42const box = new THREE.Mesh(geometry, material);
43scene.add(box);
44box.position.set(0, 0, 0);
45box.rotation.set(0, 0, 0);
46
47// Camera
48const camera = new THREE.PerspectiveCamera();
49camera.position.setZ(10); //move camera backward
50const controls = new OrbitControls(camera, threeCanvas);
51controls.target = box.position; //change center of orbit
52controls.enableDamping = true;
53
54// Renderer
55const renderer = new THREE.WebGLRenderer({
56  canvas: threeCanvas,
57});
58renderer.setSize(window.innerWidth, window.innerHeight); //take all screen
59renderer.setPixelRatio(
60  window.devicePixelRatio > 2 ? 2 : window.devicePixelRatio
61); //match device's pixel ratio or limit to 2
62camera.aspect = window.innerWidth / window.innerHeight;
63camera.updateProjectionMatrix(); //to call for every aspect camera change
64
65// Resize Render
66window.addEventListener("resize", () => {
67  renderer.setSize(window.innerWidth, window.innerHeight);
68  camera.aspect = window.innerWidth / window.innerHeight;
69  camera.updateProjectionMatrix(); //to call for every aspect camera change
70});
71
72window.addEventListener("dblclick", () => {
73  if (!document.fullscreenElement) threeCanvas.requestFullscreen();
74  else document.exitFullscreen();
75});
76
77const settings = {
78  pauseRotation: false,
79};
80gui.add(settings, "pauseRotation");
81
82const update = () => {
83  if (!settings.pauseRotation) {
84    box.rotation.y += 0.005;
85    box.rotation.z += 0.005;
86  }
87
88  controls.update(); //update controls
89  renderer.render(scene, camera);
90  requestAnimationFrame(update); //call next frame
91};
92
93update(); //call first frame

 

image10.png

 

Performance Warning

Le luci possono modificare radicalmente l’aspetto della nostra scena, ma attenzione a come le usiamo perché richiedono molte risorse e le performance potrebbero risentirne. Meglio, quindi, inserire poche luci.

Ovviamente quelle più grandi che agiscono su aree molto grandi sono quelle che consumano di più. Una tecnica per ottimizzare è il baking, utilizzata per disegnare luci e ombre nelle texture stesse tramite software di modellazione 3D.

In maniera parallela esiste anche la tecnica del lightmapping, dove invece di imprimere l’ombra sulla texture del singolo oggetto, è l’engine che si occupa di generare texture di ombre per l’intera scena.

 

image6.png

 

Ombre

Fino ad ora abbiamo simulato le ombre disegnando direttamente nelle texure degli oggetti. Per creare un effetto realistico abbiamo bisogno però di ombre real-time che abbiano l’abilità di disegnare ulteriori ombre sugli oggetti quando sono investiti dalle luci.

Le ombre real-time nel processo di rendering 3D sono sempre state difficili da realizzare ad un ragionevole framerate ma Three.js ha una soluzione built-in. Internamente l’approccio di Three.js è fare più rendering per ogni luce, rimpiazzando temporaneamente il material dei nostri oggetti sulla scena con un MeshDepthMaterial. Il risultato dei rendering viene salvato in delle texture chiamate shadow maps, utilizzate quindi in ogni materiale che deve ricevere le ombre.

 

image9.png

 

Questo processo, però, è completamente automatico per noi; ci basterà abilitare due proprietà importanti nelle nostre geometrie:

  • cast shadow: l’oggetto può emanare ombre;
  • receive shadow: l’oggetto può ricevere ombre da oggetti con il cashShadow attivo.

     

1import * as THREE from "three";
2import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
3
4const threeCanvas = document.querySelector("#threeCanvas");
5const scene = new THREE.Scene();
6
7// Meshes
8const plane = new THREE.Mesh(
9  new THREE.PlaneGeometry(20, 20),
10  new THREE.MeshStandardMaterial({ side: THREE.DoubleSide })
11);
12plane.position.set(0, -5, 0);
13plane.rotation.set(8, 0, 0);
14plane.receiveShadow = true;
15scene.add(plane);
16
17const box = new THREE.Mesh(
18  new THREE.BoxGeometry(5, 5, 5),
19  new THREE.MeshStandardMaterial()
20);
21box.position.set(3, 0, 0);
22box.receiveShadow = true;
23box.castShadow = true;
24scene.add(box);
25
26const sphere = new THREE.Mesh(
27  new THREE.SphereGeometry(2),
28  new THREE.MeshStandardMaterial()
29);
30sphere.position.set(-3, 0, 0);
31sphere.receiveShadow = true;
32sphere.castShadow = true;
33scene.add(sphere);
34
35//Lights
36const ambientLight = new THREE.AmbientLight("white", 0.5);
37
38const directionalLight = new THREE.DirectionalLight("yellow", 0.1);
39directionalLight.position.set(1, 1, 0); //orientation of the sun
40
41const pointLight = new THREE.PointLight("white", 0.5);
42pointLight.castShadow = true;
43
44scene.add(
45  ambientLight,
46  directionalLight,
47  pointLight,
48);
49
50// Camera
51const camera = new THREE.PerspectiveCamera();
52camera.position.setZ(30); //move camera backward
53const controls = new OrbitControls(camera, threeCanvas);
54controls.enableDamping = true;
55
56// Renderer
57const renderer = new THREE.WebGLRenderer({
58  canvas: threeCanvas,
59});
60renderer.setSize(window.innerWidth, window.innerHeight); //take all screen
61renderer.setPixelRatio(
62  window.devicePixelRatio > 2 ? 2 : window.devicePixelRatio
63); //match device's pixel ratio or limit to 2
64renderer.shadowMap.enabled = true;
65camera.aspect = window.innerWidth / window.innerHeight;
66camera.updateProjectionMatrix(); //to call for every aspect camera change
67
68// Resize Render
69window.addEventListener("resize", () => {
70  renderer.setSize(window.innerWidth, window.innerHeight);
71  camera.aspect = window.innerWidth / window.innerHeight;
72  camera.updateProjectionMatrix(); //to call for every aspect camera change
73});
74
75window.addEventListener("dblclick", () => {
76  if (!document.fullscreenElement) threeCanvas.requestFullscreen();
77  else document.exitFullscreen();
78});
79
80const update = () => {
81  controls.update(); //update controls
82  renderer.render(scene, camera);
83  requestAnimationFrame(update); //call next frame
84};
85
86update(); //call first frame

 

image1.png

 

Performance Warning 2 - La vendetta

Ma bisogna fare attenzione nuovamente. Anche le ombre sono delle entità a cui piace fare scorpacciata di CPU, GPU e RAM della nostra macchina. Possiamo, però, applicare alcune alcune tecniche di ottimizzazione delle ombre.

  • La prima cosa da controllare per provare ad ottimizzare è la shadowMap resolution, ovvero la risoluzione della texture che si viene a creare per renderizzare le ombre. Di default è 512x512;
  • Possiamo inserire gli Algoritmi di Shadow Maps: ce ne sono diversi per il calcolo delle shadow maps:
    • THREE.BasicShadowMap: veloce nel calcolo, ma perde qualità;
    • THREE.PCFShadowMap: meno veloce ma angoli più smussati (default);
    • THREE.PCFSoftShadowMap: ancora meno veloce ma angoli ancora più smussati;
    • THREE.VSMShadowMap: meno performante, più limiti ma con risultati inaspettati; Per cambiare algoritmo basta settare renderer.shadowMap.type = THREE.PCFSoftShadowMap
  • Infine il baking, ne abbiamo parlato prima nelle luci, si tratta semplicemente di disattivare tutte le ombre real-time mettendo a false tutti i castoshadow e i receiveshadow e caricare delle texture che hanno già delle ombre applicate con un software esterno come Blender. Ovviamente il problema è che questa soluzione funziona solo con oggetti statici, perché l’ombra non è real-time.

 

image7.png

 

Particellari

Esploriamo, infine, l’ultima entità importante nel processo di creazione di ambienti fotorealistici introducendo i particellari.

I particellari sono degli importanti effetti visuali che ci permettono di simulare entità come stelle, fumo, pioggia, polvere e tanto altro.

 

image2 (1).png

 

La particolarità dei particellari è che possiamo avere migliaia di istanze sullo schermo con un framerate ragionevole.

Ogni particellare è composto da un piano ruotato sempre verso la camera. Crearli è come creare una mesh (geometria + material), ma invece come risultato creiamo dei punti.

 

1import * as THREE from "three";
2import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
3
4const threeCanvas = document.querySelector("#threeCanvas");
5const scene = new THREE.Scene();
6
7// Particles
8const particlesGeometry = new THREE.SphereGeometry(1);
9const particlesMaterial = new THREE.PointsMaterial({
10  size: 0.02,
11  sizeAttenuation: true, //if particle is far from camera it become small
12});
13const particles = new THREE.Points(particlesGeometry, particlesMaterial);
14scene.add(particles);
15
16// Camera
17const camera = new THREE.PerspectiveCamera();
18camera.position.setZ(5); //move camera backward
19const controls = new OrbitControls(camera, threeCanvas);
20controls.enableDamping = true;
21
22// Renderer
23const renderer = new THREE.WebGLRenderer({
24  canvas: threeCanvas,
25});
26renderer.setSize(window.innerWidth, window.innerHeight); //take all screen
27renderer.setPixelRatio(
28  window.devicePixelRatio > 2 ? 2 : window.devicePixelRatio
29); //match device's pixel ratio or limit to 2
30camera.aspect = window.innerWidth / window.innerHeight;
31camera.updateProjectionMatrix(); //to call for every aspect camera change
32
33// Resize Render
34window.addEventListener("resize", () => {
35  renderer.setSize(window.innerWidth, window.innerHeight);
36  camera.aspect = window.innerWidth / window.innerHeight;
37  camera.updateProjectionMatrix(); //to call for every aspect camera change
38});
39
40window.addEventListener("dblclick", () => {
41  if (!document.fullscreenElement) threeCanvas.requestFullscreen();
42  else document.exitFullscreen();
43});
44
45const update = () => {
46  controls.update(); //update controls
47  renderer.render(scene, camera);
48  requestAnimationFrame(update); //call next frame
49};
50
51update(); //call first frame

 

Conclusione

Three.js offre una vasta gamma di funzionalità per personalizzare e arricchire le scene 3D. Abbiamo visto come utilizzare luci, ombre e particellari e quindi come creare esperienze immersive e realistiche per gli utenti.

Quattro articoli di sicuro non possono essere esaustivi ma speriamo di aver dato un’idea sulle possibilità offerte da questa libreria e che magari, ispirino per approfondire e sperimentare ancora col 3D sul browser.

Finisce quindi la nostra serie su Three.js, senza però escludere che ci possa essere anche un quinto articolo "straordinario" ma prima di lasciarvi una raccomandazione: anche se Three.js vi dà il potere di creare mondi virtuali incredibili, non dimenticate di apprezzare anche il mondo reale che ci circonda ;).

 

PS. È un argomento interessante per la tua azienda? Dai un'occhiata alla sezione Formazione ;)

Contattaci.

Se sei interessato a migliorare le competenze del tuo team (anche su ThreeJS) dai un’occhiata ai nostri corsi per aziende.

Anche se - semplicemente - vuoi prendere un caffè con noi o vedere la nostra collezione di Action Figures scrivici tramite questo form.

Questo sito è protetto da reCAPTCHA e si applicano le Norme sulla privacy e i Termini di servizio di Google.

Ultimi Articoli