Construire des shaders avec Babylon.js
Publié: 2022-03-10Les shaders sont un concept clé si vous souhaitez libérer la puissance brute de votre GPU. Je vais vous aider à comprendre comment ils fonctionnent et même à expérimenter leur pouvoir intérieur de manière simple, grâce à Babylon.js.
Comment ça marche?
Avant d'expérimenter, il faut voir comment les choses fonctionnent en interne.
Lorsqu'il s'agit de 3D à accélération matérielle, vous devrez faire face à deux processeurs : le processeur principal et le GPU. Le GPU est une sorte de CPU extrêmement spécialisé.
Lectures complémentaires sur SmashingMag :
- Créer un jeu WebGL multiplateforme avec Babylon.js
- Utilisation de l'API Gamepad dans les jeux Web
- Introduction à la modélisation polygonale et à Three.js
- Comment créer une boîte à rythmes 8 bits réactive
Le GPU est une machine d'état que vous configurez à l'aide du CPU. Par exemple, le CPU configurera le GPU pour restituer des lignes au lieu de triangles ; il définira si la transparence est activée ; etc.
Une fois tous les états définis, le CPU peut définir ce qu'il faut rendre : la géométrie.
La géométrie est composée de :
- une liste de points appelés sommets et stockés dans un tableau appelé vertex buffer,
- une liste d'index qui définissent les faces (ou triangles) stockées dans un tableau nommé index buffer.
La dernière étape pour le CPU est de définir comment rendre la géométrie ; pour cette tâche, le CPU définira les shaders dans le GPU. Les shaders sont des morceaux de code que le GPU exécutera pour chacun des sommets et pixels qu'il doit rendre. (Un sommet — ou des sommets lorsqu'il y en a plusieurs — est un « point » en 3D).
Il existe deux types de shaders : les vertex shaders et les pixel shaders (ou fragment).
Pipeline graphique
Avant de creuser dans les shaders, revenons en arrière. Pour restituer les pixels, le GPU prendra la géométrie définie par le CPU et fera ce qui suit :
- En utilisant le tampon d'index, trois sommets sont rassemblés pour définir un triangle.
- Le tampon d'index contient une liste d'index de vertex. Cela signifie que chaque entrée dans le tampon d'index est le numéro d'un sommet dans le tampon de sommets.
- C'est vraiment utile pour éviter la duplication des sommets.
Par exemple, le tampon d'index suivant est une liste de deux faces : [1 2 3 1 3 4]. La première face contient le sommet 1, le sommet 2 et le sommet 3. La deuxième face contient le sommet 1, le sommet 3 et le sommet 4. Il y a donc quatre sommets dans cette géométrie :
Le vertex shader est appliqué à chaque sommet du triangle. L'objectif principal du vertex shader est de produire un pixel pour chaque vertex (la projection sur l'écran 2D du vertex 3D) :
En utilisant ces trois pixels (qui définissent un triangle 2D à l'écran), le GPU interpolera toutes les valeurs attachées au pixel (au moins leurs positions), et le pixel shader sera appliqué à chaque pixel inclus dans le triangle 2D afin de générer une couleur pour chaque pixel :
Ce processus est effectué pour chaque face définie par le tampon d'index.
De toute évidence, en raison de sa nature parallèle, le GPU est capable de traiter cette étape pour de nombreux visages simultanément et d'obtenir de très bonnes performances.
GLSL
Nous venons de voir que pour restituer des triangles, le GPU a besoin de deux shaders : le vertex shader et le pixel shader. Ces shaders sont écrits dans un langage nommé Graphics Library Shader Language (GLSL). Il ressemble à C.
Voici un exemple d'un vertex shader courant :
precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Structure du nuanceur de vertex
Un vertex shader contient les éléments suivants :
- Attributs . Un attribut définit une partie d'un sommet. Par défaut, un sommet doit au moins contenir une position (un
vector3:x, y, z
). Cependant, en tant que développeur, vous pouvez décider d'ajouter plus d'informations. Par exemple, dans l'ancien shader, il y a unvector2
nomméuv
(c'est-à-dire des coordonnées de texture qui permettent d'appliquer une texture 2D à un objet 3D). - Uniformes . Un uniforme est une variable utilisée par le shader et définie par le CPU. Le seul uniforme que nous ayons ici est une matrice utilisée pour projeter la position du sommet (x, y, z) sur l'écran (x, y).
- Variant . Les variables variables sont des valeurs créées par le vertex shader et transmises au pixel shader. Ici, le vertex shader transmettra une
vUV
(une simple copie deuv
) au pixel shader. Cela signifie qu'un pixel est défini ici avec une position et des coordonnées de texture. Ces valeurs seront interpolées par le GPU et utilisées par le pixel shader. - Principal . La fonction nommée
main
est le code exécuté par le GPU pour chaque vertex et doit au moins produire une valeur pourgl_position
(la position du vertex courant à l'écran).
Nous pouvons voir dans notre exemple que le vertex shader est assez simple. Il génère une variable système (commençant par gl_
) nommée gl_position
pour définir la position du pixel associé, et il définit une variable variable appelée vUV
.
Le vaudou derrière les matrices
La chose à propos de notre shader est que nous avons une matrice nommée worldViewProjection
, et nous utilisons cette matrice pour projeter la position du sommet sur la variable gl_position
. C'est cool, mais comment obtient-on la valeur de cette matrice ? C'est un uniforme, nous devons donc le définir côté CPU (en utilisant JavaScript).
C'est l'une des parties complexes de la 3D. Vous devez comprendre des mathématiques complexes (ou vous devrez utiliser un moteur 3D tel que Babylon.js, que nous verrons plus tard).
La matrice worldViewProjection
est la combinaison de trois matrices différentes :
L'utilisation de la matrice résultante nous permet de transformer des sommets 3D en pixels 2D, tout en prenant en compte le point de vue et tout ce qui concerne la position, l'échelle et la rotation de l'objet courant.
C'est votre responsabilité en tant que développeur 3D : créer et tenir à jour cette matrice.
Retour aux shaders
Une fois que le vertex shader est exécuté sur chaque vertex (trois fois, alors), nous aurons trois pixels avec la bonne gl_position
et une valeur vUV
. Le GPU va interpoler ces valeurs sur chaque pixel contenu dans le triangle réalisé avec ces pixels.
Ensuite, pour chaque pixel, il exécutera le pixel shader :
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Structure du shader de pixel (ou de fragment)
La structure d'un pixel shader est similaire à celle d'un vertex shader :
- Variant . Les variables variables sont des valeurs créées par le vertex shader et transmises au pixel shader. Ici, le pixel shader recevra une valeur
vUV
du vertex shader. - Uniformes . Un uniforme est une variable utilisée par le shader et définie par le CPU. Le seul uniforme que nous avons ici est un échantillonneur, qui est un outil utilisé pour lire les couleurs de texture.
- Principal . La fonction nommée
main
est le code exécuté par le GPU pour chaque pixel et qui doit au moins produire une valeur pourgl_FragColor
(ie la couleur du pixel courant).
Ce pixel shader est assez simple : il lit la couleur de la texture en utilisant les coordonnées de texture du vertex shader (qui, à son tour, l'obtient du vertex).
Le problème est que lorsque les shaders sont développés, vous n'êtes qu'à mi-chemin, car vous devez alors gérer beaucoup de code WebGL. En effet, WebGL est vraiment puissant mais aussi vraiment bas niveau, et vous devez tout faire vous-même, de la création des tampons à la définition des structures de vertex. Vous devez également faire tous les calculs, définir tous les états, gérer le chargement de la texture, etc.
Trop dur? BABYLON.ShaderMaterial à la rescousse
Je sais ce que vous pensez : "Les shaders sont vraiment cool, mais je ne veux pas m'embêter avec la plomberie interne de WebGL ou même avec les maths."
Et tu as raison ! C'est une question parfaitement légitime, et c'est exactement pour cela que j'ai créé Babylon.js !
Pour utiliser Babylon.js, vous avez d'abord besoin d'une simple page Web :
<!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Normal varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; } </script> <script type="application/fragmentShader"> precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); } </script> <script src="index.js"></script> <style> html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; margin: 0px; overflow: hidden; } #renderCanvas { width: 100%; height: 100%; touch-action: none; -ms-touch-action: none; } </style> </head> <body> <canvas></canvas> </body> </html>
Vous remarquerez que les shaders sont définis par des balises <script>
. Avec Babylon.js, vous pouvez également les définir dans des fichiers séparés (fichiers .fx
).
- Source Babylon.js
- Référentiel GitHub
Enfin, le code JavaScript principal est celui-ci :
"use strict"; document.addEventListener("DOMContentLoaded", startGame, false); function startGame() { if (BABYLON.Engine.isSupported()) { var canvas = document.getElementById("renderCanvas"); var engine = new BABYLON.Engine(canvas, false); var scene = new BABYLON.Scene(engine); var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas); // Creating sphere var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene); var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] }); amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene)); sphere.material = amigaMaterial; engine.runRenderLoop(function () { sphere.rotation.y += 0.05; scene.render(); }); } };
Vous pouvez voir que j'utilise BABYLON.ShaderMaterial
pour me débarrasser du fardeau de la compilation, de la liaison et de la gestion des shaders.
Lorsque vous créez BABYLON.ShaderMaterial
, vous devez spécifier l'élément DOM utilisé pour stocker les shaders ou le nom de base des fichiers où se trouvent les shaders. Si vous choisissez d'utiliser des fichiers, vous devez créer un fichier pour chaque shader et utiliser le modèle suivant : basename.vertex.fx
et basename.fragment.fx
. Ensuite, vous devrez créer le matériel comme ceci :
var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });
Vous devez également spécifier les noms des attributs et des uniformes que vous utilisez.
Ensuite, vous pouvez définir directement les valeurs de vos uniformes et échantillonneurs à l'aide setTexture
, setFloat
, setFloats
, setColor3
, setColor4
, setVector2
, setVector3
, setVector4
, setMatrix
.
Assez simple, non?
Et vous souvenez-vous de la précédente matrice worldViewProjection
, utilisant Babylon.js et BABYLON.ShaderMaterial
. Vous n'avez tout simplement pas à vous en soucier! BABYLON.ShaderMaterial
le calculera automatiquement pour vous car vous le déclarerez dans la liste des uniformes.
BABYLON.ShaderMaterial
peut également gérer pour vous les matrices suivantes :
-
world
, -
view
, -
projection
, -
worldView
, -
worldViewProjection
.
Plus besoin de maths. Par exemple, chaque fois que vous exécutez sphere.rotation.y += 0.05
, la matrice world
de la sphère sera générée pour vous et transmise au GPU.
Voyez par vous-même le résultat en direct.
Créez votre propre shader (CYOS)
Maintenant, allons plus loin et créons une page où vous pouvez créer dynamiquement vos propres shaders et voir le résultat immédiatement. Cette page va utiliser le même code dont nous avons parlé précédemment et va utiliser l'objet BABYLON.ShaderMaterial
pour compiler et exécuter les shaders que vous allez créer.
J'ai utilisé l'éditeur de code ACE pour Create Your Own Shader (CYOS). C'est un éditeur de code incroyable, avec une coloration syntaxique. N'hésitez pas à y jeter un œil.
À l'aide de la première zone de liste déroulante, vous pourrez sélectionner des shaders prédéfinis. Nous verrons chacun d'eux juste après.
Vous pouvez également modifier le maillage (c'est-à-dire l'objet 3D) utilisé pour prévisualiser vos shaders à l'aide de la deuxième zone de liste déroulante.
Le bouton de compilation est utilisé pour créer un nouveau BABYLON.ShaderMaterial
à partir de vos shaders. Le code utilisé par ce bouton est le suivant :
// Compile shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "normal", "uv"], uniforms: ["world", "worldView", "worldViewProjection"] }); var refTexture = new BABYLON.Texture("ref.jpg", scene); refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; var amigaTexture = new BABYLON.Texture("amiga.jpg", scene); shaderMaterial.setTexture("textureSampler", amigaTexture); shaderMaterial.setTexture("refSampler", refTexture); shaderMaterial.setFloat("time", 0); shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero()); shaderMaterial.backFaceCulling = false; mesh.material = shaderMaterial;
Incroyablement simple, non ? Le matériel est prêt à vous envoyer trois matrices pré-calculées ( world
, worldView
et worldViewProjection
). Les sommets seront accompagnés de coordonnées de position, de normale et de texture. Deux textures sont également déjà chargées pour vous :
Enfin, le renderLoop
est l'endroit où je mets à jour deux uniformes pratiques :
- L'un s'appelle le
time
et obtient des animations amusantes. - L'autre s'appelle
cameraPosition
, qui obtient la position de la caméra dans vos shaders (utile pour les équations d'éclairage).
engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });
Nuanceur de base
Commençons par le tout premier shader défini dans CYOS : le shader de base.
Nous connaissons déjà ce shader. Il calcule la gl_position
et utilise les coordonnées de texture pour récupérer une couleur pour chaque pixel.
Pour calculer la position du pixel, nous avons juste besoin de la matrice worldViewProjection
et de la position du sommet :
precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Les coordonnées de texture ( uv
) sont transmises sans modification au pixel shader.
Veuillez noter que nous devons ajouter precision mediump float
sur la première ligne pour les shaders de vertex et de pixels car Chrome l'exige. Il précise que, pour de meilleures performances, nous n'utilisons pas de valeurs flottantes de précision complète.
Le pixel shader est encore plus simple, car il suffit d'utiliser les coordonnées de texture et de récupérer une couleur de texture :
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Nous avons vu précédemment que l'uniforme textureSampler
est rempli avec la texture amiga
. Ainsi, le résultat est le suivant :
Nuanceur noir et blanc
Continuons avec un nouveau shader : le shader noir et blanc. Le but de ce shader est d'utiliser le précédent mais avec un mode de rendu noir et blanc uniquement.
Pour ce faire, nous pouvons conserver le même vertex shader. Le pixel shader sera légèrement modifié.
La première option que nous avons est de ne prendre qu'un seul composant, comme le vert :
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }
Comme vous pouvez le voir, au lieu d'utiliser .rgb
(cette opération s'appelle un swizzle), nous avons utilisé .ggg
.
Mais si on veut un effet noir et blanc vraiment précis, alors calculer la luminance (qui tient compte de toutes les composantes) serait mieux :
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11)); gl_FragColor = vec4(luminance, luminance, luminance, 1.0); }
L'opération dot
(ou produit dot
) est calculée comme ceci : result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z
.
Donc, dans notre cas, luminance = r * 0.3 + g * 0.59 + b * 0.11
. (Ces valeurs sont basées sur le fait que l'œil humain est plus sensible au vert.)
Ça a l'air cool, n'est-ce pas ?
Nuanceur d'ombrage cellulaire
Passons à un shader plus complexe : le shader cell-shading.
Celui-ci nous demandera d'obtenir la normale et la position du sommet dans le pixel shader. Ainsi, le vertex shader ressemblera à ceci :
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 world; uniform mat4 worldViewProjection; // Varying varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vPositionW = vec3(world * vec4(position, 1.0)); vNormalW = normalize(vec3(world * vec4(normal, 0.0))); vUV = uv; }
Veuillez noter que nous utilisons également la matrice monde car la position et la normale sont stockées sans aucune transformation, et nous devons appliquer la matrice monde pour prendre en compte la rotation de l'objet.
Le pixel shader est le suivant :
precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { float ToonThresholds[4]; ToonThresholds[0] = 0.95; ToonThresholds[1] = 0.5; ToonThresholds[2] = 0.2; ToonThresholds[3] = 0.03; float ToonBrightnessLevels[5]; ToonBrightnessLevels[0] = 1.0; ToonBrightnessLevels[1] = 0.8; ToonBrightnessLevels[2] = 0.6; ToonBrightnessLevels[3] = 0.35; ToonBrightnessLevels[4] = 0.2; vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); vec3 color = texture2D(textureSampler, vUV).rgb; if (ndl > ToonThresholds[0]) { color *= ToonBrightnessLevels[0]; } else if (ndl > ToonThresholds[1]) { color *= ToonBrightnessLevels[1]; } else if (ndl > ToonThresholds[2]) { color *= ToonBrightnessLevels[2]; } else if (ndl > ToonThresholds[3]) { color *= ToonBrightnessLevels[3]; } else { color *= ToonBrightnessLevels[4]; } gl_FragColor = vec4(color, 1.); }
Le but de ce shader est de simuler la lumière, et au lieu de calculer un ombrage lisse, nous appliquerons la lumière en fonction de seuils de luminosité spécifiques. Par exemple, si l'intensité lumineuse est comprise entre 1 (maximum) et 0,95, la couleur de l'objet (extraite de la texture) serait appliquée directement. Si l'intensité est comprise entre 0,95 et 0,5, la couleur serait atténuée d'un facteur 0,8. Etc.
Il y a principalement quatre étapes dans ce shader.
Tout d'abord, nous déclarons des seuils et des niveaux constants.
Ensuite, nous calculons l'éclairage à l'aide de l'équation de Phong (nous considérerons que la lumière ne bouge pas) :
vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));
L'intensité de la lumière par pixel dépend de l'angle entre la direction normale et la direction de la lumière.
Ensuite, nous obtenons la couleur de texture pour le pixel.
Enfin, nous vérifions le seuil et appliquons le niveau à la couleur.
Le résultat ressemble à un objet de dessin animé :
Nuanceur de Phong
Nous avons utilisé une partie de l'équation de Phong dans le shader précédent. Utilisons-le complètement maintenant.
Le vertex shader est ici clairement simple car tout se fera dans le pixel shader :
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vUV = uv; vPosition = position; vNormal = normal; }
Selon l'équation, nous devons calculer les parties "diffuse" et "spéculaire" en utilisant la direction de la lumière et la normale au sommet :
precision highp float; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; // Uniforms uniform mat4 world; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 vLightPosition = vec3(0, 20, 10); // World values vec3 vPositionW = vec3(world * vec4(vPosition, 1.0)); vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0))); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); vec3 color = texture2D(textureSampler, vUV).rgb; // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); // Specular vec3 angleW = normalize(viewDirectionW + lightVectorW); float specComp = max(0., dot(vNormalW, angleW)); specComp = pow(specComp, max(1., 64.)) * 2.; gl_FragColor = vec4(color * ndl + vec3(specComp), 1.); }
Nous avons déjà utilisé la partie diffuse dans le shader précédent, il suffit donc ici d'ajouter la partie spéculaire. Vous pouvez trouver plus d'informations sur l'ombrage Phong sur Wikipedia.
Le résultat de notre sphère :
Supprimer le shader
Pour le shader de rejet, je voudrais introduire un nouveau concept : le mot clé de discard
.
Ce shader supprime tous les pixels non rouges et crée l'illusion d'un objet creusé.
Le vertex shader est le même que celui utilisé par le shader de base :
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }
Le pixel shader de son côté devra tester la couleur et utiliser la suppression lorsque, par exemple, la composante verte est trop élevée :
precision highp float; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { vec3 color = texture2D(textureSampler, vUV).rgb; if (color.g > 0.5) { discard; } gl_FragColor = vec4(color, 1.); }
Le résultat est un peu drôle :
Ombrage de vague
Nous avons beaucoup joué avec le pixel shader, mais je veux aussi vous faire savoir que nous pouvons faire beaucoup de choses avec les vertex shaders.
Pour le wave shader, nous réutiliserons le pixel shader de Phong.
Le vertex shader utilisera le time
nommé uniforme pour obtenir des valeurs animées. En utilisant cet uniforme, le shader générera une vague avec les positions des sommets :
precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; uniform float time; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec3 v = position; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }
Un sinus est appliqué à position.y
, et le résultat est le suivant :
Cartographie d'environnement sphérique
Celui-ci a été largement inspiré de l'article "Creating a Spherical Reflection/Environment Mapping Shader". Je vous laisse lire cet excellent article et jouer avec le shader associé.
Nuanceur de Fresnel
Je voudrais conclure cet article avec mon préféré : le shader de Fresnel.
Ce shader permet d'appliquer une intensité différente selon l'angle entre la direction de la vue et la normale du vertex.
Le vertex shader est le même que celui utilisé par le cell-shading shader, et nous pouvons facilement calculer le terme de Fresnel dans notre pixel shader (car nous avons la normale et la position de la caméra, qui peuvent être utilisées pour évaluer la direction de la vue) :
precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 color = vec3(1., 1., 1.); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Fresnel float fresnelTerm = dot(viewDirectionW, vNormalW); fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.); gl_FragColor = vec4(color * fresnelTerm, 1.); }
Votre shader ?
Vous êtes maintenant mieux préparé à créer votre propre shader. N'hésitez pas à poster sur le forum Babylon.js pour partager vos expériences !
Si vous souhaitez aller plus loin, voici quelques liens utiles :
- Babylon.js, site officiel
- Babylon.js, référentiel GitHub
- Forum Babylon.js, développeurs de jeux HTML5
- Créez votre propre shader (CYOS), Babylon.js
- Langage d'ombrage OpenGL », Wikipédia
- Langage d'ombrage OpenGL, documentation