Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Votre premier shader 3D

Vous avez décidé de commencer à écrire votre propre shader Spatial personnalisé. Vous avez peut-être vu un truc sympa en ligne qui a été réalisé avec des shaders, ou alors vous avez trouvé que le StandardMaterial3D 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 MeshInstance3D, mais vous pouvez aussi utiliser ParticulesMultiMesh (avec un MultiMeshInstance3D), ou d'autres.

En général, un matériau est associé à une surface donnée dans un maillage, mais certains nœuds, comme MeshInstance3D, 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 MeshInstance3D 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 MeshInstance3D.

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

Mise en place

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

Dans l'onglet inspecteur, définissez la propriété Mesh du MeshInstance3D à une nouvelle ressource PlaneMesh, en cliquant sur <vide> et en choisissant Nouveau PlaneMesh. Puis élargissez la ressource en cliquant sur l'image d'un plan qui apparaît.

Cela ajoute un plan à notre scène.

Ensuite, dans le viewport, cliquez dans le coin supérieur gauche sur le bouton Perspective. Dans le menu qui apparaît, sélectionnez Affichage en fil de fer.

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

../../../_images/plane.webp

Définissez maintenant Largeur de subdivision et Profondeur de subdivision du PlaneMesh à 32.

../../../_images/plane-sub-set.webp

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

../../../_images/plane-sub.webp

Les PrimitiveMeshes, comme PlaneMesh, n'ont qu'une seule surface, donc au lieu d'un tableau de matériaux, il n'y a qu'un seul matériau. Définir le Matériau à un nouveau ShaderMaterial, puis étendez le matériau en cliquant sur la sphère qui apparaît.

Note

Materials that inherit from the Material resource, such as StandardMaterial3D and ParticleProcessMaterial, can be converted to a ShaderMaterial and their existing properties will be converted to an accompanying text shader. To do so, right-click on the material in the FileSystem dock and choose Convert to ShaderMaterial. You can also do so by right-clicking on any property holding a reference to the material in the inspector.

Maintenant, définissez le Shader du matériau à un nouveau Shader en cliquant sur <vide> et sélectionnez Nouveau Shader.... Laissez les paramètres par défaut, donnez un nom à votre shader et cliquez sur Créer.

Cliquez sur le shader dans l'inspecteur, et l'éditeur de shader devrait maintenant s'afficher. Vous êtes prêt à commencer à écrire votre premier shader spatial !

La magie des Shaders

../../../_images/shader-editor.webp

Le nouveau shader est déjà généré avec une variable shader_type, la fonction vertex(), et la fonction fragment(). La première chose dont les shaders de Godot ont besoin est une déclaration de quel type de shader ils sont. Dans ce cas, le shader_type est défini à spatial car il s'agit un shader spatial.

shader_type spatial;

La fonction vertex() détermine où les sommets de votre MeshInstance3D apparaîtront dans la scène finale. Nous allons l'utiliser pour décaler la hauteur de chaque sommet et faire apparaître notre plan plat comme un petit terrain.

Sans rien dans la fonction vertex(), Godot utilisera son shader de sommet 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.webp

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. cos() et sin() ressemblent déjà à des collines. Nous faisons ainsi en redimensionnant les entrées des fonctions cos() et sin().

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

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 NoiseTexture2D 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ériau. Vous devriez voir une section appelée Paramètres du shader. Si vous l'ouvrez, vous verrez un paramètre appelé "Noise".

Définissez ce paramètre Noise à un nouveau NoiseTexture2D. Ensuite, dans votre NoiseTexture2D, définissez sa propriété Noise à un nouveau FastNoiseLite. La classe FastNoiseLite est utilisée par la NoiseTexture2D pour générer une heightmap.

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

../../../_images/noise-set.webp

Maintenant, accédez à la texture de bruit en utilisant la fonction texture() :

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

texture() prend une texture pour premier argument et un vec2 pour la position sur la texture pour second argument. Nous utilisons les canaux x et z de VERTEX pour déterminer où regarder sur la texture.

Comme les coordonnées du PlaneMesh se trouvent dans l'intervalle [-1.0, 1.0] (pour une taille de 2.0), tandis que les coordonnées de la texture sont dans [0.0, 1.0], pour ré-associer les coordonnées, nous divisons par la taille du PlaneMesh par 2.0 et ajoutons 0.5.

texture() renvoie un vec4 des canaux r, g, b, a à la position. Comme la texture du bruit est en échelle de gris, toutes les valeurs sont identiques, de sorte que nous pouvons utiliser n'importe quel canal en tant que hauteur. Dans ce cas, nous utiliserons le canal r, ou x.

Note

xyzw est le même que rgba en GLSL, donc au lieu du 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.webp

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 des données du jeu dans le shader. Elles sont très utiles pour contrôler les effets du shader. Les uniforms peuvent être presque n'importe quel 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_parameter() 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 MeshInstance3D
mesh.material.set_shader_parameter("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 MeshInstance3D, 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, ça a l'air beaucoup mieux.

../../../_images/noise-low.webp

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

Interagir avec la lumière

Tout d'abord, désactivez l'affichage en fil de fer. Pour ce faire, ouvrez à nouveau le menu Perspective en haut à gauche du viewport, et sélectionnez Affichage normal. De plus, désactiver la lumière du soleil de pré-visualisation dans la barre d'outils de la scène 3D.

../../../_images/normal.webp

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 OmniLight3D à la scène, et le tirer en l'air pour qu'il soit au dessus du terrain.

../../../_images/light.webp

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 uniform sur une autre NoiseTexture2D avec un autre FastNoiseLite. Mais cette fois, cochez Comme Normal map.

../../../_images/normal-set.webp

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, définissez la normale en utilisant NORMALMAP dans la fonction fragment(). De cette manière, 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 varying.

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

varying vec2 tex_position;

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

Et maintenant nous avons accès à tex_position depuis la fonction fragment().

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

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

../../../_images/normalmap.webp

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

../../../_images/normalmap2.webp

Code complet

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 tex_position;

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

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_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.