En una ocasión anterior, había hecho un tutorial acerca de #proyecciónTriplanar, en aquel blog se plantaron las bases de la proyección triplanar y prácticamente sirve para cualquier tipo de textura, excepto para las normales.
Esta vez haremos un shader que combine la proyección triplanar, con unas normales corregidas y que admita nieve global, advierto que hay algunas cosas que realmente no entiendo, pero que funciona como magia vudú.
Este shader es muy útil y ahorra rendimiento si se usa masivamente en formaciones rocosas y cuevas.
Archivo CGINC
Este paso ya lo sabrán hacer, así que pueden obviarlo si no lo necesita, pero si no lo sabes hacer te dejaré el link donde aprenderás a hacer shaders modulares.
La razón de este archivo es para usar funciones en común en distintos shaders, en mi caso he decidido hacer variaciones de shaders que usan distintos modelos de iluminación y distintas características, pero que comparten la proyección triplanar.
fixed3 SnowProjection(), es una función en la que les enseñé a hacer nieve en un capítulo anterior, así que les dejo el link para que vayan y aprendan a aplicar nieve.
GetTriplanarProjection(), esta función tiene la misma base de como aplicar una proyección triplanar que ya vimos en un blog anterior, así que les dejo el link para que aprendan como aplicar una proyeccion triplanar.
Básicamente una proyección triplanar, lo que hacemos es usar las coordenadas mundiales para crear tres coordenadas UV, una por cada eje, luego aplicamos tres texturas tex2D donde usamos una coordenada UV por cada eje, luego lo mezclamos gracias a una función de mezcla y listo.
En el blog anterior de texturizado triplanar, usábamos una función para calcular la mezcla así:
fixed3 projnormal =saturate(pow(IN.worldNormal * _NormalScale,4));
Esta vez usamos una función que llamamos TriplanarWeight(), que tiene leves diferencias con el anterior enfoque, en primer lugar estos cálculos se hace desde el programa de vértices para que tenga un poco más de rendimiento.
fixed3 n = max(abs(normal) - blend, 0);
return n / (n.x + n.y + n.z).x;
Se hace uso de la normal del vértice que va de 0 a 1 y le restamos la mezcla que solo son apenas unas fracciones, pero esto permite que tenga el mismo efecto de mezcla mostrado arriba.
half4 result =lerp(cXZ, cXY, projnormal.z);
Esta línea muestra como se mezclaba en el anterior blog, donde hacia uso de interpoladores.
fixed4 cx = tex2D(mainTex, uvz) * weight.z;
Ahora es simplemente una multiplicación que usamos con la variable weight.
#ifndef TRIPLANAR_CUSTOM_CGINC_INCLUDED
#define TRIPLANAR_CUSTOM_CGINC_INCLUDED
fixed3 TriplanarWeight(fixed3 normal, fixed blend) {
fixed3 n = max(abs(normal) - blend, 0);
return n / (n.x + n.y + n.z).x;
}
fixed3 SnowProjection(fixed3 worldNormal ,fixed3 windDirection, fixed amount, fixed wetness, fixed3 worldPos, fixed snowLevel, fixed3 color) {
half difference = dot(worldNormal, windDirection.xyz) - lerp(1, -1, amount);
difference = saturate(difference / wetness);
fixed _worldPos = dot(worldPos, fixed3(0, 1 / snowLevel, 0));
return lerp(color, difference + (1 - difference) * color, _worldPos);
}
float4 GetTriplanarProjection(sampler2D mainTex, fixed3 worldPos, fixed4 mainTex_ST, fixed3 weight) {
fixed2 uvx = (worldPos.yz - mainTex_ST.zw) * mainTex_ST.xy;
fixed2 uvy = (worldPos.xz - mainTex_ST.zw) * mainTex_ST.xy;
fixed2 uvz = (worldPos.xy - mainTex_ST.zw) * mainTex_ST.xy;
fixed4 cz = tex2D(mainTex, uvx) * weight.x;
fixed4 cy = tex2D(mainTex, uvy) * weight.y;
fixed4 cx = tex2D(mainTex, uvz) * weight.z;
return (cz + cy + cx);
}
float3 GetTriplanarNormal(sampler2D bumpMap, fixed3 worldPos, fixed4 bumpMap_ST, fixed3 weight) {
fixed2 uvx = (worldPos.yz - bumpMap_ST.zw) * bumpMap_ST.xy;
fixed2 uvy = (worldPos.xz - bumpMap_ST.zw) * bumpMap_ST.xy;
fixed2 uvz = (worldPos.xy - bumpMap_ST.zw) * bumpMap_ST.xy;
fixed3 bz = UnpackNormal(tex2D(bumpMap, uvx)) * weight.x;
fixed3 by = UnpackNormal(tex2D(bumpMap, uvy)) * weight.y;
fixed3 bx = UnpackNormal(tex2D(bumpMap, uvz)) * weight.z;
return abs(normalize(bz + by + bx));
}
#endif // TERRAIN_SPLATMAP_COMMON_CGINC_INCLUDED
GetTriplanarNormal(). Aquí es donde comienza lo que no entiendo. Luego de la publicación del blog de texturizado triplanar me quedé experimentando a buscar la forma de aplicar texturas normales y solo lo hacía bien por una sola cara a pesar que aplicaba la misma proyección.
Este enfoque no hace nada distinto a como se aplican las texturas comunes, solo que lo aplica en texturas normales, solo tengo la sospecha que la variable weight que proviene de la función TriplanarWeight, equilibra mejor y más fácilmente la mezcla, quizás el enfoque del anterior solo favorecía la cara superior.
return abs(normalize(bz + by + bx));
Por último está el retorno que tiene su magia vudú, por ahora solo puedo decir que gracias a la función normalize y abs, hace que funcione bien en el modelo de iluminación Standard, pero mi sospecha es que las sumas favorecen al color azul, restando participación al rojo y al verde, pero al normalizar permite que se equilibren los componentes.
Propiedades.
No hay mucho que decir de las propiedades, al tratarse de una superficie estándar, trae consigo las propiedades Standard, pero como este shader tendrá la mezcla con la nieve tiene, las propiedades para controlar la nieve.
Recuerde incluir el nombre del archivo cginc, para poder usar las funciones comunes que expliqué arriba.
#include "TriplanarCustom.cginc"
También incluya la línea para activar la característica de la nieve.
#pragma shader_feature IF_SNOW
Si no sabes que hace esta línea y que son las características y como se usan junto a los drawer, te dejo el link.
Properties {
_MainTex("Base (RGB)", 2D) = "white" {}
_BumpMap("Normalmap", 2D) = "bump" {}
_Blend("Blending", Range (0.01, 0.4)) = 0.2
_Glossiness("Smoothness", Range(0,1)) = 0.5
_Metallic("Metallic", Range(0,1)) = 0.0
[MaterialToggle(IF_SNOW)] _IfSnow("if Snow", Float) = 1
[ShowIf(IF_SNOW)]_Snow("Snow Level", Range(0,1)) = 1
[ShowIf(IF_SNOW)]_SnowDirection("Snow Direction", Vector) = (0,1,0)
[ShowIf(IF_SNOW)]_SnowDepth("Snow Depth", Range(0,0.2)) = 0.1
[ShowIf(IF_SNOW)]_Wetness("Wetness", Range(0, 0.5)) = 0.3
[ShowIf(IF_SNOW)]_SnowLevel("Snow level", Float) = 1
}
SubShader {
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
LOD 200
CGPROGRAM
#pragma surface surf Standard vertex:vert fullforwardshadows
#include "TriplanarCustom.cginc"
#pragma target 3.0
#pragma shader_feature IF_SNOW
fixed4 _Color;
sampler2D _MainTex, _BumpMap;
float4 _MainTex_ST, _BumpMap_ST;
fixed _Blend;
half _Glossiness;
half _Metallic;
#if IF_SNOW
float _Snow, _SnowDepth, _Wetness;
float4 _SnowDirection;
float _SnowLevel;
#endif
Programa de Vértices.
Un programa de vértices algo innecesario, está solo para transferir la variable de peso que será usado en el programa de superficie, solo hay unas pocas líneas que resaltar:
UNITY_INITIALIZE_OUTPUT(Input, o);
Al tratarse de una función de vértice en el que prácticamente no hacemos uso de la posición, de la normal, esta línea lo que hace es transferir tal cual los datos que entran del struct appdata_full ósea lo que envia los datos de la CPU directamente a la GPU sin modificación.
o.weight = TriplanarWeight(v.normal, _Blend);
Esta línea que multiplica los pesos, es quien decide que tanto se mezclaran las texturas entre sí.
float3 worldNormal; INTERNAL_DATA
Y estas dos líneas que son magia vudú y que no entiendo que hace.
struct Input {
float3 weight : TEXCOORD0;
float3 worldPos;
float3 worldNormal;
INTERNAL_DATA
};
void vert(inout appdata_full v, out Input o) {
UNITY_INITIALIZE_OUTPUT(Input, o);
o.weight = TriplanarWeight(v.normal, _Blend);
}
Programa de superficie.
Quienes hayan seguido los blogs mencionados, ya saben que aquí no hay nada raro o fuera de lo común, con las funciones en común que se encuentra en el archivo cginc, es solo cuestión de llenar los parámetros y los resultados serán evidentes, así lo hacemos para o.Albedo y para o.Normal, lo raro comienza cuando le incluyes la nieve y aquí se hace necesario explicar las líneas vudú.
half3 worldNormal = WorldNormalVector(IN, o.Normal);
Esta línea de facto es necesaria, porque la función WorldNormalVector calcula las normales mundiales, pero es necesario, ya que IN.worldNormal, nos viene incluida en el input.(pero parece que sin esto la nieve no se proyecta bien),
Al hacer uso de la funcion WorldNormalVector, esto automáticamente obliga el uso de la línea INTERNAL_DATA dentro del struct Input.
Si probamos lo hecho hasta el momento, la nieve cubre todo y no lo hace sobre las caras que apunta hacia arriba. Para que funcione es necesario añadir al struct Input float3 worldNormal y con esta línea todo funciona como la seda.
void surf(Input IN, inout SurfaceOutputStandard o) {
fixed4 col = GetTriplanarProjection(_MainTex, IN.worldPos, _MainTex_ST, IN.weight);
#if IF_SNOW
half3 worldNormal = WorldNormalVector(IN, o.Normal);
o.Albedo = SnowProjection(worldNormal, _SnowDirection, _Snow, _Wetness, IN.worldPos, _SnowLevel, col.rgb);
#else
o.Albedo = col.rgb;
#endif
o.Normal = GetTriplanarNormal(_BumpMap, IN.worldPos, _BumpMap_ST, IN.weight);
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = col.a;
}
ENDCG
}
FallBack "Legacy Shaders/Diffuse"
}
Conclusiones.
Hay algunas cosas que suceden bajo el capo que no logro comprender, me da mal sabor de boca haber arreglado un bug y no saber la causa que lo originó, lo único que puedo concluir es que equilibrar las normales depende de ecuaciones precisas, si no se logra este equilibrio, el efecto se pierde.
Si alguien sabe el porqué funciona o si sabe como puedo pulir este shader, ya que lo siento muy redundante, estaría agradecido de que me explicara.
Voy a dejar el enlace al código completo en GitHub, para que lo descarguen y me den retroalimentación de como mejorarlo.
No siendo mas nos vemos en un proximo Blog.
Comments