Dans mon Guide du débutant pour les shaders, je me suis concentré exclusivement sur les shaders de fragments, ce qui est suffisant pour tout effet 2D et chaque exemple de ShaderToy. Mais il existe toute une catégorie de techniques qui nécessitent des vertex shaders. Ce didacticiel vous guidera dans la création d’eaux stylisées en introduisant des vertex shaders. Je vais également vous présenter le tampon de profondeur et comment l'utiliser pour obtenir plus d'informations sur votre scène et créer des lignes de mousse..
Voici à quoi devrait ressembler l'effet final. Vous pouvez essayer une démonstration en direct ici (souris gauche pour orbite, souris droite pour faire un panoramique, molette de la souris pour zoomer).
Plus précisément, cet effet est composé de:
Ce que j'aime dans cet effet, c'est qu'il aborde un grand nombre de concepts différents en infographie, ce qui nous permettra de puiser dans les idées de didacticiels précédents et de développer des techniques que nous pourrons utiliser pour une variété d'effets futurs..
J'utiliserai PlayCanvas pour cela uniquement parce qu'il dispose d'un IDE gratuit, basé sur le Web, mais tout devrait être applicable à tout environnement exécutant WebGL. Vous pouvez trouver une version Three.js du code source à la fin. Je supposerai que vous êtes à l'aise avec l'utilisation des shaders de fragments et la navigation dans l'interface PlayCanvas. Vous pouvez modifier les shaders ici et parcourir une introduction à PlayCanvas ici.
Le but de cette section est de mettre en place notre projet PlayCanvas et de placer des objets d’environnement pour tester l’eau contre.
Si vous n'avez pas encore de compte PlayCanvas, inscrivez-vous et créez un nouveau compte. projet vierge. Par défaut, vous devez avoir deux objets, une caméra et une lumière dans votre scène..
Le projet Poly de Google est une excellente ressource pour les modèles 3D destinés au Web. Voici le modèle de bateau que j'ai utilisé. Une fois que vous avez téléchargé et décompressé, vous devriez trouver un .obj
et un .png
fichier.
.png
fichier.Maintenant, vous pouvez faire glisser le Remorqueur.json dans votre scène et supprimez les objets Boîte et Plan. Vous pouvez redimensionner le bateau s'il semble trop petit (je mets le mien à 50).
Vous pouvez ajouter d'autres modèles à votre scène de la même manière..
Pour configurer une caméra orbite, nous allons copier un script à partir de cet exemple PlayCanvas. Allez sur ce lien et cliquez sur Éditeur entrer dans le projet.
mouse-input.js
et orbite-camera.js
de ce projet de tutoriel dans les fichiers du même nom dans votre propre projet.Conseil: vous pouvez créer des dossiers dans la fenêtre d’éléments pour que tout reste organisé. Je mets ces deux scripts de caméra sous Scripts / Appareil photo /, mon modèle sous Modèles / et mon matériel sous Matériel /.
Maintenant, lorsque vous lancez le jeu (bouton de lecture en haut à droite de la vue de la scène), vous devriez pouvoir voir votre bateau et le survoler avec la souris.
Le but de cette section est de générer un maillage subdivisé à utiliser comme surface d’eau..
Pour générer la surface de l'eau, nous allons adapter du code issu de ce didacticiel de génération de terrain. Créez un nouveau fichier de script appelé Water.js
. Editez ce script et créez une nouvelle fonction appelée GeneratePlaneMesh
cela ressemble à ceci:
Water.prototype.GeneratePlaneMesh = fonction (options) // 1 - Définit les options par défaut si aucune n'est fournie si (options === undefined) options = subdivisions: 100, width: 10, height: 10; // 2 - Génère des points, des UV et des indices var positions = []; var uvs = []; var indices = []; var row, col; var normales; pour (rangée = 0; rangée <= options.subdivisions; row++) for (col = 0; col <= options.subdivisions; col++) var position = new pc.Vec3((col * options.width) / options.subdivisions - (options.width / 2.0), 0, ((options.subdivisions - row) * options.height) / options.subdivisions - (options.height / 2.0)); positions.push(position.x, position.y, position.z); uvs.push(col / options.subdivisions, 1.0 - row / options.subdivisions); for (row = 0; row < options.subdivisions; row++) for (col = 0; col < options.subdivisions; col++) indices.push(col + row * (options.subdivisions + 1)); indices.push(col + 1 + row * (options.subdivisions + 1)); indices.push(col + 1 + (row + 1) * (options.subdivisions + 1)); indices.push(col + row * (options.subdivisions + 1)); indices.push(col + 1 + (row + 1) * (options.subdivisions + 1)); indices.push(col + (row + 1) * (options.subdivisions + 1)); // Compute the normals normals = pc.calculateNormals(positions, indices); // Make the actual model var node = new pc.GraphNode(); var material = new pc.StandardMaterial(); // Create the mesh var mesh = pc.createMesh(this.app.graphicsDevice, positions, normals: normals, uvs: uvs, indices: indices ); var meshInstance = new pc.MeshInstance(node, mesh, material); // Add it to this entity var model = new pc.Model(); model.graph = node; model.meshInstances.push(meshInstance); this.entity.addComponent('model'); this.entity.model.model = model; this.entity.model.castShadows = false; // We don't want the water surface itself to cast a shadow ;
Maintenant, vous pouvez appeler cela dans le initialiser
une fonction:
Water.prototype.initialize = function () this.GeneratePlaneMesh (subdivisions: 100, width: 10, height: 10); ;
Vous devriez voir juste un avion plat lorsque vous lancez le jeu maintenant. Mais ce n'est pas juste un avion plat. C'est un maillage composé de mille sommets. Essayez de vérifier cela comme un défi (c’est une bonne excuse pour lire le code que vous venez de copier)..
Défi n ° 1: déplacer la coordonnée Y de chaque sommet d'une quantité aléatoire pour que le plan ressemble à l'image ci-dessous..
Le but de cette section est de donner à la surface de l’eau un matériau personnalisé et de créer des vagues animées..
Pour obtenir les effets souhaités, nous devons créer un matériau personnalisé. La plupart des moteurs 3D auront des shaders prédéfinis pour le rendu des objets et un moyen de les remplacer. Voici une bonne référence pour le faire dans PlayCanvas.
Créons une nouvelle fonction appelée CreateWaterMaterial
qui définit un nouveau matériau avec un shader personnalisé et le renvoie:
Water.prototype.CreateWaterMaterial = function () // Créer un nouveau matériau vierge var material = new pc.Material (); // Un nom facilite l'identification lors du débogage de material.name = "DynamicWater_Material"; // Crée la définition du shader // définit dynamiquement la précision en fonction du périphérique. var gd = this.app.graphicsDevice; var fragmentShader = "precision" + gd.precision + "float; \ n"; fragmentShader = fragmentShader + this.fs.resource; var vertexShader = this.vs.resource; // Une définition de shader utilisée pour créer un nouveau shader. var shaderDefinition = attributs: unePosition: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0,, vshader: vertexShader, fshader: fragmentShader; // Crée le shader à partir de la définition this.shader = new pc.Shader (gd, shaderDefinition); // Appliquer le shader à ce matériau material.setShader (this.shader); retourner le matériel; ;
Cette fonction récupère le code shader de sommets et de fragments à partir des attributs de script. Définissons donc ceux qui se trouvent en haut du fichier (après le pc.createScript
ligne):
Water.attributes.add ('vs', type: 'asset', assetType: 'shader', titre: 'Vertex Shader'); Water.attributes.add ('fs', type: 'actif', assetType: 'shader', titre: 'Fragment Shader');
Nous pouvons maintenant créer ces fichiers de shader et les attacher à notre script. Retournez dans l'éditeur et créez deux nouveaux fichiers de shader: Water.frag et Eau.vert. Attachez ces shaders à votre script comme indiqué ci-dessous.
Si les nouveaux attributs n'apparaissent pas dans l'éditeur, cliquez sur le bouton Analyser bouton pour actualiser le script.
Maintenant, mettez ce shader de base dans Water.frag:
vide principal (vide) vec4 color = vec4 (0,0,0,0,1,0,0,5); gl_FragColor = couleur;
Et ceci dans Water.vert:
attribut vec3 aPosition; uniforme mat4 matrix_model; uniforme mat4 matrix_viewProjection; void main (void) gl_Position = matrix_viewProjection * matrix_model * vec4 (aPosition, 1.0);
Enfin, retournez à Water.js et le faire utiliser notre nouveau matériau personnalisé au lieu du matériau standard. Donc au lieu de:
var material = new pc.StandardMaterial ();
Faire:
var material = this.CreateWaterMaterial ();
Maintenant, si vous lancez le jeu, l'avion devrait maintenant être bleu.
Jusqu'à présent, nous venons d'installer des shaders factices sur notre nouveau matériel. Avant d’écrire les effets réels, une dernière chose que je souhaite configurer est le rechargement automatique du code..
Décommenter le échanger
La fonction de tout fichier de script (tel que Water.js) permet le rechargement à chaud. Nous verrons comment l'utiliser plus tard pour maintenir l'état même lorsque nous mettons à jour le code en temps réel. Mais pour l'instant, nous souhaitons simplement réappliquer les shaders une fois que nous avons détecté un changement. Les shaders sont compilés avant d’être exécutés dans WebGL, nous aurons donc besoin de recréer le matériel personnalisé pour le déclencher..
Nous allons vérifier si le contenu de notre code de shader a été mis à jour et, le cas échéant, recréer le matériau. Tout d’abord, enregistrez les shaders actuels dans initialiser:
// initialise le code appelé une fois par entité Water.prototype.initialize = function () this.GeneratePlaneMesh (); // Enregistrer les shaders actuels this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ;
Et dans le mettre à jour, vérifiez s'il y a eu des changements:
// code de mise à jour appelé chaque image Water.prototype.update = fonction (dt) if (this.savedFS! = this.fs.resource || this.savedVS! = this.vs.resource) // Recréez le matériau afin que les shaders puissent être recompilés var newMaterial = this.CreateWaterMaterial (); // Appliquez-le au modèle var model = this.entity.model.model; model.meshInstances [0] .material = newMaterial; // Enregistrer les nouveaux shaders this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ;
Maintenant, pour confirmer cela fonctionne, lancez le jeu et changez la couleur de l'avion dans Water.frag à un bleu plus de bon goût. Une fois le fichier sauvegardé, il devrait être mis à jour sans avoir à actualiser ou à relancer! C'était la couleur que j'ai choisie:
couleur vec4 = vec4 (0,0,0,7,1,0,0,5);
Pour créer des vagues, nous devons déplacer chaque sommet dans notre maillage, chaque image. Cela semble être très inefficace, mais chaque sommet de chaque modèle est déjà transformé sur chaque image rendue. C'est ce que fait le vertex shader.
Si vous pensez à un fragment shader comme une fonction qui s'exécute sur chaque pixel, prend une position et renvoie une couleur, un vertex shader est une fonction qui s'exécute sur chaque sommet, prend une position et retourne une position.
Le vertex shader par défaut prendra la position mondiale d'un modèle donné, et renvoyer le position de l'écran. Notre scène 3D est définie en termes de x, y et z, mais votre moniteur est un plan plat bidimensionnel. Nous projetons donc notre monde 3D sur notre écran 2D. Cette projection correspond à ce que les matrices de vue, de projection et de modèle prennent en charge et sort du cadre de ce didacticiel, mais si vous souhaitez savoir exactement ce qui se passe à cette étape, voici un très bon guide.
Donc cette ligne:
gl_Position = matrix_viewProjection * matrix_model * vec4 (aPosition, 1.0);
Prend une position
comme la position du monde 3D d'un sommet particulier et le transforme en gl_Position
, qui est la position finale de l'écran 2D. Le préfixe 'a' sur aPosition signifie que cette valeur est un attribut. Rappelez-vous qu'un uniformevariable est une valeur que nous pouvons définir sur la CPU à transmettre à un shader qui conserve la même valeur pour tous les pixels / sommets. La valeur d'un attribut, d'autre part, provient d'un tableau défini sur la CPU. Le vertex shader est appelé une fois pour chaque valeur de ce tableau d'attributs.
Vous pouvez voir que ces attributs sont définis dans la définition du shader que nous avons définie dans Water.js:
var shaderDefinition = attributs: unePosition: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0,, vshader: vertexShader, fshader: fragmentShader;
PlayCanvas s’occupe de la configuration et de la transmission d’un tableau de positions de sommet pour une position
quand nous passons cette énumération, mais en général, vous pouvez passer n'importe quel tableau de données au vertex shader.
Disons que vous voulez écraser l'avion en multipliant tous X
valeurs de moitié. Si vous changez une position
ou gl_Position
?
Essayons une position
premier. Nous ne pouvons pas modifier directement un attribut, mais nous pouvons en faire une copie:
attribut vec3 aPosition; uniforme mat4 matrix_model; uniforme mat4 matrix_viewProjection; vide principal (vide) vec3 pos = aPosition; pos.x * = 0,5; gl_Position = matrix_viewProjection * matrix_model * vec4 (pos, 1.0);
L'avion devrait maintenant avoir l'air plus rectangulaire. Rien d'étrange là-bas. Maintenant, que se passe-t-il si nous essayons plutôt de modifier gl_Position
?
attribut vec3 aPosition; uniforme mat4 matrix_model; uniforme mat4 matrix_viewProjection; vide principal (vide) vec3 pos = aPosition; //pos.x * = 0.5; gl_Position = matrix_viewProjection * matrix_model * vec4 (pos, 1.0); gl_Position.x * = 0,5;
Cela peut sembler identique jusqu'à ce que vous commenciez à faire pivoter la caméra. Nous modifions les coordonnées de l’espace de l’écran, ce qui signifie que son apparence sera différente selon comment vous le regardez.
C’est ainsi que vous pouvez déplacer les sommets, et il est important de faire la distinction entre le monde et l’écran..
Défi n ° 2: pouvez-vous déplacer la surface plane entière de quelques unités vers le haut (le long de l'axe des Y) dans le vertex shader sans en déformer la forme?
Défi n ° 3: J'ai dit que gl_Position est 2D, mais gl_Position.z existe. Pouvez-vous exécuter des tests pour déterminer si cette valeur affecte quoi que ce soit et, le cas échéant, à quoi elle sert?
Une dernière chose dont nous avons besoin avant de pouvoir créer des vagues en mouvement est une variable uniforme à utiliser comme temps. Déclarez un uniforme dans votre vertex shader:
uniforme float uTime;
Ensuite, pour transmettre ceci à notre shader, retournez à Water.js et définir une variable de temps dans l'initialisation:
Water.prototype.initialize = function () this.time = 0; ///// Définissons d'abord le temps ici this.GeneratePlaneMesh (); // Enregistrer les shaders actuels this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ;
Maintenant, pour passer cela à notre shader, nous utilisons material.setParameter
. Tout d'abord, nous définissons une valeur initiale à la fin du CreateWaterMaterial
une fonction:
// Crée le shader à partir de la définition this.shader = new pc.Shader (gd, shaderDefinition); ////////////// La nouvelle partie material.setParameter ('uTime', this.time); this.material = material; // Enregistrer une référence à ce matériau //////////////// // Appliquer un shader à ce matériau material.setShader (this.shader); retourner le matériel;
Maintenant dans le mettre à jour
fonction nous pouvons incrémenter le temps et accéder au matériel en utilisant la référence que nous avons créée pour cela:
this.time + = 0.1; this.material.setParameter ('uTime', this.time);
Enfin, dans la fonction de permutation, copiez l’ancienne valeur de temps, de sorte que même si vous modifiez le code, l’incrémentation continue sans que la valeur 0.
Water.prototype.swap = fonction (old) this.time = old.time; ;
Maintenant tout est prêt. Lancez le jeu pour vous assurer qu'il n'y a pas d'erreur. Passons maintenant notre avion en fonction du temps Eau.vert
:
pos.y + = cos (uTime)
Et votre avion devrait monter et descendre maintenant! Comme nous disposons maintenant d’une fonction d’échange, vous pouvez également mettre à jour Water.js sans relancer. Essayez d'incrémenter le temps plus ou moins vite pour confirmer que cela fonctionne.
Défi n ° 4: Pouvez-vous déplacer les sommets pour qu'ils ressemblent à la vague ci-dessous?
En guise d'indice, j'ai discuté en profondeur de différentes manières de créer des vagues ici. C'était en 2D, mais les mêmes calculs s'appliquent ici. Si vous préférez jeter un coup d'œil à la solution, voici l'essentiel.
Le but de cette section est de rendre la surface de l’eau translucide.
Vous avez peut-être remarqué que la couleur que nous retournons dans Water.frag a une valeur alpha de 0,5, mais la surface est toujours complètement opaque. La transparence à bien des égards reste un problème en suspens en infographie. Un moyen peu coûteux d'y parvenir est d'utiliser le mélange.
Normalement, lorsqu'un pixel est sur le point d'être dessiné, il vérifie la valeur dans tampon de profondeur contre sa propre valeur de profondeur (sa position le long de l’axe Z) pour déterminer s’il faut écraser le pixel actuel sur l’écran ou s’écarter. C’est ce qui vous permet de restituer correctement une scène sans avoir à trier les objets.
Avec la fusion, au lieu de simplement supprimer ou écraser, nous pouvons combiner la couleur du pixel déjà dessiné (la destination) avec le pixel sur le point d'être dessiné (la source). Vous pouvez voir toutes les fonctions de fusion disponibles dans WebGL ici.
Pour que l'alpha fonctionne comme prévu, nous voulons que la couleur combinée du résultat soit la source multipliée par l'alpha plus la destination multipliée par un moins l'alpha. En d’autres termes, si l’alpha est 0.4, la couleur finale doit être:
couleur finale = source * 0,4 + destination * 0,6;
Dans PlayCanvas, l’option pc.BLEND_NORMAL fait exactement cela.
Pour l'activer, il suffit de définir la propriété sur le matériau à l'intérieur CreateWaterMaterial
:
material.blendType = pc.BLEND_NORMAL;
Si vous lancez le jeu maintenant, l'eau sera translucide! Ce n'est pas parfait, cependant. Un problème se pose si la surface translucide recouvre elle-même, comme indiqué ci-dessous.
Nous pouvons résoudre ce problème en utilisant alpha à la couverture, qui est une technique multi-échantillonnage pour atteindre la transparenceau lieu de mélanger:
//material.blendType = pc.BLEND_NORMAL; material.alphaToCoverage = true;
Mais cela n’est disponible que dans WebGL 2. Pour la suite de ce tutoriel, j’utiliserai le mélange pour rester simple..
Jusqu'à présent, nous avons créé notre environnement et créé notre surface d'eau translucide avec des vagues animées de notre vertex shader. La seconde partie couvrira l’application de la flottabilité aux objets, l’ajout de lignes d’eau à la surface et la création de lignes de mousse autour des bords des objets coupant la surface..
La dernière partie couvrira l’application de l’effet de distorsion sous-marine post-traitement et quelques idées pour aller ensuite.
Vous pouvez trouver le projet PlayCanvas hébergé terminé ici. Un port Three.js est également disponible dans ce référentiel.