Up to date

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

Using a Viewport as a texture

Введение

This tutorial will introduce you to using the Viewport 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

Примечание

This tutorial does not cover how to code a dynamic atmosphere like the one this planet has.

This tutorial assumes you are familiar with how to set up a basic scene including: a Camera3D, a light source, a MeshInstance3D with a Primitive Mesh, and applying a StandardMaterial3D to the mesh. The focus will be on using the Viewport to dynamically create textures that can be applied to the mesh.

In this tutorial, we'll cover the following topics:

  • How to use a Viewport as a render texture

  • Mapping a texture to a sphere with equirectangular mapping

  • Fragment shader techniques for procedural planets

  • Setting a Roughness map from a Viewport Texture

Setting up the Viewport

First, add a Viewport to the scene.

Next, set the size of the Viewport to (1024, 512). The Viewport 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.

../../_images/planet_new_viewport.png

Next, disable HDR and disable 3D. We don't need HDR because our planet's surface will not be especially bright, so values between 0 and 1 will be fine. And we will be using a ColorRect to render the surface, so we don't need 3D either.

Select the Viewport and add a ColorRect as a child.

Set the anchors "Right" and "Bottom" to 1, then make sure all the margins are set to 0. This will ensure that the ColorRect takes up the entire Viewport.

../../_images/planet_new_colorrect.png

Next, we add a Shader Material to the ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).

Примечание

Basic familiarity with shading is recommended for this tutorial. However, even if you are new to shaders, all the code will be provided, so you should have no problem following along.

ColorRect > CanvasItem > Material > Material > click / Edit > ShaderMaterial > Shader > New Shader > click / Edit:

shader_type canvas_item;

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

The above code renders a gradient like the one below.

../../_images/planet_gradient.png

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

Applying the texture

MeshInstance3D > GeometryInstance > Geometry > Material Override > New StandardMaterial3D:

Now we go into the MeshInstance3D and add a StandardMaterial3D to it. No need for a special Shader Material (although that would be a good idea for more advanced effects, like the atmosphere in the example above).

MeshInstance3D > GeometryInstance > Geometry > Material Override > click / Edit:

Open the newly created StandardMaterial3D and scroll down 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.png

Then, from the menu that pops up, select the Viewport that we rendered to earlier.

../../_images/planet_pick_viewport_texture.png

Your sphere should now be colored in with the colors we rendered to the Viewport.

../../_images/planet_seam.png

Заметили некрасивый шов, который образуется там, где текстура оборачивается вокруг? Это потому, что мы выбираем цвет на основе UV-координат, а UV-координаты не оборачиваются вокруг текстуры. Это классическая проблема при проекции двумерных карт. Разработчики игр часто имеют двумерную карту, которую они хотят спроецировать на сферу, но когда она оборачивается вокруг, получаются большие швы. Существует элегантный обходной путь решения этой проблемы, который мы проиллюстрируем в следующем разделе.

Making the planet texture

Теперь, когда мы рендерим в наш Viewport, он волшебным образом появляется на сфере. Но есть уродливый шов, созданный нашими текстурными координатами. Как же нам получить диапазон координат, которые красиво оборачиваются вокруг сферы? Одно из решений - использовать функцию, повторяющуюся на области нашей текстуры. sin и cos - две такие функции. Давайте применим их к текстуре и посмотрим, что получится.

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

Not too bad. If you look around, you can see that the seam has now disappeared, but in its place, we have pinching at the poles. This pinching is due to the way Godot maps textures to spheres in its StandardMaterial3D. It uses a projection technique called equirectangular projection, which translates a spherical map onto a 2D plane.

Примечание

If you are interested in a little extra information on the technique, we will be converting from spherical coordinates into Cartesian coordinates. Spherical coordinates map the longitude and latitude of the sphere, while Cartesian coordinates are, for all intents and purposes, a vector from the center of the sphere to the point.

Для каждого пикселя мы вычислим его трехмерное положение на сфере. Исходя из этого, мы будем использовать 3D-шум для определения значения цвета. Рассчитывая шум в 3D, мы решаем проблему защемления на полюсах. Чтобы понять, почему, представьте, что шум рассчитывается по поверхности сферы, а не по двухмерной плоскости. Когда вы рассчитываете по поверхности сферы, вы никогда не заденете край, а значит, не создадите шов или точку защемления на полюсе. Следующий код преобразует UV в декартовы координаты.

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

And if we use unit as an output COLOR value, we get:

../../_images/planet_normals.png

Now that we can calculate the 3D position of the surface of the sphere, we can use 3D noise to make the planet. We will be using this noise function directly from a 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 );
}

Примечание

All credit goes to the author, Inigo Quilez. It is published under the MIT licence.

Now to use noise, add the following to the fragment function:

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

Примечание

In order to highlight the texture, we set the material to unshaded.

You can see now that the noise indeed wraps seamlessly around the sphere. Although this looks nothing like the planet you were promised. So let's move onto something more colorful.

Coloring the planet

Now to make the planet colors. While there are many ways to do this, for now, we will stick with a gradient between water and land.

To make a gradient in GLSL, we use the mix function. mix takes two values to interpolate between and a third argument to choose how much to interpolate between them; in essence, it mixes the two values together. In other APIs, this function is often called lerp. However, lerp is typically reserved for mixing two floats together; mix can take any values whether it be floats or vector types.

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

The first color is blue for the ocean. The second color is a kind of reddish color (because all alien planets need red terrain). And finally, they are mixed together by n * 0.5 + 0.5. n smoothly varies between -1 and 1. So we map it into the 0-1 range that mix expects. Now you can see that the colors change between blue and red.

../../_images/planet_noise_color.png

That is a little more blurry than we want. Planets typically have a relatively clear separation between land and sea. In order to do that, we will change the last term to smoothstep(-0.1, 0.0, n). And thus the whole line becomes:

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

What smoothstep does is return 0 if the third argument is below the first and 1 if the third argument is larger than the second and smoothly blends between 0 and 1 if the third number is between the first and the second. So in this line, smoothstep returns 0 whenever n is less than -0.1 and it returns 1 whenever n is above 0.

../../_images/planet_noise_smooth.png

Еще одна вещь, чтобы сделать это немного более планетарным. Земля не должна быть такой шарообразной; давайте сделаем края немного более грубыми. Трюк, который часто используется в шейдерах для создания грубого рельефа с помощью шума, заключается в наложении друг на друга уровней шума с различными частотами. Мы используем один слой для создания общей шарообразной структуры континентов. Затем другой слой немного разбивает края, затем еще один, и так далее. Мы вычислим n с помощью четырех строк кода шейдера вместо одной. n становится:

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;

And now the planet looks like:

../../_images/planet_noise_fbm.png

And with shading turned back on, it looks like:

../../_images/planet_noise_fbm_shaded.png

Making an ocean

One final thing to make this look more like a planet. The ocean and the land reflect light differently. So we want the ocean to shine a little more than the land. We can do this by passing a fourth value into the alpha channel of our output COLOR and using it as a Roughness map.

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

This line returns 0.3 for water and 1.0 for land. This means that the land is going to be quite rough, while the water will be quite smooth.

And then, in the material, under the "Metallic" section, make sure Metallic is set to 0 and Specular is set to 1. The reason for this is the water reflects light really well, but isn't metallic. These values are not physically accurate, but they are good enough for this demo.

Next, under the "Roughness" section, set Roughness to 1 and set the roughness texture to a Viewport Texture pointing to our planet texture Viewport. 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.png

Вы заметите, что мало что изменилось, кроме того, что планета больше не отражает небо. Это происходит потому, что по умолчанию, когда что-то рендерится с альфа-значением, оно рисуется как прозрачный объект на фоне. А поскольку фон по умолчанию в Viewport непрозрачен, канал alpha в Viewport Texture равен 1, в результате чего текстура планеты рисуется с немного более тусклыми цветами и значением Roughness везде равным 1. Чтобы исправить это, мы заходим в Viewport и включаем свойство "Transparent Bg". Поскольку мы сейчас рендерим один прозрачный объект поверх другого, мы хотим включить blend_premul_alpha:

render_mode blend_premul_alpha;

This pre-multiplies the colors by the alpha value and then blends them correctly together. Typically, when blending one transparent color on top of another, even if the background has an alpha of 0 (as it does in this case), you end up with weird color bleed issues. Setting blend_premul_alpha fixes that.

Now the planet should look like it is reflecting light on the ocean but not the land. If you haven't done so already, add an OmniLight3D to the scene so you can move it around and see the effect of the reflections on the ocean.

../../_images/planet_ocean_reflect.png

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