Exploration du shader de sable du jeu Journey

Parmi les nombreux jeux indépendants sortis au cours des 10 dernières années, l'un de mes favoris est définitivement Journey . Grâce à son esthétique époustouflante et sa belle bande sonore, Journey est devenu un exemple d'excellence dans presque tous les aspects du développement.

Je suis développeur de jeux et artiste technique, j'ai donc été le plus intrigué par la façon dont le sable a été rendu. Ce n'est pas seulement beau, mais aussi directement lié au gameplay de base et au gameplay dans son ensemble. Journey est littéralement construit de sable, et sans un tel effet étonnant, le jeu lui-même ne pourrait tout simplement pas exister.


Dans cet article en deux articles, je rendrai hommage à l'héritage de Journey en vous apprenant à recréer exactement le même rendu de sable à l'aide de shaders. Que les dunes de sable soient nécessaires dans votre jeu, cette série de didacticiels vous permettra d'apprendre à recréer une esthétique spécifique dans votre propre jeu. Si vous souhaitez recréer le magnifique shader de sable utilisé dans Journey , vous devez d'abord comprendre comment il a été construit. Et bien qu'il semble extrêmement complexe, il se compose en fait de plusieurs effets relativement simples. Cette approche de l'écriture de shaders est nécessaire pour devenir un artiste technique à succès. Par conséquent, j'espère que vous ferez ce voyage avec moi, dans lequel nous explorerons non seulement la création de shaders, mais aussi apprendre à combiner l'esthétique et le gameplay.

Analyse de sable dans Journey


Cet article, comme de nombreuses autres tentatives pour recréer le rendu de sable Journey , est basé sur un rapport de GDC que l'ingénieur principal de la société John Edwards, intitulé " Sand Rendering in Journey ". Dans cet exposé, John Edwards parle de toutes les couches d'effets ajoutées aux dunes de sable de Journey pour obtenir le bon look.


Le rapport est très utile, mais dans le contexte de ce didacticiel, de nombreuses limitations et décisions prises par John Edwards ne sont pas importantes. Nous allons essayer de recréer les shaders de sable, qui rappellent le shader Journey , principalement par des références visuelles.

Commençons par un simple maillage 3D d'une dune parfaitement lisse. La crédibilité du rendu du sable dépend de deux aspects: l'éclairage et le grain. Un moyen intéressant de réfléchir la lumière du sable est fourni par un modèle d'éclairage modifié. Dans le contexte du codage des shaders, le modèle d'éclairage détermine les ombres et les reflets en fonction des propriétés du modèle et des conditions d'éclairage de la scène.

Cependant, tout cela ne suffit pas pour créer l'illusion du réalisme. Le problème est que le sable ne peut tout simplement pas être modélisé avec des surfaces planes. Le grain de sable doit être pris en considération. C'est pourquoi il existe deux effets distincts qui fonctionnent directement avec la normale à la surface , qui peuvent être utilisés pour simuler de petites particules de sable à la surface de la dune.

Le diagramme ci-dessous montre tous les effets que nous allons apprendre dans ce tutoriel. D'un point de vue technique, des calculs normaux sont effectués avant le traitement de l'éclairage. Pour faciliter l'étude, les effets seront décrits dans un ordre différent.


Couleur diffuse

L'effet de shader de sable le plus simple est sa couleur diffuse , qui décrit grossièrement la composante terne de l'apparence générale. La couleur diffuse est calculée en fonction de la couleur réelle de l'objet et des conditions d'éclairage. Une sphère peinte en blanc ne sera pas parfaitement blanche partout, car la couleur diffuse dépend de la lumière incidente sur elle. Les couleurs diffuses sont calculées à l'aide d'un modèle mathématique qui se rapproche de la réflexion de la lumière d'une surface. Grâce à un rapport de John Edwards avec le GDC, nous connaissons exactement l'équation utilisée, qu'il appelle la réflectance à contraste diffus ; il est basé sur le modèle bien connu de réflexions Lambert .



Avant et après l'application de l'équation

Sable normal

La géométrie d'origine est complètement lisse. Pour compenser cela, la normale de surface du modèle est modifiée à l'aide d'une technique appelée bump mapping . Il vous permet d'utiliser une texture pour simuler une géométrie plus complexe.



Éclairage de bord

Chaque niveau Journey utilise une palette de couleurs limitée. Pour cette raison, il est assez difficile de comprendre où se termine une dune et où commence une autre. Pour augmenter la lisibilité, la technique de petite mise en évidence de ce qui n'est visible que le long du bord de la dune est utilisée. Il s'agit de l' éclairage de jante , et il existe de nombreuses façons de le mettre en œuvre. Pour ce tutoriel, j'ai choisi une méthode basée sur les réflexions de Fresnel qui modélise les réflexions sur les surfaces polies à des angles dits d' incidence .



Miroir reflet de l'océan

L'un des aspects les plus agréables du gameplay de Journey est la possibilité de «surfer» sur les dunes de sable. C'est probablement pourquoi cette entreprise de jeux vidéo voulait que le sable ressemble plus à un liquide qu'à un solide. Pour cela, une forte réflexion a été utilisée, que l'on retrouve souvent dans les shaders à eau. John Edwards appelle cet effet l' océan spéculaire , et dans le tutoriel, nous l'implémentons en utilisant la réflexion de Blinn-Fong .



Reflet éblouissant

L'ajout d'un composant spéculaire océanique au shader de sable lui donne un aspect plus fluide. Cependant, il ne permet toujours pas de transmettre l'un des aspects visuels les plus importants du sable: les réflexions aléatoires. Dans les vraies dunes, cet effet se produit car chaque grain de sable réfléchit la lumière dans sa direction et très souvent l'un de ces rayons réfléchis pénètre dans notre œil. Une telle réflexion scintillante (réflexion des réflexions) se produit même dans des endroits où la lumière directe du soleil ne tombe pas; il complète l'océan spéculaire et renforce le sentiment de crédibilité.



Vagues de sable

Changer les normales nous a permis de simuler l'effet de petits grains de sable recouvrant la surface de la dune. Sur les dunes du monde réel, des vagues provoquées par le vent apparaissent souvent. Leur forme varie en fonction de la pente et de la position de chaque dune par rapport à la direction du vent. Potentiellement, de tels motifs peuvent être créés grâce à une texture de relief, mais dans ce cas, il sera impossible de changer la forme des dunes en temps réel. La solution proposée par John Edwards est similaire à une technique appelée ombrage triplanaire : elle utilise quatre textures différentes, mélangées selon la position et la pente de chaque dune.



Journey Sand Shader Anatomy


Unity propose de nombreux modèles de shaders pour vous aider à démarrer. Puisque nous sommes intéressés par les matériaux pouvant recevoir de l'éclairage et des ombres projetées, nous devons commencer par le shader de surface (shader de surface).

Tous les shaders de surface sont réalisés en deux étapes. Tout d'abord, une fonction de surface est appelée qui recueille les propriétés de la surface à rendre, par exemple son albédo , sa rugosité , ses propriétés métalliques , sa transparence et sa direction normale . Ensuite, toutes ces propriétés sont transférées à la fonction d'éclairage , qui prend en compte l'influence des sources de lumière externes et calcule l'ombrage et l'éclairage.

Fonction de surface


Commençons par ce qui devient le cœur de notre fonction de surface, appelé dans le code surf ci-dessous. Les seules propriétés que nous devons définir sont la couleur du sable et la normale à la surface . La normale d'un modèle 3D est un vecteur indiquant la position de la surface. Les vecteurs normaux sont utilisés par la fonction d'éclairage pour calculer la manière dont la lumière sera réfléchie. Ils sont généralement calculés lors de l'importation du maillage. Cependant, ils peuvent être modifiés pour simuler une géométrie plus complexe. C'est ici que les effets sable normal et vagues de sable déforment la norme sable pour simuler sa rugosité.

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; float3 N = float3(0, 0, 1); N = RipplesNormal(N); N = SandNormal (N); o.Normal = N; } 

Lorsque vous écrivez des normales à o.Normal elles doivent être exprimées dans un espace tangent . Cela signifie que le vecteur est sélectionné par rapport à la surface du modèle 3D. Autrement dit, float3(0, 0, 1) signifie en fait qu'aucune modification n'est réellement apportée au modèle 3D normal.

Les deux fonctions, RipplesNormal et SandNormal reçoivent SandNormal le vecteur normal et le modifient. Plus tard, nous verrons comment cela peut être fait.

Fonction d'éclairage


C'est dans la fonction d'éclairage que tous les autres effets sont implémentés. Le code ci-dessous montre comment chaque composant individuel est calculé dans des fonctions distinctes (couleur diffuse, éclairage du bord, réflexion spéculaire de l'océan et paillettes). Ensuite, ils sont tous combinés.

 #pragma surface surf Journey fullforwardshadows float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi) { float3 diffuseColor = DiffuseColor (); float3 rimColor = RimLighting (); float3 oceanColor = OceanSpecular (); float3 glitterColor = GlitterSpecular (); float3 specularColor = saturate(max(rimColor, oceanColor)); float3 color = diffuseColor + specularColor + glitterColor; return float4(color * s.Albedo, 1); } 

La méthode de combinaison des composants est assez arbitraire et nous permet de la changer pour étudier les possibilités artistiques.

En règle générale, les réflexions spéculaires s’empilent sur les couleurs diffuses. Puisqu'il n'y a pas ici une, mais trois réflexions spéculaires ( lumière du bord , spéculaire océanique et spéculaire scintillant ), nous devons être plus prudents afin de ne pas faire scintiller le sable trop . Puisque la lumière de la jante et l'océan spéculaire font partie du même effet, nous ne pouvons en choisir que la valeur maximale. Le spéculaire scintillant est ajouté séparément car ce composant crée du sable vacillant.

Partie 2. Couleur diffuse


Dans la deuxième partie de l'article, nous nous concentrerons sur le modèle d'éclairage utilisé dans le jeu et sur cela. comment le recréer dans Unity.

Dans la partie précédente, nous avons jeté les bases de ce qui deviendra progressivement notre version du shader de sable Journey. Comme mentionné précédemment, la fonction d'éclairage est utilisée dans les shaders de surface pour calculer l'effet de l'éclairage, de sorte que les ombres et les reflets apparaissent sur la surface. Nous avons découvert que Journey a plusieurs effets qui entrent dans cette catégorie. Nous commencerons par l'effet le plus basique (et le plus simple) que l'on retrouve au coeur de ce shader: son éclairage diffus ( éclairage diffus / diffus).


Pour l'instant, nous omettons tous les autres effets et composants, en nous concentrant sur l' éclairage du sable .

La fonction d'éclairage que nous avons DiffuseColor dans la partie précédente de l'article intitulée LightingJourney délègue simplement le calcul de la couleur diffuse du sable à une fonction appelée DiffuseColor .

 float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi) { // Lighting properties float3 L = gi.light.dir; float3 N = s.Normal; // Lighting calculation float3 diffuseColor = DiffuseColor(N, L); // Final color return float4(diffuseColor, 1); } 

Du fait que chaque effet est autonome et stocké dans sa propre fonction, notre code sera plus modulaire et plus propre.

Réflexion Lambert


Avant de créer un éclairage diffus "comme dans Journey", il est agréable de voir à quoi ressemble la fonction d'éclairage diffus "basique". La technique d'ombrage la plus simple pour les matériaux mats est appelée réflectance lambertienne . Ce modèle se rapproche de l'apparence de la plupart des surfaces non brillantes et non métalliques. Il doit son nom au scientifique encyclopédique suisse Johann Heinrich Lambert , qui a proposé son concept en 1760.

Le concept de réflexion de Lambert est basé sur une idée simple: la luminosité d'une surface dépend de la quantité de lumière qui y est incidente . Géométriquement, cela peut être illustré dans le diagramme ci-dessous, où la sphère est éclairée par une source de lumière à distance. Bien que les zones rouges et vertes de la sphère reçoivent la même quantité d'éclairage, leurs surfaces sont sensiblement différentes. Si la lumière dans la zone rouge est distribuée sur une plus grande zone, cela signifie que chaque unité du carré rouge reçoit moins de lumière que le vert.


Théoriquement, la réflexion de Lambert dépend de l'angle relatif entre la surface et la lumière incidente . D'un point de vue mathématique, nous disons qu'il s'agit d'une fonction de la normale à la surface et à la direction de l'illumination . Ces quantités sont exprimées à l'aide de deux vecteurs de longueur unitaire (appelés vecteurs unitaires ) N et L . Les vecteurs simples sont un moyen standard de spécifier des directions dans le contexte du codage des shaders.

La valeur de N et L
Normal à la surface N Est un vecteur unitaire dirigé loin de la surface elle-même.

Par analogie, nous pouvons supposer que la direction de l'éclairage L pointe de la source de lumière et suit la direction dans laquelle la lumière se déplace. Mais ce n'est pas le cas: la direction de l'illumination est un vecteur unique pointant dans la direction de la direction d'où provient la lumière.

Cela peut être déroutant, surtout si vous êtes nouveau dans la création de shaders. Cependant, grâce à une telle notation, les équations deviennent plus faciles.

Réflexion de Lambert dans l'unité
Avant Unity 5 Standard Shader , la réflexion Lambert était le modèle standard pour ombrer les surfaces éclairées.

Vous pouvez toujours y accéder dans l'inspecteur des matériaux: dans le shader Legacy, il est appelé Diffus .

Si vous écrivez votre propre shader de surface, la réflexion Lambert est disponible en tant que fonction d'éclairage appelée Lambert :

 #pragma surface surf Lambert fullforwardshadows 

Son implémentation se trouve dans la fonction LightingLambert définie dans le fichier CGIncludes\Lighting.cginc .

Réflexion de Lambert et climat
La réflexion de Lambert est un modèle assez ancien, mais elle permet de comprendre des concepts complexes tels que l'ombrage de surface. Il peut également être utilisé pour expliquer de nombreux autres phénomènes. Par exemple, le même diagramme explique pourquoi il fait plus froid aux pôles de la planète qu'à l'équateur.

Après avoir regardé de plus près, nous pouvons voir que la surface reçoit le maximum d'éclairement lorsque sa normale est parallèle à la direction de l'éclairage. Et vice versa: il n'y a pas de lumière si deux vecteurs unitaires sont perpendiculaires l'un à l'autre.


De toute évidence, l'angle entre N et L critique pour la réflexion selon Lambert. De plus, la luminosité est maximale et égale à 100 $ \% $ quand l'angle est 0 et minimal ( 0% ) lorsque l'angle tend à 90 $ ^ {\ circ} $ . Si vous connaissez l' algèbre vectorielle , vous pourriez comprendre qu'une quantité représentant la réflexion de Lambert I est égal à N cdotL où est l'opérateur  cdot appelé produit scalaire .

(1)

$$ afficher $$ \ commencer {équation *} I = N \ cdot L \ terminer {équation *} $$ afficher $$


Le produit scalaire est une mesure de la «coïncidence» de deux vecteurs l'un par rapport à l'autre et varie dans l'intervalle de +1 (pour deux vecteurs identiques) à 1 (pour deux vecteurs opposés). Un produit scalaire est le fondement de l'ombrage, que j'ai examiné en détail dans le didacticiel sur les modèles de rendu et d'éclairage physiquement basés .

Implémentation


Et pour N et à L Vous pouvez facilement accéder aux fonctions d'éclairage du shader de surface via s.Normal et gi.light.dirin . Pour simplifier, nous les renommerons dans le code du shader en N et L

 float3 DiffuseColor(float3 N, float3 L) { float NdotL = saturate( dot(N, L) ); return NdotL; } 

saturate fonction saturate limite la valeur de 0 avant 1 . Cependant, comme le produit scalaire se situe dans la plage de 1 avant +1 , nous devrons travailler uniquement avec ses valeurs négatives. C'est pourquoi la réflexion Lambert est souvent mise en œuvre comme suit:

 float NdotL = max(0, dot(N, L) ); 

Réflexion de contraste de la lumière ambiante


Bien que la réflexion de Lambert nuise bien à la plupart des matériaux, elle n'est ni physiquement précise ni photoréaliste. Dans les jeux plus anciens, les shaders Lambert étaient largement utilisés. Les jeux qui utilisent cette technique semblent souvent vieux car ils peuvent reproduire par inadvertance l'esthétique des vieux jeux. Si vous ne vous efforcez pas pour cela, alors la réflexion Lambert devrait être évitée et utiliser une technologie plus moderne.

Un tel modèle est le modèle de réflexion Oren-Nayyar , qui a été initialement décrit dans l'article Généralisation du modèle de réflexion de Lambert , publié en 1994 par Michael Oren et Sri C. Nayyar. Le modèle Oren-Nayyar est une généralisation de la réflexion Lambert et est spécialement conçu pour les surfaces rugueuses. Initialement, les développeurs de Journey voulaient utiliser la réflexion Oren-Nayyar comme base pour leur shader de sable. Cependant, cette idée a été abandonnée en raison des coûts de calcul élevés.

Dans son rapport de 2013, l'artiste technique John Edwards explique que le modèle de réflexion créé pour le sable Journey était basé sur une série d'essais et d'erreurs.Les développeurs n'avaient pas l'intention de recréer le rendu photoréaliste du désert, mais de donner vie à une esthétique concrète immédiatement reconnaissable.

Selon lui, le modèle d'ombrage résultant correspond à cette équation:

(2)

$$ afficher $$ \ commencer {équation *} I = 4 * \ gauche (\ gauche (N \ odot \ gauche [1, 0,3, 1 \ droite] \ droite) \ cdot L \ droite) \ end {équation *} $$ afficher $$


 odot - produit élément par élément de deux vecteurs.

 float3 DiffuseColor(float3 N, float3 L) { Ny *= 0.3; float NdotL = saturate(4 * dot(N, L)); return NdotL; } 

Modèle de réflexion (2) John Edwards appelle le contraste diffus , nous allons donc utiliser ce nom tout au long du didacticiel.

L'animation ci-dessous montre la différence d'ombrage Lambert (à gauche) et le contraste diffus de Journey (à droite).



Quelle est la signification de 4 et 0,3?
Bien que le contraste diffus n'ait pas été conçu pour être physiquement précis, nous pouvons toujours essayer de comprendre ce qu'il fait.

À la base, il utilise toujours la réflexion Lambert. La première différence évidente est que le résultat global est multiplié par 4 . Cela signifie que tous les pixels normalement reçus 25 $ \% $ l'éclairage brillera maintenant comme s'il recevait 100 $ \% $ l'éclairage. En multipliant tout par 4 l'ombrage faible selon Lambert devient beaucoup plus fort, et la région de transition entre l'obscurité et la lumière est plus petite. Dans ce cas, l'ombre devient plus nette.

Effet de la multiplication de la composante y sur la direction normale 0,3 $ expliquer est beaucoup plus difficile. À mesure que les composants du vecteur changent, la direction générale dans laquelle il pointe change. Réduire la valeur de la composante y à tout 30 % à partir de sa valeur d'origine, la réflexion du contraste diffus fait que les ombres deviennent plus verticales.

Remarque: un produit scalaire mesure directement l'angle entre deux vecteurs uniquement s'ils ont tous deux une longueur 1 . Le changement effectué réduit la longueur normale N qui n'est plus un vecteur unitaire.

Des nuances de gris à la couleur


Toutes les animations présentées ci-dessus ont des nuances de gris, car elles montrent les valeurs de leur modèle de réflexion, variant dans l'intervalle de 0 avant 1 ". Nous pouvons facilement ajouter des couleurs en utilisant NdotL comme coefficient d'interpolation entre deux couleurs: une pour le sable entièrement ombragé et l'autre pour le sable entièrement éclairé.

 float3 _TerrainColor; float3 _ShadowColor; float3 DiffuseColor(float3 N, float3 L) { Ny *= 0.3; float NdotL = saturate(4 * dot(N, L)); float3 color = lerp(_ShadowColor, _TerrainColor, NdotL); return color; } 

Partie 3. Sable normal


Dans la troisième partie, nous nous concentrerons sur la création de cartes normales qui transforment des modèles 3D lisses en dunes de sable.

Dans la partie précédente du tutoriel, nous avons implémenté l'éclairage diffus du sable Journey. En utilisant uniquement cet effet, les dunes du désert sembleront plutôt plates et ennuyeuses.


L'un des effets les plus intrigants de Journey est la granularité du sable. En regardant n'importe quelle capture d'écran, il nous semble que les dunes ne sont pas lisses et homogènes, mais créées à partir de millions de grains de sable microscopiques.


Cet effet peut être obtenu en utilisant une technique appelée bump mapping , qui permet à la lumière de rebondir sur une surface plane comme si elle était plus complexe. Voyez comment cet effet change l'apparence du rendu:



De petites différences peuvent être observées avec l'augmentation:



Nous traitons des cartes normales


Le sable se compose d'innombrables grains de sable, chacun ayant sa propre forme et composition (voir ci-dessous). Chaque particule individuelle réfléchit l'éclairage dans une direction potentiellement aléatoire. Une façon de réaliser cet effet est de créer un modèle 3D contenant tous ces grains de sable microscopiques. Mais en raison du nombre incroyable de polygones requis, cette approche n'est pas réalisable.

Mais il existe une autre solution souvent utilisée pour simuler une géométrie plus complexe par rapport à un vrai modèle 3D. Chaque sommet ou face du modèle 3D est associé à un paramètre appelé sa direction normale . Il s'agit d'un vecteur de longueur unitaire utilisé pour calculer la réflexion de la lumière sur la surface d'un modèle 3D. Autrement dit, pour simuler du sable, vous devez simuler cette distribution apparemment aléatoire de grains de sable, et donc comment ils affectent les normales de surface.


Cela peut se faire de nombreuses façons. Le plus simple est de créer une texture qui change la direction des normales d'origine du modèle de dune.

Normal à la surface N dans le cas général, il est calculé par la géométrie du modèle 3D. Cependant, vous pouvez le modifier en utilisant la carte normale . Les cartes normales sont des textures qui vous permettent de simuler une géométrie plus complexe en modifiant l'orientation locale des normales à la surface. Cette technique est souvent appelée bump mapping .

Changer les normales est une tâche assez simple qui peut être effectuée dans la fonction surf du shader de surface . Cette fonction prend deux paramètres, dont l'un est une struct appelée SurfaceOutput . Il contient toutes les propriétés nécessaires au rendu d'une partie d'un modèle 3D, de sa couleur ( o.Albedo ) à sa transparence ( o.Alpha ). Un autre paramètre qu'il contient est la direction normale ( o.Normal ), qui peut être réécrite pour changer la façon dont la lumière est réfléchie sur le modèle.

Selon la documentation Unity sur les Shaders de surface , toutes les normales écrites dans la structure o.Normal doivent être exprimées dans l' espace tangent :

 struct SurfaceOutput { fixed3 Albedo; // diffuse color fixed3 Normal; // tangent space normal, if written fixed3 Emission; half Specular; // specular power in 0..1 range fixed Gloss; // specular intensity fixed Alpha; // alpha for transparencies }; 

Ainsi, nous pouvons signaler que les vecteurs unitaires doivent être exprimés dans le système de coordonnées par rapport à la normale du maillage. Par exemple, lors de l'écriture dans o.Normal valeurs de float3(0, 0, 1) normal resteront inchangées.

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; o.Normal = float3(0, 0, 1); } 

En effet, le vecteur float3(0, 0, 1) est en fait un vecteur normal exprimé par rapport à la géométrie du modèle 3D.

Donc, pour changer la normale à la surface dans le shader de surface , il suffit d'écrire un nouveau vecteur dans la fonction de surface en o.Normal :

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; o.Normal = ... // change the normal here } 

Dans le reste de l'article, nous allons créer l'approximation initiale, que nous compliquerons dans la sixième partie du tutoriel.

Sable normal


La partie la plus problématique consiste à comprendre comment les grains de sable changent normalement à la surface. Bien qu'individuellement chaque grain de sable puisse diffuser la lumière dans n'importe quelle direction, dans l'ensemble, quelque chose d'autre se produit. Toute approche physiquement précise doit étudier la distribution des vecteurs normaux sur la surface du sable et la modéliser mathématiquement. De tels modèles existent vraiment, mais la solution présentée dans notre tutoriel est beaucoup plus simple, et en même temps très efficace.

À chaque point du modèle, un vecteur unitaire aléatoire est échantillonné à partir de la texture. Ensuite, la normale à la surface s'incline d'une certaine quantité vers ce vecteur. Avec la création correcte d'une texture aléatoire et la sélection d'une quantité appropriée de mélange, nous pouvons déplacer la normale à la surface de manière à créer un sentiment de granulation, sans perdre la courbure globale des dunes.

Les valeurs aléatoires peuvent être échantillonnées en utilisant une texture remplie de couleurs aléatoires. Les composantes R, G et B de chaque pixel sont utilisées comme composantes X, Y et Z du vecteur normal. Les composants de couleur sont dans la gamme  gauche[0,1 droite] , ils doivent donc être convertis en un intervalle  gauche[1,+1 droite] . Ensuite, le vecteur résultant est normalisé de sorte que sa longueur soit égale à 1 .


Créez des textures aléatoires
Il existe de nombreuses façons de générer des textures aléatoires. Pour obtenir l'effet souhaité, le plus important est la distribution générale des vecteurs aléatoires qui peuvent être échantillonnés à partir de la texture.

Dans l'image ci-dessus, chaque pixel est complètement aléatoire. Il n'y a pas de direction générale (couleur) qui prévaut dans la texture, car chaque valeur a la même probabilité que toutes les autres. Cette texture nous donne un type de sable qui disperse la lumière dans toutes les directions.

Lors d'une conférence GDC, John Edwards a clairement indiqué que la texture aléatoire utilisée pour le sable dans Journey était générée à partir d'une distribution gaussienne. Cela garantit que la direction dominante coïncide avec la normale à la surface.

Les vecteurs aléatoires doivent-ils être normalisés?
L'image que j'ai utilisée pour échantillonner des vecteurs aléatoires a été générée à l'aide d'un processus complètement aléatoire. Non seulement chaque pixel est généré individuellement: les composants R, G et B d'un pixel sont également indépendants les uns des autres. Autrement dit, dans le cas général, les vecteurs échantillonnés à partir de cette texture ne seront pas garantis d'avoir une longueur égale à 1 .

Bien sûr, vous pouvez générer une texture dans laquelle chaque pixel lors de la conversion de  g a u c h e [ 0 , 1 d r o i t e ]  dans  g a u c h e [ - 1 , + 1 d r o i t e ]  et devra en fait avoir une longueur 1 . Cependant, deux problèmes se posent ici.

, . -, mip-, .

, .

Implémentation


Dans la partie précédente du didacticiel, nous nous sommes familiarisés avec le concept de «cartes normales» lorsqu'il est apparu dans le tout premier contour de la fonction de surface surf . En rappelant le diagramme présenté au début de l'article, vous pouvez voir que deux effets sont nécessaires pour recréer le rendu du sable Journey. La première ( normales de sable ) que nous considérons dans cette partie de l'article, et la seconde ( vagues de sable ) que nous étudierons dans la sixième partie.

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; float3 N = float3(0, 0, 1); N = RipplesNormal(N); // Covered in Journey Sand Shader #6 N = SandNormal (N); // Covered in this article o.Normal = N; } 

Dans la section précédente, nous avons introduit le concept de bump mapping, qui nous a montré qu'une partie de l'effet nécessiterait un échantillonnage de la texture (c'est ce qu'on appelle dans le code uv_SandTex).

Le problème avec le code ci-dessus est que pour les calculs, vous devez connaître la position réelle du point que nous dessinons. En fait, vous avez besoin d'une coordonnée UV pour échantillonner la texture , qui détermine à partir de quel pixel lire. Si le modèle 3D que nous utilisons est relativement plat et a une conversion UV, alors nous pouvons utiliser son UV pour échantillonner une texture aléatoire.

 N = WavesNormal(IN.uv_SandTex.xy, N); N = SandNormal (IN.uv_SandTex.xy, N); 

Ou vous pouvez également utiliser la position dans le monde ( IN.worldPos) du point rendu.

Maintenant, nous pouvons enfin nous concentrer sur SandNormalsa mise en œuvre. Comme indiqué précédemment dans cette partie, l'idée est d'échantillonner un pixel à partir d'une texture aléatoire et de l'utiliser (après la conversion en un vecteur unitaire) comme nouvelle normale.

 sampler2D_float _SandTex; float3 SandNormal (float2 uv, float3 N) { // Random vector float3 random = tex2D(_SandTex, uv).rgb; // Random direction // [0,1]->[-1,+1] float3 S = normalize(random * 2 - 1); return S; } 

Comment zoomer une texture aléatoire?
UV- 3D- , . , .

, Unity . , _SandText_ST . Unity ( ) _SandTex .

_SandText_ST : . , Tiling Offset :


, TRANSFORM_TEX :

 sampler2D_float _SandTex; float4 _SandTex_ST; float3 SandNormal (float2 uv, float3 N) { // Random vector float3 random = tex2D(_SandTex, TRANSFORM_TEX(uv, _SandTex)).rgb; // Random direction // [0,1]->[-1,+1] float3 S = normalize(random * 2 - 1); return S; } 

Inclinez les normales


L'extrait de code illustré ci-dessus fonctionne, mais ne produit pas de très bons résultats. La raison en est simple: si nous retournons simplement une normale complètement aléatoire, mais perdons essentiellement la sensation de courbure. En fait, la direction de la normale est utilisée pour calculer comment la lumière doit être réfléchie par la surface, et son objectif principal est d'ombrer le modèle en fonction de sa courbure.

La différence peut être vue dans les images ci-dessous. Ci-dessus, les normales des dunes sont complètement aléatoires, et il est impossible de comprendre où l'une finit et où commence une autre. En dessous, seule la normale du modèle est utilisée, ce qui nous donne une surface trop lisse.



Les deux solutions ne nous conviennent pas. Nous avons besoin de quelque chose entre les deux. Une direction aléatoire échantillonnée à partir d'une texture doit être utilisée pour incliner la normale d'une certaine quantité, comme indiqué ci-dessous:


L'opération décrite dans le diagramme est appelée slerp , qui signifie interpolation linéaire sphérique (interpolation linéaire sphérique). Slerp fonctionne exactement comme lerp, à une exception près - il peut être utilisé pour interpoler en toute sécurité entre des vecteurs unitaires, et le résultat de l'opération sera d'autres vecteurs unitaires.

Malheureusement, l'implémentation correcte de slerp est assez chère. Et pour un effet, du moins basé sur le hasard, il est illogique de l'utiliser.

Montrez-moi l'équation slerp
, p0 et p1 , . Alors slerp :

(1)

slerp(p0,p1,t)=sin[(1t)Ω]sin(Ω)p0+sin(tΩ)sin(Ω)p1


Ωp0 et p1 , :

(2)

Ω=cos1(p0p1)



Il est important de noter que si nous utilisons l'interpolation linéaire traditionnelle , le vecteur résultant sera très différent:


L'opération Lerp entre deux vecteurs unitaires séparés ne crée pas toujours d'autres vecteurs unitaires. En fait, cela ne se produit jamais, sauf si le coefficient est1 ou0 .

Malgré cela, la normalisation du résultat lerp aboutit en fait à un vecteur unité qui est étonnamment proche du résultat généré par slerp:

 float3 nlerp(float3 n1, float3 n2, float t) { return normalize(lerp(n1, n2, t)); } 

Cette technique, appelée nlerp , fournit une approximation étroite de slerp. Son utilisation a été popularisée par Casey Muratori , l'un des développeurs de The Witness . Si vous souhaitez en savoir plus sur ce sujet, je vous recommande de comprendre les articles Slerp. Then Not Use It de Jonathan Blow et Math Magician - Lerp, Slerp et Nlerp .

Grâce à nlerp, nous pouvons désormais incliner efficacement les vecteurs normaux d'un côté aléatoire, échantillonnés à partir de _SandTex:

 sampler2D_float _SandTex; float _SandStrength; float3 SandNormal (float2 uv, float3 N) { // Random vector float3 random = tex2D(_SandTex, uv).rgb; // Random direction // [0,1]->[-1,+1] float3 S = normalize(random * 2 - 1); // Rotates N towards Ns based on _SandStrength float3 Ns = nlerp(N, S, _SandStrength); return Ns; } 

Le résultat est présenté ci-dessous:



Et ensuite


Dans la prochaine partie, nous considérerons les reflets vacillants, grâce auxquels les dunes ressembleront à l'océan.

Remerciements


Le jeu vidéo Journey a été développé par Thatgamecompany et publié par Sony Computer Entertainment . Il est disponible pour PC ( Epic Store ) et PS4 ( PS Store ).

Des modèles 3D d'arrière-plans de dunes et d'options d'éclairage sont créés par Jiadi Deng .

Un modèle 3D du personnage de Journey a été trouvé sur le forum FacePunch (maintenant fermé).

Forfait Unity


Si vous souhaitez recréer cet effet, le package Unity complet peut être téléchargé depuis Patreon . Il comprend tout ce dont vous avez besoin, des shaders aux modèles 3D.

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


All Articles