Création d'un shader d'herbe dans le moteur Unity


Ce tutoriel vous montrera comment écrire un ombrage géométrique pour générer des brins d'herbe à partir du haut du maillage entrant et utiliser la tessellation pour contrôler la densité de l'herbe.

L'article décrit le processus étape par étape de l'écriture d'un shader d'herbe dans Unity. Le shader reçoit le maillage entrant et, à partir de chaque sommet du maillage, génère un brin d'herbe à l'aide du shader géométrique . Par souci d'intérêt et de réalisme, les brins d'herbe auront une taille et une rotation aléatoires , et ils seront également affectés par le vent . Pour contrôler la densité de l'herbe, nous utilisons la tessellation pour séparer le maillage entrant. L'herbe pourra projeter et recevoir des ombres.

Le projet terminé est affiché à la fin de l'article. Le fichier shader généré contient un grand nombre de commentaires qui facilitent la compréhension.

Prérequis


Pour terminer ce didacticiel, vous aurez besoin de connaissances pratiques sur le moteur Unity et d'une compréhension initiale de la syntaxe et des fonctionnalités des shaders.

Téléchargez l'ébauche du projet (.zip) .

Se rendre au travail


Téléchargez le brouillon du projet et ouvrez-le dans l'éditeur Unity. Ouvrez la scène Main , puis ouvrez le shader Grass dans votre éditeur de code.

Ce fichier contient un shader qui produit une couleur blanche, ainsi que certaines fonctions que nous utiliserons dans ce tutoriel. Vous remarquerez que ces fonctions ainsi que le vertex shader sont inclus dans le bloc CGINCLUDE situé à l' extérieur de SubShader . Le code placé dans ce bloc sera automatiquement inclus dans toutes les passes du shader; cela vous sera utile plus tard car notre shader aura plusieurs passes.

Nous allons commencer par écrire un shader géométrique qui génère des triangles à partir de chaque sommet de la surface de notre maillage.

1. Shaders géométriques


Les shaders géométriques sont une partie facultative du pipeline de rendu. Ils sont exécutés après le vertex shader (ou le tessellation shader si la tessellation est utilisée) et avant que les sommets ne soient traités pour le fragment shader.


Pipeline graphique Direct3D 11. Notez que dans ce diagramme, le shader de fragment est appelé pixel shader .

Les shaders géométriques reçoivent une seule primitive en entrée et peuvent générer zéro, une ou plusieurs primitives. Nous commencerons par écrire un shader géométrique qui reçoit un sommet (ou point ) en entrée, et qui alimente un triangle représentant un brin d'herbe.

 // Add inside the CGINCLUDE block. struct geometryOutput { float4 pos : SV_POSITION; }; [maxvertexcount(3)] void geo(triangle float4 IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream) { } … // Add inside the SubShader Pass, just below the #pragma fragment frag line. #pragma geometry geo 

Le code ci-dessus déclare un shader géométrique appelé geo avec deux paramètres. Le premier, triangle float4 IN[3] , indique qu'il faudra un triangle (composé de trois points) en entrée. Le second, tel que TriangleStream , configure un shader pour produire un flux de triangles afin que chaque sommet utilise la structure geometryOutput pour transmettre ses données.

Nous avons dit ci-dessus que le shader recevra un sommet et produira un brin d'herbe. Pourquoi alors obtenons-nous un triangle?
Il sera moins coûteux de prendre un en entrée. Cela peut être fait comme suit.

 void geo(point vertexOutput IN[1], inout TriangleStream<geometryOutput> triStream) 

Cependant, étant donné que notre maillage entrant (dans ce cas GrassPlane10x10 , situé dans le dossier Mesh ) a une topologie en triangle , cela entraînera une incompatibilité entre la topologie du maillage entrant et la primitive d'entrée requise. Bien que cela soit autorisé dans DirectX HLSL, il n'est pas autorisé dans OpenGL , donc une erreur sera affichée.

De plus, nous ajoutons le dernier paramètre entre crochets au-dessus de la déclaration de fonction: [maxvertexcount(3)] . Il indique au GPU que nous publierons (mais ne sommes pas obligés de le faire) pas plus de 3 sommets. Nous faisons également en sorte que SubShader utilise un shader géométrique en le déclarant dans Pass .

Notre shader géométrique ne fait encore rien; pour dessiner un triangle, ajoutez le code suivant à l'intérieur du shader géométrique.

 geometryOutput o; o.pos = float4(0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(-0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(0, 1, 0, 1); triStream.Append(o); 


Cela a donné des résultats très étranges. Lorsque vous déplacez la caméra, il devient clair que le triangle est rendu dans l' espace écran . C'est logique: puisque le shader géométrique est exécuté immédiatement avant le traitement des sommets, il enlève au vertex shader la responsabilité des vertices à afficher dans l'espace de troncature . Nous allons changer notre code pour refléter cela.

 // Update the return call in the vertex shader. //return UnityObjectToClipPos(vertex); return vertex; … // Update each assignment of o.pos in the geometry shader. o.pos = UnityObjectToClipPos(float4(0.5, 0, 0, 1)); … o.pos = UnityObjectToClipPos(float4(-0.5, 0, 0, 1)); … o.pos = UnityObjectToClipPos(float4(0, 1, 0, 1)); 


Maintenant, notre triangle est rendu correctement dans le monde. Cependant, il semble qu'un seul soit créé. En fait, un triangle est dessiné pour chaque sommet de notre maillage, mais les positions attribuées aux sommets du triangle sont constantes - elles ne changent pas pour chaque sommet entrant. Par conséquent, tous les triangles sont situés l'un au-dessus de l'autre.

Nous allons résoudre ce problème en compensant les positions des sommets sortants par rapport au point entrant.

 // Add to the top of the geometry shader. float3 pos = IN[0]; … // Update each assignment of o.pos. o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0)); … o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0)); … o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0)); 


Pourquoi certains sommets ne créent-ils pas un triangle?

Bien que nous ayons déterminé que la primitive entrante sera un triangle , un brin d'herbe n'est transmis que par l' un des points du triangle, en écartant les deux autres. Bien sûr, nous pouvons transférer un brin d'herbe à partir des trois points entrants, mais cela conduira au fait que les triangles voisins créent excessivement des brins d'herbe les uns sur les autres.

Ou vous pouvez résoudre ce problème en prenant des maillages ayant le type de points de topologie comme maillages entrants du shader géométrique.

Les triangles sont désormais dessinés correctement et leur base est située au sommet qui les émet. Avant de continuer, GrassPlane objet GrassPlane dans la scène et GrassBall objet GrassBall . Nous voulons que l'herbe se génère correctement sur différents types de surfaces, il est donc important de la tester sur des maillages de formes différentes.


Jusqu'à présent, tous les triangles sont émis dans une seule direction, et non vers l'extérieur de la surface de la sphère. Pour résoudre ce problème, nous allons créer des brins d'herbe dans un espace tangent .

2. Espace tangent


Idéalement, nous aimerions créer des brins d'herbe en définissant une largeur, une hauteur, une courbure et une rotation différentes, sans tenir compte de l'angle de la surface à partir de laquelle le brin d'herbe est émis. Autrement dit, nous définissons un brin d'herbe dans un espace local au sommet qui l'émet , puis le transformons pour qu'il soit local au maillage . Cet espace est appelé espace tangent .


Dans l'espace tangent, les axes X , Y et Z sont définis par rapport à la normale et à la position de la surface (dans notre cas, les sommets).

Comme tout autre espace, nous pouvons définir l'espace tangent d'un sommet avec trois vecteurs: droite , avant et haut . À l'aide de ces vecteurs, nous pouvons créer une matrice pour transformer le brin d'herbe de l'espace tangent à l'espace local.

Vous pouvez accéder aux vecteurs de droite à droite en ajoutant de nouvelles données de sommet en entrée.

 // Add to the CGINCLUDE block. struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct vertexOutput { float4 vertex : SV_POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; … // Modify the vertex shader. vertexOutput vert(vertexInput v) { vertexOutput o; o.vertex = v.vertex; o.normal = v.normal; o.tangent = v.tangent; return o; } … // Modify the input for the geometry shader. Note that the SV_POSITION semantic is removed. void geo(triangle vertexOutput IN[3], inout TriangleStream<geometryOutput> triStream) … // Modify the existing line declaring pos. float3 pos = IN[0].vertex; 

Le troisième vecteur peut être calculé en prenant le produit vectoriel entre deux autres. Un produit vectoriel renvoie un vecteur perpendiculaire à deux vecteurs entrants.

 // Place in the geometry shader, below the line declaring float3 pos. float3 vNormal = IN[0].normal; float4 vTangent = IN[0].tangent; float3 vBinormal = cross(vNormal, vTangent) * vTangent.w; 

Pourquoi le résultat du produit vectoriel est-il multiplié par la coordonnée de la tangente w?
Lors de l'exportation d'un maillage à partir d'un éditeur 3D, il contient généralement des binormaux (également appelés tangentes à deux points ) déjà stockés dans les données de maillage. Au lieu d'importer ces binormaux, Unity prend simplement la direction de chaque binormal et les affecte à la coordonnée de la tangente w . Cela vous permet d'économiser de la mémoire, tout en offrant la possibilité de recréer le binormal correct. Une discussion détaillée de ce sujet peut être trouvée ici .

Ayant les trois vecteurs, nous pouvons créer une matrice pour la transformation entre les espaces tangents et locaux. Nous multiplierons chaque sommet du brin d'herbe par cette matrice avant de le passer à UnityObjectToClipPos , qui attend un sommet dans l'espace local.

 // Add below the lines declaring the three vectors. float3x3 tangentToLocal = float3x3( vTangent.x, vBinormal.x, vNormal.x, vTangent.y, vBinormal.y, vNormal.y, vTangent.z, vBinormal.z, vNormal.z ); 

Avant d'utiliser la matrice, nous transférons le code de sortie de vertex à la fonction afin de ne pas écrire encore et encore les mêmes lignes de code. C'est ce qu'on appelle le principe DRY , ou ne vous répétez pas .

 // Add to the CGINCLUDE block. geometryOutput VertexOutput(float3 pos) { geometryOutput o; o.pos = UnityObjectToClipPos(pos); return o; } … // Remove the following from the geometry shader. //geometryOutput o; //o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0)); //triStream.Append(o); //o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0)); //triStream.Append(o); //o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0)); //triStream.Append(o); // ...and replace it with the code below. triStream.Append(VertexOutput(pos + float3(0.5, 0, 0))); triStream.Append(VertexOutput(pos + float3(-0.5, 0, 0))); triStream.Append(VertexOutput(pos + float3(0, 1, 0))); 

Enfin, nous multiplions les sommets de sortie par la matrice tangentToLocal , en les alignant correctement avec la normale de leur point d'entrée.

 triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 1, 0)))); 

image

Cela ressemble plus à ce dont nous avons besoin, mais pas tout à fait raison. Le problème ici est qu'initialement, nous avons attribué la direction «haut» (haut) de l'axe Y ; cependant, dans l'espace tangent, la direction vers le haut est généralement située le long de l'axe Z. Nous allons maintenant effectuer ces modifications.

 // Modify the position of the third vertex being emitted. triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)))); 


3. Aspect de l'herbe


Pour que les triangles ressemblent davantage à des brins d'herbe, vous devez ajouter des couleurs et des variations. On commence par ajouter un dégradé descendant du haut du brin d'herbe.

3.1 dégradé de couleurs


Notre objectif est de permettre à l'artiste de définir deux couleurs - haut et bas, et d'interpoler entre ces deux couleurs il bascule à la base du brin d'herbe. Ces couleurs sont déjà définies dans le fichier shader comme _TopColor et _BottomColor . Pour leur bon échantillonnage, vous devez passer les coordonnées UV au fragment shader.

 // Add to the geometryOutput struct. float2 uv : TEXCOORD0; … // Modify the VertexOutput function signature. geometryOutput VertexOutput(float3 pos, float2 uv) … // Add to VertexOutput, just below the line assigning o.pos. o.uv = uv; … // Modify the existing lines in the geometry shader. triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)), float2(0.5, 1))); 

Nous avons créé des coordonnées UV pour un brin d'herbe en forme de triangle, dont les deux sommets de la base sont situés en bas à gauche et à droite, et la pointe supérieure est située au centre en haut.


Coordonnées UV des trois sommets des brins d'herbe. Bien que nous peignions les brins d'herbe avec un dégradé simple, une disposition similaire de textures vous permet de superposer des textures.

Maintenant, nous pouvons échantillonner les couleurs du haut et du bas dans le fragment shader avec UV, puis les interpoler avec lerp . Nous devrons également modifier les paramètres du fragment shader, en faisant geometryOutput en entrée, et pas seulement la position de float4 .

 // Modify the function signature of the fragment shader. float4 frag (geometryOutput i, fixed facing : VFACE) : SV_Target … // Replace the existing return call. return float4(1, 1, 1, 1); return lerp(_BottomColor, _TopColor, i.uv.y); 


3.2 Direction aléatoire de la lame


Pour créer de la variabilité et donner à l'herbe un aspect plus naturel, nous allons faire en sorte que chaque brin d'herbe regarde dans une direction aléatoire. Pour ce faire, nous devons créer une matrice de rotation qui fait tourner le brin d'herbe de façon aléatoire autour de son axe vers le haut .

Il y a deux fonctions dans le fichier shader qui nous aideront à le faire: rand , qui génère un nombre aléatoire à partir d'une entrée en trois dimensions, et AngleAxis3x3 , qui reçoit l'angle (en radians ) et renvoie une matrice qui fait pivoter cette valeur autour de l'axe spécifié. Cette dernière fonction fonctionne exactement de la même manière que la fonction C # Quaternion.AngleAxis (seul AngleAxis3x3 renvoie une matrice, pas un quaternion).

La fonction rand renvoie un nombre compris entre 0 et 1; nous le multiplions par 2 Pi pour obtenir la gamme complète des valeurs angulaires.

 // Add below the line declaring the tangentToLocal matrix. float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1)); 

Nous utilisons la position pos entrante comme graine pour une rotation aléatoire. Pour cette raison, chaque brin d'herbe aura sa propre rotation, constante dans chaque cadre.

La rotation peut être appliquée au brin d'herbe en le multipliant par la matrice tangentToLocal créée. Notez que la multiplication matricielle n'est pas commutative ; l'ordre des opérandes est important .

 // Add below the line declaring facingRotationMatrix. float3x3 transformationMatrix = mul(tangentToLocal, facingRotationMatrix); … // Replace the multiplication matrix operand with our new transformationMatrix. triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0.5, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-0.5, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, 1)), float2(0.5, 1))); 


3.3 Flexion avant aléatoire


Si tous les brins d'herbe sont parfaitement alignés, ils auront la même apparence. Cela peut convenir à l'herbe bien entretenue, par exemple, sur une pelouse taillée, mais dans la nature, l'herbe ne pousse pas comme ça. Nous allons créer une nouvelle matrice pour faire pivoter l'herbe le long de l'axe X , ainsi qu'une propriété pour contrôler cette rotation.

 // Add as a new property. _BendRotationRandom("Bend Rotation Random", Range(0, 1)) = 0.2 … // Add to the CGINCLUDE block. float _BendRotationRandom; … // Add to the geometry shader, below the line declaring facingRotationMatrix. float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0)); 

Encore une fois, nous utilisons la position du brin d'herbe comme une graine aléatoire, cette fois en la balayant pour créer une graine unique. Nous multiplierons également UNITY_PI par 0,5 ; cela nous donnera un intervalle aléatoire de 0 ... 90 degrés.

Nous appliquons à nouveau cette matrice par rotation, en multipliant tout dans le bon ordre.

 // Modify the existing line. float3x3 transformationMatrix = mul(mul(tangentToLocal, facingRotationMatrix), bendRotationMatrix); 


3.4 Largeur et hauteur


Alors que la taille du brin d'herbe est limitée à une largeur de 1 unité et une hauteur de 1 unité. Nous ajouterons des propriétés pour contrôler la taille, ainsi que des propriétés pour ajouter une variation aléatoire.

 // Add as new properties. _BladeWidth("Blade Width", Float) = 0.05 _BladeWidthRandom("Blade Width Random", Float) = 0.02 _BladeHeight("Blade Height", Float) = 0.5 _BladeHeightRandom("Blade Height Random", Float) = 0.3 … // Add to the CGINCLUDE block. float _BladeHeight; float _BladeHeightRandom; float _BladeWidth; float _BladeWidthRandom; … // Add to the geometry shader, above the triStream.Append calls. float height = (rand(pos.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight; float width = (rand(pos.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth; … // Modify the existing positions with our new height and width. triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(width, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-width, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, height)), float2(0.5, 1))); 


Les triangles sont maintenant beaucoup plus comme des brins d'herbe, mais aussi trop peu. Il n'y a tout simplement pas assez de pics dans le maillage entrant pour créer l'impression d'un champ densément envahi.

Une solution consiste à créer un nouveau maillage plus dense, en utilisant C # ou dans un éditeur 3D. Cela fonctionnera, mais ne nous permettra pas de contrôler dynamiquement la densité de l'herbe. Au lieu de cela, nous diviserons le maillage entrant à l'aide de la tessellation .

4. Pavage


La tessellation est une étape facultative du pipeline de rendu, effectuée après le vertex shader et avant le géométrique shader (le cas échéant). Sa tâche consiste à subdiviser une surface entrante en plusieurs primitives. La tessellation est implémentée en deux étapes programmables: les shaders de coque et de domaine .

Pour les shaders de surface, Unity a une implémentation de pavage intégrée . Cependant, comme nous n'utilisons pas de shaders de surface, nous devrons implémenter nos propres shaders de shell et de domaine. Dans cet article, je ne discuterai pas de la mise en œuvre de la tessellation en détail, et nous utilisons simplement le fichier CustomTessellation.cginc existant. Ce fichier est adapté de l' article Catlike Coding , qui est une excellente source d'informations sur la mise en œuvre de la tessellation dans Unity.

Si nous incluons l'objet TessellationExample dans la scène, nous verrons qu'il contient déjà du matériel qui implémente la tessellation. La modification de la propriété Uniforme de pavage illustre l'effet de subdivision.


Nous mettons en place une tessellation dans le shader pour contrôler la densité de l'avion, et donc contrôler le nombre de brins d'herbe générés. Vous devez d'abord ajouter le fichier CustomTessellation.cginc . Nous y ferons référence par son chemin relatif vers le shader.

 // Add inside the CGINCLUDE block, below the other #include statements. #include "Shaders/CustomTessellation.cginc" 

Si vous ouvrez CustomTessellation.cginc , vous remarquerez que les vertexOutput vertexInput et vertexOutput , ainsi que les vertex shaders, y sont déjà définis. Pas besoin de les redéfinir dans notre shader d'herbe; ils peuvent être supprimés.

 /*struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct vertexOutput { float4 vertex : SV_POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; vertexOutput vert(vertexInput v) { vertexOutput o; o.vertex = v.vertex; o.normal = v.normal; o.tangent = v.tangent; return o; }*/ 

Notez que le CustomTessellation.cginc vertex shader dans CustomTessellation.cginc passe simplement l'entrée directement à l'étape de tessellation; la fonction vertexOutput , appelée à l'intérieur du shader de domaine, se charge de créer la structure vertexOutput .

Maintenant, nous pouvons ajouter des shaders de shell et de domaine au shader d'herbe. Nous ajouterons également une nouvelle propriété _TessellationUniform pour contrôler la taille de l'unité - la variable correspondant à cette propriété a déjà été déclarée dans CustomTessellation.cginc .

 // Add as a new property. _TessellationUniform("Tessellation Uniform", Range(1, 64)) = 1 … // Add below the other #pragma statements in the SubShader Pass. #pragma hull hull #pragma domain domain 

La modification de la propriété Tessellation Uniform nous permet désormais de contrôler la densité de l'herbe. J'ai trouvé que de bons résultats sont obtenus avec une valeur de 5 .


5. Le vent


Nous implémentons le vent en échantillonnant la texture de distorsion . Cette texture ressemblera à une carte normale , seulement il y aura seulement deux au lieu de trois canaux. Nous utiliserons ces deux canaux comme directions du vent le long de X et Y.


Avant d'échantillonner la texture du vent, nous devons créer une coordonnée UV. Au lieu d'utiliser les coordonnées de texture affectées au maillage, nous appliquons la position du point entrant. Grâce à cela, s'il y a plusieurs mailles d'herbe dans le monde, l'illusion sera créée qu'elles font toutes partie du même système de vent. Nous utilisons également la _Time intégrée _Time shader pour faire défiler la texture du vent le long de la surface de l'herbe.

 // Add as new properties. _WindDistortionMap("Wind Distortion Map", 2D) = "white" {} _WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0) … // Add to the CGINCLUDE block. sampler2D _WindDistortionMap; float4 _WindDistortionMap_ST; float2 _WindFrequency; … // Add to the geometry shader, just above the line declaring the transformationMatrix. float2 uv = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y; 

Nous appliquons l'échelle et le décalage de _WindDistortionMap à la position, puis le _Time.y vers _Time.y , mis à l'échelle vers _WindFrequency . Nous allons maintenant utiliser ces UV pour échantillonner la texture et créer une propriété pour contrôler la force du vent.

 // Add as a new property. _WindStrength("Wind Strength", Float) = 1 … // Add to the CGINCLUDE block. float _WindStrength; … // Add below the line declaring float2 uv. float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength; 

Notez que nous mettons à l'échelle la valeur échantillonnée à partir de la texture de l'intervalle 0 ... 1 à l'intervalle -1 ... 1. Ensuite, nous pouvons créer un vecteur normalisé indiquant la direction du vent.

 // Add below the line declaring float2 windSample. float3 wind = normalize(float3(windSample.x, windSample.y, 0)); 

Nous pouvons maintenant créer une matrice pour tourner autour de ce vecteur et la multiplier par notre transformationMatrix .

 // Add below the line declaring float3 wind. float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind); … // Modify the existing line. float3x3 transformationMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix), bendRotationMatrix); 

Enfin, nous transférons la texture Wind (située à la racine du projet) dans le champ Wind Distortion Map du matériau d'herbe dans l'éditeur Unity. Nous avons également défini le paramètre de mosaïque de la texture sur 0.01, 0.01 .


Si l'herbe n'est pas animée dans la fenêtre Scène , cliquez sur le bouton Basculer la skybox, le brouillard et divers autres effets pour activer les matériaux animés.

De loin, l'herbe semble droite, mais si on regarde de près le brin d'herbe, on remarque que tout le brin d'herbe tourne, c'est pourquoi la base n'est plus attachée au sol.


La base du brin d'herbe n'est plus attachée au sol, mais l'intersecte (représentée en rouge ) et pend au-dessus du plan du sol (indiqué par la ligne verte ).

Nous allons corriger cela en définissant une deuxième matrice de transformation, qui ne s'applique qu'à deux sommets de la base. Dans cette matrice ne sera pas matrice incluse windRotationet bendRotationMatrix, grâce à laquelle la base est fixée à la surface de l' herbe.

 // Add below the line declaring float3x3 transformationMatrix. float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix); … // Modify the existing lines outputting the base vertex positions. triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(width, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(-width, 0, 0)), float2(1, 0))); 

6. Courbure des brins d'herbe


Désormais, les brins d'herbe sont définis par un triangle. À grande distance, ce n'est pas un problème, mais près du brin d'herbe, ils ont l'air très rigides et géométriques, plutôt qu'organiques et vivants. Nous allons résoudre ce problème en construisant des brins d'herbe à partir de plusieurs triangles et en les pliant le long de la courbe .

Chaque brin d'herbe sera divisé en plusieurs segments . Chaque segment aura une forme rectangulaire et se composera de deux triangles, à l'exception du segment supérieur - ce sera un triangle désignant la pointe du brin d'herbe.

Jusqu'à présent, nous n'avons dessiné que trois sommets, créant un seul triangle. Comment, s'il y a plus de sommets, le shader géométrique sait-il lesquels joindre et former des triangles? La réponse est dans la structure des donnéesbande de triangle . Les trois premiers sommets se rejoignent et forment un triangle, et chaque nouveau sommet forme un triangle avec les deux précédents.


Brin d'herbe subdivisé, représenté par une bande triangulaire et créé un sommet à la fois. Après les trois premiers sommets, chaque nouveau sommet forme un nouveau triangle avec les deux sommets précédents.

Ceci est non seulement plus efficace en termes d'utilisation de la mémoire, mais vous permet également de créer facilement et rapidement des séquences de triangles dans votre code. Si nous voulions créer plusieurs bandes de triangles, nous pourrions appeler RestartStrip pour la TriangleStreamfonction . Avant de commencer à dessiner plus de sommets à partir du shader géométrique, nous devons l'augmenter . Nous utiliserons la conception pour permettre à l'auteur du shader de contrôler le nombre de segments et de calculer le nombre de sommets affichés à partir de celui-ci.

maxvertexcount#define

 // Add to the CGINCLUDE block. #define BLADE_SEGMENTS 3 … // Modify the existing line defining the maxvertexcount. [maxvertexcount(BLADE_SEGMENTS * 2 + 1)] 

Initialement, nous avons défini le nombre de segments sur 3 et mis à jour maxvertexcountpour calculer le nombre de sommets en fonction du nombre de segments.

Pour créer un brin d'herbe segmenté, nous utilisons un cycle for. Chaque itération de la boucle ajoutera deux sommets : gauche et droite . Après avoir terminé la pointe, nous ajoutons le dernier sommet à la pointe du brin d'herbe.

Avant de faire cela, il sera utile de déplacer une partie de la position de calcul des sommets des brins d'herbe du code dans la fonction, car nous utiliserons ce code plusieurs fois à l'intérieur et à l'extérieur de la boucle. Ajoutez ce qui CGINCLUDEsuit au bloc :

 geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float2 uv, float3x3 transformMatrix) { float3 tangentPoint = float3(width, 0, height); float3 localPosition = vertexPosition + mul(transformMatrix, tangentPoint); return VertexOutput(localPosition, uv); } 

Cette fonction effectue les mêmes tâches car elle passe les arguments que nous avons passés précédemment VertexOutputpour générer les sommets du brin d'herbe. En obtenant une position, une hauteur et une largeur, il transforme correctement le sommet à l'aide de la matrice transmise et lui assigne une coordonnée UV. Nous mettrons à jour le code existant pour que la fonction fonctionne correctement.

 // Update the existing code outputting the vertices. triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing)); triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing)); triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); 

La fonction a commencé à fonctionner correctement et nous sommes prêts à déplacer le code de génération de vertex dans la boucle for. Ajoutez ce qui float widthsuit sous la ligne :

 for (int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; } 

Nous annonçons un cycle qui sera exécuté une fois pour chaque brin d'herbe. À l'intérieur de la boucle, ajoutez une variable t. Cette variable stockera une valeur dans la plage 0 ... 1, indiquant jusqu'où nous nous sommes déplacés le long du brin d'herbe. Nous utilisons cette valeur pour calculer la largeur et la hauteur du segment à chaque itération de la boucle.

 // Add below the line declaring float t. float segmentHeight = height * t; float segmentWidth = width * (1 - t); 

Lorsque vous montez un brin d'herbe, la hauteur augmente et la largeur diminue. Maintenant, nous pouvons ajouter des appels à la boucle GenerateGrassVertexpour ajouter des sommets au flux de triangles. Nous ajouterons également un appel en GenerateGrassVertexdehors de la boucle pour créer la pointe du brin d'herbe.

 // Add below the line declaring float segmentWidth. float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix; triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, float2(1, t), transformMatrix)); … // Add just below the loop to insert the vertex at the tip of the blade. triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); … // Remove the existing calls to triStream.Append. //triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing)); //triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing)); //triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); 

Jetez un oeil à la ligne avec la déclaration float3x3 transformMatrix- ici, nous sélectionnons l'une des deux matrices de transformation: nous prenons transformationMatrixFacingpour les sommets de la base et transformationMatrixpour toutes les autres.


Les brins d'herbe sont maintenant divisés en plusieurs segments, mais la surface des lames est toujours plate - de nouveaux triangles ne sont pas encore impliqués. Nous allons ajouter une lame de courbure de l' herbe, décalant la position du sommet de Y . Tout d'abord, nous devons modifier la fonction GenerateGrassVertexafin qu'elle obtienne un décalage en Y , que nous appellerons forward.

 // Update the function signature of GenerateGrassVertex. geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix) … // Modify the Y coordinate assignment of tangentPoint. float3 tangentPoint = float3(width, forward, height); 

Pour calculer le déplacement de chaque sommet, nous substituons une powvaleur à la fonction t. Après avoir atteint tune puissance, son effet sur le déplacement vers l'avant sera non linéaire et transformera le brin d'herbe en courbe.

 // Add as new properties. _BladeForward("Blade Forward Amount", Float) = 0.38 _BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2 … // Add to the CGINCLUDE block. float _BladeForward; float _BladeCurve; … // Add inside the geometry shader, below the line declaring float width. float forward = rand(pos.yyz) * _BladeForward; … // Add inside the loop, below the line declaring segmentWidth. float segmentForward = pow(t, _BladeCurve) * forward; … // Modify the GenerateGrassVertex calls inside the loop. triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix)); … // Modify the GenerateGrassVertex calls outside the loop. triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix)); 

Il s'agit d'un morceau de code assez volumineux, mais tout le travail est effectué de la même manière que pour la largeur et la hauteur du brin d'herbe. Avec des valeurs plus basses _BladeForwardet _BladeCurvenous obtenons une pelouse bien entretenue et ordonnée, et des valeurs plus grandes donneront l'effet inverse.


7. Éclairage et ombres


Comme dernière étape pour terminer le shader, nous ajouterons la possibilité de projeter et de recevoir des ombres. Nous ajouterons également un éclairage simple à partir de la principale source de lumière directionnelle.

7.1 Projection d'ombres


Pour projeter des ombres dans Unity, vous devez ajouter une deuxième passe au shader. Ce passage sera utilisé par les sources de lumière créant des ombres dans la scène pour rendre la profondeur de l'herbe dans leur carte des ombres . Cela signifie que le shader géométrique devra être lancé dans le passage d'ombre, afin que les brins d'herbe puissent projeter des ombres.

Étant donné que le shader géométrique est écrit à l'intérieur des blocs CGINCLUDE, nous pouvons l'utiliser dans toutes les passes du fichier. Créez un deuxième passage qui utilisera les mêmes shaders que le premier, à l'exception du fragment shader - nous en définirons un nouveau dans lequel nous écrirons une macro qui traite la sortie.

 // Add below the existing Pass. Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma geometry geo #pragma fragment frag #pragma hull hull #pragma domain domain #pragma target 4.6 #pragma multi_compile_shadowcaster float4 frag(geometryOutput i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } 

En plus de créer un nouveau shader de fragment, il y a quelques différences importantes dans ce passage. Le libellé LightModeimporte ShadowCaster, non ForwardBase- cela indique à Unity que ce passage doit être utilisé pour rendre l'objet en textures ombrées. Il existe également une directive de préprocesseur ici multi_compile_shadowcaster. Il garantit que le shader compile toutes les options nécessaires pour projeter des ombres.

Rendez l'objet de jeu Fence actif dans la scène; nous obtenons donc une surface sur laquelle les brins d'herbe peuvent projeter une ombre.


7.2 Obtention d'ombres


Une fois que Unity a rendu la carte des ombres du point de vue de la source de lumière créant l'ombre, il lance un passage qui "recueille" les ombres dans la texture de l' espace d'écran . Pour échantillonner cette texture, nous devrons calculer les positions des sommets dans l'espace d'écran et les transférer vers le fragment shader.

 // Add to the geometryOutput struct. unityShadowCoord4 _ShadowCoord : TEXCOORD1; … // Add to the VertexOutput function, just above the return call. o._ShadowCoord = ComputeScreenPos(o.pos); 

Dans le fragment shader du passage, ForwardBasenous pouvons utiliser une macro pour obtenir une valeur floatindiquant si la surface est dans l'ombre ou non. Cette valeur se situe dans la plage 0 ... 1, où 0 est l'ombrage complet, 1 est l'éclairage complet.

Pourquoi la coordonnée UV de l'espace d'écran s'appelle _ShadowCoord? Cela n'est pas conforme aux conventions de dénomination précédentes.
De nombreux shaders Unity intégrés font des hypothèses sur les noms de certains champs dans diverses structures de shaders (certains font même des hypothèses sur les noms des structures elles-mêmes). Il en va de même pour la macro utilisée ci-dessous SHADOW_ATTENUATION. Si nous extrayons le code source de cette macro Autolight.cginc, nous verrons que la coordonnée ombrée doit avoir un nom spécifique.

 #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) 

Si nous souhaitons créer un autre nom pour cette coordonnée, ou pour une raison quelconque, nous en aurions besoin, nous pourrions simplement copier cette définition dans notre propre shader.

 // Add to the ForwardBase pass's fragment shader, replacing the existing return call. return SHADOW_ATTENUATION(i); //return lerp(_BottomColor, _TopColor, i.uv.y); 

Enfin, nous devons configurer le shader correctement pour recevoir les ombres. Pour ce faire, nous allons ajouter une ForwardBasedirective de préprocesseur à la passe afin qu'elle compile toutes les options de shader nécessaires.

 // Add to the ForwardBase pass's preprocessor directives, below #pragma target 4.6. #pragma multi_compile_fwdbase 


Après avoir rapproché la caméra, nous pouvons remarquer des artefacts à la surface des brins d'herbe; elles sont causées par le fait que des brins d'herbe individuels se projettent des ombres sur eux-mêmes. Nous pouvons résoudre ce problème en appliquant un décalage linéaire ou en éloignant légèrement les positions des sommets de l'espace de troncature de l'écran. Nous utiliserons la macro Unity pour cela et l'inclurons dans la conception #ifafin que l'opération soit effectuée uniquement dans le chemin de l'ombre.

 // Add at the end of the VertexOutput function, just above the return call. #if UNITY_PASS_SHADOWCASTER // Applying the bias prevents artifacts from appearing on the surface. o.pos = UnityApplyLinearShadowBias(o.pos); #endif 


Après avoir appliqué le décalage d'ombre linéaire, les artefacts d'ombre sous forme de rayures disparaissent de la surface des triangles.

Pourquoi y a-t-il des artefacts le long des bords des brins d'herbe ombragés?

(multisample anti-aliasing MSAA ) Unity , . , .

— , , Unity . ( ); Unity .

7.3 Éclairage


Nous allons implémenter l'éclairage en utilisant un algorithme de calcul d'éclairage diffus très simple et commun.


... où N est la normale à la surface, L est la direction normalisée de la source principale d'éclairage directionnel et I est l'éclairage calculé. Dans ce didacticiel, nous n'implémenterons pas d' éclairage indirect.

Pour l'instant, les normales ne sont pas attribuées aux sommets des brins d'herbe. Comme pour les positions des sommets, nous calculons d'abord les normales dans l' espace tangent puis les convertissons en locales.

Lorsque la quantité de courbure de lame est 1 , tous les brins d'herbe dans l'espace tangent sont dirigés dans une direction: directement en face de l'axe Y. Comme premier passage de notre solution, nous calculons la normale, en supposant qu'il n'y a pas de courbure.

 // Add to the GenerateGrassVertex function, belowing the line declaring tangentPoint. float3 tangentNormal = float3(0, -1, 0); float3 localNormal = mul(transformMatrix, tangentNormal); 

tangentNormal, défini comme directement opposé à l'axe Y , est transformé par la même matrice que celle utilisée pour convertir les points tangents en espace local. Maintenant, nous pouvons le passer à une fonction VertexOutput, puis à une structure geometryOutput.

 // Modify the return call in GenerateGrassVertex. return VertexOutput(localPosition, uv, localNormal); … // Add to the geometryOutput struct. float3 normal : NORMAL; … // Modify the existing function signature. geometryOutput VertexOutput(float3 pos, float2 uv, float3 normal) … // Add to the VertexOutput function to pass the normal through to the fragment shader. o.normal = UnityObjectToWorldNormal(normal); 

Notez qu'avant la conclusion, nous transformons la normale en espace mondial ; L'unité transmet aux shaders la direction de la principale source de lumière directionnelle dans l'espace mondial, cette transformation est donc nécessaire.

Nous pouvons maintenant visualiser les normales dans le fragment de shader ForwardBasepour vérifier le résultat de notre travail.

 // Add to the ForwardBase fragment shader. float3 normal = facing > 0 ? i.normal : -i.normal; return float4(normal * 0.5 + 0.5, 1); // Remove the existing return call. //return SHADOW_ATTENUATION(i); 

Puisqu'une Cullvaleur est assignée dans notre shader Off, les deux côtés du brin d'herbe sont rendus. Pour que la normale soit dirigée dans la bonne direction, nous utilisons un paramètre auxiliaire VFACEque nous avons ajouté au fragment shader.

L'argument fixed facingrenverra un nombre positif si nous affichons la face avant de la surface, et un nombre négatif si c'est le contraire. Nous utilisons cela dans le code ci-dessus pour retourner la normale si nécessaire.


Lorsque la quantité de courbure de lame est supérieure à 1, la position tangente Z de chaque sommet sera décalée de la quantité forwardtransmise à la fonction GenerateGrassVertex. Nous utiliserons cette valeur pour mettre à l'échelle proportionnellement l'axe Z des normales.

 // Modify the existing line in GenerateGrassVertex. float3 tangentNormal = normalize(float3(0, -1, forward)); 

Enfin, ajoutez le code au fragment shader pour combiner les ombres, l'éclairage directionnel et l'éclairage ambiant. Je recommande d'étudier des informations plus détaillées sur la mise en œuvre de l'éclairage personnalisé dans les shaders dans mon tutoriel sur les toon shaders .

 // Add to the ForwardBase fragment shader, below the line declaring float3 normal. float shadow = SHADOW_ATTENUATION(i); float NdotL = saturate(saturate(dot(normal, _WorldSpaceLightPos0)) + _TranslucentGain) * shadow; float3 ambient = ShadeSH9(float4(normal, 1)); float4 lightIntensity = NdotL * _LightColor0 + float4(ambient, 1); float4 col = lerp(_BottomColor, _TopColor * lightIntensity, i.uv.y); return col; // Remove the existing return call. //return float4(normal * 0.5 + 0.5, 1); 


Conclusion


Dans ce didacticiel, l'herbe couvre une petite zone de 10 x 10 unités. Pour que le shader couvre de grands espaces ouverts tout en maintenant des performances élevées, des optimisations doivent être introduites. Vous pouvez appliquer la tessellation en fonction de la distance afin que moins de brins d'herbe soient éloignés de la caméra. De plus, sur de longues distances, au lieu de brins d'herbe individuels, des groupes de brins d'herbe peuvent être dessinés en utilisant un seul quadrilatère avec une texture superposée.


Texture de l'herbe incluse dans le package d' actifs standard du moteur Unity. De nombreux brins d'herbe sont dessinés sur un seul quadrilatère, ce qui réduit le nombre de triangles dans la scène.

Bien que nous ne puissions pas utiliser nativement des shaders géométriques avec des shaders de surface, pour améliorer ou étendre les fonctionnalités d'éclairage et d'ombrage, si vous devez utiliser le modèle d'éclairage Unity standard, vous pouvez étudier ce référentiel GitHub , qui démontre la solution au problème par un rendu retardé et un remplissage manuel des tampons G.

Code source de shader dans le référentiel GitHub

Addition: coopération


Sans interopérabilité, les effets graphiques peuvent sembler statiques ou sans vie aux joueurs. Ce tutoriel est déjà très long, donc je n'ai pas ajouté de section sur l'interaction des objets du monde avec l'herbe.

Une implémentation naïve d'herbes interactives contiendrait deux éléments: quelque chose dans le monde du jeu qui peut transmettre des données au shader pour lui dire avec quelle partie de l'herbe interagit, et coder dans le shader pour interpréter ces données.

Un exemple de la façon dont cela peut être mis en œuvre avec de l'eau est présenté ici . Il peut être adapté pour travailler avec de l'herbe; au lieu de dessiner des ondulations à l'endroit où se trouve le personnage, vous pouvez tourner le brin d'herbe vers le bas pour simuler les effets des pas.

Source: https://habr.com/ru/post/fr458060/


All Articles