Tag Archives: Shaders

Unity Shaders 3: Nieve realista


Este es el tercero de 6 artículos que fueron publicados originalmente en inglés en la web Unity Gems por Mike Talbot quien me ha autorizado a traducirlos al español.

Si no has leído los anteriores artículos puedes encontrarlos aquí:

El título original de este tercer artículo es:

En la segunda parte de esta serie hemos cubierto la construcción de shader de nieve acumulativo. Esta parte amplía el shader usando matemáticas para crear una mezcla de colores en el límite de la nieve con la roca.

Continue reading

Unity Shaders 2: Un shader simple de nieve

Este es el segundo de 6 artículos que fueron publicados originalmente en inglés en la web Unity Gems por Mike Talbot quien me ha autorizado a traducirlos al español.

Si no habéis leído el primer artículo, podéis encontrarlo aquí: Unity Shaders 1: Introducción

El título original de este segundo artículo es:

Habiendo cubierto los aspectos básicos de la estructura de un surface shader en la primera parte, esta entrada muestra cómo usarlos para crear un shader de acumulación de nieve, con deformación de malla y bump mapping

Continue reading

Unity Shaders 1: Introducción

Este es el primero de 6 artículos que fueron publicados originalmente en inglés en la web Unity Gems por Mike Talbot quien me ha autorizado a traducirlos al español.

Personalmente considero esto no solo una gran oportunidad de aportar algo de documentación en español sobre este tema sino también una buena ocasión de empezar yo mismo a aprender programación de Shaders.

El título original de este primer artículo es:

Enfrentarse a la programación de Shaders puede ser una tarea desalentadora, pues la documentación está muy dispersa. Este primer artículo explica en detalle los elementos de los Surface Shaders como base para un trabajo más complejo.

Motivación

Si has comenzado recientemente a tener interés en la programación de Shaders entonces puede ser difícil empezar con ello. Este tutorial te guiará a través de los pasos básicos para tener un surface y un fragment shader funcionando. También te presentará algunas de las funciones y variables de los ficheros include de los Shaders de Unity que pueden variar con respecto a la documentación que se encuentra online.

Deberías leer este artículo si:

  • Eres nuevo con respecto a la programación de shaders en general
  • Quieres crear shaders para hacer cosas chulas en tus juegos pero no encuentras ninguno que se ajuste a lo que necesitas.
  • Strumpy Shader Editor no es de ayuda porque no entiendes los principios básicos
  • Quieres manipular texturas dentro de tu shader

Este es el primero de varios artículo que irán construyendo shaders muchos más complejos. Este primer artículo es en realidad muy simple.

Sobre el autor original

Yo también soy nuevo en la programación de shaders, así que decidí escribir esta guía para ayudar a los demás a entender lo básico de los shaders que a mi me costó tanto trabajo. Decididamente no soy un programador de shaders experto.

Leí y releí la documentación intentando entender qué necesitaba saber y simplemente esa información no estaba en el orden que yo necesitaba. Así que pensé que crearía este tutorial y compartiría el conocimiento. ¡Ahora cuando leo la documentación tiene mucho más sentido!

Aunque todos los ejemplos de este tutorial funcionan, puede que haya mejores formas de hacer algunas de las cosas. Si la conoces, ¡por favor añade un comentario!

Mi motivo para meterme en la programación de shaders era construir algo que necesitaba para un mundo poblado por una cantidad interminable de personajes diferentes. Necesitaba construir una malla combinada desde múltiples partes de forma que solo necesitara una draw call (llamada que se hace a la API gráfica para dibujar un objeto en pantalla) por cada personaje.

Habiendo activado y desactivado distintas opciones relacionadas con la ropa modifiqué la malla base con Megafiers, por lo que necesitaba tener una sola textura para todos los modelos pero aún así ser capaz de dar distinto color a la piel, ropa y todo lo demás de cada uno de los personajes.

Opté por un plan consistente en usar 3 pequeñas texturas de 4×4 únicas para cada personaje y un shader que hiciera el trabajo usándolas para colorear el modelo. Tengo planeado explicar completamente este shader en estos tutoriales pero por ahora creo que os gustará ver a mis personajes bailando un improvisado flash mob:

Shaders y Materiales

Bien, entonces el trabajo de un shader es coger tu malla y renderizarla en la pantalla. Un shader puede definir un número de propiedades que se usarás para afectar a lo que se muestra cuando el modelo se renderiza. El valor de estas propiedades al guardarse son un material.

Hay shaders de varios tipos:

  • Surface Shaders: Eliminan la mayor parte del trabajo duro y son apropiados en muchas circunstancias
  • Fragment Shaders: Tienes que hacer mucho más trabajo y son más difíciles de escribir pero te permiten hacer cosas a bajo nivel como iluminación por vértices (que es muy útil en dispositivos móviles) o múltiples pasadas (necesario para efectos más avanzados).

En este artículo nos centraremos en los Surface Shaders.

Recursos para escribir Shaders

Los recursos más importantes que puedes usar para obtener información sobre la escritura de shaders son:

La cadena de procesamiento de Shaders (The Shader Pipeline)

Vas a encontrar mucha información sobre la cadena de procesado (pipeline) de los shaders, así que voy a bajar todo esto a mi propio nivel.

El trabajo de un shader es tomar datos de geometría 3D y convertirlos en píxeles en una pantalla 2D. La buena noticia es que tu sólo te ves envuelto en un par de partes de ese proceso. En un Surface Shader es algo así:

Date cuenta de que realmente no estás asignando el color final del píxel, sino que éste se iluminará después de que tu función termine. Esto significa que puedes pasar una normal que afectará a la iluminación.

Un Fragment Shader sin embargo tiene una ruta similar pero tiene que tener una función Vertex y necesita hacer un montón de trabajo en ella para obtener información para la parte del Fragment . Los Surface Shaders esconden esto.

Una vez que sabemos cómo se llama al código, vemos qué aspecto debe tener:

Vas a escribir un shader y seguramente vaya a tener varias propiedades y uno a más subshaders. Los subshaders usados dependerán de la plataforma en que se ejecuten. Deberías especificar también un shader de respaldo (Fallback) que se usará si ninguno de los subshaders anteriores pueden ejecutarse en el dispositivo.

Cada subshader efectúa al menos una pasada por los datos de entrada y los de salida. Puedes usar estas pasadas para realizar distintas operaciones. Por ejemplo en una pasada de captura puedes obtener todos los píxeles que ya se hayan representado en la pantalla donde se mostrarán tus objetos. Esto puede se útil si quieres crear algún efecto avanzado de distorsión. ¡Cuando se empieza con los shaders sin embargo, no se suele hacer esto! Otro motivo para tener múltiples pasadas es tener diferentes cosas, como escribir e ignorar el buffer de profundidad en diferentes fases de la creación de tu efecto.

Cuando se escribe un Surface Shader se escribe directamente en el subshader. El sistema lo compila posteriormente en múltiples pasadas.
Mientras un shader está escribiendo en la pantalla 2D, también mantiene información sobre cuán lejos de la cámara está el píxel que está escribiendo. Esto es así para que si alguna pieza posterior de geometría está detrás de algo que ya está dibujado en pantalla no se sobrescriba el píxel que ya está ahí.

Puedes controlar si este Z Buffer tiene algún efecto en tu shader con comandos en tu sección Pass. Por ejemplo “ZWrite Off“, que hace que no se actualice el buffer Z para lo que sea que saques en ese pasada de tu shader.

Puedes usar esta técnica para hacer agujeros en otros objetos: Escribiendo en el buffer Z pero no escribiendo en realidad ningún píxel coloreado, el objeto detrás del modelo que use este shader no se representará en pantalla (porque el buffer Z dice que ya hay algo ahí).

Veamos algo de código de un shader:

Podrás reconocer las secciones Propiedades, SubHeader y el shader de respaldo (Fallback).

Entendiendo la magia del código de shaders

El resto del artículo se enfocará en entender la magia que ocurre en ese simple bloque de código, porque la magia de la “programación por convención” entra en juego y tienes que entenderlo.

Cuando programas shaders se te exige que nombres las cosas de forma correcta. De hecho en algunos casos nombrar una variable hace que la misma asuma un valor particular.

Introducción a las propiedades

Se definen las propiedades de tu shader en la seccción Properties {…} de la definición del shader. Estas propiedades son compartidas por todos los subshaders.

Estas definiciones siguen el siguiente formato:

_Nombre ( "Nombre Mostrado", tipo ) = Valor por defecto [{opciones}]

  • _Nombre es el nombre por el que esta propiedad será referido en tu programa.
  • Nombre Mostrado aparecerá en el editor de materiales.
  • tipo es el tipo de la propiedad cuyos valores pueden ser:
    • Color – El valor será un único color para todo el program
    • 2D – Textura potencia de 2 que puede ser muestreada por el programa para obtener un valor particular de un píxel basado en las coordenadas UV del modelo.
    • Rect – Textura que NO es potencia de 2.
    • Cube – Mapa de texturas cúbico 3D usado para reflexiones. Puede ser muestreado por el programa para obtener un píxel en particular.
    • Range(min, max) – El valor en un número real de punto flotante comprendido entre un valor mínimo y otro máximo.
    • Float – El valor en un número real de punto flotante.
    • Vector – El valor es un vector de 4 dimensiones.
  • Valor por defecto es, obviamente, el valor que toma la propiedad por defecto. Varía en función del tipo:
    • Color – El color expresado por la representación en punto flotante de las partes (r, g, b, a). Por ejemplo: (1, 1, 1, 1)
    • 2D/Rect/Cube – Para los tipos textura el valor por defecto pueden ser: una cadena vacía, o “white”, “black”, “gray”, “bump”.
    • Float/Range – El valor a adoptar.
    • Vector – El vector 4D expresado como: (x, y, z, w)
  • { opciones } solo es necesario para los tipos de textura (2D, Rect y Cube) y debe estar especificado al menos como { }. Se pueden combinar múltiples opciones separándolas mediante espacios. Las opciones son:
    • TexGen texgenmode: Generación automática de las coordenadas de texturas para esta textura. Puede ser uno de los siguientes modos: ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal; Estos corresponden directamente con los modos de generación de texturas de OpenGL. Nótese que TexGen es ignorado si se escribe una vertex function.

Así que una propiedad tendría este aspecto:

// Define un color con un valor por defecto de rojo semi trasparente.
_MainColor ("Main Color", Color) = (1,0,0,0.5)

//Define una textura cuyo valor por defecto es el color blanco.
 _Texture ("Texture", 2D) = "white" {}
Fíjate que no se escribe “;” al final de la definición de las propiedades.

Etiquetas (Tags)

Tu surface shader puede ser decorado con una o más etiquetas (tags). Estos tags definen cosas que permiten al hardware decidir cuándo llamar a tu shader.

En nuestro ejemplo tenemos Tags { “RenderType” = “Opaque” } que indica al sistema que debe llamarnos cuando vaya a representar geometría opaca. Unity define algunas de estos tags. El otro tipo obvio es “RenderType” = “Transparent” que dice que nuestro shader podría tener como resultado píxeles semi o totalmente trasparentes.

Otras etiquetas útiles son IgnoreProjector”=”True” que indica que nuestros objetos no se verán afectados por proyectores y Queue”=”xxxx”.

El tag Cola (Queue) tiene algunos efectos muy interesantes y se usa con el tag “RenderType” = “Transparent”. Básicamente dice cuándo será dibujado nuestro objeto:

  • Background – Esta cola de render se renderiza antes que ninguna otra. Se utiliza para skyboxes y similares.
  • Geometry (valor por defecto) – Se usa para la mayoría de objetos. Los opacos usan esta cola.
  • AlphaTest – Para la geometría con test de alfa. Es una cola separada de la Geometry dado que es más eficiente renderizar los objetos con test de alfa después de los solidos.
  • Transparent – Esta cola es renderizada después de Geometry y AlphaTest, en orden de atrás a delante. Cualquier cosa con mezcla de alfa (como shaders que no escriben en el buffer de profundidad) debería ir aquí (cristal, efectos de partículas…)
  • Overlay – Esta cola es para efectos de sobreimpresión, como el interfaz de usuario. Cualquier cosa renderizada en último lugar debería ir aquí (por ejemplo, lens flares).

Lo interesante es que puedea añadir o quitar valores de estas colas básicas. Esto tiene un efecto importante en los efectos que usan objetos trasparentes. Si algunas vez has tenido un plano de agua superpuesto a los billboard de los árboles, entonces es esta cola la causante de tus problemas y la fuente de la solución.

Por ejemplo, usando “Queue”=”Transparent-102” consigo que mi agua trasparente esté siempre por detrás de mis árboles.

Estructura de un Shader

Ok, revisemos la estructura del código dentro de nuestro shader:

Nuestro programa CG está escrito en el lenguaje CG, que se parece mucho a C con algunos giros interesantes. Lee la documentación de nVidia para montones de detalles. Intenteré cubrir aquí lo básico.

Lo más importante es que los números en punto flotante (floats) y las variables de tipo vector pueden ser definidas usando un número entre 2 y 4 tras ella. ¡Esto permite crear floats con hasta 4 elementos y gestionarlos de forma individual!

//Define una variable tipo float
float coordinate;

//Define una variable que albergará un color
float4 color;

//Multiplica un color.
float3 multipliedColor = color.rgb * coordinate.x;

Puedes abordar los elementos de forma individual o como un todo. Puedes usar la notación .xyzw y .rgba de forma intercambiable para reflejar qué estás almacenando en la variable (color, posición, normal, etc). Obviamente puedes usar un float para un solo valor. El uso de .rgba etc se conoce como swizzling (sin traducción).

También encontrarás los tipos half y double, que representan valores de punto flotante con el doble y mitad (respectivamente) de resolución que un float normal. A menudo se usa half por razones de rendimiento. También existe fixed que es un real de punto fijo.

Puede que quieras mantener los valores en el rango 0..1 para los colores. Para hacerlo usa la función saturate, que también funciona en las versiones swizzled de las variables. Por ejemplo: return saturate(unColor.rgb);

Se puede encontrar también la longitud de un vector usando el comando length así: float size = length(unVector.xz);

Sacando información de un Surface Shader

Nuestra función surface va a ser llamada una vez por píxel. El sistema ha obtenido los valores actuales para la estructura de entra del píxel en el que estamos trabajando. Básicamente está interpolando valores en la estructura Input para cada vértice de nuestra malla.

Echemos un vistazo a nuestra función surf:

void surf (Input IN, inout SurfaceOutput o) {
    o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
}

Claramente estamos devolviendo algo en o.Albedo, que es un miembro de la estructura SurfaceOutput que Unity ha definido por nosotros. Echemos un vistazo a esta estructura:

struct SurfaceOutput {
    half3 Albedo; // El color del píxel
    half3 Normal; // La normal del píxel
    half3 Emission; // El color de emisión del píxel
    half Specular; // Fuerza especular del píxel
    half Gloss; // Intensidad de brillo del píxel
    half Alpha; // Valor alfa (transparencia) del píxel
};

Solo se necesita rellenar estos valores en la estructura y Unity hará el resto del trabajo necesario cuando genere las pasadas reales.

Te prometí algo de magia…

Ok, ahora la magia. Primero miremos lo que entra en nuestra función surf. Hemos definido una estructura de entrada con el siguiente aspecto:

struct Input {
    float2 uv_MainTex;
};

Simplemente creando esta estructura le hemos dicho al sistema que nos de las coordenadas de textura de MainText para el píxel actual cada vez que llamamos a la función surf. Si tuviéramos una segunda textura llamada _OtherTexture podríamos obtener sus coordenadas uv simplemente añadiendo lo siguiente:

struct Input {
    float2 uv_MainTex;
    float2 uv_OtherTexture;
};

Si tuviéramos un segundo juego de coordenadas de textura para la otra textura, podríamos obtenerlas también:

struct Input {
    float2 uv_MainTex;
    float2 uv2_OtherTexture;
};

Nuestra estructura de entrada normalmente contiene un juego entero de coordenadas uv o uv2 para todas las texturas que estemos usando.

Si nuestro shader fuera muy complicado y necesitásemos conocer otras cosas sobre el píxel que se está procesando, podríamos pedirlas incluyéndolas en la estructura de entrada:

  • float3 viewDir – Contiene la dirección de la vista, para calcular efectos parallax, rim lightning, etc.
  • float4 con la semántica COLOR – contendrá el color interpolado por vértices.
  • float4 screenPos – Contiene la posición en pantalla para efectos de reflejo.
  • float3 worldPos – Posición en el mundo.
  • float3 worldRefl – Contendrá el vector de reflejo en espacio del mundo si el shader no escribe en o.Normal
  • float3 worldNormal -El vector normal en el mundo si el shader no escribe en o.Normal.
  • INTERNAL_DATA – Una estructura usada por algunas funciones como WorldNormalVector para calcular cosas cuando se escribe en o.Normal

Te estarás preguntando… “¿Qué demonios es la semántica COLOR?”. Pues bien, cuando se escribe un fragment shader normal se le dice a la estructura de entrada qué representa realmente cada variable. Así que si estuvieras loco podrías decir… float2 MiTioPAco : TEXCOORD0; y MiTipoPaco serían las coordenadas uv del modelo. Con los surface shaders solo te tienes que preocupar de COLOR. Por lo tanto, float4 currentColor : COLOR; será interpretado como el color para este píxel. Probablemente no es necesario que te preocupes demasiado por esto.

Haciendo algo de verdad

Por último nos quedan dos líneas que no han sido discutidas:

Sampler2D _MainTex;
Para cada propiedad definida en la sección de propiedades tienes que crear una variable en el programa CG para poder acceder a ella. Debe tener el mismo nombre que la propiedad:

Cuando es una textura, la estructura Input (o como la llames) debe usar el mismo nombre tras uv o uv2 para obtener las coordenadas de texturas correspondientes.

La variable _MainTex es un Sampler2D enlazado a la textura principal. Puede leer píxeles de la textura dada una coordenada uv.

Si hubiéramos definido una propiedad llamada _Color, deberíamos definir la variable como:
float4 _Color;

Y ahora la única línea funcional de nuestro shader:

o.Albedo = tex2d( _MainTex, IN.uv_MainTex).rgb;

tex2d muestrea _MainTex en la coordenada uv que obtuvo del sistema. Como esta textura es un float4 (incluye alfa), solo necesitamos los valores .rgb para o.Albedo. Si quisieramos establecer el alfa también (aunque no tiene mucho sentido, ya que es un shader de tipo Opaque), lo haríamos así:

float4 texColor = tex2d( _MainTex, IN.uv_MainTex );
o.Albedo = texColor.rgb;
o.Alpha = texColor.a;

Resumen

Has leído un montón de cosas y lo único que hemos hecho es construir un shader que está bastante limitado, pero armados con este conocimiento en la segunda parte empezaremos a construir shaders que usen múltiples texturas, normales y demás. Cosas que molarán de verdad 🙂

  • En la segunda parte crearemos un shader de acumulación de nieve donde el nivel de la misma modifica también el modelo.
  • En la tercera parte mejoraremos el shader de la nieve para “fundir” la nieve en los márgenes.
  • En la parte #4 crearemos un toon shader usando bordes negros y texturas ramp.
  • En la parte #5 crearemos un vertex/fragment shader multi-pasada con bump (relieve), aprendiendo las complejidades de ir más allá del paradigma de los surface shaders.
  • En la parte #6 crearemos un vertex/fragment shader con un mejor efecto toon que el que conseguimos con el surface shader de la parte #4