Cet article fait partie d'une série d'articles sur three.js. Le premier article concernait les bases de three.js. L'article précédent expliquait comment se préparer pour cet article. Si vous ne l'avez pas encore lu, vous pourriez vouloir commencer par là.
Les textures sont un sujet assez vaste dans Three.js et je ne suis pas sûr à 100% du niveau auquel les expliquer, mais je vais essayer. Il y a de nombreux sujets et beaucoup d'entre eux sont interdépendants, il est donc difficile d'expliquer tout en une seule fois. Voici une table des matières rapide pour cet article.
Les textures sont généralement 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.
Nous allons modifier un de nos premiers exemples. Tout ce que nous avons à faire est de créer un TextureLoader
. Appelez sa
méthode load
avec l'URL d'une
image et définissez la propriété map
du matériau sur le résultat au lieu de définir sa color
.
+const loader = new THREE.TextureLoader(); +const texture = loader.load( 'resources/images/wall.jpg' ); +texture.colorSpace = THREE.SRGBColorSpace; const material = new THREE.MeshBasicMaterial({ - color: 0xFF8844, + map: texture, });
Notez que nous utilisons MeshBasicMaterial
, donc pas besoin de lumières.
Que diriez-vous de 6 textures, une sur chaque face d'un cube ?
Nous créons simplement 6 matériaux et les passons sous forme de tableau lorsque nous créons le Mesh
const loader = new THREE.TextureLoader(); -const texture = loader.load( 'resources/images/wall.jpg' ); -texture.colorSpace = THREE.SRGBColorSpace; -const material = new THREE.MeshBasicMaterial({ - map: texture, -}); +const materials = [ + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-1.jpg')}), + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-2.jpg')}), + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-3.jpg')}), + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-4.jpg')}), + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-5.jpg')}), + new THREE.MeshBasicMaterial({map: loadColorTexture('resources/images/flower-6.jpg')}), +]; -const cube = new THREE.Mesh(geometry, material); +const cube = new THREE.Mesh(geometry, materials); +function loadColorTexture( path ) { + const texture = loader.load( path ); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +}
Ça marche !
Il convient de noter cependant que tous les types de géométrie ne supportent pas plusieurs
matériaux. BoxGeometry
peut utiliser 6 matériaux, un pour chaque face.
ConeGeometry
peut utiliser 2 matériaux, un pour le fond et un pour le cône.
CylinderGeometry
peut utiliser 3 matériaux : fond, haut et côté.
Dans d'autres cas, vous devrez construire ou charger une géométrie personnalisée et/ou modifier les coordonnées de texture.
Il est beaucoup plus courant dans d'autres moteurs 3D et beaucoup plus performant d'utiliser un atlas de textures si vous souhaitez autoriser plusieurs images sur une seule géométrie. Un atlas de textures est un endroit où vous placez plusieurs images dans une seule texture et utilisez ensuite les coordonnées de texture sur les sommets de votre géométrie pour sélectionner quelles parties d'une texture sont utilisées 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'une pièce de géométrie qui spécifient quelle partie de la texture correspond à ce sommet spécifique. Nous les aborderons lorsque nous commencerons à construire une géométrie personnalisée.
La plupart du code sur ce site utilise la méthode la plus simple pour charger des textures.
Nous créons un TextureLoader
, puis appelons sa méthode load
.
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, moment auquel elle mettra à jour la texture avec l'image téléchargée.
Cela présente le grand avantage de ne pas avoir à attendre le chargement de la texture et notre page commencera à s'afficher immédiatement. C'est probablement acceptable pour un grand nombre de cas d'utilisation mais si nous le souhaitons, nous pouvons demander à three.js de nous informer lorsque la texture a fini de se télécharger.
Pour attendre le chargement d'une texture, la méthode load
du chargeur de textures prend un rappel
qui sera appelé lorsque la texture aura fini de se charger. En reprenant notre premier exemple,
nous pouvons attendre le chargement de la texture avant de créer notre Mesh
et de l'ajouter à la scène
comme ceci
const loader = new THREE.TextureLoader(); loader.load('resources/images/wall.jpg', (texture) => { texture.colorSpace = THREE.SRGBColorSpace; 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 que vous ne vidiez le cache de votre navigateur et que vous ayez une connexion lente, il est peu probable que vous voyiez une différence, mais soyez assuré qu'elle 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 passez-le au TextureLoader
, puis définissez sa propriété onLoad
sur un rappel.
+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); // add to our list of cubes to rotate +};
Le LoadingManager
a également une propriété onProgress
que nous pouvons définir sur un autre rappel pour afficher un indicateur de progression.
Nous allons d'abord ajouter 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 le CSS associé
#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 l'échelle de la progressbar
dans notre rappel onProgress
. Il est
appelé 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 à charger.
+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); // add to our list of cubes to rotate }; +loadManager.onProgress = (urlOfLastItemLoaded, itemsLoaded, itemsTotal) => { + const progress = itemsLoaded / itemsTotal; + progressBarElem.style.transform = `scaleX(${progress})`; +};
À moins que vous ne vidiez votre cache et que vous ayez une connexion lente, il est possible que vous ne voyiez pas la barre de chargement.
Pour utiliser des images provenant d'autres serveurs, ces serveurs doivent envoyer les en-têtes corrects. S'ils ne le font pas, vous ne pouvez pas utiliser les images dans three.js et vous obtiendrez une erreur. Si vous gérez le serveur fournissant les images, assurez-vous qu'il envoie les en-têtes corrects. Si vous ne contrôlez pas le serveur hébergeant les images et qu'il n'envoie pas les en-têtes d'autorisation, vous ne pouvez pas utiliser les images de ce serveur.
Par exemple, imgur, flickr et github envoient tous des en-têtes vous permettant d'utiliser les images hébergées sur leurs serveurs dans 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, les textures prennent largeur * hauteur * 4 * 1.33
octets de mémoire.
Notez que cela ne dit rien sur la compression. Je peux créer une image .jpg et régler sa compression très élevée. Par exemple, disons que je créais une scène d'une maison. À 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 fait que 157k, elle se téléchargera donc relativement rapidement, mais sa taille est en réalité de 3024 x 3761 pixels. En suivant l'équation ci-dessus, cela donne
3024 * 3761 * 4 * 1.33 = 60505764.5
Cette image prendra 60 MÉGAOCTETS DE MÉMOIRE ! dans three.js. Quelques textures comme celle-là et vous serez à court de mémoire.
Je soulève ce point car il est important de savoir que l'utilisation des textures a un coût caché. Pour que three.js puisse utiliser la texture, il doit la transmettre au GPU, et le GPU en général nécessite que les données de la texture soient décompressées.
La morale de l'histoire est de rendre vos textures petites en dimensions, pas seulement petites en taille de fichier. Petite taille de fichier = téléchargement rapide. Petites dimensions = prend moins de mémoire. Quelle taille devraient-elles avoir ? Aussi petites que possible tout en conservant l'apparence dont vous avez besoin.
C'est à peu près la même chose qu'en HTML classique : 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 supportent la transparence. Les PNG sont également probablement le format approprié pour les données non-image comme les normal maps et d'autres types de maps 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 dans WebGL. Voir ci-dessus.
Appliquons cette texture 16x16
À un cube
Dessinons ce cube très petit
Hmmm, je suppose que c'est difficile à voir. Agrandissons ce tout petit cube
Comment le GPU sait-il quelles couleurs donner à chaque pixel qu'il dessine pour le petit cube ? Que se passerait-il si le cube était si petit qu'il ne fasse qu'un ou deux pixels ?
C'est à cela que sert le filtrage.
Si c'était Photoshop, Photoshop ferait la moyenne de presque tous les pixels pour déterminer la couleur à donner à ces 1 ou 2 pixels. Ce serait une opération très lente. Les GPU résolvent ce problème en utilisant les mipmaps.
Les mips sont des copies de la texture, chacune faisant la moitié de la largeur et la moitié de la hauteur du mip précédent, où les pixels ont été mélangés pour créer le mip suivant plus petit. Les mips sont créés jusqu'à ce que l'on arrive à un mip de 1x1 pixel. Pour l'image ci-dessus, tous les mips ressembleraient à ceci
Maintenant, lorsque le cube est dessiné si petit qu'il ne fait qu'un ou deux pixels, le GPU peut choisir d'utiliser uniquement le mip le plus petit ou le mip juste avant le plus petit pour décider de la couleur à donner au 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,
vous définissez la propriété texture.magFilter
sur THREE.NearestFilter
ou
THREE.LinearFilter
. NearestFilter
signifie
simplement choisir le pixel unique le plus proche de la texture d'origine. Avec une texture
basse résolution, cela donne un aspect très pixélisé comme dans 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,
vous définissez la propriété texture.minFilter
sur l'une des 6 valeurs suivantes.
THREE.NearestFilter
identique à ci-dessus, choisir le pixel le plus proche dans la texture
THREE.LinearFilter
identique à ci-dessus, choisir 4 pixels de la texture et les mélanger
THREE.NearestMipmapNearestFilter
choisir le mip approprié puis choisir un pixel
THREE.NearestMipmapLinearFilter
choisir 2 mips, choisir un pixel de chaque, mélanger les 2 pixels
THREE.LinearMipmapNearestFilter
choisir le mip approprié puis choisir 4 pixels et les mélanger
THREE.LinearMipmapLinearFilter
choisir 2 mips, choisir 4 pixels de chaque et mélanger les 8 en 1 pixel
Voici un exemple montrant les 6 paramètres
Une chose à remarquer est que le coin supérieur gauche et le milieu supérieur utilisant NearestFilter
et LinearFilter
n'utilisent pas les mips. De ce fait, ils scintillent au loin car le GPU sélectionne
des pixels de la texture d'origine. À gauche, un seul pixel est choisi et
au milieu, 4 sont choisis et mélangés, mais ce n'est pas suffisant pour obtenir une bonne
couleur représentative. Les 4 autres bandes s'en sortent mieux,
celle en bas à droite, LinearMipmapLinearFilter
, étant la meilleure.
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 rend plus clair ce qui se passe. Vous pouvez voir en haut à gauche et au milieu supérieur que le premier mip est utilisé jusqu'au loin. En haut à droite et au milieu inférieur, 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 lisse, de la plus haute qualité. Vous pourriez vous demander pourquoi ne pas toujours utiliser ce mode. La raison la plus évidente est que parfois vous voulez que les choses soient pixélisées pour un look rétro ou pour une autre raison. La raison suivante la plus courante est que lire 8 pixels et les mélanger est plus lent que de lire 1 pixel et de le mélanger. Bien qu'il soit peu probable qu'une seule texture fasse la différence entre rapide et lent, à mesure que nous progresserons dans ces articles, nous aurons finalement des matériaux qui utilisent 4 ou 5 textures en même temps. 4 textures * 8 pixels par texture, c'est rechercher 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 dans 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 l'habillage horizontal
et wrapT
pour l'habillage vertical.
Ils peuvent être définis sur l'une des valeurs suivantes :
THREE.ClampToEdgeWrapping
le dernier pixel sur chaque bord est répété indéfiniment
THREE.RepeatWrapping
la texture est répétée
THREE.MirroredRepeatWrapping
la texture est mise en miroir et répétée
Par exemple, pour activer l'habillage dans les deux directions :
someTexture.wrapS = THREE.RepeatWrapping; someTexture.wrapT = THREE.RepeatWrapping;
La répétition est définie avec la propriété [repeat] 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. Autrement dit, 0 = pas de décalage
et 1 = décalage d'une quantité de texture complète.
const xOffset = .5; // offset by half the texture const yOffset = .25; // offset by 1/4 the texture 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.
Elle est par défaut à 0,0, ce qui correspond à une rotation depuis le coin inférieur gauche. Comme pour le décalage,
ces unités sont en taille de texture, donc les définir à .5, .5
effectuerait une rotation
autour du centre de la texture.
someTexture.center.set(.5, .5); someTexture.rotation = THREE.MathUtils.degToRad(45);
Modifions l'exemple du haut ci-dessus pour jouer avec ces valeurs
Tout d'abord, nous allons conserver une référence à la texture afin de pouvoir 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, nous utiliserons à nouveau lil-gui pour fournir une interface simple.
import {GUI} from 'three/addons/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 qui définira une propriété 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 de caractères comme "123"
en un
nombre comme 123
, car three.js nécessite des nombres pour les paramètres d'énumération
comme wrapS
et wrapT
, mais lil-gui n'utilise que des chaînes de caractères 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 changez wrapS
ou
wrapT
sur la texture, vous devez également définir texture.needsUpdate
afin que three.js sache qu'il doit appliquer ces paramètres. Les autres paramètres sont appliqués automatiquement.
Ce n'est qu'une étape dans le sujet des textures. À un moment donné, nous aborderons les coordonnées de texture ainsi que 9 autres types de textures qui peuvent être appliqués aux matériaux.
Pour l'instant, passons aux lumières.