Three.js è una potente libreria per implementare WebGL (e quindi grafica 3D realtime) nel browser.
E’ indubbiamente la libreria con la community più attiva (Github, Stackoverflow, etc) ma molto spesso i problemi, e le loro risoluzioni, scalfiscono solo la superficie delle potenzialità di tale libreria in quanto la quasi totalità degli utilizzi si ferma ad un suo uso basilare.
Di seguito una raccolta, costantemente aggiornata, di spunti e riflessioni su argomenti non propriamente “di base” ma con cui sicuramente ci si può scontrare appena si entra nel vivo dell’utilizzo di Three.js
In questo articolo
Risolvere aberrazioni visive di Z-buffer dovute alla distanza delle camera
Il problema dello Z-buffer è uno dei più noti in ambito 3D in quanto produce delle aberrazioni visive notevoli in concomitanza con la distanza degli oggetti rispetto alla camera. Questo perché il Depth Buffer non riesce a visualizzare correttamente superfici che si sono in overlap (o quasi).
Per ovviare a questo problema, Three.js espone un parametro del renderer che imposta il Depth Buffer su scala logaritmica e che, se impostato a true (non di default), non mostra aberrazioni di sorta.
renderer.logarithmicDepthBuffer = true;
Qui si può trovare un approfondimento “matematico” al problema.
E’ da segnalare come l’abilitazione di questa opzione possa impattare negativamente sulle performance dell’antialias del renderer.
Ottimizzare le ombre di una scena statica
Qualora si debba lavorare con una scena statica (quindi senza elementi in scena che si muovono come modelli o le luci stesse), è possibile ottimizzare la scena generale disabilitando, dopo qualche istante, l’aggiornamento automatico delle ombre (della shadowMap). Questo farà in modo che nei primi istanti la shadowMap venga generata correttamente ma subito dopo non venga più ricalcolata. Eventualmente, se qualcosa in scena si potrà muovere, si potrà riabilitare e disabilitare tale funzione.
setTimeout(() => { this.renderer.shadowMap.autoUpdate = false; }, 500);
Caricare una immagine HDR ed assegnarla come envMap ad ogni materiale fisico in scena
Per scene statiche è possibile ottimizzare l’uso delle luci escludendo quelle dinamiche in scena (point, direction, etc) ed implementando un ambiente con immagine HDR (un particolare formato “raw” che include tutto lo spettro dei colori). Questa tecnica permette di illuminare gli oggetti in scena utilizzando le informazioni presenti in tale immagine. E’ necessario caricare (attraverso il componente aggiuntivo RGBELoader di Three.js) una immagine .hdr, mapparla come EquirectangularReflectionMapping e impostarla per il background e l’environment della scena, unitamente a specificarla in ogni mappa envMap degli oggetti Mesh presenti.
new THREE.RGBELoader().load('Contrastata.hdr', function (texture) {
texture.mapping = THREE.EquirectangularReflectionMapping;
mainScene.background = texture;
mainScene.environment = texture;
mainScene.traverse(function (obj) {
if (obj instanceof THREE.Mesh) {
try {
obj.material.envMap = texture;
obj.material.envMapIntensity = 1.0;
} catch{ console.log(obj.name + ": setting envMap ERROR"); }
}
});
});
Qui il download della immagine HDR dell’esempio.
Flippare le mappe dei materiali di un file GTLF/GLB sull’asse verticale (V)
Sembra essere una prerogativa del formato .gltf/.glb ma, qualora si volesse applicare una mappa ad un materiale di un file esportato in tale formato, è necessario invertire l’asse verticale della mappa UV (quindi sull’asse V).
Di seguito un modo semplice per flippare entrambi gli assi a piacimento.
let repeatX, repeatY = 1.0;
let flipU = false;
let flipV = true;
texture.center.set(0.5, 0.5);
texture.repeat.set(repeatX * (flipU ? -1 : 1), repeatY * (flipV ? -1 : 1));
Aggiungere un set di coordinate UV2 aggiuntivo ad una mesh
Può capitare di dover aggiungere un set aggiuntivo di coordinate ad una mesh, ad esempio per collocare una aoMap generata custom. Per fare questo l’unico modo plausibile è quello di duplicare il primo set (solitamente già presente) attraverso questa semplice associazione:
child.geometry.setAttribute('uv2', child.geometry.attributes.uv);
Gestire le coordinate UVs in un modello GLTF
Come da guida di Three.js, la mappa di Ambient Occlusion (aoMap) e quella delle luci (lightMap) necessita di un set di UV differente rispetto alle altre mappe dei materiali. Questo perché è logico pensare che il primo set riguardi la possibilità di ruotare, ripetere o scalare a piacimento l’aspetto estetico del materiale, mentre il secondo riguarda informazioni di mappa proveniente da un calcolo di scena che tendenzialmente è fisso (luci, ombre, occlusioni, etc).
Il formato GLTF, per quanto riguarda le UVS, considera questi assunti:
- Se è presente una sola UV: questa viene impostata sull’attributo geometry.attributes.uv (solitamente la Color Map)
- Se sono presenti due UVs: sull’attributo geometry.attributes.uv viene associata la Baked Map (es: aoMap), sull’attributo geometry.attributes.uv2 viene associata la Color Map
Come si può immaginare questo crea non pochi problemi in quanto la Color Map (la mappa principale di un materiale, presente praticamente sempre) si può trovare in due posizioni diverse in base ai casi (uv o uv2). Per questo motivo può essere necessario prevedere una modalità per invertire le mappature in modo da riportare la Color Map sempre sulla uv (e non uv2), cosi:
if (child.geometry.attributes.uv != null && child.geometry.attributes.uv2 != null) {
let tempUV = child.geometry.attributes.uv2;
child.geometry.setAttribute('uv2', child.geometry.attributes.uv);
child.geometry.setAttribute('uv', tempUV);
}
Ottimizzare il filtro anisotropico
Il filtro anisotropico applicato alle mappe dei modelli permette di ridurre la sfuocatura delle texture sulla distanza. Di base questo valore è 1, oggettivamente molto basso. In nostro aiuto c’è una funzione del renderer che ritorna il valore anisotropico massimo impostabile dalla scheda video. Tale valore è solitamente un multiplo di 2 (2, 4, 8, 16, etc). Un valore troppo alto però mostra le immagini in modo troppo nette producendo anch’esso (strano a dirsi) artefatti sulla distanza. Per questo motivo il consiglio è di dividere per 2 il valore ritornato dalla funzione del renderer.
texture.anisotropy = renderer.capabilities.getMaxAnisotropy() / 2;