Exploration du jeu de shader de sable Journey

Début d'une série d'articles ici

image

Partie 4: Image miroir


Dans cette partie, nous nous concentrerons sur les reflets miroir, grâce auxquels les dunes ressemblent à un océan de sable.

L'un des effets les plus intrigants du rendu du sable de Journey est la façon dont les dunes scintillent dans les rayons de lumière. Cette réflexion est appelée spéculaire . Le nom vient du mot latin spéculum , qui signifie «miroir» . La réflexion spéculaire est un concept «parapluie» qui combine tous les types d'interactions dans lesquelles la lumière est fortement réfléchie dans une direction plutôt que dispersée ou absorbée. Grâce aux reflets spéculaires, l'eau et les surfaces polies sous un certain angle semblent scintillantes.

En voyageIl existe trois types de reflets de miroir: l' éclairage de la jante , les reflets spéculaires de l'océan et les paillettes , illustrés dans le diagramme ci-dessous. Dans cette partie, nous examinerons les deux premiers types.




Avant et après l'application des réflexions miroir

Éclairage sur jante


Vous remarquerez peut-être qu'à chaque niveau de voyage , un ensemble limité de couleurs est présenté. Et bien que cela crée une esthétique forte et propre, cette approche complique le rendu du sable. Les dunes ne sont rendues que par un nombre limité de nuances, il est donc difficile pour le joueur de comprendre où l'une se termine à longue distance et où commence l'autre.

Pour compenser cela, le bord de chaque dune a un léger effet d'éclat, mettant en valeur ses contours. Cela permet aux dunes de ne pas se cacher dans l'horizon et crée l'illusion d'un environnement beaucoup plus large et plus complexe.

Avant de commencer à comprendre comment implémenter cet effet, développons la fonction d'éclairage en y ajoutant des couleurs diffuses (considéré par nous dans la partie précédente de l'article) et une nouvelle composante généralisée de la réflexion spéculaire.

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);
    float3 rimColor     = RimLighting  (N, V);

    // Combining
    float3 color = diffuseColor + rimColor;

    // Final color
    return float4(color * s.Albedo, 1);
}

Dans l'extrait de code ci-dessus, nous voyons que le composant miroir de l'éclairage de la jante, qui est appelé rimColor, est simplement ajouté à la couleur diffuse d'origine.

Effets de plage dynamique et de floraison élevés
La composante diffuse et la lueur des bords sont des couleurs RVB dans la plage de 0avant 1. . , 1.

, , 01. , , 1. High Dynamic Range, 1«» . bloom, . .

Réflexions de Fresnel


L'éclat des bords peut être réalisé de différentes manières. Le codage de shader le plus populaire utilise le modèle de réflexion de Fresnel bien connu .

Pour comprendre l'équation sous-jacente à la réflexion de Fresnel, il est utile de visualiser où elle se produit. Le diagramme ci-dessous montre comment la dune est visible à travers la caméra (en bleu). La flèche rouge indique la surface normale du sommet de la dune, où elle devrait être une image miroir. Il est facile de voir que tous les bords de la dune ont une propriété commune: leur normale (N, rouge) est perpendiculaire à la direction de la vue (V, de couleur bleue).


Semblable à ce que nous avons fait dans la partie sur la couleur diffuse, vous pouvez utiliser le produit scalaire Net Vpour obtenir une mesure de leur parallélisme. Dans ce casNV équivaut à 0, car deux vecteurs unitaires sont perpendiculaires; à la place, nous pouvons utiliser1NVpour obtenir des mesures de leur non-parallélisme.

Utilisation directe1NVne nous donnera pas de bons résultats, car la réflexion sera trop forte. Si nous voulons rendre la réflexion plus nette , nous pouvons simplement prendre l'expression en degrés. Degré de grandeur de0 avant 1reste limité à un intervalle, mais la transition entre l'obscurité et la lumière devient plus nette.

Le modèle de réflexion de Fresnel indique que la luminosité de la lumièreI est défini comme suit:

I=(1NV)powerstrength(1)


poweret strength- Ce sont deux paramètres qui peuvent être utilisés pour contrôler le contraste et la force de l'effet. Paramètrespower et strengthparfois appelé spéculaire et brillant , mais les noms peuvent varier.

L'équation (1) est très facile à convertir en code:

float _TerrainRimPower;
float _TerrainRimStrength;
float3 _TerrainRimColor;

float3 RimLighting(float3 N, float3 V)
{
    float rim = 1.0 - saturate(dot(N, V));
    rim = saturate(pow(rim, _TerrainRimPower) * _TerrainRimStrength);
    rim = max(rim, 0); // Never negative
    return rim * _TerrainRimColor;
}

Son résultat est montré dans l'animation ci-dessous.


Océan spéculaire


L'un des aspects les plus originaux du gameplay de Journey est que parfois un joueur peut littéralement «surfer» sur les dunes. L'ingénieur en chef John Edwards a expliqué que cette entreprise de jeu cherchait à rendre le sable plus senti non pas solide, mais liquide.

Et ce n'est pas tout à fait faux, car le sable peut être perçu comme une approximation très grossière d'un liquide. Et dans certaines conditions, par exemple, dans un sablier, il se comporte même comme un liquide.

Pour renforcer l'idée que le sable peut avoir une composante liquide, Journey a ajouté un deuxième effet de réflexion, que l'on retrouve souvent dans les corps liquides. John Edwards l'appelle spéculaire océanique: L'idée est d'obtenir le même type de reflets visibles à la surface de l'océan ou du lac au coucher du soleil (voir ci-dessous).


Comme précédemment, nous apporterons des modifications à la fonction d'éclairage LightingJourneypour y ajouter un nouveau type de réflexion spéculaire.

float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi)
{
    // Lighting properties
    float3 L = gi.light.dir;
    float3 N = s.Normal;
    float3 V = viewDir;

    // Lighting calculation
    float3 diffuseColor	= DiffuseColor  (N, L);
    float3 rimColor     = RimLighting   (N, V);
    float3 oceanColor   = OceanSpecular (N, L, V);

    // Combining
    float3 specularColor = saturate(max(rimColor, oceanColor));
    float3 color = diffuseColor + specularColor;

    // Final color
    return float4(color * s.Albedo, 1);
}

Pourquoi prenons-nous au maximum deux composantes de réflexion?
, rim lighting ocean specular. , , -. , .

, .

Les réflexions miroir sur l'eau sont souvent réalisées en utilisant la réflexion Blinn-Fong , qui est une solution à faible coût pour les matériaux brillants. Il a été décrit pour la première fois par James F. Blinn en 1977 (article: " Modèles de réflexion de la lumière pour les images synthétisées par ordinateur ") comme une approximation d'une technique d'ombrage antérieure développée par Bui Tyong Fong en 1973 (article: " Illumination pour les images générées par ordinateur ") .

Lors de l'utilisation de l'ombrage Blinnu-Phong, la luminositéI surfaces est donnée par l'équation suivante:

I=(NH)powerstrength(2)




H=V+LV+L(3)


Le dénominateur de l'équation (3) divise le vecteur V+Lsur sa longueur. Cela garantit queH a une longueur 1. La fonction de shader équivalente pour effectuer cette opération est la suivante normalize. D'un point de vue géométrique,H représente le vecteur entre Vet L, et est donc appelé un demi-vecteur .



Pourquoi H est-il entre V et L?
, , HVL.

, . VLLVVL.

, , V+LL+V, . , :


, , . , ( V+L) . , ( ).


, V+LVL, 1. , 1, ( ).

Une description plus détaillée de la réflexion Blinn-Fong peut être trouvée dans le didacticiel Modèles de rendu et d'éclairage à base physique . Voici une implémentation simple de celui-ci dans le code du shader.

float _OceanSpecularPower;
float _OceanSpecularStrength;
float3 _OceanSpecularColor;

float3 OceanSpecular (float3 N, float3 L, float3 V)
{
    // Blinn-Phong
    float3 H = normalize(V + L); // Half direction
    float NdotH = max(0, dot(N, H));
    float specular = pow(NdotH, _OceanSpecularPower) * _OceanSpecularStrength;
    return specular * _OceanSpecularColor;
}

L'animation présente une comparaison de l'ombrage diffus traditionnel selon Lambert et du miroir selon Blinn-Fong:


Partie 5: réflexion brillante


Dans cette partie, nous recréerons les reflets brillants qui sont généralement visibles sur les dunes de sable.

Peu de temps après avoir publié ma série d'articles, Julian Oberbek et Paul Nadelek ont fait leur propre tentative pour recréer une scène inspirée du jeu Journey in Unity. Le tweet ci-dessous montre comment ils ont perfectionné des reflets brillants pour fournir une plus grande intégrité temporelle. En savoir plus sur leur mise en œuvre dans un article sur IndieBurg Mip Map Folding .


Dans la partie précédente du cours, nous avons révélé l'implémentation de deux fonctions de miroir dans le rendu de sable Journey : l' éclairage de la jante et l' océan spéculaire . Dans cette partie, je vais vous expliquer comment implémenter la dernière version de la réflexion spéculaire: les paillettes .


Si vous êtes déjà allé dans le désert, vous avez probablement remarqué à quel point le sable est réellement brillant. Comme discuté dans la partie des normales de sable, chaque grain de sable peut potentiellement réfléchir la lumière dans une direction aléatoire. En raison de la nature des nombres aléatoires, une partie de ces rayons réfléchis tombera dans la caméra. Pour cette raison, des points de sable aléatoires apparaissent très brillants. Ce lustre est très sensible au mouvement, car le moindre décalage empêchera les rayons réfléchis de pénétrer dans la caméra.

Dans d'autres jeux, comme Astroneer et Slime Rancher, des reflets brillants ont été utilisés pour le sable et les grottes.



Brillance: avant et après l'application de l'effet.

Il est plus facile d'évaluer ces caractéristiques de brillance sur une image plus grande:



Sans aucun doute, l'effet scintillant sur les vraies dunes dépend entièrement du fait que certains grains de sable réfléchissent au hasard la lumière dans nos yeux. À proprement parler, c'est exactement ce que nous avons déjà modélisé dans la deuxième partie du cours consacrée aux normales de sable, lorsque nous avons modélisé une distribution aléatoire de normales. Alors pourquoi avons-nous besoin d'un autre effet pour cela?

La réponse n'est peut-être pas trop évidente. Imaginons que nous essayons de recréer l'effet de brillance uniquement à l'aide de normales. Même si toutes les normales sont dirigées vers la caméra, le sable ne brillera toujours pas, car les normales ne peuvent refléter que la quantité de lumière disponible dans la scène. Autrement dit, nous ne réfléchirons que 100% de la lumière (si le sable est complètement blanc).

Mais nous avons besoin d'autre chose. Si nous voulons que le pixel apparaisse si lumineux que la lumière se propage sur les pixels adjacents, alors la couleur doit être plus grande1. Cela est dû au fait que dans Unity, lorsque le filtre de floraison est appliqué à l'appareil photo à l' aide de l'effet de post-traitement , les couleurs sont plus lumineuses1se propager aux pixels voisins et produire un halo qui donne l'impression que certains pixels brillent. C'est la base du rendu HDR .

Donc non, les normales superposées ne peuvent pas être utilisées de manière simple pour créer des surfaces brillantes. Par conséquent, cet effet est plus pratique à mettre en œuvre en tant que processus distinct.

Théorie des microfaces


Pour aborder la situation de manière plus formelle, nous devons percevoir les dunes comme constituées de miroirs microscopiques, dont chacun a une direction aléatoire. Cette approche est appelée la théorie du microfacet , où chacun de ces minuscules miroirs est appelé microfacet . La base mathématique de la plupart des modèles d'ombrage modernes est basée sur la théorie des micro-faces, y compris le modèle d' ombrage standard d'Unity .

La première étape consiste à diviser la surface de la dune en micro faces et à déterminer l'orientation de chacune d'elles. Comme déjà mentionné, nous avons fait quelque chose de similaire dans la partie du tutoriel sur les normales de sable, où la position UV du modèle 3D de la dune a été utilisée pour échantillonner une texture aléatoire. La même approche peut être utilisée ici pour attacher une orientation aléatoire à chaque micro-facette. La taille de chaque microface dépendra de l'échelle de la texture et de son niveau de texturation par mip . Notre tâche est de recréer une certaine esthétique, et non le désir de photoréalisme; cette approche nous suffira.

Après avoir échantillonné une texture aléatoire, nous pouvons associer une direction aléatoire à chaque grain de sable / micro-facette de la dune. Appelons-leG. Il indique la direction de la luminosité , c'est-à-dire la direction de la normale des grains de sable que nous regardons. Un rayon de lumière tombant sur un grain de sable sera réfléchi, compte tenu du fait que la micro facette est un miroir idéal orienté dans le sensG. Le rayon lumineux réfléchi résultant devrait pénétrer dans la caméra.R (voir ci-dessous).


Ici, nous pouvons à nouveau utiliser le produit scalaire Ret Vpour obtenir des mesures de leur parallélisme.

Une approche est l'exponentiationRV, comme expliqué dans la quatrième (quatrième) partie de l'article. Si vous essayez de le faire, nous verrons que le résultat est très différent de celui de Journey . Les reflets brillants doivent être rares et très lumineux. La façon la plus simple sera de ne considérer que les brillantes réflexions pour lesquellesRV est en dessous d'une certaine valeur seuil.

la mise en oeuvre


Nous pouvons facilement implémenter l'effet de brillant décrit ci-dessus en utilisant une fonction reflecten Cg, ce qui le rend très facile à calculerR.

sampler2D_float _GlitterTex;
float _GlitterThreshold;
float3 _GlitterColor;

float3 GlitterSpecular (float2 uv, float3 N, float3 L, float3 V)
{
    // Random glitter direction
    float3 G = normalize(tex2D(_GlitterTex, uv).rgb * 2 - 1); // [0,1]->[-1,+1]

    // Light that reflects on the glitter and hits the eye
    float3 R = reflect(L, G);
    float RdotV = max(0, dot(R, V));
	
    // Only the strong ones (= small RdotV)
    if (RdotV > _GlitterThreshold)
        return 0;
	
    return (1 - RdotV) * _GlitterColor;
}

À proprement parler, si Gcomplètement par hasard alors Rsera également complètement aléatoire. Il peut sembler que l'utilisation est reflectfacultative. Et bien que cela soit vrai pour un cadre statique, mais que se passe-t-il si la source lumineuse se déplace? Cela peut être dû soit au mouvement du soleil lui-même, soit à une source ponctuelle de lumière liée au joueur. Dans les deux cas, le sable perdra son intégrité temporelle entre les images actuelles et suivantes, en raison de quoi l'effet scintillant apparaîtra à des endroits aléatoires. Cependant, l'utilisation de la fonction reflectoffre un rendu beaucoup plus stable.

Les résultats sont montrés plus bas:


Comme nous le rappelons de la toute première partie du tutoriel, le composant brillant est ajouté à la couleur finale.

#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);
}

Il y a une forte probabilité que certains pixels finissent par obtenir plus de couleurs 1, ce qui entraînera l'effet de floraison. Voilà ce dont nous avons besoin. L'effet est également ajouté en plus de la réflexion spéculaire déjà existante (discutée dans la partie précédente de l'article), de sorte que des grains de sable brillants peuvent même être trouvés là où les dunes sont bien éclairées.

Il existe de nombreuses façons d'améliorer cette technique. Tout dépend du résultat que vous souhaitez atteindre. Dans Astroneer et Slime Rancher , par exemple, cet effet n'est utilisé que la nuit. Ceci peut être réalisé en réduisant la force de l'effet brillant en fonction de la direction de la lumière solaire.

Par exemple, la valeur max(dot(L, fixed3(0,1,0),0))est1quand le soleil tombe d'en haut, et est égal à zéro quand il est au-delà de l'horizon. Mais vous pouvez créer votre propre système, dont l'apparence dépend de vos préférences.

Pourquoi la réflexion n'est-elle pas utilisée dans la réflexion de Blinn-Fong?
ocean specular, , -.

, 3D- , , . , reflect . - RVNH, .

Partie 6: vagues


Dans la dernière partie de l'article, nous recréerons des vagues de sable typiques résultant de l'interaction des dunes et du vent.




Vagues à la surface des dunes: avant et après

Théoriquement, il serait logique de mettre cette partie après la partie sur les normales de sable. Je l'ai laissé au final car c'est le plus difficile des effets du tutoriel. Une partie de cette complexité est due à la façon dont les cartes normales sont stockées et traitées par un shader de surface qui effectue de nombreuses étapes supplémentaires.

Cartes normales


Dans la précédente (cinquième) partie, nous avons exploré une méthode de production de sable hétérogène. Dans la partie consacrée aux normales de sable, une technique de cartographie normale très populaire a été utilisée pour changer la façon dont la lumière interagit avec la surface géométrique . Il est souvent utilisé dans les graphiques 3D pour créer l'illusion que l'objet a une géométrie plus complexe et est généralement utilisé pour rendre les surfaces courbes plus lisses (voir ci-dessous).


Pour obtenir cet effet, chaque pixel est mappé dans la direction de la normale , indiquant son orientation. Et il est utilisé pour calculer l'éclairage au lieu de la véritable orientation du maillage.

En lisant les directions des normales à partir d'une texture apparemment aléatoire, nous avons pu simuler le grain. Malgré l'inexactitude physique, elle semble toujours crédible.

Vagues dans le sable


Cependant, les dunes de sable présentent une autre caractéristique qui ne peut être ignorée: les vagues. Sur chaque dune, il y a des dunes plus petites, apparaissant en raison de l'influence du vent et maintenues ensemble par le frottement de grains de sable individuels.

Ces vagues sont très visibles et visibles sur la plupart des dunes. Sur la photo ci-dessous, prise à Oman, on voit que la partie proche de la dune a un motif ondulé prononcé.


Ces vagues varient considérablement en fonction non seulement de la forme de la dune, mais également de la composition, de la direction et de la vitesse du vent. La plupart des dunes avec un pic pointu ont des vagues d'un seul côté (voir ci-dessous).


L'effet présenté dans le tutoriel est conçu pour des dunes plus lisses avec des vagues des deux côtés. Ce n'est pas toujours physiquement précis, mais suffisamment réaliste pour être crédible, et c'est une bonne première étape vers des implémentations plus complexes.

Réalisation des vagues


Il existe de nombreuses façons de mettre en œuvre des vagues. Le moins cher est de simplement les dessiner sur une texture, mais dans le tutoriel nous voulons réaliser autre chose. La raison est simple: les ondes ne sont pas «plates» et doivent interagir correctement avec la lumière. Si vous les dessinez simplement, il sera impossible d'obtenir un effet réaliste lorsque la caméra (ou le soleil) bouge.

Une autre façon d'ajouter des vagues est de changer la géométrie du modèle de dune. Mais l'augmentation de la complexité du modèle n'est pas recommandée, car elle affecte considérablement les performances globales.

Comme nous l'avons vu dans la partie sur les normales de sable, vous pouvez contourner ce problème en utilisant des cartes normales . En fait, ils sont dessinés sur la surface comme des textures traditionnelles, mais sont utilisés dans les calculs d'éclairage pour simuler une géométrie plus complexe.

Autrement dit, la tâche s'est transformée en une autre: comment créer ces cartes normales. Le rendu manuel prendra trop de temps. De plus, à chaque fois que vous changez les dunes, vous devrez redessiner les vagues. Cela ralentira considérablement le processus de création de ressources, ce que de nombreux artistes techniques cherchent à éviter.

Une solution beaucoup plus efficace et optimale consisterait à ajouter des vagues de manière procédurale . Cela signifie que les directions normales des dunes changent en fonction de la géométrie locale pour prendre en compte non seulement les grains de sable, mais aussi les vagues.

Étant donné que les ondes doivent être simulées sur une surface 3D, il sera plus logique de mettre en œuvre un changement de direction des normales pour chaque pixel. Il est plus facile d'utiliser une carte normale transparente avec un motif de vagues. Cette carte sera ensuite combinée avec la carte normale existante précédemment utilisée pour le sable.

Cartes normales


Jusqu'à ce moment, nous avons rencontré trois normales différentes :

  • Géométrie normale : l'orientation de chaque face du modèle 3D, qui est stockée directement aux sommets;
  • Sable normal : calculé en utilisant la texture du bruit;
  • Wave Normal : Le nouvel effet discuté dans cette partie.

L'exemple ci-dessous, tiré de la page d' exemples d' Unity Surface Shader , illustre une manière standard de réécrire la normale d'un modèle 3D. Cela nécessite de modifier la valeur o.Normal, ce qui se fait généralement après l'échantillonnage de la texture (le plus souvent appelé la carte normale ).

  Shader "Example/Diffuse Bump" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
        float2 uv_MainTex;
        float2 uv_BumpMap;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      void surf (Input IN, inout SurfaceOutput o) {
        o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
        o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Nous avons utilisé exactement la même technique pour remplacer la normale de géométrie par la normale de sable.

Qu'est-ce que UnpackNormal?
. ( 1), , . X, Y Z R, G B .

1+1. 01. , «» «» . (normal packing) (normal unpacking). :

R=X2+12G=Y2+12B=Z2+12(1)


:

X=2R1Y=2G1Z=2B1(2)


Unity (2), UnpackNormal. .

Normal Map Technical Details polycount.

Raideur des dunes


Cependant, la première difficulté est liée au fait que la forme d'onde change en fonction de la netteté des dunes. Les dunes basses et plates ont de petites vagues; sur les dunes escarpées, les configurations des vagues sont plus distinctes. Cela signifie que vous devez tenir compte de la pente de la dune.

Le moyen le plus simple de contourner ce problème consiste à créer deux cartes normales différentes, respectivement, pour les dunes plates et abruptes. L'idée de base est de mélanger les deux cartes normales en fonction de la pente de la dune.


Carte normale pour la dune escarpée


Carte normale pour une dune plate

Cartes normales et canal bleu
, .

, . , (X Y) (Z) .

length(N)=1X2+Y2+Z2=1(3)


:

X2+Y2+Z2=1X2+Y2+Z2=12Z2=1X2Y2Z=1X2Y2(4)


UnpackNormal Unity. Shader API .

inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(SHADER_API_GLES)  defined(SHADER_API_MOBILE)
    return packednormal.xyz * 2 - 1;
#else
    fixed3 normal;
    normal.xy = packednormal.wy * 2 - 1;
    normal.z = sqrt(1 - normal.x*normal.x - normal.y * normal.y);
    return normal;
#endif
}

Unity DXT5nm, (packednormal.xy) - (packednormal.wy).

Normal Map Compression polycount.

Pourquoi les cartes normales semblent-elles lilas?
, , .

, . , [0,0,1].

[0,0,1][0.5,0.5,1], RGB.

; , [0,0,1].

, «». , B 1+1, 0+1. .

La pente peut être calculée à l'aide d'un produit scalaire , qui est souvent utilisé dans le codage des shaders pour calculer le degré de «parallélisme» de deux directions. Dans ce cas, nous prenons la direction de la normale de la géométrie (montrée ci-dessous en bleu) et la comparons avec un vecteur pointant vers le ciel (montré ci-dessous en jaune). Le produit scalaire de ces deux vecteurs renvoie des valeurs proches de1lorsque les vecteurs sont presque parallèles (dunes plates), et proches de 0lorsque l'angle entre eux est de 90 degrés (dunes escarpées).


Cependant, nous sommes confrontés au premier problème - deux vecteurs sont impliqués dans cette opération. Le vecteur normal obtenu à partir de la fonction surfutilisant o.Normalest exprimé en espace tangent . Cela signifie que le système de coordonnées utilisé pour coder la direction normale est relatif à la géométrie de surface locale (voir ci-dessous). Nous avons brièvement abordé ce sujet dans la partie sur le sable normal.


Un vecteur pointant vers le ciel est exprimé dans l' espace mondial . Pour obtenir le produit scalaire correct, les deux vecteurs doivent être exprimés dans le même système de coordonnées. Cela signifie que nous devons transformer l'un d'eux pour que les deux soient exprimés dans un seul espace.

Heureusement, l' unité vient à la rescousse avec une fonction WorldNormalVectorqui nous permet de transformer le vecteur normal de l' espace tangent à l' espace mondial . Pour utiliser cette fonctionnalité, nous devons changer la structure Input, afin qu'elle soit incluse float3 worldNormalet INTERNAL_DATA:

struct Input
{
    ...

    float3 worldNormal;
    INTERNAL_DATA
};

Ceci est expliqué dans un article de la documentation Unity Writing Surface Shaders qui dit:

INTERNAL_DATA — , o.Normal.

, WorldNormalVector (IN, o.Normal).

Cela devient souvent la principale source de problèmes lors de l'écriture de shaders de surface. En fait, la valeur o.Normalqui est disponible dans la fonction surf, varie en fonction de la façon dont vous utilisez. Si vous le lisez uniquement, o.Normalcontient le vecteur normal du pixel actuel dans l' espace mondial . Si vous modifiez sa valeur, elle se o.Normaltrouve dans l' espace tangent .

Si vous enregistrez o.Normal, mais que vous avez toujours besoin d'accéder à l' espace normal dans le monde (comme dans notre cas), vous pouvez l'utiliser WorldNormalVector (IN, o.Normal). Cependant, pour cela, vous devez apporter une petite modification à la structure illustrée ci-dessus Input.

Qu'est-ce que INTERNAL_DATA?
INTERNAL_DATA Unity.

, , WorldNormalVector. , , . ( ).

, 3D- 3×3. , , (tangent to world matrix), Unity TtoW.

INTERNAL_DATA TtoW Input. , «Show generated code» :


, INTERNAL_DATA — , TtoW:

#define INTERNAL_DATA
    half3 internalSurfaceTtoW0;
    half3 internalSurfaceTtoW1;
    half3 internalSurfaceTtoW2;

half3x3, half3.

WorldNormalVector, , ( ) TtoW:

#define WorldNormalVector(data,normal)
    fixed3
    (
        dot(data.internalSurfaceTtoW0, normal),
        dot(data.internalSurfaceTtoW1, normal),
        dot(data.internalSurfaceTtoW2, normal)
    )

mul, TtoW , .

, :

[ToW1,1ToW1,2ToW1,3ToW2,1ToW2,2ToW2,3ToW3,1ToW3,2ToW3,3][N1N2N3]=[[ToW1,1ToW1,2ToW1,3][N1N2N3][ToW2,1ToW2,2ToW2,3][N1N2N3][ToW3,1ToW3,2ToW3,3][N1N2N3]]


Vous pouvez en savoir plus sur les mathématiques normales à partir d'un article sur LearnOpenGL .

la mise en oeuvre


Un extrait du code ci-dessous convertit la normale de la tangente à l' espace mondial et calcule la pente par rapport à la direction ascendante .

// Calculates normal in world space
float3 N_WORLD = WorldNormalVector(IN, o.Normal);
float3 UP_WORLD = float3(0, 1, 0);

// Calculates "steepness"
// => 0: steep (90 degrees surface)
//  => 1: shallow (flat surface)
float steepness = saturate(dot(N_WORLD, UP_WORLD));

Maintenant que nous avons calculé la pente de la dune, nous pouvons l'utiliser pour mélanger les deux cartes normales. Les deux cartes normales sont échantillonnées, à la fois plates et fraîches (dans le code ci-dessous, elles sont appelées _ShallowTexet _SteepTex). Ensuite, ils sont mélangés en fonction de la valeur steepness:

float2 uv = W.xz;

// [0,1]->[-1,+1]
float3 shallow = UnpackNormal(tex2D(_ShallowTex, TRANSFORM_TEX(uv, _ShallowTex)));
float3 steep   = UnpackNormal(tex2D(_SteepTex,   TRANSFORM_TEX(uv, _SteepTex  )));

// Steepness normal
float3 S = normalerp(steep, shallow, steepness);

Comme indiqué dans la partie sur les normales de sable, il est assez difficile de combiner correctement les cartes normales, et cela ne peut pas être fait avec lerp. Dans ce cas slerp, il est plus correct d'utiliser , mais à la place, une fonction moins coûteuse est appelée normalerp.

Mélange de vagues


Si nous utilisons le code ci-dessus, les résultats peuvent nous décevoir. En effet, les dunes ont très peu de pente, ce qui conduit à trop de mélange des deux textures normales. Pour résoudre ce problème, nous pouvons appliquer une transformation non linéaire à la pente, ce qui augmentera la netteté du mélange:

// Steepness to blending
steepness = pow(steepness, _SteepnessSharpnessPower);

Lors du mélange de deux textures, il est souvent utilisé pour contrôler leur netteté et leur contraste pow. Nous avons appris comment et pourquoi cela fonctionne dans mon didacticiel sur le rendu physique .

Ci-dessous, nous voyons deux gradients. Le haut montre les couleurs du noir au blanc, interpolées linéairement le long de l'axe X avec c = uv.x. En bas, le même dégradé est représenté par c = pow(uv.x*1.5)*3.0:



Il est facile à remarquer, ce powqui vous permet de créer une transition beaucoup plus nette entre le noir et le blanc. Lorsque nous travaillons avec des textures, cela réduit leur chevauchement, créant des bords plus nets.

Direction des dunes


Tout ce que nous avons fait plus tôt fonctionne parfaitement. Mais nous devons résoudre un autre dernier problème. Les vagues varient avec la pente , mais pas avec la direction . Comme mentionné ci-dessus, les vagues ne sont généralement pas symétriques du fait que le vent souffle principalement dans une direction.

Pour rendre les vagues encore plus réalistes, nous devons ajouter deux autres cartes normales (voir le tableau ci-dessous). Ils peuvent être mélangés en fonction du parallélisme de la dune de l'axe X ou de l'axe Z.

CoolAppartement
Xcool xplat x
Zcool zplat z

Ici, nous devons implémenter le calcul du parallélisme de la dune par rapport à l'axe Z. Cela peut être fait de manière similaire au calcul de la pente, mais float3 UP_WORLD = float3(0, 1, 0);peut être utilisé à la place float3 Z_WORLD = float3(0, 0, 1);.

Je vous laisse cette dernière étape. Si vous rencontrez des problèmes, à la fin de ce didacticiel, vous trouverez un lien pour télécharger le package Unity complet.

Conclusion


Ceci est la dernière partie d'une série de tutoriels sur le rendu du sable de Journey.

Ce qui suit montre jusqu'où nous avons pu avancer dans cette série:



Avant et après,

je tiens à vous remercier d'avoir lu ce tutoriel assez long jusqu'à la fin. J'espère que vous avez aimé explorer et recréer ce shader.

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 ).

Les modèles de dunes 3D, les arrière-plans et les options d'éclairage ont été créés par Jiadi Deng .

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

Forfait Unity


Si vous souhaitez recréer cet effet, le package Unity complet est disponible en téléchargement sur Patreon . Il a tout ce dont vous avez besoin, des shaders aux modèles 3D.

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


All Articles