Usar un Viewport como textura

Introducción

Este tutorial te introducirá en el uso del Viewport como una textura que puede ser aplicada a objetos 3D. Para ello, te guiará a través del proceso de creación de un planeta procedimental como el que se muestra a continuación:

../../_images/planet_example.png

Nota

Este tutorial no cubre cómo codificar una atmósfera dinámica como la que tiene este planeta.

Este tutorial asume que estás familiarizado con la configuración de una escena básica que incluye: un Camera, un light source, un Mesh Instance con un Primitive Mesh, y aplicando un SpatialMaterial a la malla. El foco estará en usar el Viewport para crear dinámicamente texturas que puedan ser aplicadas a la malla.

En este tutorial cubriremos los siguientes temas:

  • Cómo usar un Viewports como una 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 el Viewport

Primero, agrega un Viewport a la escena.

A continuación, establezca el tamaño del Viewport a (1024, 512). El Viewport puede ser de cualquier tamaño siempre y cuando el ancho sea el doble de la altura. El ancho debe ser el doble de la altura para que la imagen se asemeje con precisión a la esfera, ya que usaremos la proyección equidireccional, pero más sobre eso más adelante.

../../_images/planet_new_viewport.png

A continuación, deshabilite el HDR y deshabilite el 3D. No necesitamos el HDR porque la superficie de nuestro planeta no será especialmente brillante, así que los valores entre 0 y 1 estarán bien. Y usaremos un ColorRect para renderizar la superficie, así que tampoco necesitamos 3D.

Selecciona el Viewport y añade un ColorRect como un hijo.

Ponga los anclajes "Derecha" y "Abajo" en 1, y asegúrese de que todos los márgenes estén en 0. Esto asegurará que el ColorRect ocupe todo el Viewport.

../../_images/planet_new_colorrect.png

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.

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

shader_type canvas_item;

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

El código de arriba muestra un gradiente como el de abajo.

../../_images/planet_gradient.png

Ahora tenemos lo básico de un Viewport al que renderizamos y tenemos una imagen única que podemos aplicar a la esfera.

Aplicando la textura

MeshInstance > GeometryInstance > Geometría > Material Override > Nuevo Material Espacial:

Ahora vamos a la Mesh Instance y añadimos un SpatialMaterial a ella. No es necesario un Shader Material especial (aunque sería una buena idea para efectos más avanzados, como la atmósfera en el ejemplo anterior).

MeshInstance > GeometryInstance > Geometry > Material Override > Click / Edit:

Abre la recién creada SpatialMaterial y baja a la sección "Albedo" y haz clic al lado de la propiedad "Texture" para añadir un Albedo Texture. Aquí aplicaremos la textura que hemos creado. Elige "New ViewportTexture"

../../_images/planet_new_viewport_texture.png

Luego, en el menú que aparece, selecciona el Viewport que hemos renderizado anteriormente.

../../_images/planet_pick_viewport_texture.png

Su esfera debe ser coloreada con los colores que renderizamos al Viewport.

../../_images/planet_seam.png

¿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 a nuestro Viewport, 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.

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

No está mal. Si miras alrededor, puedes ver que la costura ha desaparecido, pero en su lugar, tenemos pellizcos en los postes. Este pellizco se debe a la forma en que Godot mapea las texturas a las esferas en su SpatialMaterial. Utiliza una técnica de proyección llamada proyección equidireccional, que traduce un mapa esférico a un plano 2D.

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:

../../_images/planet_normals.png

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);
../../_images/planet_noise.png

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.

../../_images/planet_noise_color.png

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.

../../_images/planet_noise_smooth.png

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:

../../_images/planet_noise_fbm.png

Y ahora el planeta parece:

../../_images/planet_noise_fbm_shaded.png

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.

A continuación, en la sección "Rugosidad", pon Rugosidad en 1 y pon la textura de la rugosidad en "ref:Textura de Viewport <clase_ViewportTexture>" apuntando a la textura de nuestro planeta ref:`Viewport <class_Viewport>. Por último, establece el Texture Channel en Alpha. Esto instruye al renderizador para usar el canal alfa de nuestra salida COLOR como valor de Rugosidad.

../../_images/planet_ocean.png

Notarás que hay muy pocos cambios, excepto que el planeta ya no refleja el cielo. Esto ocurre porque, por defecto, cuando algo se representa con un valor alfa, se dibuja como un objeto transparente sobre el fondo. Y como el fondo por defecto de Viewport es opaco, el canal alfa de Viewport Texture es 1, lo que hace que la textura del planeta se dibuje con colores ligeramente más tenues y un valor Rugosidad de 1 en todas partes. Para corregir esto, vamos a la propiedad Viewport y activamos la propiedad "Transparent Bg". Ya que ahora estamos renderizando un objeto transparente sobre otro, queremos habilitar 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.

Ahora el planeta debería parecer que está reflejando luz en el océano pero no en la tierra. Si aún no lo has hecho, añade una OmniLight a la escena para que puedas moverla y ver el efecto de los reflejos en el océano.

../../_images/planet_ocean_reflect.png

Y ahí lo tienes. Un planeta procedimental generado usando un Viewport.