Tu primer shader Espacial: parte 2

Desde un alto nivel, lo que Godot hace es dar al usuario un montón de parámetros que pueden ser opcionalmente establecidos (AO, SSS_Strength, RIM, etc.). Estos parámetros corresponden a diferentes efectos complejos (oclusión ambiental, dispersión de la sub-superficie, iluminación del borde, etc.). Cuando no se escribe, el código se desecha antes de ser compilado y así el shader no incurre en el costo de la característica extra. Esto hace que sea fácil para los usuarios tener un shader complejo y correcto de PBR, sin tener que escribir shaders complejos. Por supuesto, Godot también permite ignorar todos estos parámetros y escribir un shader totalmente personalizado.

Para una lista completa de estos parámetros ver el documento de referencia spatial shader.

Una diferencia entre la función de vértice y la función de un fragmento es que la función de vértice se ejecuta por vértice y establece propiedades como VERTEX (posición) y NORMAL, mientras que el shader de fragmentos se ejecuta por píxel y, lo más importante, establece el color ALBEDO de la "ref:`Mesh<class_MeshInstance>".

Su primera función de fragmento espacial

Como se mencionó en la parte anterior de este tutorial. El uso estándar de la función de fragmento en Godot es establecer diferentes propiedades materiales y dejar que Godot se encargue del resto. Con el fin de proporcionar aún más flexibilidad, Godot también proporciona cosas llamadas modos de representación. Los modos de renderización se establecen en la parte superior del shader, directamente debajo del shader_type, y especifican qué tipo de funcionalidad quieres que tengan los aspectos incorporados del shader.

Por ejemplo, si no quieres que las luces afecten a un objeto, establece el modo de renderización en unshaded:

render_mode unshaded;

También puedes apilar varios modos de renderización. Por ejemplo, si quieres usar el toon shading en lugar del shader PBR más realista, establece el modo difuso y el modo especular en toon:

render_mode diffuse_toon, specular_toon;

Este modelo de funcionalidad incorporada te permite escribir complejos shaders personalizados cambiando sólo unos pocos parámetros.

Para una lista completa de los modos de renderizado, ver Spatial shader reference.

En esta parte del tutorial, veremos cómo tomar el terreno accidentado de la parte anterior y convertirlo en un océano.

Primero fijemos el color del agua. Lo hacemos poniendo ALBEDO.

ALBEDO es un vec3 que contiene el color del objeto.

Pongámoslo en un bonito tono de azul.

void fragment() {
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/albedo.png

Lo pusimos en un tono azul muy oscuro porque la mayor parte del azul del agua vendrá de los reflejos del cielo.

El modelo de PBR que Godot utiliza se basa en dos parámetros principales: METALLIC y ROUGHNESS.

ROUGHNESS especifica cuan suave es la superficie de un material. Una baja ROUGHNESS hará que un material parezca un plástico brillante, mientras que una alta rugosidad hace que el material parezca más sólido en color.

METALLIC especifica cuánto se parece el objeto a un metal. Es mejor ponerlo cerca de 0 o 1. Piensa en METALLIC como un cambio en el balance entre el reflejo y el color ALBEDO. Un alto METALLIC casi ignora al ALBEDO por completo, y se ve como un espejo del cielo. Mientras que un METALLIC bajo tiene una representación más igualitaria del color del cielo y el ALBEDO color.

ROUGHNESS aumenta de 0 a 1 de izquierda a derecha mientras que METALLIC aumenta de 0 a 1 de arriba a abajo.

../../../_images/PBR.png

Nota

METALLIC debería estar cerca de 0 o 1 para un buen shader PBR. Sólo lo ponemos entre ellos para mezclar los materiales.

El agua no es un metal, así que pondremos su propiedad METALLIC a 0.0. El agua también es altamente reflectante, por lo que estableceremos su propiedad de ROUGHNESS para que sea bastante baja también.

void fragment() {
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/plastic.png

Ahora tenemos una superficie lisa de aspecto plástico. Es hora de pensar en algunas propiedades particulares del agua que queremos emular. Hay dos principales que llevarán esto de una superficie plástica extraña a una bonita agua estilizada. La primera son los reflejos especulares. Los reflejos especulares son esos puntos brillantes que ves desde donde el sol se refleja directamente en tu ojo. El segundo es la reflectancia de fresnel. La reflectancia de Fresnel es la propiedad de los objetos de ser más reflectantes en ángulos poco profundos. Es la razón por la que puedes ver en el agua debajo de ti, pero más lejos refleja el cielo.

Para aumentar los reflejos especulares, haremos dos cosas. Primero, cambiaremos el modo de representación para especular a toon porque el modo de representación de toon tiene mayores reflejos especulares.

render_mode specular_toon;
../../../_images/specular-toon.png

En segundo lugar, añadiremos iluminación de borde. La iluminación del borde aumenta el efecto de la luz en los ángulos de visión. Normalmente se usa para emular la forma en que la luz pasa a través de la tela en los bordes de un objeto, pero la usaremos aquí para ayudar a conseguir un bonito efecto acuático.

void fragment() {
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01;
  ALBEDO = vec3(0.1, 0.3, 0.5);
}
../../../_images/rim.png

Para añadir la reflectancia de fresnel, calcularemos un término de frescura en nuestro shader de fragmentos. Aquí, no vamos a usar un término de fresnel real por razones de rendimiento. En su lugar, lo aproximaremos usando el producto del punto de los vectores NORMAL y VIEW. El vector NORMAL apunta lejos de la superficie de la malla, mientras que el vector VIEW es la dirección entre tu ojo y ese punto en la superficie. El producto del punto entre ellos es una forma práctica de saber cuando estás mirando a la superficie de frente o en un ángulo de visión.

float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));

Y mezclarlo en ambos, ROUGHNESS y ALBEDO. Este es el beneficio de los ShaderMaterials sobre los SpatialMaterials. Con los SpatialMaterials, podríamos establecer estas propiedades con una textura, o con un número plano. Pero con los shaders podemos establecerlos basados en cualquier función matemática que podamos imaginar.

void fragment() {
  float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
  RIM = 0.2;
  METALLIC = 0.0;
  ROUGHNESS = 0.01 * (1.0 - fresnel);
  ALBEDO = vec3(0.1, 0.3, 0.5) + (0.1 * fresnel);
}
../../../_images/fresnel.png

Y ahora, con sólo 5 líneas de código, puedes tener agua de aspecto complejo. Ahora que tenemos iluminación, esta agua se ve demasiado brillante. Vamos a oscurecerla. Esto se hace fácilmente disminuyendo los valores del "Vec3" que pasamos a ALBEDO. Pongámoslos en Vec3 (0.01, 0.03, 0.05).

../../../_images/dark-water.png

Animando con TIME

Volviendo a la función de vértice, podemos animar las ondas usando la variable incorporada TIME.

TIME es una variable incorporada que es accesible desde las funciones de vértice y fragmento.

En el último tutorial calculamos la altura leyendo un mapa de altura. Para este tutorial, haremos lo mismo. Poner el código del heightmap en una función llamada height().

float height(vec2 position) {
  return texture(noise, position / 10.0).x; // Scaling factor is based on mesh size (this PlaneMesh is 10×10).
}

Para usar TIME en la función height(), necesitamos pasarla.

float height(vec2 position, float time) {
}

Y asegúrese de pasarlo correctamente dentro de la función de vértice.

void vertex() {
  vec2 pos = VERTEX.xz;
  float k = height(pos, TIME);
  VERTEX.y = k;
}

En lugar de usar un mapa de normales para calcular las normales. Vamos a calcularlos manualmente en la función vertex(). Para hacerlo, usa la siguiente línea de código.

NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));

Necesitamos calcular "NORMAL" manualmente porque en la próxima sección usaremos las matemáticas para crear ondas de aspecto complejo.

Ahora, vamos a hacer la función de height() un poco más complicada desplazando la position por el coseno del TIME.

float height(vec2 position, float time) {
  vec2 offset = 0.01 * cos(position + time);
  return texture(noise, (position / 10.0) - offset).x;
}

Esto resulta en ondas que se mueven lentamente, pero no de una manera muy natural. La siguiente sección profundizará en el uso de los shaders para crear efectos más complejos, en este caso ondas realistas, añadiendo algunas funciones matemáticas más.

Efectos avanzados: olas

Lo que hace que los shaders sean tan poderosos es que se pueden lograr efectos complejos usando las matemáticas. Para ilustrar esto, vamos a llevar nuestras ondas al siguiente nivel modificando la función height() e introduciendo una nueva función llamada wave().

wave() tiene un parámetro, position, que es el mismo que está en height().

Vamos a llamar a wave() varias veces en height para fingir el aspecto de las ondas.

float wave(vec2 position){
  position += texture(noise, position / 10.0).x * 2.0 - 1.0;
  vec2 wv = 1.0 - abs(sin(position));
  return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
}

Al principio esto parece complicado. Así que vamos a ir línea por línea.

position += texture(noise, position / 10.0).x * 2.0 - 1.0;

Desfase la posición por la textura del noise. Esto hará que las ondas se curven, por lo que no serán líneas rectas completamente alineadas con la rejilla.

vec2 wv = 1.0 - abs(sin(position));

Define una función de onda usando "sin()" y position. Normalmente las ondas sin() son muy redondas. Usamos abs() absoluto para darles una cresta afilada y restringirlas al rango 0-1. Y luego lo restamos de 1.0 para poner el pico en la parte superior.

return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);

Multiplica la onda direccional x por la onda direccional y levántala a una potencia que agudice los picos. Luego reste eso de 1.0 para que las crestas se conviertan en picos y aumente la potencia para afinar las crestas.

Ahora podemos reemplazar el contenido de nuestra función de height() con wave().

float height(vec2 position, float time) {
  float h = wave(position);
  return h;
}

Using this, you get:

../../../_images/wave1.png

La forma de la onda de pecado es demasiado obvia. Así que vamos a extender las ondas un poco. Lo hacemos escalando la position.

float height(vec2 position, float time) {
  float h = wave(position * 0.4);
  return h;
}

Ahora se ve mucho mejor.

../../../_images/wave2.png

Podemos hacerlo aún mejor si ponemos varias ondas encima de cada una a diferentes frecuencias y amplitudes. Lo que esto significa es que vamos a escalar la posición de cada una para hacer las ondas más delgadas o más anchas (frecuencia). Y vamos a multiplicar la salida de la onda para hacerlas más cortas o más altas (amplitud).

Aquí hay un ejemplo de cómo se pueden formar capas de las cuatro ondas para conseguir unas ondas de mejor aspecto.

float height(vec2 position, float time) {
  float d = wave((position + time) * 0.4) * 0.3;
  d += wave((position - time) * 0.3) * 0.3;
  d += wave((position + time) * 0.5) * 0.2;
  d += wave((position - time) * 0.6) * 0.2;
  return d;
}

Note que sumamos el tiempo a dos y lo restamos de los otros dos. Esto hace que las ondas se muevan en diferentes direcciones creando un efecto complejo. Observa también que las amplitudes (el número por el que se multiplica el resultado) suman todas 1.0. Esto mantiene la onda en el rango de 0-1.

¡Con este código deberías terminar con ondas de aspecto más complejo y todo lo que tenías que hacer era añadir un poco de matemáticas!

../../../_images/wave3.png

Para más información sobre los Spatial shaders lea el Shading Language doc y el Spatial Shaders doc. También puedes ver tutoriales más avanzados en la sección Shading section y la sección 3D.