Cet article fait partie d'une série consacrée à Three.js. Le premier article s'intitule Principes de base. L'article précédent concerné la configuration nécessaire pour cet article. Si vous ne l'avez pas encore lu, vous voudriez peut-être commencer par là.
Les textures sont un gros sujet dans Three.js et je ne suis pas sûr de pouvoir les expliquer à 100% mais je vais essayer. Il y a de nombreuses choses à voir et beaucoup d'entre elles sont interdépendantes, il est donc difficile de les expliquer tous en même temps. Voici une table des matières rapide pour cet article.
Les textures sont generallement des images qui sont le plus souvent créées dans un programme tiers comme Photoshop ou GIMP. Par exemple, mettons cette image sur un cube.
Modifions l'un de nos premiers échantillons. Tout ce que nous avons à faire, c'est de créer un TextureLoader
. Appelons-le avec sa méthode
load
et l'URL d'une image définissons la propriété map
du matériau sur le résultat au lieu de définir sa color
.
+const loader = new THREE.TextureLoader(); const material = new THREE.MeshBasicMaterial({ - color: 0xFF8844, + map: loader.load('resources/images/wall.jpg'), });
Notez que nous utilisons un MeshBasicMaterial
, donc pas besoin de lumières.
Que diriez-vous de 6 textures, une sur chaque face d'un cube ?
Fabriquons juste 6 materiaux et passons-les sous forme de tableau lors de la création de la Mesh
const loader = new THREE.TextureLoader(); -const material = new THREE.MeshBasicMaterial({ - map: loader.load('resources/images/wall.jpg'), -}); +const materials = [ + new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}), + new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}), + new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}), + new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}), + new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}), + new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}), +]; -const cube = new THREE.Mesh(geometry, material); +const cube = new THREE.Mesh(geometry, materials);
Ça marche !
Il convient de noter, cependant, que tous les types de géométries ne peuvent supporter la prise en charge de plusieurs matériaux. BoxGeometry
ne peut utiliser que 6 materiaux, un pour chaque face.
ConeGeometry
, seulement 2, un pour la base et un pour le cône.
CylinderGeometry
peut recevoir 3 materiaux pour le bas, le haut et le côté.
Dans les autres cas, vous devrez créer ou charger une géométrie personnalisée et/ou modifier les coordonnées de texture.
Il est bien plus performant d'utiliser, comme dans bien d'autres moteurs 3D, un atlas de texture si vous voulez utiliser plusieurs images sur une même géométrie. Un atlas de texture est l'endroit où vous placez plusieurs images dans une seule texture, puis utilisez les coordonnées de texture sur les sommets de votre géométrie pour sélectionner les parties d'une texture à utiliser sur chaque triangle de votre géométrie.
Que sont les coordonnées de texture ? Ce sont des données ajoutées à chaque sommet d'un morceau de géométrie qui spécifient quelle partie de la texture correspond à ce sommet spécifique. Nous les examinerons lorsque nous commencerons à créer une géométrie personnalisée.
La plupart du code de ce site utilise la méthode la plus simple pour charger les textures. Nous créons un TextureLoader
puis appelons sa méthode de chargement
.
Cela renvoie un objet Texture
.
const texture = loader.load('resources/images/flower-1.jpg');
Il est important de noter qu'en utilisant cette méthode, notre texture sera transparente jusqu'à ce que l'image soit chargée de manière asynchrone par Three.js, auquel cas elle mettra à jour la texture avec l'image téléchargée.
Le gros avantage, c'est que nous n'avons pas besoin d'attendre que la texture soit chargée pour que notre page s'affiche. C'est probablement correct pour un grand nombre de cas d'utilisation, mais si nous le voulons, nous pouvons demander à Three.js de nous dire quand le téléchargement de la texture est terminé.
Pour attendre qu'une texture se charge, la méthode load
du chargeur de texture prend une 'callback function' qui sera appelée lorsque la texture aura fini de se charger. Pour en revenir à notre meilleur exemple, nous pouvons attendre que la texture se charge avant de créer notre Mesh
et de l'ajouter à une scène comme celle-ci
const loader = new THREE.TextureLoader(); loader.load('resources/images/wall.jpg', (texture) => { const material = new THREE.MeshBasicMaterial({ map: texture, }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cubes.push(cube); // add to our list of cubes to rotate });
À moins de vider le cache de votre navigateur et d'avoir une connexion lente, il est peu probable que vous voyiez la différence, mais soyez assuré qu'il attend le chargement de la texture.
Pour attendre que toutes les textures soient chargées, vous pouvez utiliser un LoadingManager
. Créez-en un et transmettez-le à TextureLoader
, puis définissez sa propriété onLoad
avec une 'callback function'.
+const loadManager = new THREE.LoadingManager(); *const loader = new THREE.TextureLoader(loadManager); const materials = [ new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-1.jpg')}), new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-2.jpg')}), new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-3.jpg')}), new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-4.jpg')}), new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-5.jpg')}), new THREE.MeshBasicMaterial({map: loader.load('resources/images/flower-6.jpg')}), ]; +loadManager.onLoad = () => { + const cube = new THREE.Mesh(geometry, materials); + scene.add(cube); + cubes.push(cube); // ajouter à la liste des cubes +};
Le LoadingManager
a également une propriété onProgress
que nous pouvons définir sur une autre 'callback' pour afficher un indicateur de progression.
Ajoutons d'abord une barre de progression en HTML
<body> <canvas id="c"></canvas> + <div id="loading"> + <div class="progress"><div class="progressbar"></div></div> + </div> </body>
et un peu de CSS
#loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; } #loading .progress { margin: 1.5em; border: 1px solid white; width: 50vw; } #loading .progressbar { margin: 2px; background: white; height: 1em; transform-origin: top left; transform: scaleX(0); }
Ensuite, dans le code, nous mettrons à jour la progressbar
dans la 'callback function' onProgress
. Elle est appelée avec l'URL du dernier élément chargé, le nombre d'éléments chargés jusqu'à présent et le nombre total d'éléments chargés.
+const loadingElem = document.querySelector('#loading'); +const progressBarElem = loadingElem.querySelector('.progressbar'); loadManager.onLoad = () => { + loadingElem.style.display = 'none'; const cube = new THREE.Mesh(geometry, materials); scene.add(cube); cubes.push(cube); // ajouter à la liste des cubes }; +loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => { + const progress = itemsLoaded / itemsTotal; + progressBarElem.style.transform = `scaleX(${progress})`; +};
À moins que vous ne vidiez votre cache et que votre connexion soit lente, vous ne verrez peut-être pas la barre de chargement.
Pour utiliser des images d'autres serveurs, ces serveurs doivent envoyer les en-têtes corrects. Si ce n'est pas le cas, vous ne pouvez pas utiliser les images dans Three.js et vous obtiendrez une erreur. Si vous utilisez un serveur distant, assurez-vous qu'il envoie les bons en-têtes. Sinon, vous ne pourrez pas utiliser les images provenant de ce serveur.
Par exemple imgur, flickr, et github envoient des en-têtes vous permettant d'utiliser des images hébergées sur leurs serveurs avec Three.js. La plupart des autres sites web ne le font pas.
Les textures sont souvent la partie d'une application Three.js qui utilise le plus de mémoire. Il est important de comprendre qu'en général, textures prennent width * height * 4 * 1.33
octets de mémoire.
Remarquez que cela ne dit rien sur la compression. Je peux créer une image .jpg et régler sa compression à un niveau très élevé. Par exemple, disons que je souhaite créé une maison. A l'intérieur de la maison il y a une table et je décide de mettre cette texture de bois sur la surface supérieure de la table
Cette image ne pèse que 157ko, elle sera donc téléchargée relativement vite mais sa taill est en réalité de 3024 x 3761 pixels. En suivant l'équation ci-dessous
3024 * 3761 * 4 * 1.33 = 60505764.5
Cette image prendra 60 MEGA de MEMOIRE! dans Three.js. Encore quelques textures comme celle-la et vous serez à court de mémoire.
J'en parle car il est important de savoir que l'utilisation de textures a un coût caché. Pour que Three.js utilise la texture, il doit la transmettre au GPU et le GPU en général nécessite que les données de texture soient décompressées.
La morale de l'histoire, c'est d'utiliser des textures de petite taille, pas seulement petite en taille de fichier. Petit en taille de fichier = rapide à télécharger. Petit en dimensions = prend moins de mémoire. Quelle est la bonne taille ? Aussi petite que possible et toujours aussi belle que nécessaire.
C'est à peu près la même chose qu'en HTML, en ce sens que les JPG ont une compression avec perte, les PNG ont une compression sans perte, donc les PNG sont généralement plus lents à télécharger. Mais, les PNG prennent en charge la transparence. Les PNG sont aussi probablement le format approprié pour les données non-image comme les normal maps, et d'autres types de map non-image que nous verrons plus tard.
Il est important de se rappeler qu'un JPG n'utilise pas moins de mémoire qu'un PNG en WebGL. Voir au ci-dessus.
Appliquons cette texture 16x16
sur un cube
Rétrécissons-le au max
Hmmm, je suppose que c'est trop difficile à voir. Agrandissons-le un peu
Comment le GPU sait-il quelles couleurs créer pour chaque pixel qu'il dessine pour le petit cube ? Et si le cube était si petit qu'il ne faisait que 1 ou 2 pixels ?
C'est à cela que sert le filtrage.
S'il s'agissait de Photoshop, il ferait la moyenne de presque tous les pixels ensemble pour déterminer la couleur de ces 1 ou 2 pixels. Ce serait une opération très lente. Les GPU résolvent ce problème à l'aide de mipmaps.
Le MIP mapping consiste à envoyer au processeur graphique (GPU) des échantillons de texture de résolutions décroissantes qui seront utilisés à la place de la texture originale, en fonction de la distance du point de vue à l'objet texturé et du niveau de détails nécessaire. Pour l'image précédente seront produites les mêmes images avec des résolutions inférieure jusqu'à obtenir 1 x 1 pixel.
Désormais, lorsque le cube est dessiné si petit qu'il ne fait que 1 ou 2 pixels de large, le GPU peut choisir d'utiliser uniquement le plus petit ou le plus petit niveau de mip pour décider de la couleur du petit cube.
Dans Three.js, vous pouvez choisir ce qui se passe à la fois lorsque la texture est dessinée plus grande que sa taille d'origine et ce qui se passe lorsqu'elle est dessinée plus petite que sa taille d'origine.
Pour définir le filtre lorsque la texture est dessinée plus grande que sa taille d'origine, définissez la propriété texture.magFilter
sur THREE.NearestFilter
ou
THREE.LinearFilter
. NearestFilter
signifie simplement choisir le pixel le plus proche dans la texture d'origine. Avec une texture basse résolution, cela vous donne un look très pixelisé comme Minecraft.
LinearFilter
signifie choisir les 4 pixels de la texture qui sont les plus proches de l'endroit où nous devrions choisir une couleur et les mélanger dans les proportions appropriées par rapport à la distance entre le point réel et chacun des 4 pixels.
Pour définir le filtre lorsque la texture est dessinée plus petite que sa taille d'origine, définissez la propriété texture.minFilter
sur l'une des 6 valeurs :
THREE.NearestFilter
comme ci-dessus, choisissez le pixel le plus proche dans la texture
THREE.LinearFilter
comme ci-dessus, choisissez 4 pixels dans la texture et mélangez-les
THREE.NearestMipmapNearestFilter
choisissez le mip approprié puis choisissez un pixe
THREE.NearestMipmapLinearFilter
choisissez 2 mips, choisissez un pixel de chacun, mélangez les 2 pixels
THREE.LinearMipmapNearestFilter
choisissez le mip approprié puis choisissez 4 pixels et mélangez-les
THREE.LinearMipmapLinearFilter
choisissez 2 mips, choisissez 4 pixels de chacun et mélangez les 8 en 1 pixel
Voici un exemple montrant les 6 paramètres
Une chose à noter est que la texture en haut/gauche et la haut/milieu utilisent NearestFilter et LinearFilter et pas les mips. À cause de cela, ils scintillent au loin car le GPU sélectionne les pixels de la texture d'origine. Sur la gauche, un seul pixel est choisi et au milieu, 4 sont choisis et mélangés, mais il ne suffit pas de proposer une bonne couleur représentative. Les 4 autres bandes font mieux avec le bas à droite, LinearMipmapLinearFilter étant le meilleur.
Si vous cliquez sur l'image ci-dessus, elle basculera entre la texture que nous avons utilisée ci-dessus et une texture où chaque niveau de mip est d'une couleur différente.
Cela clarifie les choses. Vous pouvez voir en haut à gauche et en haut au milieu que le premier mip est utilisé au loin. En haut à droite et en bas au milieu, vous pouvez clairement voir où un MIP différent est utilisé.
En revenant à la texture d'origine, vous pouvez voir que celle en bas à droite est la plus douce et la plus haute qualité. Vous pourriez vous demander pourquoi ne pas toujours utiliser ce mode. La raison la plus évidente, c'est que parfois vous voulez que les choses soient pixelisées pour un look rétro ou pour une autre raison. La deuxième raison la plus courante, c' est que lire 8 pixels et les mélanger est plus lent que lire 1 pixel et mélanger. Bien qu'il soit peu probable qu'une seule texture fasse la différence entre rapide et lente à mesure que nous progressons dans ces articles, nous finirons par avoir des matériaux qui utilisent 4 ou 5 textures à la fois. 4 textures * 8 pixels par texture se transforment 32 pixels pour chaque pixel rendu. Cela peut être particulièrement important à considérer sur les appareils mobiles.
Les textures ont des paramètres pour la répétition, le décalage et la rotation d'une texture.
Par défaut, les textures de three.js ne se répètent pas. Pour définir si une texture se répète ou non, il existe 2 propriétés, wrapS
pour un habillage horizontal et wrapT
pour un habillage vertical.
Ils peuvent être définis sur l'un des éléments suivants :
THREE.ClampToEdgeWrapping
le dernier pixel de chaque bord est répété indéfiniment
THREE.RepeatWrapping
la texture est répétée
THREE.MirroredRepeatWrapping
la texture est reflétée et répétée
Par exemple pour activer le wrapping dans les deux sens :
someTexture.wrapS = THREE.RepeatWrapping; someTexture.wrapT = THREE.RepeatWrapping;
La répétition est définie avec la propriété [repeat].
const timesToRepeatHorizontally = 4; const timesToRepeatVertically = 2; someTexture.repeat.set(timesToRepeatHorizontally, timesToRepeatVertically);
Le décalage de la texture peut être effectué en définissant la propriété offset
. Les textures sont décalées avec des unités où 1 unité = 1 taille de texture. En d'autres termes 0 = aucun décalage et 1 = décalage d'une quantité de texture complète.
const xOffset = .5; // décalage de la moitié de la texture const yOffset = .25; // décalage d'un quart someTexture.offset.set(xOffset, yOffset);
La rotation de la texture peut être définie en définissant la propriété rotation
en radians ainsi que la propriété center
pour choisir le centre de rotation. La valeur par défaut est 0,0 qui tourne à partir du coin inférieur gauche. Comme le décalage, ces unités ont une taille de texture, donc les régler sur .5, .5
tournerait autour du centre de la texture.
someTexture.center.set(.5, .5); someTexture.rotation = THREE.MathUtils.degToRad(45);
Modifions l'échantillon supérieur ci-dessus pour jouer avec ces valeurs.
Tout d'abord, nous allons garder une référence à la texture afin que nous puissions la manipuler
+const texture = loader.load('resources/images/wall.jpg'); const material = new THREE.MeshBasicMaterial({ - map: loader.load('resources/images/wall.jpg'); + map: texture, });
Ensuite, utilisons lil-gui pour fournir une interface simple.
import {GUI} from '/examples/jsm/libs/lil-gui.module.min.js';
Comme nous l'avons fait dans les exemples précédents avec lil-gui, nous utiliserons une classe simple pour donner à lil-gui un objet qu'il peut manipuler en degrés mais qu'il définira en radians.
class DegRadHelper { constructor(obj, prop) { this.obj = obj; this.prop = prop; } get value() { return THREE.MathUtils.radToDeg(this.obj[this.prop]); } set value(v) { this.obj[this.prop] = THREE.MathUtils.degToRad(v); } }
Nous avons également besoin d'une classe qui convertira une chaîne telle que "123"
en un nombre tel que 123, car Three.js nécessite des nombres pour les paramètres d'énumération tels que wrapS
et wrapT
, mais lil-gui n'utilise que des chaînes pour les énumérations.
class StringToNumberHelper { constructor(obj, prop) { this.obj = obj; this.prop = prop; } get value() { return this.obj[this.prop]; } set value(v) { this.obj[this.prop] = parseFloat(v); } }
En utilisant ces classes, nous pouvons configurer une interface graphique simple pour les paramètres ci-dessus
const wrapModes = { 'ClampToEdgeWrapping': THREE.ClampToEdgeWrapping, 'RepeatWrapping': THREE.RepeatWrapping, 'MirroredRepeatWrapping': THREE.MirroredRepeatWrapping, }; function updateTexture() { texture.needsUpdate = true; } const gui = new GUI(); gui.add(new StringToNumberHelper(texture, 'wrapS'), 'value', wrapModes) .name('texture.wrapS') .onChange(updateTexture); gui.add(new StringToNumberHelper(texture, 'wrapT'), 'value', wrapModes) .name('texture.wrapT') .onChange(updateTexture); gui.add(texture.repeat, 'x', 0, 5, .01).name('texture.repeat.x'); gui.add(texture.repeat, 'y', 0, 5, .01).name('texture.repeat.y'); gui.add(texture.offset, 'x', -2, 2, .01).name('texture.offset.x'); gui.add(texture.offset, 'y', -2, 2, .01).name('texture.offset.y'); gui.add(texture.center, 'x', -.5, 1.5, .01).name('texture.center.x'); gui.add(texture.center, 'y', -.5, 1.5, .01).name('texture.center.y'); gui.add(new DegRadHelper(texture, 'rotation'), 'value', -360, 360) .name('texture.rotation');
La dernière chose à noter à propos de l'exemple est que si vous modifiez wrapS
ou wrapT
sur la texture, vous devez également définir texture.needsUpdate
afin que three.js sache appliquer ces paramètres. Les autres paramètres sont automatiquement appliqués.
Ce n'est qu'une étape dans le sujet des textures. À un moment donné, nous passerons en revue les coordonnées de texture ainsi que 9 autres types de textures pouvant être appliquées aux matériaux.
Pour le moment, passons aux lumières.