La transparence dans three.js est à la fois facile et difficile.
Nous allons d'abord aborder la partie facile. Créons une scène avec 8 cubes placés sur une grille 2x2x2.
Nous allons commencer par l'exemple de
l'article sur le rendu à la demande
qui contenait 3 cubes et le modifier pour en avoir 8. Modifions d'abord notre
fonction makeInstance
pour qu'elle prenne
x, y et z
-function makeInstance(geometry, color) { +function makeInstance(geometry, color, x, y, z) { const material = new THREE.MeshPhongMaterial({color}); const cube = new THREE.Mesh(geometry, material); scene.add(cube); - cube.position.x = x; + cube.position.set(x, y, z); return cube; }
Ensuite, nous pouvons créer 8 cubes.
+function hsl(h, s, l) { + return (new THREE.Color()).setHSL(h, s, l); +} -makeInstance(geometry, 0x44aa88, 0); -makeInstance(geometry, 0x8844aa, -2); -makeInstance(geometry, 0xaa8844, 2); +{ + const d = 0.8; + makeInstance(geometry, hsl(0 / 8, 1, .5), -d, -d, -d); + makeInstance(geometry, hsl(1 / 8, 1, .5), d, -d, -d); + makeInstance(geometry, hsl(2 / 8, 1, .5), -d, d, -d); + makeInstance(geometry, hsl(3 / 8, 1, .5), d, d, -d); + makeInstance(geometry, hsl(4 / 8, 1, .5), -d, -d, d); + makeInstance(geometry, hsl(5 / 8, 1, .5), d, -d, d); + makeInstance(geometry, hsl(6 / 8, 1, .5), -d, d, d); + makeInstance(geometry, hsl(7 / 8, 1, .5), d, d, d); +}
J'ai aussi ajusté la caméra.
const fov = 75; const aspect = 2; // the canvas default const near = 0.1; -const far = 5; +const far = 25; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); -camera.position.z = 4; +camera.position.z = 2;
Définissez le fond en blanc.
const scene = new THREE.Scene(); +scene.background = new THREE.Color('white');
Et ajouté une deuxième lumière pour que toutes les faces des cubes reçoivent de l'éclairage.
-{ +function addLight(...pos) { const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); - light.position.set(-1, 2, 4); + light.position.set(...pos); scene.add(light); } +addLight(-1, 2, 4); +addLight( 1, -1, -2);
Pour rendre les cubes transparents, il suffit de définir le
drapeau transparent
et de définir un
niveau d'opacity
, 1 étant complètement opaque
et 0 étant complètement transparent.
function makeInstance(geometry, color, x, y, z) { - const material = new THREE.MeshPhongMaterial({color}); + const material = new THREE.MeshPhongMaterial({ + color, + opacity: 0.5, + transparent: true, + }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cube.position.set(x, y, z); return cube; }
et avec cela, nous obtenons 8 cubes transparents.
Faites glisser sur l'exemple pour faire pivoter la vue.
Cela semble donc facile mais... regardez de plus près. Les cubes n'ont pas de faces arrière.
Nous avons découvert la propriété de matériau side
dans
l'article sur les matériaux.
Définissons-la donc sur THREE.DoubleSide
pour que les deux faces de chaque cube soient dessinées.
const material = new THREE.MeshPhongMaterial({ color, map: loader.load(url), opacity: 0.5, transparent: true, + side: THREE.DoubleSide, });
Et nous obtenons
Faites-le tourner. Cela semble fonctionner car nous pouvons voir les faces arrière, sauf qu'en y regardant de plus près, parfois ce n'est pas le cas.
Cela se produit en raison de la manière dont les objets 3D sont généralement dessinés. Pour chaque géométrie, chaque triangle est dessiné un par un. Lorsque chaque pixel du triangle est dessiné, 2 choses sont enregistrées. Premièrement, la couleur de ce pixel, et deuxièmement, la profondeur de ce pixel. Lorsque le triangle suivant est dessiné, pour chaque pixel, si la profondeur est plus importante que la profondeur précédemment enregistrée, aucun pixel n'est dessiné.
Cela fonctionne très bien pour les objets opaques, mais échoue pour les objets transparents.
La solution consiste à trier les objets transparents et à dessiner ceux situés à l'arrière avant
de dessiner ceux à l'avant. THREE.js le fait pour les objets comme les Mesh
,
sinon le tout premier exemple aurait échoué entre les cubes, certains cubes bloquant les autres.
Malheureusement, pour les triangles individuels, le tri serait extrêmement lent.
Le cube a 12 triangles, 2 pour chaque face, et l'ordre dans lequel ils sont dessinés est le même que celui dans lequel ils sont construits dans la géométrie. Ainsi, selon la direction dans laquelle nous regardons, les triangles les plus proches de la caméra peuvent être dessinés en premier. Dans ce cas, les triangles à l'arrière ne sont pas dessinés. C'est pourquoi parfois nous ne voyons pas les faces arrière.
Pour un objet convexe comme une sphère ou un cube, une sorte de solution consiste à ajouter chaque cube à la scène deux fois. Une fois avec un matériau qui dessine uniquement les triangles orientés vers l'arrière, et une autre fois avec un matériau qui dessine uniquement les triangles orientés vers l'avant.
function makeInstance(geometry, color, x, y, z) { + [THREE.BackSide, THREE.FrontSide].forEach((side) => { const material = new THREE.MeshPhongMaterial({ color, opacity: 0.5, transparent: true, + side, }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cube.position.set(x, y, z); + }); }
Et avec cela, cela semble fonctionner.
Cela suppose que le tri de three.js est stable. C'est-à-dire que parce que nous
avons ajouté le mesh side: THREE.BackSide
en premier et parce qu'il est exactement à la même
position, il sera dessiné avant le mesh side: THREE.FrontSide
.
Créons 2 plans qui se croisent (après avoir supprimé tout le code relatif aux cubes). Nous allons ajouter une texture à chaque plan.
const planeWidth = 1; const planeHeight = 1; const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight); const loader = new THREE.TextureLoader(); function makeInstance(geometry, color, rotY, url) { const texture = loader.load(url, render); const material = new THREE.MeshPhongMaterial({ color, map: texture, opacity: 0.5, transparent: true, side: THREE.DoubleSide, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); mesh.rotation.y = rotY; } makeInstance(geometry, 'pink', 0, 'resources/images/happyface.png'); makeInstance(geometry, 'lightblue', Math.PI * 0.5, 'resources/images/hmmmface.png');
Cette fois, nous pouvons utiliser side: THREE.DoubleSide
car nous ne pouvons jamais voir qu'une
seule face d'un plan à la fois. Notez également que nous passons notre fonction render
à la fonction de chargement de texture
afin que lorsque la texture a fini de charger, nous rendions à nouveau la scène.
C'est parce que cet exemple effectue un rendu à la demande
au lieu d'un rendu continu.
Et encore une fois, nous voyons un problème similaire.
La solution ici est de diviser manuellement chaque plan en 2 plans afin qu'il n'y ait réellement aucune intersection.
function makeInstance(geometry, color, rotY, url) { + const base = new THREE.Object3D(); + scene.add(base); + base.rotation.y = rotY; + [-1, 1].forEach((x) => { const texture = loader.load(url, render); + texture.offset.x = x < 0 ? 0 : 0.5; + texture.repeat.x = .5; const material = new THREE.MeshPhongMaterial({ color, map: texture, opacity: 0.5, transparent: true, side: THREE.DoubleSide, }); const mesh = new THREE.Mesh(geometry, material); - scene.add(mesh); + base.add(mesh); - mesh.rotation.y = rotY; + mesh.position.x = x * .25; }); }
La manière d'y parvenir dépend de vous. Si j'utilisais un logiciel de modélisation comme
Blender, je le ferais probablement manuellement en ajustant
les coordonnées de texture. Ici cependant, nous utilisons PlaneGeometry
qui, par défaut,
étire la texture sur tout le plan. Comme nous l'avons vu précédemment,
en définissant texture.repeat
et texture.offset
, nous pouvons mettre à l'échelle et déplacer la texture pour obtenir
la bonne moitié de la texture de face sur chaque plan.
Le code ci-dessus crée également un Object3D
et lui attache les 2 plans en tant qu'enfants.
Il semblait plus facile de faire pivoter un Object3D
parent que de faire les calculs nécessaires
pour le faire sans.
Cette solution ne fonctionne vraiment que pour des choses simples comme 2 plans dont la position d'intersection ne change pas.
Pour les objets texturés, une autre solution consiste à définir un test alpha.
Un test alpha est un niveau d'alpha en dessous duquel three.js ne dessinera pas le pixel. Si nous ne dessinons pas du tout un pixel, alors les problèmes de profondeur mentionnés ci-dessus disparaissent. Pour les textures aux bords relativement nets, cela fonctionne assez bien. Les exemples incluent les textures de feuilles sur une plante ou un arbre, ou souvent une parcelle d'herbe.
Essayons sur les 2 plans. Utilisons d'abord des textures différentes. Les textures ci-dessus étaient 100% opaques. Ces 2 utilisent la transparence.
Retournons aux 2 plans qui se croisent (avant de les diviser) et utilisons
ces textures et définissons un alphaTest
.
function makeInstance(geometry, color, rotY, url) { const texture = loader.load(url, render); const material = new THREE.MeshPhongMaterial({ color, map: texture, - opacity: 0.5, transparent: true, + alphaTest: 0.5, side: THREE.DoubleSide, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); mesh.rotation.y = rotY; } -makeInstance(geometry, 'pink', 0, 'resources/images/happyface.png'); -makeInstance(geometry, 'lightblue', Math.PI * 0.5, 'resources/images/hmmmface.png'); +makeInstance(geometry, 'white', 0, 'resources/images/tree-01.png'); +makeInstance(geometry, 'white', Math.PI * 0.5, 'resources/images/tree-02.png');
Avant d'exécuter cela, ajoutons une petite interface utilisateur pour pouvoir jouer plus facilement avec les paramètres alphaTest
et transparent
. Nous utiliserons lil-gui comme nous l'avons présenté
dans l'article sur le graphe de scène de three.js.
Nous allons d'abord créer une aide pour lil-gui qui définit une valeur pour chaque matériau de la scène.
class AllMaterialPropertyGUIHelper { constructor(prop, scene) { this.prop = prop; this.scene = scene; } get value() { const {scene, prop} = this; let v; scene.traverse((obj) => { if (obj.material && obj.material[prop] !== undefined) { v = obj.material[prop]; } }); return v; } set value(v) { const {scene, prop} = this; scene.traverse((obj) => { if (obj.material && obj.material[prop] !== undefined) { obj.material[prop] = v; obj.material.needsUpdate = true; } }); } }
Ensuite, nous allons ajouter l'interface graphique.
const gui = new GUI(); gui.add(new AllMaterialPropertyGUIHelper('alphaTest', scene), 'value', 0, 1) .name('alphaTest') .onChange(requestRenderIfNotRequested); gui.add(new AllMaterialPropertyGUIHelper('transparent', scene), 'value') .name('transparent') .onChange(requestRenderIfNotRequested);
et bien sûr, nous devons inclure lil-gui.
import * as THREE from 'three'; import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
et voici les résultats.
Vous pouvez voir que cela fonctionne, mais zoomez et vous verrez qu'un plan a des lignes blanches.
C'est le même problème de profondeur qu'auparavant. Ce plan a été dessiné en premier,
donc le plan situé derrière n'est pas dessiné. Il n'y a pas de solution parfaite.
Ajustez l'alphaTest
et/ou désactivez transparent
pour trouver une solution
qui correspond à votre cas d'utilisation.
La conclusion de cet article est que la transparence parfaite est difficile. Il y a des problèmes, des compromis et des solutions de contournement.
Par exemple, disons que vous avez une voiture. Les voitures ont généralement des pare-brise sur les 4 côtés. Si vous voulez éviter les problèmes de tri ci-dessus, vous devrez faire de chaque fenêtre son propre objet afin que three.js puisse trier les fenêtres et les dessiner dans le bon ordre.
Si vous créez des plantes ou de l'herbe, la solution du test alpha est courante.
La solution que vous choisissez dépend de vos besoins.