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.
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.
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.
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.
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.
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.
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 .
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))));
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.
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.
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
.
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.
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 .
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.
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.
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.
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.
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.
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
.
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.
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.
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.
Nous pouvons maintenant créer une matrice pour tourner autour de ce vecteur et la multiplier par notre
transformationMatrix
.
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 windRotation
et bendRotationMatrix
, grâce à laquelle la base est fixée à la surface de l' herbe.
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 TriangleStream
fonction . 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
Initialement, nous avons défini le nombre de segments sur 3 et mis à jour maxvertexcount
pour 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 CGINCLUDE
suit 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 VertexOutput
pour 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.
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 width
suit 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.
Lorsque vous montez un brin d'herbe, la hauteur augmente et la largeur diminue. Maintenant, nous pouvons ajouter des appels à la boucle GenerateGrassVertex
pour ajouter des sommets au flux de triangles. Nous ajouterons également un appel en GenerateGrassVertex
dehors de la boucle pour créer la pointe du brin d'herbe.
Jetez un oeil à la ligne avec la déclaration float3x3 transformMatrix
- ici, nous sélectionnons l'une des deux matrices de transformation: nous prenons transformationMatrixFacing
pour les sommets de la base et transformationMatrix
pour 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 GenerateGrassVertex
afin qu'elle obtienne un décalage en Y , que nous appellerons forward
.
Pour calculer le déplacement de chaque sommet, nous substituons une pow
valeur à la fonction t
. Après avoir atteint t
une puissance, son effet sur le déplacement vers l'avant sera non linéaire et transformera le brin d'herbe en courbe.
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 _BladeForward
et _BladeCurve
nous 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.
En plus de créer un nouveau shader de fragment, il y a quelques différences importantes dans ce passage. Le libellé LightMode
importe 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.
Dans le fragment shader du passage, ForwardBase
nous pouvons utiliser une macro pour obtenir une valeur float
indiquant 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.
Enfin, nous devons configurer le shader correctement pour recevoir les ombres. Pour ce faire, nous allons ajouter une ForwardBase
directive de préprocesseur à la passe afin qu'elle compile toutes les options de shader nécessaires.
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 #if
afin que l'opération soit effectuée uniquement dans le chemin de l'ombre.
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.
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
.
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 ForwardBase
pour vérifier le résultat de notre travail.
Puisqu'une Cull
valeur 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 VFACE
que nous avons ajouté au fragment shader.L'argument fixed facing
renverra 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é forward
transmise à la fonction GenerateGrassVertex
. Nous utiliserons cette valeur pour mettre à l'échelle proportionnellement l'axe Z des normales.
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 .
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 GitHubAddition: 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.