Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Votre premier shader 3D

You have decided to start writing your own custom Spatial shader. Maybe you saw a cool trick online that was done with shaders, or you have found that the StandardMaterial3D isn't quite meeting your needs. Either way, you have decided to write your own and now you need to figure out where to start.

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

This is a two-part tutorial. In this first part we will create terrain using vertex displacement from a heightmap in the vertex function. In the second part we will take the concepts from this tutorial and set up custom materials in a fragment shader by writing an ocean water shader.

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.

There are multiple node types that you can use to draw a mesh. The main one is MeshInstance3D, but you can also use GPUParticles3D, MultiMeshes (with a MultiMeshInstance3D), or others.

Typically, a material is associated with a given surface in a mesh, but some nodes, like MeshInstance3D, allow you to override the material for a specific surface, or for all surfaces.

If you set a material on the surface or mesh itself, then all MeshInstance3Ds that share that mesh will share that material. However, if you want to reuse the same mesh across multiple mesh instances, but have different materials for each instance then you should set the material on the MeshInstance3D.

For this tutorial we will set our material on the mesh itself rather than taking advantage of the MeshInstance3D's ability to override materials.

Mise en place

Add a new MeshInstance3D node to your scene.

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

Now set Subdivide Width and Subdivide Depth of the PlaneMesh to 32.

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

You can see that there are now many more triangles in the MeshInstance3D. This will give us more vertices to work with and thus allow us to add more detail.

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

PrimitiveMeshes, like PlaneMesh, only have one surface, so instead of an array of materials there is only one. Click beside "Material" where it says "[empty]" and select "New ShaderMaterial". Then click the sphere that appears.

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-editor.webp

The new shader is already generated with a shader_type variable and the fragment() function. The first thing Godot shaders need is a declaration of what type of shader they are. In this case the shader_type is set to spatial because this is a spatial shader.

shader_type spatial;

For now ignore the fragment() function and define the vertex() function. The vertex() function determines where the vertices of your MeshInstance3D appear in the final scene. We will be using it to offset the height of each vertex and make our flat plane appear like a little 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 provides the NoiseTexture2D resource for generating a noise texture that can be accessed from a 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. Regardez maintenant dans l'inspecteur sous votre matériau. Vous devriez voir une section appelée "Shader Params". Si vous l’ouvrez, vous verrez une section appelée "noise".

Click beside it where it says "[empty]" and select "New NoiseTexture2D". Then in your NoiseTexture2D click beside where it says "Noise" and select "New FastNoiseLite".

Note

FastNoiseLite is used by the NoiseTexture2D to generate a heightmap.

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

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

Now, access the noise texture using the texture() function. texture() takes a texture as the first argument and a vec2 for the position on the texture as the second argument. We use the x and z channels of VERTEX to determine where on the texture to look up. Note that the PlaneMesh coordinates are within the [-1,1] range (for a size of 2), while the texture coordinates are within [0,1], so to normalize we divide by the size of the PlaneMesh by 2.0 and add 0.5. texture() returns a vec4 of the r, g, b, a channels at the position. Since the noise texture is grayscale, all of the values are the same, so we can use any one of the channels as the height. In this case we'll use the r, or x channel.

void vertex() {
  float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
  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 lets you initialize a uniform with a value; here, height_scale is set to 0.5. You can set uniforms from GDScript by calling the function set_shader_parameter() on the material corresponding to the shader. The value passed from GDScript takes precedence over the value used to initialize it in the shader.

# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)

Note

Changing uniforms in Spatial-based nodes is different from CanvasItem-based nodes. Here, we set the material inside the PlaneMesh resource. In other mesh resources you may need to first access the material by calling surface_get_material(). While in the MeshInstance3D you would access the material using get_surface_material() or material_override.

Remember that the string passed into set_shader_parameter() must match the name of the uniform variable in the Shader. You can use the uniform variable anywhere inside your Shader. Here, we will use it to set the height value instead of arbitrarily multiplying by 0.5.

VERTEX.y += height * height_scale;

Maintenant, ça a l'air beaucoup mieux.

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

Using uniforms, we can even change the value every frame to animate the height of the terrain. Combined with Tweens, this can be especially useful for animations.

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 !

First, we will add an OmniLight3D to the scene.

../../../_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;

Set this second uniform texture to another NoiseTexture2D with another FastNoiseLite. But this time, check As Normalmap.

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

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() {
}

When we have normals that correspond to a specific vertex we set NORMAL, but if you have a normalmap that comes from a texture, set the normal using NORMAL_MAP. This way Godot will handle the wrapping of texture around the mesh automatically.

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é 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;
  ...
}

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