Votre premier shader Spatial

Vous avez décidé de commencer à écrire votre propre shader spatial personnalisé. Vous avez peut-être vu en ligne un truc sympa qui a été fait avec des shaders, ou vous avez trouvé que le SpatialMaterial ne répond pas tout à fait à vos besoins. Quoi qu'il en soit, vous avez décidé d'écrire le votre et vous devez maintenant trouver par où commencer.

Ce tutoriel explique comment écrire un shader spatial et couvre plus de sujets que le tutoriel CanvasItem.

Les shaders spatiaux ont plus de fonctionnalités intégrées que les shaders CanvasItem. On s'attend à ce que les shaders spatiaux offrent déjà les fonctionnalités nécessaires pour les cas d'utilisation courants et que l'utilisateur n'ait qu'à définir les paramètres appropriés dans le shader. C'est particulièrement vrai pour un flux de travail PBR (rendu basé physiquement).

Il s'agit d'un tutoriel en deux parties. Dans cette première partie, nous allons voir comment réaliser un terrain simple en utilisant le déplacement des vertex à partir d'une heightmap dans la fonction vertex. Dans la seconde partie nous allons reprendre les concepts de ce tutoriel et montrer comment mettre en place des matériaux personnalisés dans un fragment shader en écrivant un shader d'eau de mer.

Note

Ce tutoriel suppose quelques connaissances de base sur les shaders, telles que les types (vec2, float, sampler2D), et les fonctions. Si vous n'êtes pas à l'aise avec ces concepts, il est préférable d'avoir une douce introduction avec The Book of Shaders avant de faire ce tutoriel.

Où assigner mon matériel

En 3D, les objets sont dessinés en utilisant des Maillages. Les Maillages sont un type de ressource qui stocke la géométrie (la forme de votre objet) et les matériaux (la couleur et la façon dont l'objet réagit à la lumière) dans des unités appelées "surfaces". Un Maillage peut avoir plusieurs surfaces, ou une seule. En général, vous importerez un Maillage d'un autre programme (par exemple Blender). Mais Godot a aussi quelques PrimitiveMeshes qui permettent d'ajouter une géométrie de base à une scène sans importer de Maillages.

Il existe plusieurs types de nœuds que vous pouvez utiliser pour dessiner un maillage. La principale est MeshInstance, mais vous pouvez aussi utiliser Particules, MultiMesh (avec un MultiMeshInstance), ou d'autres.

En général, un matériau est associé à une surface donnée dans un maillage, mais certains nœuds, comme MeshInstance, vous permettent d'outrepasser le matériau pour une surface spécifique, ou pour toutes les surfaces.

Si vous placez un matériau sur la surface ou sur le maillage lui-même, alors toutes les MeshInstances qui partagent ce maillage partageront ce matériau. Toutefois, si vous souhaitez réutiliser le même maillage dans plusieurs instances de maillage, mais que vous avez des matériaux différents pour chaque instance, vous devez alors définir le matériau sur la Meshinstance.

Pour ce tutoriel, nous allons placer notre matériau sur le maillage lui-même plutôt que de profiter de la capacité du MeshInstance à passer outre les matériaux.

Mise en place

Ajoutez un nouveau nœud MeshInstance à votre scène.

Dans l'onglet inspecteur à côté de "Mesh", cliquez sur "[empty]" et sélectionnez "New PlaneMesh". Cliquez ensuite sur l'image d'un plan qui apparaît.

Cela ajoute un PlaneMesh à notre scène.

Ensuite, dans le viewport, cliquez dans le coin supérieur gauche sur le bouton "Perspective". Un menu apparaîtra. Au milieu du menu se trouvent des options sur la façon d'afficher la scène. Sélectionnez 'Affichage en fil de fer'.

Cela vous permettra de voir les triangles qui composent le plan.

../../../_images/plane.png

Réglez maintenant Subdivide Width et Subdivide Depth à 32.

../../../_images/plane-sub-set.png

Vous pouvez voir qu'il y a maintenant beaucoup plus de triangles dans la Mesh. Cela nous permettra de travailler avec plus de sommets et donc d'ajouter plus de détails.

../../../_images/plane-sub.png

PrimitiveMeshes, comme PlaneMesh, n'ont qu'une seule surface, donc au lieu d'un tableau de matériaux, il n'y en a qu'un seul. Cliquez à côté de "Material" où il est écrit "[empty]" et sélectionnez "New ShaderMaterial". Cliquez ensuite sur la sphère qui apparaît.

Cliquez maintenant à côté de "Shader" où il est écrit "[empty]" et sélectionnez "New Shader".

L'éditeur de shader devrait maintenant afficher un pop up et vous êtes maintenant prêt à commencer à écrire votre premier shader Spatial !

La magie des Shaders

../../../_images/shader-error.png

Vous avez remarqué qu'il y a déjà une erreur ? En effet, l'éditeur de shaders recharge les shaders à la volée. La première chose dont les shaders Godot ont besoin est une déclaration de quel type de shader ils sont. Nous définissons la variable shader_type à spatial car il s'agit d'un shader spatial.

shader_type spatial;

Ensuite, nous définirons la fonction vertex(). La fonction vertex() détermine où les sommets de votre Mesh apparaissent dans la scène finale. Nous l'utiliserons pour compenser la hauteur de chaque sommet et faire apparaître notre plan plat comme un petit terrain.

Nous définissons ainsi le shader de vertex :

void vertex() {

}

Sans rien dans la fonction vertex(), Godot utilisera son shader de vertex par défaut. Nous pouvons facilement commencer à apporter des changements en ajoutant une seule ligne :

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

En ajoutant cette ligne, vous devriez obtenir une image comme celle ci-dessous.

../../../_images/cos.png

Bon, déballons tout ça. La valeur y de VERTEX est augmentée. Et nous passons les composantes x et z de VERTEX comme arguments à cos et sin ; cela nous donne une apparence ondulée sur les axes x et z.

Ce que nous voulons obtenir, c'est l'aspect de petites collines ; après tout. Les cos et les sin ressemblent déjà à des collines. Nous le faisons en mettant les entrées à l'échelle des fonctions cos et sin.

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

Cela semble mieux, mais c'est encore trop pointu et répétitif, rendons-le un peu plus intéressant.

Heightmap bruit

Le bruit est un outil très populaire pour simuler l'aspect du terrain. Pensez-y comme à la fonction cosinus où vous avez des collines répétitives, sauf que, avec le bruit, chaque colline a une hauteur différente.

Godot fournit la ressource NoiseTexture pour générer une texture de bruit accessible depuis un shader.

Pour accéder à une texture dans un shader, ajoutez le code suivant près du haut de votre shader, en dehors de la fonction vertex().

uniform sampler2D noise;

Cela vous permettra d'envoyer une texture de bruit au shader. Maintenant, regardez dans l'inspecteur sous votre matériel. Vous devriez voir une section intitulée "Shader Params". Si vous l'ouvrez, vous verrez une section intitulée "noise".

Cliquez à côté de la mention "[empty]" et sélectionnez "New NoiseTexture". Ensuite, dans votre NoiseTexture, cliquez à côté de la mention "Noise" et sélectionnez "New OpenSimplexNoise".

OpenSimplexNoise est utilisé par le NoiseTexture pour générer une carte de hauteur (heightmap).

Une fois que vous l'aurez configuré et il devrait ressembler à ceci.

../../../_images/noise-set.png

Maintenant, accédez à la texture du bruit en utilisant la fonction texture(). texture() prend une texture comme premier argument et un vec2 pour la position sur la texture comme second argument. Nous utilisons les canaux x et z de VERTEX pour déterminer l'endroit de la texture où il faut regarder. texture() renvoie un vec4 des canaux r, g, b, a à la position. Comme la texture du bruit est en niveaux de gris, toutes les valeurs sont les mêmes, de sorte que nous pouvons utiliser n'importe lequel des canaux comme hauteur. Dans ce cas, nous utiliserons le canal r, ou x.

float height = texture(noise, VERTEX.xz / 2.0 ).x; //divide by the size of the PlaneMesh
VERTEX.y += height;

Note : xyzw est le même que rgba dans GLSL, donc au lieu de texture().x ci-dessus, nous pourrions utiliser texture().r. Voir la documentation OpenGL pour plus de détails.

En utilisant ce code, vous pouvez voir que la texture crée des collines d'apparence aléatoire.

../../../_images/noise.png

Actuellement, c'est trop pointu, nous voulons adoucir un peu les collines. Pour ce faire, nous utiliserons un uniform. Vous avez déjà utilisé un uniform ci-dessus pour transmettre la texture de bruit, maintenant apprenons comment ils fonctionnent.

Uniforms

Les variables uniforms vous permettent de faire passer les données du jeu dans le shader. Elles sont très utiles pour contrôler les effets du shader. Les uniforms peuvent être presque tous les types de données qui peuvent être utilisés dans le shader. Pour utiliser un uniform, vous devez le déclarer dans votre Shader en utilisant le mot-clé uniform.

Faisons un uniform qui change la hauteur du terrain.

uniform float height_scale = 0.5;

Godot vous permet d'initialiser un uniform avec une valeur ; ici, height_scale est fixé à 0.5. Vous pouvez définir des uniforms à partir du GDScript en appelant la fonction set_shader_param() sur le matériel correspondant au shader. La valeur passée depuis GDScript est prioritaire sur la valeur utilisée pour l'initialiser dans le shader.

# called from the MeshInstance
mesh.material.set_shader_param("height_scale", 0.5)

Note

La modification des uniformes des nœuds basés sur Spatial est différente de celle des nœuds basés sur CanvasItem. Ici, nous définissons le matériau à l'intérieur de la ressource PlaneMesh. Dans d'autres ressources de maillage, vous devrez peut-être d'abord accéder au matériau en appelant surface_get_material(). Dans le MeshInstance, vous accéderez au matériau à l'aide de get_surface_material() ou material_override.

Rappelez-vous que la chaîne passée dans set_shader_param() doit correspondre au nom de la variable uniform dans le Shader. Vous pouvez utiliser la variable uniform n'importe où dans votre Shader. Ici, nous allons l'utiliser pour définir la valeur de la hauteur au lieu de la multiplier arbitrairement par 0.5.

VERTEX.y += height * height_scale;

Maintenant, c'est beaucoup mieux.

../../../_images/noise-low.png

En utilisant des uniforms, nous pouvons même changer la valeur de chaque image pour animer la hauteur du terrain. Combiné avec Tweens, cela peut être particulièrement utile pour les animations simples.

Interagir avec la lumière

Tout d'abord, désactiver l'affichage en fil de fer. Pour ce faire, cliquez à nouveau en haut à gauche de la fenêtre de visualisation, où il est indiqué "Perspective", et sélectionnez "Affichage normal".

../../../_images/normal.png

Remarquez comment la couleur du maillage est plate. Cela s'explique par le fait que son éclairage est plat. Ajoutons une lumière !

Tout d'abord, nous allons ajouter un OmniLight à la scène.

../../../_images/light.png

Vous pouvez voir la lumière qui affecte le terrain, mais elle semble étrange. Le problème est que la lumière affecte le terrain comme s'il s'agissait d'un plan plat. C'est parce que le shader de lumière utilise les normales du Mesh pour calculer la lumière.

Les normales sont stockées dans le Mesh, mais nous changeons la forme du Mesh dans le shader, donc les normales ne sont plus correctes. Pour y remédier, nous pouvons recalculer les normales dans le shader ou utiliser une texture de normales qui correspond à notre bruit. Godot nous facilite les deux possibilités.

Vous pouvez calculer la nouvelle normale manuellement dans la fonction vertex et ensuite simplement définir NORMAL. Avec NORMAL défini, Godot fera pour nous les calculs d'éclairage complexes. Nous aborderons cette méthode dans la prochaine partie de ce tutoriel, pour l'instant nous allons lire les normales à partir d'une texture.

Au lieu de cela, nous nous fierons à nouveau au NoiseTexture pour calculer les normales pour nous. Nous le faisons en passant une deuxième texture de bruit.

uniform sampler2D normalmap;

Réglez cette deuxième texture uniforme sur une autre NoiseTexture avec un autre OpenSimplexNoise. Mais cette fois, cochez "As Normalmap".

../../../_images/normal-set.png

Maintenant, comme il s'agit d'une normalmap et non de normale par sommet, nous allons l'assigner à la fonction fragment(). Cette fonction fragment() sera expliquée plus en détail dans la prochaine partie de ce tutoriel.

void fragment() {
}

Lorsque nous avons des normales qui correspondent à un sommet spécifique, nous définissons NORMAL, mais si vous avez une normalmap qui provient d'une texture, on définit la normale en utilisant NORMALMAP. Ainsi, Godot s'occupera automatiquement de recouvrir le maillage avec la texture.

Enfin, afin de s'assurer que nous lisons aux mêmes endroits sur la texture de bruit et sur la texture de la normalmap, nous allons passer la position VERTEX.xz de la fonction vertex() à la fonction fragment(). Nous le faisons avec des varyings.

Au-dessus de vertex() définissez un vec2 appelé vertex_position. Et à l'intérieur de la fonction vertex() assignez VERTEX.xz à vertex_position.

varying vec2 vertex_position;

void vertex() {
  ...
  vertex_position = VERTEX.xz / 2.0;
}

Ainsi, nous avons accès à vertex_position depuis la fonction fragment().

void fragment() {
  NORMALMAP = texture(normalmap, vertex_position).xyz;
}

Avec les normales en place, la lumière réagit maintenant dynamiquement à la hauteur du maillage.

../../../_images/normalmap.png

Nous pouvons même faire glisser la lumière et l'éclairage se mettra automatiquement à jour.

../../../_images/normalmap2.png

Voici le code complet pour ce tutoriel. Vous pouvez voir que ce n'est pas très long car Godot s'occupe de la plupart des choses difficiles pour vous.

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 vertex_position;

void vertex() {
  vertex_position = VERTEX.xz / 2.0;
  float height = texture(noise, vertex_position).x * height_scale;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMALMAP = texture(normalmap, vertex_position).xyz;
}

C'est tout pour cette partie. Espérons que vous comprenez maintenant les bases des shaders de vertex dans Godot. Dans la prochaine partie de ce tutoriel, nous allons écrire une fonction de fragment pour accompagner cette fonction de vertex et nous allons couvrir une technique plus avancée pour transformer ce terrain en un océan de vagues en mouvement.