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.
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.
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:
Utilizzeremo da ora in poi il MeshStandardMaterial, proprio perché include calcoli per come interagire con la luce e la fisica della scena.
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:
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.
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
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.
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.
Questo processo, però, è completamente automatico per noi; ci basterà abilitare due proprietà importanti nelle nostre geometrie:
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
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.
renderer.shadowMap.type = THREE.PCFSoftShadowMap
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.
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
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 ;)
Programmatore a tutto tondo, ha avuto esperienza dal mondo dello sviluppo di applicazioni 3D (videogames, virtual reality e augmented reality) al mondo dello sviluppo per il web.
Appassionato di tecnologia e informatica, dopo gli studi ha fatto molteplici esperienze internazionali per poi tornare nella sua amata Sicilia. Lavora principalmente con JavaScript/TypeScript, React.js/Next.js e Node.js lato web, Unity/C#, Unreal Engine e Three.js lato 3D.
Ha a cuore la condivisione delle skills e delle conoscenze, e gli piace dare una mano concreta nelle community.
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.