Tu primer shader 3D

Has decidido comenzar a escribir tu propio shader espacial personalizado. Quizás viste un truco genial en línea realizado con shaders, o has descubierto que el SpatialMaterial no satisface completamente tus necesidades. De cualquier manera, has decidido escribir el tuyo propio y ahora necesitas saber por dónde empezar.

Este tutorial explicará cómo escribir un Spatial shader y cubrirá más temas que el tutorial CanvasItem.

Los Spatial shaders tienen más funcionalidad incorporada que los shaders CanvasItem. Lo que se espera de los shaders espaciales es que Godot ya ha proporcionado la funcionalidad para los casos de uso común y todo lo que el usuario necesita hacer en el shader es establecer los parámetros adecuados. Esto es especialmente cierto para un flujo de trabajo de PBR (renderización basada en la física).

Este es un tutorial de dos partes. En esta primera parte vamos a repasar cómo hacer un terreno simple usando el desplazamiento de vértices de un mapa de altura en la función de vértices. En la second part vamos a tomar los conceptos de este tutorial y vamos a caminar a través de cómo configurar los materiales personalizados en un shader de fragmentos escribiendo un shader de agua del océano.

Nota

Este tutorial asume algunos conocimientos básicos de shader como los tipos (vec2, float, sampler2D), y las funciones. Si te sientes incómodo con estos conceptos es mejor obtener una introducción suave de The Book of Shaders antes de completar este tutorial.

Dónde asignar mi material

En 3D, los objetos se dibujan usando Meshes. Las mallas son un tipo de recurso que almacena la geometría (la forma de su objeto) y los materiales (el color y cómo el objeto reacciona a la luz) en unidades llamadas "superficies". Una Malla puede tener múltiples superficies, o sólo una. Típicamente, importarías una malla de otro programa (por ejemplo, Blender). Pero Godot también tiene unos cuantos PrimitiveMeshes que te permiten añadir geometría básica a una escena sin importar las Mallas.

Hay múltiples tipos de nodos que puedes usar para dibujar una malla. El principal es MeshInstance, pero también puedes usar Particles, MultiMeshes (con un MultiMeshInstance), u otros.

Típicamente, un material se asocia con una superficie dada en una malla, pero algunos nodos, como MeshInstance, permite sobreescribir el material para una superficie específica, o para todas las superficies.

Si colocas un material en la superficie o en la malla misma, entonces todas las MeshInstances que comparten esa malla compartirán ese material. Sin embargo, si desea reutilizar la misma malla en múltiples instancias de la malla, pero tiene diferentes materiales para cada instancia, entonces debe colocar el material en la MeshInstance.

Para este tutorial pondremos nuestro material en la malla misma en lugar de aprovechar la habilidad de la MeshInstance para sobreescribir materiales.

Configurando

Añade un nuevo nodo MeshInstance a tu escena.

En la pestaña del inspector al lado de "Mesh" haga clic en "[empty]" y seleccione "New PlaneMesh". Luego haga clic en la imagen de un avión que aparece.

Esto agrega un PlaneMesh a nuestra escena.

Luego, en el viewport, haga clic en la esquina superior izquierda en el botón que dice "Perspectiva". Aparecerá un menú. En el medio del menú hay opciones para mostrar la escena. Selecciona 'Display Wireframe'.

Esto te permitirá ver los triángulos que componen el avión.

../../../_images/plane.png

Ahora pon Subdivide Width y Subdivide Depth en 32.

../../../_images/plane-sub-set.png

Puedes ver que ahora hay muchos más triángulos en la Mesh. Esto nos dará más vértices con los que trabajar y nos permitirá añadir más detalles.

../../../_images/plane-sub.png
PrimitiveMeshes, como PlaneMesh, solo tienen uno

Superficie, así que en lugar de un array de materiales, solo hay uno. Haz clic al lado de "Material" donde dice "[vacío]" y selecciona "Nuevo ShaderMaterial". Luego, haz clic en la esfera que aparece.

Ahora haz clic al lado de "Shader" donde dice "[empty]" y selecciona "New Shader".

El editor shader debería aparecer ahora y estás listo para empezar a escribir tu primer shader Espacial!

Magia Shader

../../../_images/shader-error.png

¿Notas que ya hay un error? Esto se debe a que el editor de shaders recarga los shader al vuelo. Lo primero que necesitan los shaders Godot es una declaración de qué tipo de shader son. Ponemos la variable shader_type en spatial porque es un shader espacial.

shader_type spatial;

A continuación, definiremos la función vertex(). La función vertex() determina dónde aparecen los vértices de tu Mesh en la escena final. La usaremos para desplazar la altura de cada vértice y hacer que nuestro plano llano aparezca como un pequeño terreno.

Definimos el shader de vértices así:

void vertex() {

}

Sin nada en la función vertex(), Godot usará su shader de vértices por defecto. Podemos empezar a hacer cambios fácilmente añadiendo una sola línea:

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

Añadiendo esta línea, deberías obtener una imagen como la de abajo.

../../../_images/cos.png

Bien, vamos a desempacar esto. El valor y del VERTEX se está incrementando. Y estamos pasando los componentes x y z del VERTEX como argumentos para cos y sin; eso nos da una apariencia de onda a través de los ejes x y z.

Lo que queremos lograr es el aspecto de pequeñas colinas; después de todo. cos y sin ya se parecen a las colinas. Lo hacemos escalando las entradas a las funciones cos y sin.

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

Esto se ve mejor, pero sigue siendo demasiado puntiagudo y repetitivo, hagámoslo un poco más interesante.

Noise heightmap

Noise(Ruido) es una herramienta muy popular para fingir el aspecto del terreno. Piense en ello como en la función del coseno, donde tiene colinas que se repiten, excepto que, con el ruido, cada colina tiene una altura diferente.

Godot proporciona el recurso NoiseTexture para generar una textura de ruido a la que se puede acceder desde un shader.

Para acceder a una textura en un shader agregue el siguiente código cerca de la parte superior de su shader, fuera de la función vertex().

uniform sampler2D noise;

Esto te permitirá enviar una textura de ruido al shader. Ahora mira en el inspector debajo de tu material. Deberías ver una sección llamada "Shader Params". Si la abres, verás una sección llamada "noise".

Haz clic al lado donde dice "[empty]" y selecciona "New NoiseTexture". Luego en tu NoiseTexture haz clic al lado donde dice "Noise" y selecciona "New OpenSimplexNoise".

OpenSimplexNoise es usado por el NoiseTexture para

generar un mapa de alturas.

Una vez que esté todo configurado, debería verse así.

../../../_images/noise-set.png

Ahora, accede a la textura del ruido usando la función texture(). texture() toma una textura como primer argumento y un vec2 para la posición en la textura como segundo argumento. Usamos los canales x y z de VERTEX para determinar en qué parte de la textura buscar. Observa que las coordenadas del PlaneMesh están dentro del rango [-1,1] (para un tamaño de 2), mientras que las coordenadas de la textura están dentro de [0,1], así que para normalizar dividimos por el tamaño del PlaneMesh 2.0 y sumamos 0.5. texture devuelve un vec4 de los canales r, g, b, a en la posición. Como la textura del ruido es en escala de grises, todos los valores son iguales, así que podemos usar cualquiera de los canales como la altura. En este caso usaremos el canal r, o x.

float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
VERTEX.y += height;

Nota: xyzw es lo mismo que rgba en GLSL, así que en lugar de texture().x arriba, podríamos usar texture().r. Mira la ``Documentación OpenGL <https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Vectores>`_ para mayor detalle.

Usando este código puedes ver que la textura crea colinas de aspecto aleatorio.

../../../_images/noise.png

Ahora mismo es demasiado puntiagudo, queremos suavizar un poco las colinas. Para ello, usaremos un uniforme. Ya usaste un uniforme arriba para pasar la textura del ruido, ahora vamos a aprender cómo funcionan.

Uniforms

Las variables uniformes permiten pasar datos del juego al shader. Son muy útiles para controlar los efectos shader. Los uniformes pueden ser casi cualquier tipo de dato que se pueda usar en el shader. Para usar un uniforme, lo declaras en tu Shader usando la palabra clave uniform.

Hagamos un uniforme que cambie la altura del terreno.

uniform float height_scale = 0.5;

Godot te permite inicializar un uniforme con un valor; aquí, la height_scale se establece en 0.5. Puedes establecer uniformes desde GDScript llamando a la función set_shader_param() en el material correspondiente al shader. El valor pasado desde GDScript tiene prioridad sobre el valor usado para inicializarlo en el shader.

# called from the MeshInstance
mesh.material.set_shader_param("height_scale", 0.5)

Nota

El cambio de uniformes en los nodos basados en el espacio es diferente de los nodos basados en el CanvasItem. Aquí, ponemos el material dentro del recurso PlaneMesh. En otros recursos de malla puede que necesites acceder primero al material llamando a surface_get_material(). Mientras que en el recurso MeshInstance accederías al material usando get_surface_material() o material_override.

Recuerda que la string pasada a set_shader_param() debe coincidir con el nombre de la variable uniforme en el Shader. Puedes usar la variable uniforme en cualquier lugar dentro de tu Shader. Aquí, la usaremos para establecer el valor de la altura en lugar de multiplicarla arbitrariamente por 0.5.

VERTEX.y += height * height_scale;

Ahora se ve mucho mejor.

../../../_images/noise-low.png

Usando uniformes, podemos incluso cambiar el valor en cada cuadro para animar la altura del terreno. Combinado con Tweens, esto puede ser especialmente útil para animaciones simples.

Interactuar con la luz

Primero, apaga el marco de alambre. Para ello, haz clic en la parte superior izquierda del Viewport de nuevo, donde dice "Perspectiva", y selecciona "Visualización Normal".

../../../_images/normal.png

Fíjese en cómo el color de la malla se vuelve plano. Esto se debe a que la iluminación de la misma es plana. ¡Añadamos una luz!

Primero, agregaremos una OmniLight a la escena.

../../../_images/light.png

Se puede ver la luz que afecta al terreno, pero se ve rara. El problema es que la luz está afectando al terreno como si fuera plano. Esto se debe a que el shader de luz utiliza las normales de la Mesh para calcular la iluminación.

Los normales se almacenan en la Mesh, pero estamos cambiando la forma de la Malla en el shader, por lo que los normales ya no son correctos. Para arreglar esto, podemos recalcular los normales en el shader o usar una textura normal que corresponda a nuestro ruido. Godot hace ambas cosas fáciles para nosotros.

Puedes calcular la nueva normalidad manualmente en la función de vértice y luego sólo tienes que poner "NORMAL". Con "NORMAL", Godot hará todos los cálculos de iluminación difíciles para nosotros. Cubriremos este método en la próxima parte de este tutorial, por ahora leeremos normales de una textura.

En cambio, confiaremos en el NoiseTexture de nuevo para calcular las normales por nosotros. Lo hacemos pasando una segunda textura de ruido.

uniform sampler2D normalmap;

Establece esta segunda textura de uniforme con otro NoiseTexture utilizando otro OpenSimplexNoise. Pero esta vez, marca la opción "As Normalmap".

../../../_images/normal-set.png

Ahora, porque este es un mapa normal y no un normal por vértice, vamos a asignarlo en la función fragment(). La función fragment() se explicará con más detalle en la siguiente parte de este tutorial.

void fragment() {
}

Cuando tenemos normales que corresponden a un vértice específico ponemos "NORMAL", pero si tienes un mapa de normales que viene de una textura, pon la normal usando NORMALMAP. De esta manera Godot manejará el envolver la textura alrededor de la malla automáticamente.

Por último, para asegurarnos de que estamos leyendo de los mismos lugares en la textura del ruido y la textura del mapa normal, vamos a pasar la posición VERTEX.xz de la función vertex() a la función fragment(). Lo hacemos con variaciones.

Sobre el vertex() define un vec2 llamado tex_position. Y dentro de la función vertex() asigna VERTEX.xz a la tex_position.

varying vec2 tex_position;

void vertex() {
  ...
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  ...
}

Y ahora podemos acceder a tex_position desde la función fragment().

void fragment() {
  NORMALMAP = texture(normalmap, tex_position).xyz;
}

Con los normales en su lugar la luz ahora reacciona a la altura de la malla de forma dinámica.

../../../_images/normalmap.png

Incluso podemos arrastrar la luz y la iluminación se actualizará automáticamente.

../../../_images/normalmap2.png

Aquí está el código completo de este tutorial. Puedes ver que no es muy largo ya que Godot maneja la mayoría de las cosas difíciles para ti.

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMALMAP = texture(normalmap, tex_position).xyz;
}

Eso es todo para esta parte. Esperemos que ahora entiendas lo básico de los sombreadores de vértices en Godot. En la próxima parte de este tutorial escribiremos una función de fragmento para acompañar esta función de vértice y cubriremos una técnica más avanzada para convertir este terreno en un océano de olas en movimiento.