Using a SubViewport as a texture
Introducción
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:
Nota
Este tutorial no cubre cómo codificar una atmósfera dinámica como la que tiene este planeta.
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 SubViewport to dynamically create textures that can be applied to the mesh.
En este tutorial cubriremos los siguientes temas:
How to use a SubViewport as a render texture
Mapear una textura a una esfera con mapeo equirectangular
Técnicas de sombreado de fragmentos para planetas procedimentales
Estableciendo un mapa de rugosidad desde un Viewport Texture
Configurando la escena
Create a new scene and add the following nodes exactly as shown below.
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.
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.
A continuación, añadimos un Shader Material al ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).
Nota
Para este tutorial se recomienda una familiaridad básica con shaders. Sin embargo, incluso si es nuevo en con los shaders, se le proporcionará todo el código, por lo que no debería tener problemas para seguirlo.
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.
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.
Aplicando la textura
Now 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).
MeshInstance > GeometryInstance > Geometría > Material Override > New StandardMaterial3D
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"
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.
Su esfera debe ser coloreada con los colores que renderizamos al Viewport.
¿Notan la fea costura que se forma donde la textura se envuelve? Esto se debe a que estamos eligiendo un color basado en coordenadas UV y las coordenadas UV no envuelven la textura. Este es un problema clásico en la proyección de mapas en 2D. Los desarrolladores de juegos a menudo tienen un mapa bidimensional que quieren proyectar en una esfera, pero cuando se envuelve alrededor, tiene grandes costuras. Hay una solución elegante para este problema que ilustraremos en la siguiente sección.
Haciendo la textura del planeta
Así que ahora, cuando renderizamos en nuestro SubViewport, aparece mágicamente en la esfera. Pero hay una fea costura creada por las coordenadas de nuestra textura. Entonces, ¿cómo obtenemos un rango de coordenadas que envuelvan la esfera de una manera agradable? Una solución es usar una función que se repita en el dominio de nuestra textura. sin y cos son dos de esas funciones. Apliquémoslas a la textura y veamos qué sucede. Reemplace el código de color existente en el shader con lo siguiente:
COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
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.
Nota
Si le interesa un poco más de información sobre la técnica, convertiremos de coordenadas esféricas a coordenadas cartesianas. Las coordenadas esféricas trazan la longitud y la latitud de la esfera, mientras que las coordenadas cartesianas son, a todos los efectos, un vector desde el centro de la esfera hasta el punto.
Para cada píxel, calcularemos su posición 3D en la esfera. A partir de eso, usaremos el ruido 3D para determinar un valor de color. Calculando el ruido en 3D, resolvemos el problema del pellizco en los polos. Para entender por qué, imagina que el ruido se calcula a través de la superficie de la esfera en lugar de a través del plano 2D. Cuando se calcula a través de la superficie de la esfera, nunca se golpea un borde, y por lo tanto nunca se crea una costura o un punto de pellizco en el polo. El siguiente código convierte los ``UV``s en coordenadas cartesianas.
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);
Y si usamos unit como valor de salida de COLOR, obtenemos:
Ahora que podemos calcular la posición 3D de la superficie de la esfera, podemos usar el ruido 3D para hacer el planeta. Usaremos esta función de ruido directamente de 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 );
}
Nota
Todo el crédito es para el autor, Inigo Quilez. Se publica bajo la licencia MIT.
Ahora para usar noise, agrega lo siguiente a la función fragment:
float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
Nota
Para resaltar la textura, fijamos el material en sin sombrear.
Puedes ver ahora que el ruido se envuelve sin problemas alrededor de la esfera. Aunque no se parece en nada al planeta que se les prometió. Así que pasemos a algo más colorido.
Coloreando el planeta
Ahora para hacer que el planeta se coloree. Aunque hay muchas maneras de hacerlo, por ahora, nos quedaremos con un gradiente entre el agua y la tierra.
Para hacer un gradiente en el GLSL, usamos la función mix. mix toma dos valores para interpolar entre ellos y un tercer argumento para elegir cuánto interpolar entre ellos; en esencia, mezcla los dos valores entre sí. En otras APIs, esta función se llama a menudo lerp. Sin embargo, lerp se reserva típicamente para mezclar dos reales juntos; mix puede tomar cualquier valor, ya sea reales o tipos de vectores.
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);
El primer color es el azul para el océano. El segundo color es una especie de color rojizo (porque todos los planetas alienígenas necesitan terreno rojo). Y finalmente, se mezclan entre sí por n * 0.5 + 0.5. n varía suavemente entre -1 y 1. Así que lo ubicamos en el rango de 0-1 que la mix espera. Ahora puedes ver que los colores cambian entre el azul y el rojo.
Eso es un poco más borroso de lo que queremos. Los planetas suelen tener una separación relativamente clara entre la tierra y el mar. Para ello, cambiaremos el último término a smoothstep(-0.1, 0.0, n). Y así toda la línea se convierte:
COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
Lo que hace Smoothstep es devolver 0 si el tercer argumento está por debajo del primero y 1 si el tercer argumento es más grande que el segundo y se mezcla suavemente entre 0 y 1 si el tercer número está entre el primero y el segundo. Así que en esta línea, smoothstep devuelve 0 cuando n es menor que -0,1 y devuelve 1 cuando n está por encima de 0.
Una cosa más para hacer esto un poco más planetario. La tierra no debería ser tan desordenada; hagamos los bordes un poco más ásperos. Un truco que se usa a menudo con los shaders para hacer que el terreno se vea más áspero con el ruido, es poner capas de ruido unas sobre otras a varias frecuencias. Usamos una capa para hacer la estructura general de los continentes. Luego otra capa rompe un poco los bordes, y luego otra, y así sucesivamente. Lo que haremos es calcular n con cuatro líneas de código de shader en lugar de sólo una. n se convierte en:
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;
Y ahora el planeta se ve como:
Haciendo un océano
Una última cosa para hacer que esto se parezca más a un planeta. El océano y la tierra reflejan la luz de forma diferente. Así que queremos que el océano brille un poco más que la tierra. Podemos hacerlo pasando un cuarto valor al canal alfa de nuestro COLOR de salida y usándolo como un mapa de rugosidad.
COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
Esta línea devuelve 0.3 para el agua y 1.0 para la tierra. Esto significa que la tierra va a ser bastante áspera, mientras que el agua será bastante suave.
Y luego, en el material, bajo la sección "Metallic", asegúrate de que Metallic esté en 0 y Specular en 1. La razón de esto es que el agua refleja la luz muy bien, pero no es metálica. Estos valores no son físicamente exactos, pero son lo suficientemente buenos para esta demostración.
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.
You'll notice that very little changes except that the planet is no longer reflecting the sky.
This is happening because, by default, when something is rendered with an
alpha value, it gets drawn as a transparent object over the background. And since the default background
of the SubViewport is opaque, the alpha channel of the
Viewport Texture is 1, resulting in the planet texture being
drawn with slightly fainter colors and a Roughness value of 1 everywhere. To correct this, we
go into the SubViewport and enable the "Transparent Bg" property. Since we are now
rendering one transparent object on top of another, we want to enable blend_premul_alpha:
render_mode blend_premul_alpha;
Esto pre-multiplica los colores por el valor alfa y luego los mezcla correctamente. Típicamente, al mezclar un color tranparente sobre otro, incluso si el fondo tiene un valor alfa de 0 (como en este caso), terminas con extraños problemas de sangrado de color. Poner blend_premul_alpha arregla eso.
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.
And there you have it. A procedural planet generated using a SubViewport.