
Il existe de nombreuses introductions aux bases de l'utilisation de Three.js sur le Web, mais vous remarquerez peut-être une pénurie de documents sur des sujets plus avancés. Et l'un de ces sujets est la combinaison de shaders et de scènes avec des modèles tridimensionnels. Aux yeux de nombreux développeurs novices, ce sont des choses apparemment incompatibles de différents mondes. Aujourd'hui, en utilisant un exemple simple de «sphère de plasma», nous verrons ce qu'est ShaderMaterial et avec quoi il est mangé, quel est l'effet Effect et à quelle vitesse il est possible de faire du post-traitement pour une scène rendue.
Il est supposé que le lecteur connaît les bases de l'utilisation de Three.js et comprend le fonctionnement des shaders. Si vous ne l'avez jamais rencontré auparavant, je recommande fortement de lire ceci en premier:
Mais commençons ...
ShaderMaterial - qu'est-ce que c'est?
Nous avons déjà vu comment une texture plate est utilisée et comment elle est étirée sur un objet tridimensionnel. Comme cette texture était une image ordinaire. Lorsque nous avons examiné l'écriture des shaders de fragments, tout était plat là aussi. Donc: si nous pouvons générer une image plate à l'aide d'un shader, alors pourquoi ne pas l'utiliser comme texture?
C'est cette idée qui constitue la base du matériau de shader. Lors de la création de matériau pour un objet tridimensionnel, nous indiquons des shaders au lieu d'une texture pour celui-ci. Dans sa forme de base, il ressemble à ceci:
const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
Le fragment shader sera utilisé pour créer la texture du matériau, et vous vous demandez, bien sûr, que fera le vertex shader? Fera-t-il à nouveau un banal recomptage des coordonnées? Oui, nous allons commencer avec cette option simple, mais nous pouvons également définir un décalage ou effectuer d'autres manipulations pour chaque sommet d'un objet tridimensionnel - il n'y a maintenant aucune restriction sur le plan. Mais il vaut mieux regarder tout cela avec un exemple. En mots, on comprend peu. Créez une scène et créez une sphère au centre.

Comme matériau pour la sphère, nous utiliserons ShaderMaterial:
const geometry = new THREE.SphereBufferGeometry(30, 64, 64); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
Le vertex shader sera neutre:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Notez que Three.js transmet ses variables uniformes. Nous n'avons rien à faire, ils sont implicites. En eux-mêmes, ils contiennent toutes sortes de matrices, auxquelles nous avons déjà accès depuis JS, ainsi que la position de la caméra. Imaginez qu'au début des shaders eux-mêmes quelque chose soit inséré:
De plus, plusieurs variables d'attribut sont passées au vertex shader:
attribute vec3 position; attribute vec3 normal; attribute vec2 uv;
Par les noms, il est clair ce que c'est - la position du sommet actuel, la normale à la surface à ce point, et les coordonnées sur la texture à laquelle le sommet correspond.
Traditionnellement, les coordonnées dans l'espace sont désignées par (x, y, z) et les coordonnées sur le plan de texture par (u, v). D'où le nom de la variable. Vous le rencontrerez souvent dans divers exemples. En théorie, nous devons transférer ces coordonnées dans le fragment shader afin d'y travailler avec elles. Nous le ferons.
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Pour commencer, le shader de fragment devrait ressembler à ceci:
#define EPSILON 0.02 varying vec2 vUv; void main() { if ((fract(vUv.x * 10.0) < EPSILON) || (fract(vUv.y * 10.0) < EPSILON)) { gl_FragColor = vec4(vec3(0.0), 1.0); } else { gl_FragColor = vec4(1.0); } }
Créez simplement un maillage. Si vous réfléchissez un peu, alors dans l'avion, ce ne sera qu'une grille de carrés, mais comme nous la superposons à une sphère, elle est déformée et se transforme en globe. Il y a une bonne image sur Wikipedia illustrant ce qui se passe:

C'est-à-dire que dans le fragment shader, nous faisons une texture plate, comme au centre de cette illustration, et Three.js la tire ensuite sur la sphère. Très confortable.
Bien sûr, pour les modèles plus complexes, le balayage sera plus compliqué. Mais généralement, lors de la création de divers sites de conception, nous travaillons avec des formes géométriques simples et il est facile d'imaginer un balayage dans votre tête.
Ok, que pouvez-vous y faire?
La principale caractéristique est que le matériau du shader peut changer avec le temps. Ce n'est pas une chose statique que nous avons dessinée une fois et oublié, nous pouvons l'animer. De plus, à la fois en couleur (dans le fragment shader) et en forme (dans le sommet). Il s'agit d'un outil très puissant.
Dans notre exemple, nous ferons un feu enveloppant une sphère. Il y aura deux sphères - une ordinaire (à l'intérieur) et la seconde à partir du matériau du shader (à l'extérieur, avec un grand rayon). L'ajout d'une autre sphère ne fera aucun commentaire.

Tout d'abord, ajoutez du temps comme variable uniforme pour les shaders de notre matériau. Nulle part sans temps. Nous l'avons déjà fait en JS pur, mais dans Three.js, c'est tout aussi simple. Laissez le temps dans les shaders être appelé uTime et stocké dans la variable TIME:
function updateUniforms() { SCENE.traverse((child) => { if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') { child.material.uniforms.uTime.value = TIME; child.material.needsUpdate = true; } }); }
Nous mettons à jour tout à chaque appel à la fonction d'animation:
function animate() { requestAnimationFrame(animate); TIME += 0.005; updateUniforms(); render(); }
Le feu
La création d'un feu est essentiellement très similaire à la génération d'un paysage, mais au lieu des hauteurs, la couleur. Ou la transparence, comme dans notre cas.
Les fonctions d'aléatoire et de bruit que nous avons déjà vues, nous ne les analyserons pas en détail. Tout ce que nous devons faire est de faire du bruit à différentes fréquences pour ajouter de la variété et faire bouger chacun de ces bruits à des vitesses différentes. Vous obtiendrez quelque chose comme des flammes, les grandes se déplacent lentement, les petites se déplacent plus rapidement:
uniform float uTime; varying vec2 vUv; float rand(vec2); float noise(vec2); void main() { vec2 position1 = vec2(vUv.x * 4.0, vUv.y - uTime); vec2 position2 = vec2(vUv.x * 4.0, vUv.y - uTime * 2.0); vec2 position3 = vec2(vUv.x * 4.0, vUv.y - uTime * 3.0); float color = ( noise(position1 * 5.0) + noise(position2 * 10.0) + noise(position3 * 15.0)) / 3.0; gl_FragColor = vec4(0.0, 0.0, 0.0, color - smoothstep(0.1, 1.3, vUv.y)); } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } float noise(vec2 position) { vec2 blockPosition = floor(position); float topLeftValue = rand(blockPosition); float topRightValue = rand(blockPosition + vec2(1.0, 0.0)); float bottomLeftValue = rand(blockPosition + vec2(0.0, 1.0)); float bottomRightValue = rand(blockPosition + vec2(1.0, 1.0)); vec2 computedValue = smoothstep(0.0, 1.0, fract(position)); return mix(topLeftValue, topRightValue, computedValue.x) + (bottomLeftValue - topLeftValue) * computedValue.y * (1.0 - computedValue.x) + (bottomRightValue - topRightValue) * computedValue.x * computedValue.y; }
Pour que la flamme ne couvre pas toute la sphère, nous jouons avec le quatrième paramètre de couleur - la transparence - et le lions à la coordonnée y. Dans notre cas, cette option est très pratique. De manière plus générale, nous appliquons un dégradé transparent au bruit.
Dans des moments comme ceux-ci, il est utile de se souvenir de la fonction smoothstep.
En général, une telle approche pour créer du feu à l'aide de shaders est un classique. Vous le rencontrerez souvent à divers endroits. Il sera utile de jouer avec des nombres magiques - ils sont définis au hasard dans l'exemple, et à quoi ressemblera le plasma dépend d'eux.
Pour rendre le feu plus intéressant, passons au vertex shader et à un petit chaman ...
Comment rendre la flamme un peu "coulée" dans l'espace? Pour les débutants, cette question peut poser de grandes difficultés, malgré sa simplicité. J'ai vu des approches très complexes pour résoudre ce problème, mais essentiellement - nous devons déplacer en douceur les sommets de la sphère le long des lignes «à partir de son centre». Ça va et vient, ça va et vient. Three.js nous a déjà passé la position actuelle du sommet et de la normale - nous les utiliserons. Pour les allers-retours, certaines fonctions limitées conviendront, par exemple, à un sinus. Vous pouvez bien sûr expérimenter, mais le sinus est l'option par défaut.
Je ne sais pas quoi prendre - prenez le sinus. Mieux encore, la somme des sinus de fréquences différentes.
Nous déplaçons les coordonnées normales à la valeur obtenue et recalculons selon la formule précédemment connue.
uniform float uTime; varying vec2 vUv; void main() { vUv = uv; vec3 delta = normal * sin(position.x * position.y * uTime / 10.0); vec3 newPosition = position + delta; gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); }
Ce que nous obtenons n'est plus tout à fait une sphère. Ceci ... Je ne sais même pas si celui-ci a un nom. Mais, encore une fois, n'oubliez pas de jouer avec les cotes - elles affectent beaucoup. Lors de la création de tels effets, souvent quelque chose est sélectionné par essais et erreurs et il est très utile de développer en soi une «intuition mathématique» - la capacité d'imaginer plus ou moins comment une fonction se comporte, comment cela dépend de quelles variables.
À ce stade, nous avons une image intéressante, mais un peu maladroite. Alors d'abord, jetons un coup d'œil au post-traitement, puis passons à un exemple vivant.
Post-traitement
La capacité de faire quelque chose avec l'image Three.js rendue est une chose très utile, tout en étant injustement oubliée dans de nombreuses séries de leçons. Techniquement, cela est implémenté comme suit: l'image que le rendu nous a donnée est envoyée à EffectComposer (tant qu'il s'agit d'une boîte noire), qui chamanise quelque chose en soi et affiche l'image finale sur la toile. C'est-à-dire qu'après le rendu, un module supplémentaire est ajouté. Nous transférons les paramètres à ce compositeur - que faire de l'image reçue. Un de ces paramètres est appelé pass. Dans un sens, le compositeur fonctionne comme certains Gulp - il ne fait rien, nous lui donnons des plugins qui font déjà le travail. Il n'est peut-être pas tout à fait exact de le dire, mais l'idée devrait être claire.
Tout ce que nous utiliserons plus loin n'est pas inclus dans la structure de base de Three.js, nous connectons donc quelques dépendances et dépendances des dépendances elles-mêmes:
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/EffectComposer.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/RenderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/ShaderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/CopyShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/LuminosityHighPassShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/UnrealBloomPass.js'></script>
N'oubliez pas que ces scripts sont inclus dans les trois packages et que vous pouvez mettre tout cela en un seul paquet en utilisant un webpack ou des analogues.
Dans sa forme de base, le compositeur est créé comme ceci:
COMPOSER = new THREE.EffectComposer(RENDERER); COMPOSER.setSize(window.innerWidth, window.innerHeight); const renderPass = new THREE.RenderPass(SCENE, CAMERA); renderPass.renderToScreen = true; COMPOSER.addPass(renderPass);
RenderPass ne fait rien de nouveau. Il rend juste ce que nous obtenions d'un rendu normal. En fait, si vous regardez le code source de RenderPass, vous pouvez y trouver le rendu standard. Puisque maintenant le rendu se passe là-bas, nous devons remplacer le rendu par le compositeur dans notre script:
function render() {
Cette approche utilisant RenderPass comme première passe est une pratique standard lorsque vous travaillez avec EffectComposer. Habituellement, nous devons d'abord obtenir une image rendue de la scène, puis en faire quelque chose.
Dans les exemples de Three.js, dans la section de post-traitement, vous pouvez trouver une chose appelée UnrealBloomPass. Il s'agit d'un script porté par le moteur Unreal. Il ajoute une petite lueur qui peut être utilisée pour créer un éclairage plus beau. Ce sera souvent la première étape vers l'amélioration de l'image.
const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 1, 0.1); bloomPass.renderToScreen = true; COMPOSER.addPass(bloomPass);
Remarque: l'option renderToScreen est définie uniquement sur le dernier passage que nous avons passé au compositeur.
Mais voyons déjà quel genre d'éclat ce bloomPass nous a donné et comment il s'intègre à la sphère:
D'accord, c'est beaucoup plus intéressant qu'une simple sphère et une source de lumière ordinaire, comme ils sont généralement montrés dans les premières leçons sur Three.js.
Mais nous irons encore plus loin ...
Plus de shaders au dieu shader!

Il est très utile d'utiliser console.log et de regarder la structure du compositeur. Vous y trouverez des éléments portant les noms renderTarget1, renderTarget2, etc., où les nombres correspondent aux indices des passes passées. Et puis, il devient clair pourquoi EffectComposer est ainsi appelé. Il fonctionne sur le principe des filtres en SVG. Rappelez-vous, là, vous pouvez utiliser le résultat de l'exécution de certains filtres dans d'autres? Voici la même chose - vous pouvez combiner des effets.
L'utilisation de console.log pour comprendre la structure interne des objets Three.js et de nombreuses autres bibliothèques est très utile. Utilisez cette approche plus souvent pour mieux comprendre ce qui est quoi.
Ajoutez un autre pass. Cette fois, ce sera ShaderPass.
const shader = { uniforms: { uRender: { value: COMPOSER.renderTarget2 }, uTime: { value: TIME } }, vertexShader: document.getElementById('postprocessing-vertex-shader').textContent, fragmentShader: document.getElementById('postprocessing-fragment-shader').textContent }; const shaderPass = new THREE.ShaderPass(shader); shaderPass.renderToScreen = true; COMPOSER.addPass(shaderPass);
RenderTarget2 contient le résultat de la passe précédente - bloomPass (c'était le deuxième d'affilée), nous l'utilisons comme texture (c'est essentiellement une image rendue plate) et la transmettons comme variable uniforme au nouveau shader.
Il vaut probablement la peine de freiner et de réaliser toute la magie ici ...
Ensuite, créez un vertex shader simple. Dans la plupart des cas, à ce stade, nous n'avons rien à faire avec les sommets, nous passons uniquement les coordonnées (u, v) au fragment shader:
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Et de manière fragmentaire, nous pouvons nous amuser à notre goût et notre couleur. Par exemple, nous pouvons ajouter un léger effet de pépin, tout rendre noir et blanc et jouer avec la luminosité / le contraste:
uniform sampler2D uRender; uniform float uTime; varying vec2 vUv; float rand(vec2); void main() { float randomValue = rand(vec2(floor(vUv.y * 7.0), uTime / 1.0)); vec4 color; if (randomValue < 0.02) { color = texture2D(uRender, vec2(vUv.x + randomValue - 0.01, vUv.y)); } else { color = texture2D(uRender, vUv); } float lightness = (color.r + color.g + color.b) / 3.0; color.rgb = vec3(smoothstep(0.02, 0.7, lightness)); gl_FragColor = color; } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); }
Regardons le résultat:
Comme vous pouvez le voir, des filtres ont été superposés à la sphère. Il est toujours en trois dimensions, rien n'a cassé, mais sur la toile nous avons une image traitée.
Conclusion
Les matériaux de shader et le post-traitement dans Three.js sont deux petits outils très puissants qui valent vraiment la peine d'être utilisés. Il existe de nombreuses options pour leur utilisation - tout est limité par votre imagination. Même les scènes les plus simples avec leur aide peuvent être modifiées au-delà de la reconnaissance.