Shader - Texture Array with Blendmap Neighbour textures bleeding

I am creating a mobile game with a nice ground model. The model is somewhat large (in scale), but only few (less than 200) polygons.

We do not plan on using the build-in unity terrain due it is recommanded not to on mobile development, and we can get build better looking 3D model with fewer polygons ourself.

We need a texture on this big model and kinda stumpled into a problem. Either we put a humongus texture on the ground, and can get all the details we want, or we put a tileing texture on the ground . The humongus texture is not really ideal, we need it to be something like 16kx16k for it to somewhat look good. So the humongus texture is out of the question.

The tile texture have the problem we cannot add any walking visual paths, it is only one tiling texture.

In Unity terrain it is possible to use paint on the terrain and add visual paths this way.

After looking around I have found this solution. And in general using splatmaps.

The best shader for this is in the above link:

Shader "Texture Array with Blendmap" {

	Properties
	{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_BlendTex("Blend (RGB)", 2D) = "red" {}

	}

		Category{
		Fog{ Color[_AddFog] }

		// ------------------------------------------------------------------
		// ARB fragment program

		SubShader
		{
			Pass
			{
				Tags{ "LightMode" = "Always" }

				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag 

				#include "UnityCG.cginc" 

				struct v2f {
					float4 pos : POSITION;
					float2 uv_diffuse: TEXCOORD0;
					float2 uv_blend : TEXCOORD1;
				};

					float4 _MainTex_ST, _BlendTex_ST; 

					v2f vert(appdata_base v)
					{ 
						v2f o;
						o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
						o.uv_diffuse = TRANSFORM_TEX(v.texcoord, _MainTex);
						o.uv_blend = TRANSFORM_TEX(v.texcoord, _BlendTex);
						return o;
					}


					uniform sampler2D _MainTex;
					uniform sampler2D _BlendTex;

					float4 frag(v2f i) : COLOR
					{
						half4 blend = tex2D(_BlendTex, i.uv_blend);

						//TODO: Scale and offset sampling area so it is one pixel in from all
						//      sides - this will prevent bleeding from neighbour textures.
						//      That will become more difficult for lower mips, though.
						float2 sampleScaled = float2(frac(i.uv_diffuse.x) * 0.5, frac(i.uv_diffuse.y) * 0.5);

						half4 array_00 = tex2D(_MainTex, sampleScaled);
						half4 array_01 = tex2D(_MainTex, sampleScaled + float2(0.0, 0.5));
						half4 array_10 = tex2D(_MainTex, sampleScaled + float2(0.5, 0.0));

						half4 color = array_00 * blend.r + array_01 * blend.g + array_10 * blend.b;

						return color;

					}
			ENDCG
		}
	}
	}
		FallBack "VertexLit"
}

I have found 2 problems with this shader:

  1. The problem is this shader “bleeds” one texture into another at the edges, and I don’t know how to fix it.

  2. It only allows for 3 tileing subtextures, 4 would be prefered

Thanks

I came up with this anwser. The tiling should have a few pixels on either side as a “buffer” pixels which then makes sure it doesn’t bleed. I have not yet tested with 4 sub tiletextures due I haven’t a texture with a alpha map.

EDIT: Updated the shader

  • The Shader is now a surface shader for better lighting support.
  • The Alpha channel “kinda” works, if you make the control/blend texture part transparent it works as intended.
  • Added a Margin to fix the “bleeding” between textures

Shader:

Shader "Custom/Splatmap_Surface" {
	Properties {
		_MainTex ("Albedo (RGB)", 2D) = "white" {}
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_BlendTex("Blend (RGB)", 2D) = "red" {}
		_Margin ("Margin", Range(0,1)) = 0.05
	}
	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;
		sampler2D _BlendTex;

		struct Input {
			float2 uv_MainTex;
			float2 uv_BlendTex;
			float3 worldNormal;
			float3 worldPos;
			float4 color;
		};

		half _Glossiness;
		half _Margin;

		void surf (Input IN, inout SurfaceOutputStandard o) {

			// Metallic and smoothness come from slider variables
			o.Smoothness = _Glossiness;
			
			const float halfMargin = _Margin / 2;
			
			fixed4 tex_00 = tex2D(_MainTex, (frac(IN.uv_MainTex) * (0.5f - _Margin)) + float2(0 + halfMargin , 0 + halfMargin), ddx(IN.uv_MainTex), ddy(IN.uv_MainTex));
            fixed4 tex_10 = tex2D(_MainTex, (frac(IN.uv_MainTex) * (0.5f - _Margin)) + float2(0 + halfMargin , 0.5 + halfMargin ), ddx(IN.uv_MainTex), ddy(IN.uv_MainTex));
            fixed4 tex_01 = tex2D(_MainTex, (frac(IN.uv_MainTex) * (0.5f - _Margin)) + float2(0.5 + halfMargin , 0 + halfMargin), ddx(IN.uv_MainTex), ddy(IN.uv_MainTex));
			fixed4 tex_11 = tex2D(_MainTex, (frac(IN.uv_MainTex) * (0.5f - _Margin)) + float2(0.5 + halfMargin, 0.5 + halfMargin), ddx(IN.uv_MainTex), ddy(IN.uv_MainTex));
			
			half4 blend = tex2D(_BlendTex, IN.uv_BlendTex); 
			fixed4 c = tex_00 * blend.r + tex_01 * blend.g + tex_10 * blend.b + tex_11 * abs(1 - blend.a);
			
            o.Albedo = c.rgb;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

EDIT2:
Sadly the above shader did not have a acceptable performance. I actually ended up not using one big texture containing 4 sub tiletextures, but one texture per tile texture.
This shader preforms way better than the above one

Shader "Custom/Splatmap_Surface" {
	Properties {
		_TileTextureR ("TileTexture R (RGB)", 2D) = "white" {}
		_TileTextureG ("TileTexture G (RGB)", 2D) = "white" {}
		_TileTextureB ("TileTexture B (RGB)", 2D) = "white" {}
		_TileTextureA ("TileTexture A (RGB)", 2D) = "white" {}
		_BlendTex("Blend (RGB)", 2D) = "red" {}
	}
	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 _TileTextureR;
		sampler2D _TileTextureG;
		sampler2D _TileTextureB;
		sampler2D _TileTextureA;
		sampler2D _BlendTex;

		struct Input {
			float2 uv_TileTextureR;
			float2 uv_TileTextureA;
			float2 uv_BlendTex;
		};

	

		void surf (Input IN, inout SurfaceOutputStandard o) 
		{
			fixed4 blend = tex2D(_BlendTex, IN.uv_BlendTex); 
			fixed4 c = 
				tex2D(_TileTextureR, IN.uv_TileTextureR) * blend.r + 
				tex2D(_TileTextureG, IN.uv_TileTextureR) * blend.g + 
				tex2D(_TileTextureB, IN.uv_TileTextureR) * blend.b + 
				tex2D(_TileTextureA, IN.uv_TileTextureA) * abs(1 - blend.a);
            o.Albedo = c.rgb;
		}
		ENDCG
	}
	FallBack "Diffuse"
}