Shader n'est pas magique. Écriture de shaders dans Unity. Vertex Shaders

Bonjour à tous! Je m'appelle Grigory Dyadichenko et je suis le fondateur et directeur technique de Foxsys Studios. Aujourd'hui, nous allons parler des vertex shaders. L'article examinera la pratique du point de vue de Unity, des exemples très simples, ainsi que de nombreux liens pour étudier des informations sur les shaders dans Unity. Si vous savez écrire des shaders, vous ne trouverez rien de nouveau pour vous. Quiconque veut commencer à écrire des shaders dans Unity, bienvenue sur cat.



Un peu de théorie



Pour mieux comprendre le processus du shader, jetons un coup d'œil à une petite théorie. Un vertex shader ou vertex shader est une étape programmable d'un shader qui fonctionne avec des vertex individuels. Les sommets stockent à leur tour divers attributs qui sont traités par cette partie du shader afin d'obtenir des attributs convertis à la sortie.

Exemples d'utilisation de vertex shaders





Déformation des objets - vagues réalistes, effet des ondulations de la pluie, déformation lorsqu'une balle frappe, tout cela peut être fait avec des vertex shaders, et cela aura l'air plus réaliste que la même chose faite via Bump Mapping dans la partie fragment du shader. Puisqu'il s'agit d'un changement de géométrie. Les shaders de niveau 3.0 sur ce sujet ont une technique appelée mappage de dispersion, car ils ont désormais accès aux textures dans la partie vertex du shader.



Animation d'objets. Les jeux semblent plus vivants et intéressants lorsque les plantes réagissent à un personnage ou que les arbres se balancent au vent. Pour cela, des vertex shaders sont également utilisés.



Éclairage de dessin animé ou stylisé. Dans de nombreux jeux, du point de vue du style, ce n'est pas l'éclairage pbr qui semble beaucoup plus intéressant, mais la stylisation. En même temps, cela n'a aucun sens de calculer quoi que ce soit dans la partie fragment.



Dépouillement. À un moment donné dans les moteurs de jeu, ce problème est résolu, mais il est néanmoins utile de comprendre les vertex shaders afin de comprendre comment cela fonctionne.

Exemples simples de travail avec des sommets




Je ne veux pas que cela se produise, comme dans les anciennes leçons sur la façon de dessiner un hibou, alors allons-y par étapes. Créez un ombrage de surface standard. Cela peut être fait avec le bouton droit de la souris dans la vue du projet ou dans le panneau supérieur de l'onglet Actifs. Créer-> Shader-> Shader de surface standard.

Et nous obtenons un tel blanc standard.

Shader de surface
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows

// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

sampler2D _MainTex;

struct Input
{
float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = ca;
}
ENDCG
}
FallBack "Diffuse"
}

Comment cela fonctionne et en général, nous allons l'analyser en détail dans l'article après la pratique de base, plus nous le comprendrons partiellement lors de la mise en œuvre des shaders. Pour l'instant, que certaines choses restent inchangées. En bref, il n'y a pas de magie (en ce qui concerne la façon dont les paramètres sont récupérés, etc.) Juste pour certains mots clés, l'unité génère du code pour vous afin de ne pas l'écrire à partir de zéro. Par conséquent, ce processus n'est pas assez évident. Vous pouvez en savoir plus sur le shader de surface et ses propriétés dans Unity ici. docs.unity3d.com/Manual/SL-SurfaceShaders.html

Nous en retirerons tout ce qui est superflu pour ne pas le distraire, car à un moment donné il n'est pas nécessaire. Et obtenez un shader si court.

Shader simplifié
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;

void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}




Juste la couleur du modèle avec éclairage. Dans ce cas, Unity est responsable du calcul de l'éclairage.

Tout d'abord, ajoutez l'effet le plus simple des exemples Unity. L'extrusion est normale, et sur son exemple, nous analyserons son fonctionnement.

Pour ce faire, ajoutez le modificateur vertex: vert à la ligne #pragma surface surf Standard fullforwardshadows . Si nous passons inout appdata_full v en tant que paramètre à une fonction, alors en substance cette fonction est un modificateur de sommet. À sa base, il fait partie du vertex shader, qui est créé par l'unité de génération de code, qui effectue un traitement préliminaire des sommets. Toujours dans le bloc Propriétés , ajoutez le champ _Amount acceptant des valeurs de 0 à 1. Pour utiliser le champ _Amount dans le shader, nous devons également le définir à cet endroit . Dans la fonction, nous allons simplement passer à la normale en fonction de _Amount , où 0 est la position standard du sommet (décalage zéro) et 1 est le décalage exactement à la normale.

SimpleVertexExtrusionShader
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Amount ("Extrusion Amount", Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;
float _Amount;

void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amount;
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}


Vous pouvez remarquer une caractéristique importante des shaders. Bien que le shader soit exécuté à chaque image, le résultat obtenu lors de l'opération du shader n'est pas stocké dans le maillage, mais n'est utilisé que pour le rendu. Par conséquent, il est impossible de se rapporter aux fonctions du shader, ainsi qu'à la mise à jour dans les scripts. Ils sont appliqués à chaque image sans changer les données du maillage, mais simplement en modifiant le maillage pour un rendu supplémentaire.

Par exemple, l'un des moyens les plus simples de créer une animation consiste à utiliser le temps pour modifier l'amplitude. L'unité a des variables intégrées, dont une liste complète peut être trouvée ici docs.unity3d.com/Manual/SL-UnityShaderVariables.html Dans ce cas, nous écrirons un nouveau shader basé sur notre shader passé. Au lieu de _Amount, faisons la valeur flottante _Amplitude et utilisons la variable intégrée Unity _SinTime . _SinTime est le sinus du temps, et donc il prend des valeurs de -1 à 1. Cependant, n'oubliez pas que toutes les variables de temps intégrées dans les shaders unitaires sont des vecteurs float4 . Par exemple, _SinTime est défini comme (sin (t / 8), sin (t / 4), sin (t / 2), sin (t)) , où t est le temps. Par conséquent, nous prenons le composant z pour que l'animation soit plus rapide. Et nous obtenons:

SimpleVertexExtrusionWithTime
Shader "Custom/SimpleVertexExtrusionWithTime"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Amplitude ("Extrusion Amplitude", float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;
float _Amplitude;

void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amplitude * (1 - _SinTime.z);
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}




Il s'agissait donc d'exemples simples. Il est temps de dessiner un hibou!

Déformation d'objets





J'ai déjà écrit un article entier sur le sujet d'un effet de déformation avec une analyse détaillée des mathématiques du processus et de la logique de la pensée lors du développement d'un tel effet habr.com/en/post/435828 Ce sera notre chouette.

Tous les shaders de l'article sont écrits en hlsl. Ce langage possède en fait sa propre documentation volumineuse, dont beaucoup oublient et se demandent d'où viennent la moitié des fonctions câblées, bien qu'elles soient définies dans HLSL docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl- fonctions intrinsèques

Mais en fait, les shaders de surface dans une unité sont un sujet vaste et volumineux en soi. De plus, vous ne voulez pas toujours jouer avec l'éclairage Unity. Parfois, vous devez tricher et écrire le shader le plus rapide qui n'a que le bon ensemble d'effets prédéfinis. Dans l'unité, vous pouvez écrire des shaders de niveau inférieur.

Shaders de bas niveau





Selon la bonne vieille tradition de travailler avec des shaders, nous tourmenterons ci-après le lapin de Stanford.

En général, le soi-disant Unity ShaderLab est essentiellement une visualisation d'un inspecteur avec des champs dans les matériaux et une simplification de l'écriture des shaders.

Prenez la structure générale du shader Shaderlab:

Structure générale des shaders
Shader "MyShaderName"
{
Properties
{
//
}
SubShader // ( )
{
Pass
{
//
}
//
}
//
FallBack "VertexLit" // ,
}


Directives de compilation telles que
#pragma vertex vert
#pragma fragment frag
déterminer les fonctions de shader à compiler en tant que vertex et fragment shaders, respectivement.

Disons que nous prenons l'un des exemples les plus courants - un shader pour afficher la couleur des normales:

SimpleNormalVisualization
Shader "Custom/SimpleNormalVisualization"
{
Properties
{
}
SubShader
{
Pass
{
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + 0.5;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return fixed4 (i.color, 1);
}
ENDCG
}
}
FallBack "VertexLit"
}




Dans ce cas, dans la partie vertex, nous écrivons la valeur normale convertie dans la couleur vertex, et dans la partie pixel, nous utilisons cette couleur comme couleur du modèle.

La fonction UnityObjectToClipPos est une fonction auxiliaire Unity (du fichier UnityCG.cginc ) qui traduit les sommets de l'objet à la position associée à la caméra. Sans lui, un objet, lorsqu'il entre dans la visibilité de la caméra (frustrum), sera dessiné dans les coordonnées de l'écran, quelle que soit la position de la transformation. Puisque initialement les positions des sommets sont présentées dans les coordonnées de l'objet. Juste des valeurs par rapport à son pivot.

Ce bloc.
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

Il s'agit de la définition de la structure qui sera traitée dans la partie vertex et transférée dans la partie fragment. Dans ce cas, il est déterminé que deux paramètres sont pris dans le maillage - la position du sommet et la couleur du sommet. Vous pouvez en savoir plus sur les données qui peuvent être jetées dans une unité sur ce lien docs.unity3d.com/Manual/SL-VertexProgramInputs.html

Précision importante. Peu importe le nom des attributs de maillage. Autrement dit, disons que dans l'attribut de couleur, vous pouvez écrire le vecteur de déviation par rapport à la position d'origine (de cette façon, ils font parfois un effet lorsque le personnage s'en va pour que l'herbe «s'en repousse»). La façon dont cet attribut sera traité dépend entièrement de votre shader.

Conclusion



Merci de votre attention! Il est problématique d'écrire des effets complexes sans partie fragmentaire, pour cette raison, nous discuterons de similaires dans des articles séparés. J'espère qu'au cours de cet article, il est devenu un peu plus clair comment le code des vertex shaders est écrit en général, et où vous pouvez trouver des informations à étudier, car les shaders sont un sujet très profond.

Dans les prochains articles, nous analyserons les autres types de shaders, les effets individuels, et j'essaierai de décrire ma logique de pensée lors de la création d'effets nouveaux ou complexes.

Un référentiel a également été créé où tous les résultats de cette série d'articles github.com/Nox7atra/ShaderExamples seront ajoutés. J'espère que ces informations seront utiles aux débutants qui commencent tout juste leur voyage dans l'étude de ce sujet.

Quelques liens utiles (y compris les sources):


www.khronos.org/opengl/wiki/Vertex_Shader
docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-reference
docs.unity3d.com/en/current/Manual/SL-Reference.html
docs.unity3d.com/Manual/GraphicsTutorials.html
www.malbred.com/3d-grafika-3d-redaktory/sovremennaya-terminologiya-3d-grafiki/vertex-shader-vershinnyy-sheyder.html
3dpapa.ru/accurate-displacement-workflow

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


All Articles