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 Particules, MultiMesh (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.
In the inspector tab, set the MeshInstance3D's Mesh property to a new
PlaneMesh resource, by clicking on <empty> and
choosing New PlaneMesh. Then expand the resource by clicking on the image of
a plane that appears.
Cela ajoute un plan à notre scène.
Then, in the viewport, click in the upper left corner on the Perspective button. In the menu that appears, select Display Wireframe.
Cela vous permettra de voir les triangles qui composent le plan.
Now set Subdivide Width and Subdivide Depth of the PlaneMesh to 32.
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.
PrimitiveMeshes, like PlaneMesh, only have one surface, so instead of an array of materials there is only one. Set the Material to a new ShaderMaterial, then expand the material by clicking on the sphere that appears.
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.
Now set the material's Shader to a new Shader by clicking <empty> and
select New Shader.... Leave the default settings, give your shader a name,
and click Create.
Click on the shader in the inspector, and the shader editor should now pop up. You are ready to begin writing your first Spatial shader!
La magie des Shaders
The new shader is already generated with a shader_type variable, the
vertex() function, 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;
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.
With nothing in the vertex() function, Godot will use its default vertex
shader. We can start to make changes by adding a single line:
void vertex() {
VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}
En ajoutant cette ligne, vous devriez obtenir une image comme celle ci-dessous.
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.
What we want to achieve is the look of little hills; after all. cos() and
sin() already look kind of like hills. We do so by scaling the inputs to the
cos() and sin() functions.
void vertex() {
VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
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;
This will allow you to send a noise texture to the shader. Now look in the inspector under your material. You should see a section called Shader Parameters. If you open it up, you'll see a parameter called "Noise".
Set this Noise parameter to a new NoiseTexture2D. Then in your NoiseTexture2D, set its Noise property to a new FastNoiseLite. The FastNoiseLite class is used by the NoiseTexture2D to generate a heightmap.
Une fois que vous l'aurez configuré et il devrait ressembler à ceci.
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() 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.
Since the PlaneMesh coordinates are within the [-1.0, 1.0] range (for a size
of 2.0), while the texture coordinates are within [0.0, 1.0], to remap
the coordinates 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.
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.
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
Uniform variables allow you to pass data
from the game into the shader. They are
very useful for controlling shader effects. Uniforms can be almost any datatype
that can be used in the shader. To use a uniform, you declare it in your
Shader using the keyword 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.
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
First, turn wireframe off. To do so, open the Perspective menu in the upper-left of the viewport again, and select Display Normal. Additionally in the 3D scene toolbar, turn off preview sunlight.
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.
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.
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 in the fragment() function. This way Godot will handle
wrapping the 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 varying.
Above the vertex() define a varying vec2 called tex_position. And
inside the vertex() function assign VERTEX.xz to 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.
Nous pouvons même faire glisser la lumière et l'éclairage se mettra automatiquement à jour.
Full code
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.