top of page

Texturizado triplanar, la mejor forma de texturizar formas irregulares.




Uno de los Game assets mas buscados por internet, son los shaders que tienen que ver con el texturizado #triplanar, esta vez aprenderás a hacer el tuyo.


Uno de los problemas mas comunes en el desarrollo de videojuegos ocurre cuando nos atravesamos con objetos irregulares como las rocas, cuevas o formaciones rocosas, unity por defecto no incluye dentro de sus shaders predeterminados ninguno que tenga esta característica en los terrenos o en materiales sólidos para uno crear sus propios voxeles.


El shader de terrenos por defecto que maneja unity, maneja bien el efecto de la luz sobre la superficie, usando el enfoque de materiales basados en física y está muy bien, pero a la mínima que realizas una elevación del terreno, notarás que las texturas se estiran al punto de deformarse. Aún no entiendo por qué unity desde un principio, no proporciona shaders de agua y de terrenos decentes.


Antes de ir por el tema de la aplicación de la nieve en terrenos, primero hay que aprender una de las bases del texturizado para terrenos, en este caso se trata del texturizado triplanar, en el que usamos 3 coordenadas de texturas en un espacio mundial, que nos permite revestir los objetos desde los 3 ejes y no desde uno solo.


Este shader, será un ejemplo sencillo, pero tiende a ser un poco costoso si se abusa mucho de este tipo de materiales, como es un ejemplo sencillo y pretendo ser demostrativo, advierto a que si se quiere aplicar este enfoque con varias texturas, hay técnicas donde usando solo un material, puede cubrir hasta 4 texturas distintas.


Preparando la escena y las propiedades.


Antes de cualquier realizar cualquier cosa, primero descarga estas 3 texturas que las usaremos para el shader de este blog.


Para la escena solo se necesita crear un cubo con un material nuevo aplicado en él y un shader de superficie aplicado al material.


Usaremos casi que las mismas propiedades que vienen por defecto en un shader de superficie, incluiremos 2 texturas más aparte de _MainTex, no controlaremos ni el tamaño ni el ajuste de las propiedades por defecto que viene con las mismas texturas, sino que lo controlaremos por los parámetros de tamaño en X y en Y.


El parámetro _NormalScale, controlará el tamaño de las coordenadas normales para el objeto.


Note que la estructura de entrada, no contiene coordenadas uv de ninguna textura, en su lugar contiene la posición mundial (_worldPos) y las normales mundiales (_worldNormal)


Shader "Surface/CheapTriplanar" {
Properties{
	[NoScaleOffset]_MainTex("Albedo (RGB)", 2D) = "white" {}
	[NoScaleOffset]_Tex2("Albedo 2", 2D) = "white" {}
	[NoScaleOffset]_Tex3("Albedo 3", 2D) = "white" {}
	_SizeX("SizeX", Float) = 20
	_SizeY("SizeY", Float) = 20
	_NormalScale("Normal Scale", Range(-2,2)) = 1
	_Glossiness ("Smoothness", Range(0,1)) = 0.5
	_Metallic ("Metallic", Range(0,1)) = 0.0
	}
SubShader {
	Tags { "RenderType"="Opaque" }
	LOD 200

	CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
	#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
	#pragma target 3.0

	sampler2D _MainTex, _Tex2, _Tex3;
	fixed _NormalScale;
	half _SizeX, _SizeY;

	struct Input {
		float3 worldPos;
		fixed3 worldNormal;
	};

	half _Glossiness;
	half _Metallic;

Función SURF.


Primero comenzamos creando un vector de 2 dimensiones, donde usaremos como componentes el tamaño en X y en Y.

half2 scale = half2(_SizeX, _SizeY);

Ahora crearemos lo 3 planos o coordenadas UV para las imágenes, las nombraremos XY, XZ y YZ respectivamente, donde en cada uno de ellos dividiremos la posición mundial del objeto por la escala.


Aquí hay un truco, ¿por qué dividir y no mejor multiplicar por la escala?, resulta que la posición mundial va en un valor de -1 a 1 y este valor corresponde de -1 metro a 1 metro en las coordenadas de la escena de unity, entonces si dividimos por la escala, la posición se hará mas chica, pero por alguna razón que no entiendo, la única forma que da para que funcione esta ecuación seria si:

XY = (1/IN.worldPos.xy)/scale;

El caso es que misteriosamente funciona y ya lo he demostrado en el blog de: Como aplicar nieve en superficies sólidas.


Luego pasamos a obtener el color de las tres texturas, cada textura para un plano distinto. Como este es solo un ejemplo usaremos 3 texturas distintas, pero lo habitual es usar una sola textura en los 3 planos.


void surf (Input IN, inout SurfaceOutputStandard o) {
	half2 scale = half2(_SizeX, _SizeY);
	
	float2 XY = IN.worldPos.xy / scale;
	float2 XZ = IN.worldPos.xz / scale;
	float2 YZ = IN.worldPos.yz / scale;

	fixed4 cXY = tex2D(_MainTex, XY);
	fixed4 cXZ = tex2D(_Tex2, XZ);
	fixed4 cYZ = tex2D(_Tex3, YZ);

fixed3 projnormal = saturate(pow(IN.worldNormal * _NormalScale, 4));

	half4 result = lerp(cXZ, cXY, projnormal.z);
	result = lerp(result, cYZ, projnormal.x);

	o.Albedo = result.rgb;
	// Metallic and smoothness come from slider variables
	o.Metallic = _Metallic;
	o.Smoothness = _Glossiness;
	o.Alpha = result.a;
	}
ENDCG

Ya tenemos 3 texturas, pero como le indicamos al objeto cuál de los 3 planos debe tomar, para eso debemos calcular las normales del objeto en el espacio mundial.

fixed3 projnormal = saturate(pow(IN.worldNormal * _NormalScale, 4));

Realmente podríamos obviar el parámetro _NormalScale y solo elevar el valor a 4, pero eso ya lo dejo a gusto del que lo use, lo que realmente hace es ajustar cuál eje tendrá más protagonismo, yo prefiero que todos los ejes tenga igual importancia.

half4 result = lerp(cXZ, cXY, projnormal.z);
result = lerp(result, cYZ, projnormal.x);

La variable result, vendría siendo el color final de la mezcla de las 3 texturas, para esto hacemos una interpolación entre la textura XZ y XY controlado por la proyección normal en el eje Z


Luego le asignamos la interpolación entre el color resultante y la textura del eje YZ, controlado por la proyección normal del eje X.


El resto de la historia ya la conocen, a continuación les dejo el shader completo para que lo estudien, recuerden que el uso de coordenadas triplanares, normalmente es para una sola textura, como reto añádale soporte para las normales de las texturas.


Shader "Surface/CheapTriplanar" {
Properties{
	[NoScaleOffset]_MainTex("Albedo (RGB)", 2D) = "white" {}
	[NoScaleOffset]_Tex2("Albedo 2", 2D) = "white" {}
	[NoScaleOffset]_Tex3("Albedo 3", 2D) = "white" {}
	_SizeX("SizeX", Float) = 20
	_SizeY("SizeY", Float) = 20
	_NormalScale("Normal Scale", Range(-2,2)) = 1
	_Glossiness ("Smoothness", Range(0,1)) = 0.5
	_Metallic ("Metallic", Range(0,1)) = 0.0
	}
SubShader {
	Tags { "RenderType"="Opaque" }
	LOD 200

	CGPROGRAM
	// Physically based Standard lighting model, and enable shadows on all light types
	#pragma surface surf Standard fullforwardshadows

	// Use shader model 3.0 target, to get nicer looking lighting
	#pragma target 3.0

	sampler2D _MainTex, _Tex2, _Tex3;
	fixed _NormalScale;
	half _SizeX, _SizeY;

	struct Input {
		float3 worldPos;
		fixed3 worldNormal;
	};

	half _Glossiness;
	half _Metallic;
		
	void surf (Input IN, inout SurfaceOutputStandard o) {
		half2 scale = half2(_SizeX, _SizeY);
		// Albedo comes from a texture tinted by color
		float2 XY = IN.worldPos.xy / scale;
		float2 XZ = IN.worldPos.xz / scale;
		float2 YZ = IN.worldPos.yz / scale;

		fixed4 cXY = tex2D(_MainTex, XY);
		fixed4 cXZ = tex2D(_Tex2, XZ);
		fixed4 cYZ = tex2D(_Tex3, YZ);

		fixed3 projnormal = saturate(pow(IN.worldNormal, 4));

		half4 result = lerp(cXZ, cXY, projnormal.z);
		result = lerp(result, cYZ, projnormal.x);

		o.Albedo = result.rgb;
		// Metallic and smoothness come from slider variables
		o.Metallic = _Metallic;
		o.Smoothness = _Glossiness;
		o.Alpha = result.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

Esto ha sido todo por ahora, en el próximo blog espero poder hablar del shader de terrenos donde aplicaremos texturas triplanares y el efecto de nieve para completar la saga.


No siendo mas, nos vemos en el siguiente blog.

28 visualizaciones0 comentarios

Entradas Recientes

Ver todo
bottom of page