Up to date

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

Using a SubViewport as a texture

Introduction

This tutorial will introduce you to using the SubViewport as a texture that can be applied to 3D objects. In order to do so, it will walk you through the process of making a procedural planet like the one below:

../../_images/planet_example.png

Note

Ce tutoriel ne couvre pas la façon de coder une atmosphère dynamique comme celle de cette planète.

Ce tutoriel suppose que vous êtes familier avec la façon de mettre en place une scène de base comprenant : une Camera3D, une source de lumière, une MeshInstance3D avec un maillage primitif, et appliquer un StandardMaterial3D au maillage. L'accent sera mis sur l'utilisation de SubViewport pour créer dynamiquement des textures qui peuvent être appliquées au maillage.

Durant ce tutoriel, nous couvrirons les sujets suivant :

  • How to use a SubViewport as a render texture

  • Mappage d'une texture sur une sphère avec un mappage équirectangulaire

  • Techniques de fragment shader pour planètes procédurales

  • Paramétrage d'une carte de rugosité à partir d'une Viewport Texture

Mise en place de la scène

Create a new scene and add the following nodes exactly as shown below.

../../_images/viewport_texture_node_tree.webp

Go into the the MeshInstance3D and make the mesh a SphereMesh

Setting up the SubViewport

Click on the SubViewport node and set its size to (1024, 512). The SubViewport can actually be any size so long as the width is double the height. The width needs to be double the height so that the image will accurately map onto the sphere, as we will be using equirectangular projection, but more on that later.

Next disable 3D. We will be using a ColorRect to render the surface, so we don't need 3D either.

../../_images/planet_new_viewport.webp

Select the ColorRect and in the inspector set the anchors preset to Full Rect. This will ensure that the ColorRect takes up the entire SubViewport.

../../_images/planet_new_colorrect.webp

Ensuite, on ajoute un Shader Material au ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).

Note

Une connaissance de base du shading est recommandée pour ce tutoriel. Cependant, même si vous êtes nouveau dans les shaders, tout le code sera fourni, donc vous ne devriez pas avoir de problème pour suivre.

Click the dropdown menu button for the shader material and click / Edit. From here go to Shader > New Shader. give it a name and click "Create". click the shader in the inspector to open the shader editor. Delete the default code and add the following:

shader_type canvas_item;

void fragment() {
    COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}

save the shader code, you'll see in the inspector that the above code renders a gradient like the one below.

../../_images/planet_gradient.png

Now we have the basics of a SubViewport that we render to and we have a unique image that we can apply to the sphere.

Appliquer la texture

Maintenant, nous allons dans le MeshInstance3D et nous y ajoutons un StandardMaterial3D. Pas besoin d'un Shader Material spécial (bien que ce serait une bonne idée pour des effets plus avancés, comme l'atmosphère dans l'exemple ci-dessus).

MeshInstance3D > GeometryInstance > Géométrie > Redéfinition du matériau > Nouveau SpatialMaterial3D

Then click the dropdown for the StandardMaterial3D and click "Edit"

Go to the "Resource" section and check the Local to scene box. Then, go to the "Albedo" section and click beside the "Texture" property to add an Albedo Texture. Here we will apply the texture we made. Choose "New ViewportTexture"

../../_images/planet_new_viewport_texture.webp

Click on the ViewportTexture you just created in the inspector, then click "Assign". Then, from the menu that pops up, select the Viewport that we rendered to earlier.

../../_images/planet_pick_viewport_texture.webp

Votre sphère devrait maintenant être colorée avec les couleurs que nous avons rendues dans le Viewport.

../../_images/planet_seam.webp

Remarquez-vous la jointure laide qui se forme à l'endroit où la texture s'enroule ? C'est parce que nous choisissons une couleur basée sur les coordonnées UV et que les coordonnées UV ne s'enroulent pas autour de la texture. C'est un problème classique dans la projection de cartes 2D. Les développeurs de jeux ont souvent une carte en 2 dimensions qu'ils veulent projeter sur une sphère, mais quand elle s'enroule autour, elle a de larges jointures. Il existe une solution élégante à ce problème, que nous illustrerons dans la prochaine section.

Créer la texture de la planète

So now, when we render to our SubViewport, it appears magically on the sphere. But there is an ugly seam created by our texture coordinates. So how do we get a range of coordinates that wrap around the sphere in a nice way? One solution is to use a function that repeats on the domain of our texture. sin and cos are two such functions. Let's apply them to the texture and see what happens. Replace the existing color code in the shader with the following:

COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
../../_images/planet_sincos.webp

Pas trop mal. Si vous regardez autour de vous, vous pouvez voir que la jointure a maintenant disparu, mais à sa place, nous avons des pincements aux pôles. Ce pincement est dû à la façon dont Godot mappe les textures aux sphères dans son StandardMaterial3D. Il utilise une technique de projection appelée projection équi-rectangulaire, qui traduit une carte sphérique sur un plan 2D.

Note

Si vous êtes intéressé par un peu plus d'informations sur la technique, nous allons convertir des coordonnées sphériques en coordonnées cartésiennes. Les coordonnées sphériques représentent la longitude et la latitude de la sphère, tandis que les coordonnées cartésiennes sont, à toutes fins utiles, un vecteur allant du centre de la sphère au point.

Pour chaque pixel, nous allons calculer sa position 3D sur la sphère. A partir de là, nous allons utiliser un bruit 3D pour déterminer une valeur de couleur. En calculant le bruit en 3D, nous résolvons le problème du pincement aux pôles. Pour comprendre pourquoi, imaginez le bruit calculé sur la surface de la sphère plutôt que sur le plan 2D. Lorsque vous calculez sur la surface de la sphère, vous ne touchez jamais un bord, et donc vous ne créez jamais de couture ou de point de pincement sur le pôle. Le code suivant convertit les UVs en coordonnées cartésiennes.

float theta = UV.y * 3.14159;
float phi = UV.x * 3.14159 * 2.0;
vec3 unit = vec3(0.0, 0.0, 0.0);

unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
unit = normalize(unit);

Et si nous utilisons unit comme valeur de sortie COLOR, nous obtenons :

../../_images/planet_normals.webp

Maintenant que nous pouvons calculer la position 3D de la surface de la sphère, nous pouvons utiliser le bruit 3D pour faire la planète. Nous utiliserons cette fonction de bruit directement depuis un Shadertoy :

vec3 hash(vec3 p) {
    p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
             dot(p, vec3(269.5, 183.3, 246.1)),
             dot(p, vec3(113.5, 271.9, 124.6)));

    return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}

float noise(vec3 p) {
  vec3 i = floor(p);
  vec3 f = fract(p);
  vec3 u = f * f * (3.0 - 2.0 * f);

  return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
                     dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
                 mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
                     dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
             mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
                     dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
                 mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
                     dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
}

Note

Tout les crédits vont à l'auteur, Inigo Quilez, Cela est publié sous une licence MIT.

Maintenant pour utiliser le noise, ajoutez ce qui suit à la fonction fragment :

float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
../../_images/planet_noise.webp

Note

Afin de mettre en valeur la texture, nous avons réglé le matériau sur unshaded.

Vous pouvez voir maintenant que le bruit s'enroule en effet sans jointure autour de la sphère. Bien que cela ne ressemble en rien à la planète qu'on vous a promise. Alors passons à quelque chose de plus coloré.

Colorer la planète

Maintenant pour faire les couleurs de la planète. Bien qu'il y ait de nombreuses façons de le faire, pour l'instant, nous nous en tiendrons à un gradient entre l'eau et la terre.

Pour faire un gradient dans GLSL, on utilise la fonction mix. mix prend deux valeurs à interpoler entre elles et un troisième argument pour choisir la quantité à interpoler entre elles ; en substance, il mixe les deux valeurs ensemble. Dans d'autres API, cette fonction est souvent appelée lerp. Cependant, lerp est typiquement réservé au mélange de deux flottants ensemble ; mix peut prendre n'importe quelle valeur, que ce soit des flottants ou des types vecteur.

COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);

La première couleur est le bleu pour l'océan. La deuxième couleur est une sorte de couleur rougeâtre (parce que toutes les planètes extraterrestres ont besoin d'un terrain rouge). Et finalement, ils sont mélangés par n * 0.5 + 0.5. n varie en douceur entre -1 et 1. Donc nous le mappons dans l'intervalle 0-1 que mix attend. Maintenant vous pouvez voir que les couleurs changent entre le bleu et le rouge.

../../_images/planet_noise_color.webp

C'est un peu plus flou que ce que nous voulons. Les planètes ont généralement une séparation relativement nette entre la terre et la mer. Pour ce faire, nous allons changer le dernier terme en smoothstep(-0.1, 0.0, n). Et ainsi toute la ligne devient :

COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));

Ce que fait smoothstep est de retourner 0 si le troisième argument est en dessous du premier et 1 si le troisième argument est plus grand que le second et se mélange doucement entre 0 et 1 si le troisième nombre est entre le premier et le second. Donc, dans cette ligne, smoothstep renvoie 0 lorsque n est inférieur à -0.1 et 1 lorsque n est supérieur à 0.

../../_images/planet_noise_smooth.webp

Une dernière chose pour rendre ça un peu plus planétaire. La terre ne devrait pas être aussi tachetée ; rendons les bords un peu plus rugueux. Une astuce souvent utilisée dans les shaders pour rendre un terrain rugueux avec du bruit consiste à superposer des niveaux de bruit à différentes fréquences. Nous utilisons une couche pour faire la structure globale des continents. Ensuite, une autre couche casse un peu les bords, puis une autre, et ainsi de suite. Ce que nous allons faire, c'est calculer n avec quatre lignes de code de shader au lieu d'une seule. n devient :

float n = noise(unit * 5.0) * 0.5;
n += noise(unit * 10.0) * 0.25;
n += noise(unit * 20.0) * 0.125;
n += noise(unit * 40.0) * 0.0625;

Et maintenant, la planète ressemble à :

../../_images/planet_noise_fbm.webp

Créer un océan

Une dernière chose pour que ça ressemble plus à une planète. L'océan et la terre reflètent la lumière différemment. Nous voulons donc que l'océan brille un peu plus que la terre. Nous pouvons faire cela en passant une quatrième valeur dans le canal alpha de notre sortie COLOR et en l'utilisant comme carte de rugosité.

COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);

Cette ligne renvoie 0.3 pour l'eau et 1.0 pour la terre. Cela signifie que le terrain sera assez accidenté, tandis que l'eau sera assez lisse.

Et ensuite, dans le matériau, sous la section "Metallic", assurez-vous que Metallic est réglé à 0 et Specular est réglé à 1. La raison en est que l'eau réfléchit très bien la lumière, mais n'est pas métallique. Ces valeurs ne sont pas physiquement exactes, mais elles sont suffisamment bonnes pour cette démo.

Next, under the "Roughness" section set the roughness texture to a Viewport Texture pointing to our planet texture SubViewport. Finally, set the Texture Channel to Alpha. This instructs the renderer to use the alpha channel of our output COLOR as the Roughness value.

../../_images/planet_ocean.webp

Vous remarquerez que très peu de changements, sauf que la planète ne reflète plus le ciel. Cela se produit parce que, par défaut, quand quelque chose est rendu avec une valeur alpha, il est dessiné comme un objet transparent sur l'arrière-plan. Et comme l'arrière-plan par défaut de SubViewport est opaque, le canal alpha du Viewport Texture est 1, ce qui fait que la texture de la planète est dessinée avec des couleurs légèrement plus faibles et une valeur de Roughness de 1 partout. Pour corriger cela, nous allons dans le SubViewport et activons la propriété "Transparent Bg". Puisque nous rendons maintenant un objet transparent par-dessus un autre, nous voulons activer blend_premul_alpha :

render_mode blend_premul_alpha;

Ceci pré-multiplie les couleurs par la valeur alpha et les mélange ensuite correctement ensemble. Typiquement, en mélangeant une couleur transparente sur une autre, même si le fond a un alpha de 0 (comme c'est le cas ici), vous vous retrouvez avec d'étranges problèmes de saignement de couleur. Le paramètre blend_premul_alpha corrige cela.

Now the planet should look like it is reflecting light on the ocean but not the land. move around the OmniLight3D in the scene so you can see the effect of the reflections on the ocean.

../../_images/planet_ocean_reflect.webp

And there you have it. A procedural planet generated using a SubViewport.