Votre premier shader spatial : partie 2

Au plus haut niveau, ce que fait Godot, c’est donner à l’utilisateur un tas de paramètres qui peuvent être définis de manière optionnelle (AO, SSS_Strength, RIM, etc.). Ces paramètres correspondent à différents effets complexes (Ambient Occlusion, SubSurface Scattering, Rim Lighting, etc.) Lorsqu’il n’est pas écrit, le code est jeté avant d’être compilé et le shader n’encourt donc pas le coût de la fonction supplémentaire. Il est ainsi facile pour les utilisateurs d’avoir des shaders complexes PBR-correct, sans avoir à écrire des shaders complexes. Bien sûr, Godot vous permet également d’ignorer tous ces paramètres et d’écrire un shader entièrement personnalisé.

Pour une liste complète de ces paramètres, voir le document de référence spatial shader.

Une différence entre la fonction vertex et la fonction fragment est que la fonction vertex s’exécute par vertex et définit des propriétés telles que VERTEX (position) et NORMAL, tandis que le shader fragment s’exécute par pixel et, plus important encore, définit la couleur ALBEDO de la Mesh.

Votre première fonction fragment spatial

Comme mentionné dans la partie précédente de ce tutoriel. L’utilisation standard de la fonction fragment dans Godot est de mettre en place différentes propriétés matérielles et de laisser Godot s’occuper du reste. Afin d’offrir encore plus de flexibilité, Godot fournit également des choses appelées modes de rendu. Les modes de rendu sont définis en haut du shader, directement en dessous de shader_type, et ils spécifient le type de fonctionnalité que vous voulez que les aspects intégrés du shader aient.

Par exemple, si vous ne voulez pas que les lumières affectent un objet, réglez le mode de rendu sur unshaded :

render_mode unshaded;

Vous pouvez également empiler plusieurs modes de rendu. Par exemple, si vous voulez utiliser toon au lieu de PBR plus réaliste, réglez le mode diffus et le mode spéculaire sur toon :

render_mode diffuse_toon, specular_toon;

Ce modèle de fonctionnalité intégrée vous permet d’écrire des shaders personnalisés complexes en ne modifiant que quelques paramètres.

Pour une liste complète des modes de rendu, voir le Spatial shader reference.

Dans cette partie du tutoriel, nous allons voir comment prendre le terrain bosselé de la partie précédente et le transformer en océan.

Commençons par définir la couleur de l’eau. Nous le faisons en définissant ALBEDO.

ALBEDO est un vec3 qui contient la couleur de l’objet.

Mettons une belle nuance de bleu.

void fragment() {
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/albedo.png

Nous l’avons réglé sur une teinte de bleu très sombre car la plus grande partie du bleu de l’eau proviendra des reflets du ciel.

Le modèle de PBR que Godot utilise repose sur deux paramètres principaux : METALLIC et ROUGHNESS.

Le terme ROUGHNESS indique le degré de lissage ou de rugosité de la surface d’un matériau. Une faible ROUGHNESS fait apparaître un matériau comme un plastique brillant, tandis qu’une forte rugosité fait apparaître un matériau de couleur plus solide.

Le terme METALLIC indique à quel point l’objet ressemble à un métal. Il est préférable qu’il soit proche de 0 ou 1. Pensez que le METALLIC modifie l’équilibre entre le reflet et la couleur ALBEDO. Un haut METALLIC ignore presque complètement ALBEDO et ressemble à un miroir du ciel. Alors qu’un METALLIC bas a une représentation plus égale de la couleur du ciel et de la couleur ALBEDO.

ROUGHNESS passe de 0 à 1 de gauche à droite, tandis que la METALLIC passe de 0 à 1 de haut en bas.

../../../_images/PBR.png

Note

METALLIC devrait être proche de 0 ou 1 pour un shading PBR correct. Il ne doit être placé entre eux que pour les mélanges entre matériaux.

L’eau n’est pas un métal, nous allons donc fixer sa propriété METALLIC à 0.0. L’eau est également très réfléchissante, aussi nous allons fixer sa propriété de ROUGHNESS à un niveau assez bas également.

void fragment() {
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/plastic.png

Nous avons maintenant une surface lisse d’aspect plastique. Il est temps de réfléchir à certaines propriétés particulières de l’eau que nous voulons imiter. Il y en a deux principales qui vont faire passer cette eau d’une étrange surface en plastique à une belle eau stylisée. La première est celle des réflexions spéculaires. Les réflexions spéculaires sont ces points lumineux que vous voyez d’où le soleil se reflète directement dans votre œil. La seconde est la réflectance de Fresnel. La réflectance de Fresnel est la propriété des objets à devenir plus réfléchissants à des angles faibles. C’est la raison pour laquelle vous pouvez voir dans l’eau en dessous de vous, mais plus loin, elle reflète le ciel.

Afin d’augmenter les réflexions spéculaires, nous ferons deux choses. Tout d’abord, nous allons changer le mode de rendu de spéculaire à toon parce que le mode de rendu toon a de plus larges lumières spéculaires.

render_mode specular_toon;
../../../_images/specular-toon.png

Ensuite, nous allons ajouter de l’éclairage de bord. Cela va augmenter l’effet de la lumière aux angles rasants. L’éclairage de bord est habituellement utilisé pour imiter le comportement du tissu ou des bords d’un objet vis a vis de la lumière, mais nous allons l’utiliser pour obtenir un sympathique effet d’eau.

void fragment() {
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/rim.png

Afin d’ajouter une réflectance de Fresnel, nous calculerons un terme de Fresnel dans notre shader fragment. Nous n’allons pas utiliser un véritable terme de Fresnel pour des raisons de performance, mais nous allons plutôt l’approximer en utilisant le produit scalaire des vecteurs NORMAL et VIEW. Le vecteur NORMAL pointe à l’opposé de la surface du mesh, tandis que le vecteur VIEW est la direction entre votre œil et ce point de la surface. Le produit scalaire entre les deux est un moyen pratique de savoir quand vous regardez la surface de face ou à un angle oblique.

float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));

Et le mélanger à la fois dans ROUGHNESS et ALBEDO. C’est l’avantage de ShaderMaterials par rapport à SpatialMaterials. Avec SpatialMaterials, nous pourrions définir ces propriétés avec une texture, ou à un nombre plat. Mais avec les shaders, nous pouvons les régler en fonction de n’importe quelle fonction mathématique que nous pouvons imaginer.

void fragment() {
  float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01 * (1.0 - fresnel);
  ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
}
../../../_images/fresnel.png

Et maintenant, avec seulement 5 lignes de code, vous pouvez avoir une eau d’apparence complexe. Maintenant que nous avons de la lumière, cette eau semble trop claire. Assombrissons la. Cela se fait facilement en diminuant les valeurs du vec3 que l’on passe dans ALBEDO. Réglons-les sur vec3(0.01, 0.03, 0.05).

../../../_images/dark-water.png

Animation avec TIME

Pour en revenir à la fonction vertex, nous pouvons animer les vagues en utilisant la variable intégrée TIME.

TIME est une variable intégrée qui est accessible à partir des fonctions vertex et fragment.

Dans le dernier tutoriel, nous avons calculé la hauteur en lisant une heightmap. Pour ce tutoriel, nous ferons de même. Mettez le code de la heightmap dans une fonction appelée height().

float height(vec2 position) {
  return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}

Pour pouvoir utiliser TIME dans la fonction height(), nous devons le passer dedans.

float height(vec2 position, float time) {
}

Et assurez-vous de le faire passer correctement à l’intérieur de la fonction vertex.

void vertex() {
  vec2 pos = VERTEX.xz;
  float k = height(pos, TIME);
  VERTEX.y = k;
}

Au lieu d’utiliser une normalmap pour calculer les normales. Nous allons les calculer manuellement dans la fonction vertex(). Pour ce faire, utilisez la ligne de code suivante.

NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));

Nous devons calculer NORMAL manuellement car dans la prochaine section, nous utiliserons les mathématiques pour créer des vagues d’apparence complexe.

Maintenant, nous allons rendre la fonction height() un peu plus compliquée en compensant la position par le cosinus de TIME.

float height(vec2 position, float time) {
  vec2 offset = 0.01 * cos(position + time);
  return texture(noise, (position / 10.0) - offset).x;
}

Il en résulte des vagues qui se déplacent lentement, mais pas de manière très naturelle. La section suivante approfondit l’utilisation des shaders pour créer des effets plus complexes, dans ce cas des vagues réalistes, en ajoutant quelques fonctions mathématiques supplémentaires.

Effets avancés : les vagues

Ce qui rend les shaders si puissants, c’est que vous pouvez obtenir des effets complexes en utilisant les mathématiques. Pour illustrer cela, nous allons faire passer nos vagues au niveau supérieur en modifiant la fonction height() et en introduisant une nouvelle fonction appelée wave().

wave() a un paramètre, position, qui est le même que dans height().

Nous allons appeler wave() plusieurs fois dans height() afin de simuler l’aspect des vagues.

float wave(vec2 position){
  position += texture(noise, position / 10.0).x * 2.0 - 1.0;
  vec2 wv = 1.0 - abs(sin(position));
  return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
}

Au début, cela semble compliqué. Voyons donc cela ligne par ligne.

position += texture(noise, position / 10.0).x * 2.0 - 1.0;

Décalez la position par la texture noise. Cela courbera les vagues, de sorte qu’elles ne soient pas des lignes droites complètement alignées sur la grille.

vec2 wv = 1.0 - abs(sin(position));

Définir une fonction de type vague en utilisant sin() et position. Normalement, les vagues sin() sont très rondes. Nous utilisons abs() pour leur donner une crête nette et les contraindre à la plage 0-1. Et puis nous soustrayons 1.0 pour mettre le pic au dessus.

return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);

Multipliez la vague directionnelle x par la vague directionnelle y et augmentez-la à une puissance pour affiner les pics. Ensuite, soustrayez cela de 1.0 pour que les crêtes deviennent des pics et augmentez cela à une puissance pour aiguiser les crêtes.

Nous pouvons maintenant remplacer le contenu de notre fonction height() avec wave().

float height(vec2 position, float time) {
  float h = wave(position);
}

En utilisant cela, vous obtenez :

../../../_images/wave1.png

La forme de la vague de sin est trop évidente. Écartons donc un peu les vagues. Nous le faisons en modifiant l’échelle de position.

float height(vec2 position, float time) {
  float h = wave(position*0.4);
}

Maintenant, ça a l’air beaucoup mieux.

../../../_images/wave2.png

Nous pouvons faire encore mieux si nous superposons plusieurs vagues à des fréquences et des amplitudes variables. Cela signifie que nous allons mettre à l’échelle la position de chacune pour rendre les vagues plus fines ou plus larges (fréquence). Et nous allons multiplier la sortie de la vague pour les rendre plus courtes ou plus hautes (amplitude).

Voici un exemple de la façon dont vous pourriez superposer les quatre vagues pour obtenir de plus belles vagues.

float height(vec2 position, float time) {
  float d = wave((position + time) * 0.4) * 0.3;
  d += wave((position - time) * 0.3) * 0.3;
  d += wave((position + time) * 0.5) * 0.2;
  d += wave((position - time) * 0.6) * 0.2;
  return d;
}

Notez que nous ajoutons le temps à deux et le soustrayons aux deux autres. Cela fait que les vagues se déplacent dans différentes directions, créant un effet complexe. Notez également que les amplitudes (le nombre par lequel le résultat est multiplié) s’additionnent toutes à 1.0. Cela permet de maintenir la vague dans la fourchette 0-1.

Avec ce code vous devriez vous retrouver avec des vagues d’apparence plus complexe et tout ce que vous aviez à faire était d’ajouter un peu de mathématiques !

../../../_images/wave3.png

Pour plus d’informations sur les Spatial shaders, consultez le document Langue des shaders doc et le document Spatial Shaders doc. Regardez aussi les tutoriels plus avancés dans les sections Shading section et 3D.