Apply multiple "octaves" of textures/noise to avoid repetition.
(You can read more about a related technique as used for generating procedural terrains here: https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-1-generating-complex-procedural-terrains-using-gpu - particularly "1.3.3 Making an Interesting Density Function" which mentions other ways to break up repetition like warping coordinates for higher octaves)
But the key takeaway is that you blend multiple copies of the (texture|noise) at different scales with varying weights.
As an example, you might pick (pseudocode)
color = texture_at(x, y) * .6 + texture_at(x/5, y/5) * .2 + texture_at(x/10, y/10) * .1 + texture_at(x/50, y/50) * .1; That way, when you're up close, the detail is clearly visible and the lower amplitude blurs of lower octaves are barely visible, whilst at longer range, the lower frequency features -that span multiple tiles- tend to dominate.
Obviously you can tweak the number of samples and relative weighting to get the effect you want.
A separate technique I've used elsewhere is to use the normal of the surface to blend between three textures [Triplanar texturing].
This was quite some time ago, so excuse the vagueness but...
Properties{ //_MainTex ("Albedo (RGB)", 2D) = "white" {} _TopWeighting("Vertical Falloff", Range(0.1, 10)) = 5 _UpTex("Top", 2D) = "red" {} _SideTex("Side", 2D) = "green" {} _DownTex("Bottom", 2D) = "blue" {} } // .... struct Input { float3 worldPos : SV_POSITION; float3 worldNormal; float3 pos; }; void vert(inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.pos = mul(_Object2World, v.vertex); } // .... void surf(Input IN, inout SurfaceOutputStandard o) { // Sample the various textures we're using fixed4 up = tex2D(_UpTex, fmod(IN.pos.xz * _UpTex_ST, 1)); fixed4 sidex = tex2D(_SideTex, IN.pos.yz * _SideTex_ST); fixed4 sidez = tex2D(_SideTex, IN.pos.xy * _SideTex_ST); fixed4 down = tex2D(_DownTex, IN.pos.xz * _DownTex_ST); // Work out if we're doing the top or bottom float top = clamp(IN.worldNormal.y * 100000, 0, 1); // Work out how much weight should be given to the various textures float3 blending = abs(float3(IN.worldNormal.x / _TopWeighting, IN.worldNormal.y, IN.worldNormal.z / _TopWeighting)); // Force weights to sum to 1.0 blending = normalize(max(blending, 0.00001)); // scale the various weights proportionally float b = (blending.x + blending.y + blending.z); blending /= float3(b, b, b); // Combine the textures with the appropriate weighting float4 tex = sidex * blending.x + (top) * up * blending.y + (1-top) * down * blending.y + sidez * blending.z; o.Albedo = tex; // Metallic and smoothness aren't currently used, // set them to some neutral values o.Metallic = 0; o.Smoothness = .1; o.Alpha = 0; } A separate technique I've used elsewhere is to apply multiple "octaves" of textures/noise to avoid repetition.
(You can read more about a related technique as used for generating procedural terrains here: https://developer.nvidia.com/gpugems/gpugems3/part-i-geometry/chapter-1-generating-complex-procedural-terrains-using-gpu - particularly "1.3.3 Making an Interesting Density Function" which mentions other ways to break up repetition like warping coordinates for higher octaves)
But the key takeaway is that you blend multiple copies of the (texture|noise) at different scales with varying weights.
As an example, you might pick (pseudocode)
color = texture_at(x, y) * .6 + texture_at(x/5, y/5) * .2 + texture_at(x/10, y/10) * .1 + texture_at(x/50, y/50) * .1; That way, when you're up close, the detail is clearly visible and the lower amplitude blurs of lower octaves are barely visible, whilst at longer range, the lower frequency features -that span multiple tiles- tend to dominate.
Obviously you can tweak the number of samples and relative weighting to get the effect you want.