Procedural Terrain Texture Blending Shader

Inspired by the Seamless texture sampler without repeating patterns / tiling.

– Multi-Texture Support : Seamlessly blend up to 2 complete texture sets (albedo, normal, roughness, AO, height maps)

– Advanced Noise-Based Blending : Intelligent texture mixing using procedural noise patterns

– Superior Interpolation : Replaces simple smoothstep with quintic Hermite interpolation for ultra-smooth transitions

– Multi-Layer Domain Warping : Employs multiple layers of domain distortion noise for natural, organic texture distribution

– Fractal Noise Mixing : Combines multiple noise octaves to eliminate any trace of repetitive patterns

– High-Quality Hash Functions : Uses advanced mathematical constants and prime offsets to ensure truly random sampling

– Flexible Configuration : Adjustable texture scaling, noise intensity, blend sharpness, and UV rotation parameters

 

– Perlin-style gradient noise with complete grid artifact elimination

– Non-integer frequency sampling (1.31, 1.73, 2.17) to break periodicity

– Three-layer domain warping with independent offset vectors

– Smart texture validation and transparent handling for disabled textures

– Optimized performance with intelligent mipmap usage

Shader code
// Floor material shader - Supports dual texture blending and anti-tiling techniques
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;


// First texture set
uniform sampler2D texture1_albedo : source_color, filter_linear_mipmap, repeat_enable;      // Albedo map
uniform sampler2D texture1_normal : hint_normal, filter_linear_mipmap, repeat_enable;        // Normal map
uniform sampler2D texture1_roughness : hint_default_white, filter_linear_mipmap, repeat_enable; // Roughness map
uniform sampler2D texture1_ao : hint_default_white, filter_linear_mipmap, repeat_enable;     // Ambient occlusion map
uniform sampler2D texture1_height : hint_default_black, filter_linear_mipmap, repeat_enable; // Height map


// Second texture set
uniform sampler2D texture2_albedo : source_color, filter_linear_mipmap, repeat_enable;      // Albedo map
uniform sampler2D texture2_normal : hint_normal, filter_linear_mipmap, repeat_enable;        // Normal map
uniform sampler2D texture2_roughness : hint_default_white, filter_linear_mipmap, repeat_enable; // Roughness map
uniform sampler2D texture2_ao : hint_default_white, filter_linear_mipmap, repeat_enable;     // Ambient occlusion map
uniform sampler2D texture2_height : hint_default_black, filter_linear_mipmap, repeat_enable; // Height map




// Noise texture for blend control
uniform sampler2D noise_texture : filter_linear_mipmap, repeat_enable;

// Anti-tiling noise texture
uniform sampler2D tile_noise : source_color, filter_linear_mipmap, repeat_enable;


// Mathematical constants - Used for generating non-periodic noise
const float GOLDEN_RATIO = 1.618033988749;   // Golden ratio
const float SQRT2_PLUS_1 = 2.414213562373;   // √2 + 1
const float MY_E = 2.718281828459;           // Natural constant e
const float SQRT18 = 4.242640687119;         // √18
const float SQRT32 = 5.656854249492;         // √32


// Prime offsets - Used to avoid noise pattern repetition
const vec2 PRIME_OFFSET_1 = vec2(127.1, 311.7);
const vec2 PRIME_OFFSET_2 = vec2(269.5, 183.3);
const vec2 PRIME_OFFSET_3 = vec2(419.2, 371.9);
const vec2 PRIME_OFFSET_4 = vec2(73.2, 157.8);
const vec2 PRIME_OFFSET_5 = vec2(191.3, 271.9);
const vec2 PRIME_OFFSET_6 = vec2(317.5, 421.7);


// Shader configuration constants
const int MIN_VALID_TEXTURE_SIZE = 4;     // Minimum valid texture size
const float MIN_WEIGHT_THRESHOLD = 0.001; // Minimum weight threshold
const float DEFAULT_DEPTH_SCALE = 0.1;    // Default depth scale


// Basic parameters
uniform float texture_scale : hint_range(0.1, 10.0) = 1.0;   // Texture scale
uniform float noise_scale : hint_range(0.1, 5.0) = 1.0;      // Noise scale
uniform float blend_sharpness : hint_range(0.1, 5.0) = 1.0;  // Blend sharpness
uniform float uv_rotation : hint_range(0.0, 360.0) = 0.0;    // UV rotation angle
uniform float tile_offset : hint_range(0.0, 5.0) = 2.0;      // Anti-tiling offset


// Noise parameters group
group_uniforms noise_parameters;
uniform float hash_multiplier : hint_range(0.05, 0.2) = 0.1031;        // Hash multiplier
uniform float hash_offset : hint_range(20.0, 50.0) = 33.33;             // Hash offset
uniform float noise_frequency_1 : hint_range(1.0, 3.0) = 1.618;         // Noise frequency 1
uniform float noise_frequency_2 : hint_range(2.0, 4.0) = 2.414;         // Noise frequency 2
uniform float noise_frequency_3 : hint_range(3.0, 5.0) = 3.141;         // Noise frequency 3
uniform float noise_strength_1 : hint_range(0.05, 0.3) = 0.15;          // Noise strength 1
uniform float noise_strength_2 : hint_range(0.05, 0.3) = 0.12;          // Noise strength 2
uniform float noise_strength_3 : hint_range(0.05, 0.3) = 0.09;          // Noise strength 3
uniform float fbm_frequency_multiplier : hint_range(1.5, 3.0) = 2.07;   // FBM frequency multiplier
group_uniforms;


// Blending parameters group
group_uniforms blending_parameters;
uniform float blend_threshold_low : hint_range(0.0, 0.5) = 0.2;         // Blend threshold lower bound
uniform float blend_threshold_high : hint_range(0.5, 1.0) = 0.8;        // Blend threshold upper bound
uniform float smoothing_intensity_1 : hint_range(0.01, 0.1) = 0.02;     // Smoothing intensity 1
uniform float smoothing_intensity_2 : hint_range(0.01, 0.1) = 0.05;     // Smoothing intensity 2
uniform float smoothing_intensity_3 : hint_range(0.01, 0.2) = 0.1;      // Smoothing intensity 3
uniform float weight_threshold : hint_range(0.0001, 0.01) = 0.001;      // Weight threshold
uniform float weight_epsilon : hint_range(0.0001, 0.01) = 0.001;        // Weight epsilon
uniform float default_albedo : hint_range(0.0, 1.0) = 0.5;              // Default albedo
uniform float default_roughness : hint_range(0.0, 1.0) = 1.0;           // Default roughness
uniform float default_ao : hint_range(0.0, 1.0) = 1.0;                  // Default ambient occlusion
uniform float default_threshold : hint_range(0.01, 0.5) = 0.1;          // Default threshold
group_uniforms;


// Texture sampling parameters group
group_uniforms texture_sampling;
uniform int texture_size_threshold : hint_range(1, 16) = 4;              // Texture size threshold
uniform float tile_noise_scale_1 : hint_range(0.001, 0.01) = 0.003;     // Tile noise scale 1
uniform float tile_noise_scale_2 : hint_range(0.005, 0.015) = 0.007;    // Tile noise scale 2
uniform float tile_noise_scale_3 : hint_range(0.008, 0.02) = 0.011;     // Tile noise scale 3
uniform float tile_randomness_range : hint_range(8.0, 32.0) = 16.0;     // Tile randomness range
uniform float depth_scale : hint_range(0.01, 0.5) = 0.1;                // Depth scale
group_uniforms;


// Anti-tiling constants parameters group
group_uniforms anti_tiling_constants;
uniform float hash_prime_1 : hint_range(10.0, 20.0) = 12.9898;          // Hash prime 1
uniform float hash_prime_2 : hint_range(70.0, 90.0) = 78.233;           // Hash prime 2
uniform float hash_offset_1 : hint_range(40000.0, 50000.0) = 43758.5453; // Hash offset 1
uniform float hash_offset_2 : hint_range(25000.0, 35000.0) = 28001.8384; // Hash offset 2
uniform float extra_hash_prime_1 : hint_range(6.0, 10.0) = 7.13;        // Extra hash prime 1
uniform float extra_hash_prime_2 : hint_range(10.0, 15.0) = 11.17;      // Extra hash prime 2
uniform float extra_hash_offset_1 : hint_range(120.0, 140.0) = 127.1;   // Extra hash offset 1
uniform float extra_hash_offset_2 : hint_range(300.0, 320.0) = 311.7;   // Extra hash offset 2
uniform vec2 tile_sample_offset_1 = vec2(0.31, 0.47);                   // Tile sample offset 1
uniform vec2 tile_sample_offset_2 = vec2(0.73, 0.19);                   // Tile sample offset 2
uniform float blend_smoothstep_low : hint_range(0.05, 0.2) = 0.1;       // Blend smoothstep lower bound
uniform float blend_smoothstep_high : hint_range(0.8, 0.95) = 0.9;      // Blend smoothstep upper bound
uniform float blend_variation_factor : hint_range(0.1, 0.3) = 0.15;     // Blend variation factor
uniform float extra_offset_multiplier : hint_range(0.3, 0.7) = 0.5;     // Extra offset multiplier
group_uniforms;


// Domain warp parameters group
group_uniforms domain_warp;
uniform float domain_warp_freq_1 : hint_range(1.0, 2.0) = 1.3;          // Domain warp frequency 1
uniform float domain_warp_freq_2 : hint_range(1.5, 2.5) = 1.7;          // Domain warp frequency 2
uniform float domain_warp_freq_3 : hint_range(2.0, 3.0) = 2.1;          // Domain warp frequency 3
uniform float domain_warp_freq_4 : hint_range(2.0, 3.0) = 2.3;          // Domain warp frequency 4
uniform float domain_warp_freq_5 : hint_range(4.0, 6.0) = 4.7;          // Domain warp frequency 5
uniform float domain_warp_freq_6 : hint_range(4.5, 6.5) = 5.1;          // Domain warp frequency 6
uniform float domain_warp_strength_2 : hint_range(0.3, 0.8) = 0.6;      // Domain warp strength 2
uniform float domain_warp_strength_3 : hint_range(0.1, 0.5) = 0.3;      // Domain warp strength 3
group_uniforms;




// Hash function - Generate pseudo-random numbers
float hash(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * hash_multiplier);
    p3 += dot(p3, p3.yzx + hash_offset);
    return fract((p3.x + p3.y) * p3.z);
}


// Quintic interpolation function - Provides smooth transitions
float quintic(float t) {
    return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}


// Value noise function - Grid-based noise generation
float noise(vec2 uv) {
    vec2 i = floor(uv);  // Grid coordinates
    vec2 f = fract(uv);  // Offset within grid
    
    // Get random values for four corners
    float a = hash(i);
    float b = hash(i + vec2(1.0, 0.0));
    float c = hash(i + vec2(0.0, 1.0));
    float d = hash(i + vec2(1.0, 1.0));
    
    // Use quintic interpolation for smoothing
    vec2 u = vec2(quintic(f.x), quintic(f.y));
    
    // Bilinear interpolation
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}


// Gradient noise function - Gradient vector-based noise generation
float gradient_noise(vec2 uv) {
    vec2 i = floor(uv);  // Grid coordinates
    vec2 f = fract(uv);  // Offset within grid
    
    // Generate gradient vectors for four corners
    vec2 g00 = normalize(vec2(hash(i) - 0.5, hash(i + vec2(0.1, 0.1)) - 0.5));
    vec2 g10 = normalize(vec2(hash(i + vec2(1.0, 0.0)) - 0.5, hash(i + vec2(1.1, 0.1)) - 0.5));
    vec2 g01 = normalize(vec2(hash(i + vec2(0.0, 1.0)) - 0.5, hash(i + vec2(0.1, 1.1)) - 0.5));
    vec2 g11 = normalize(vec2(hash(i + vec2(1.0, 1.0)) - 0.5, hash(i + vec2(1.1, 1.1)) - 0.5));
    
    // Calculate dot product of gradient and distance vectors
    float n00 = dot(g00, f);
    float n10 = dot(g10, f - vec2(1.0, 0.0));
    float n01 = dot(g01, f - vec2(0.0, 1.0));
    float n11 = dot(g11, f - vec2(1.0, 1.0));
    
    // Use quintic interpolation for smoothing
    vec2 u = vec2(quintic(f.x), quintic(f.y));
    
    return mix(mix(n00, n10, u.x), mix(n01, n11, u.x), u.y) * 0.5 + 0.5;
}


// Fractal Brownian Motion - Multi-layer noise stacking
float fbm(vec2 uv, int octaves) {
    float value = 0.0;
    float amplitude = 0.5;
    float frequency = 1.0;
    
    for (int i = 0; i < octaves; i++) {
        // Blend value noise and gradient noise
        float n1 = noise(uv * frequency);
        float n2 = gradient_noise(uv * frequency + vec2(100.0, 200.0));
        value += amplitude * mix(n1, n2, 0.6);
        amplitude *= 0.5;
        frequency *= fbm_frequency_multiplier; // Use non-integer frequency multiplier to reduce periodicity
    }
    
    return value;
}


// Domain warp noise - Create complex noise patterns through multi-layer warping
float domain_warp_noise(vec2 uv, float strength) {
    // First layer warp
    vec2 warp1 = vec2(
        noise(uv * domain_warp_freq_1 + PRIME_OFFSET_1),
        noise(uv * domain_warp_freq_2 + PRIME_OFFSET_2)
    ) * strength;
    
    // Second layer warp (based on first layer)
    vec2 warp2 = vec2(
        gradient_noise(uv * domain_warp_freq_3 + warp1 + PRIME_OFFSET_3),
        gradient_noise(uv * domain_warp_freq_4 + warp1 + PRIME_OFFSET_1)
    ) * strength * domain_warp_strength_2;
    
    // Third layer warp (based on previous two layers)
    vec2 warp3 = vec2(
        noise(uv * domain_warp_freq_5 + warp1 + warp2 + PRIME_OFFSET_2),
        noise(uv * domain_warp_freq_6 + warp1 + warp2 + PRIME_OFFSET_3)
    ) * strength * domain_warp_strength_3;
    
    // Apply all warp layers to generate final noise
    return gradient_noise(uv + warp1 + warp2 + warp3);
}


// UV rotation function - Rotate UV coordinates around center point
vec2 rotate_uv(vec2 uv, float angle) {
    float rad = radians(angle);
    float cos_a = cos(rad);
    float sin_a = sin(rad);
    mat2 rotation_matrix = mat2(vec2(cos_a, -sin_a), vec2(sin_a, cos_a));
    return rotation_matrix * (uv - 0.5) + 0.5;
}


// Get varied UV coordinates - Apply scaling and rotation
vec2 get_varied_uv(vec2 base_uv, float scale_mult, float rotation_offset) {
    vec2 scaled_uv = base_uv * texture_scale * scale_mult;
    return rotate_uv(scaled_uv, uv_rotation + rotation_offset);
}


// Check if texture is valid - Avoid using invalid or placeholder textures
bool is_texture_valid(sampler2D tex) {
    // Sample texture center point
    vec4 sample_color = texture(tex, vec2(0.5, 0.5));
    
    // Check if pure white or pure black (usually placeholders)
    bool is_white = all(equal(sample_color.rgb, vec3(1.0)));
    bool is_black = all(equal(sample_color.rgb, vec3(0.0)));
    
    // Check if texture size is too small
    ivec2 tex_size = textureSize(tex, 0);
    bool is_small = tex_size.x <= texture_size_threshold && tex_size.y <= texture_size_threshold;
    
    // If texture is small and solid color, consider it invalid
    return !(is_small && (is_white || is_black));
}


// Anti-tiling texture sampling (RGB) - Eliminate texture repetition patterns
vec3 textureNoTile(sampler2D tex, vec2 uv, float v) {
    // Get random values from noise texture
    float k1 = texture(tile_noise, tile_noise_scale_1 * uv).x;
    float k2 = texture(tile_noise, tile_noise_scale_2 * uv + tile_sample_offset_1).y;
    float k3 = texture(tile_noise, tile_noise_scale_3 * uv + tile_sample_offset_2).z;
    float k = (k1 + k2 + k3) / 3.0;
    
    // Calculate UV derivatives for texture filtering
    vec2 duvdx = dFdx(uv);
    vec2 duvdy = dFdy(uv);
    
    // Generate random indices
    float l = k * tile_randomness_range;
    float f = fract(l);
    
    float ia = floor(l);
    float ib = ia + 1.0;
    
    // Calculate two random offsets
    vec2 offa = normalize(sin(vec2(ia * hash_prime_1, ia * hash_prime_2) + vec2(hash_offset_1, hash_offset_2))) * v;
    vec2 offb = normalize(sin(vec2(ib * hash_prime_1, ib * hash_prime_2) + vec2(hash_offset_1, hash_offset_2))) * v;
    
    // Add extra random offsets
    vec2 extra_offset_a = sin(vec2(ia * extra_hash_prime_1, ia * extra_hash_prime_2) + vec2(extra_hash_offset_1, extra_hash_offset_2)) * v * extra_offset_multiplier;
    vec2 extra_offset_b = sin(vec2(ib * extra_hash_prime_1, ib * extra_hash_prime_2) + vec2(extra_hash_offset_1, extra_hash_offset_2)) * v * extra_offset_multiplier;
    
    // Sample textures at two offset positions
    vec3 cola = textureGrad(tex, uv + offa + extra_offset_a, duvdx, duvdy).xyz;
    vec3 colb = textureGrad(tex, uv + offb + extra_offset_b, duvdx, duvdy).xyz;
    
    // Calculate blend factor based on color differences
    vec3 ab = cola - colb;
    float sum_ab = abs(ab.x) + abs(ab.y) + abs(ab.z);
    float blend_factor = smoothstep(blend_smoothstep_low, blend_smoothstep_high, f - blend_variation_factor * sum_ab);
    
    return mix(cola, colb, blend_factor);
}


// Anti-tiling texture sampling (single channel) - For roughness, AO and other single-channel maps
float textureNoTileSingle(sampler2D tex, vec2 uv, float v) {
    // Get random values from noise texture
    float k1 = texture(tile_noise, tile_noise_scale_1 * uv).x;
    float k2 = texture(tile_noise, tile_noise_scale_2 * uv + tile_sample_offset_1).y;
    float k3 = texture(tile_noise, tile_noise_scale_3 * uv + tile_sample_offset_2).z;
    float k = (k1 + k2 + k3) / 3.0;
    
    // Calculate UV derivatives for texture filtering
    vec2 duvdx = dFdx(uv);
    vec2 duvdy = dFdy(uv);
    
    // Generate random indices
    float l = k * tile_randomness_range;
    float f = fract(l);
    
    float ia = floor(l);
    float ib = ia + 1.0;
    
    // Calculate two random offsets
    vec2 offa = normalize(sin(vec2(ia * hash_prime_1, ia * hash_prime_2) + vec2(hash_offset_1, hash_offset_2))) * v;
    vec2 offb = normalize(sin(vec2(ib * hash_prime_1, ib * hash_prime_2) + vec2(hash_offset_1, hash_offset_2))) * v;
    
    // Add extra random offsets
    vec2 extra_offset_a = sin(vec2(ia * extra_hash_prime_1, ia * extra_hash_prime_2) + vec2(extra_hash_offset_1, extra_hash_offset_2)) * v * extra_offset_multiplier;
    vec2 extra_offset_b = sin(vec2(ib * extra_hash_prime_1, ib * extra_hash_prime_2) + vec2(extra_hash_offset_1, extra_hash_offset_2)) * v * extra_offset_multiplier;
    
    // Sample textures at two offset positions (red channel only)
    float cola = textureGrad(tex, uv + offa + extra_offset_a, duvdx, duvdy).r;
    float colb = textureGrad(tex, uv + offb + extra_offset_b, duvdx, duvdy).r;
    
    // Calculate blend factor based on value differences
    float ab = cola - colb;
    float blend_factor = smoothstep(blend_smoothstep_low, blend_smoothstep_high, f - blend_variation_factor * abs(ab));
    
    return mix(cola, colb, blend_factor);
}

// Fragment shader main function
void fragment() {
    vec2 base_uv = UV;
    
    // Generate different UV coordinates for two texture sets
    vec2 uv1 = get_varied_uv(base_uv, 1.0, 0.0);    // First texture set UV
    vec2 uv2 = get_varied_uv(base_uv, 1.3, 45.0);   // Second texture set UV (slightly larger and rotated)
    
    // Noise UV coordinates
    vec2 noise_uv = base_uv * noise_scale;
    
    // Generate multi-layer domain warp noise
    float domain_noise1 = domain_warp_noise(noise_uv * noise_frequency_1, noise_strength_1);
    float domain_noise2 = domain_warp_noise(noise_uv * noise_frequency_2 + PRIME_OFFSET_1, noise_strength_2);
    float domain_noise3 = domain_warp_noise(noise_uv * noise_frequency_3 + PRIME_OFFSET_2, noise_strength_3);
    
    // Generate detail noise layers
    float detail_noise1 = gradient_noise(noise_uv * MY_E + PRIME_OFFSET_4);
    float detail_noise2 = noise(noise_uv * SQRT18 + PRIME_OFFSET_5);
    float detail_noise3 = gradient_noise(noise_uv * SQRT32 + PRIME_OFFSET_6);
    
    // Sample from noise texture
    float noise_val = texture(noise_texture, noise_uv * GOLDEN_RATIO).r;
    
    // Combine all noise layers
    float base_noise = (domain_noise1 * 0.45 + domain_noise2 * 0.35 + domain_noise3 * 0.2);
    float detail_blend = (detail_noise1 * 0.4 + detail_noise2 * 0.35 + detail_noise3 * 0.25);
    float combined_noise = (base_noise * 0.6 + detail_blend * 0.3 + noise_val * 0.1);
    
    // Multi-layer smoothing to enhance contrast
    combined_noise = smoothstep(0.02, 0.98, combined_noise);
    combined_noise = smoothstep(0.05, 0.95, combined_noise);
    combined_noise = smoothstep(0.1, 0.9, combined_noise);
    

    // Check texture validity
    bool texture1_valid = is_texture_valid(texture1_albedo);
    bool texture2_valid = is_texture_valid(texture2_albedo);
    
    // Count valid textures
    int enabled_count = 0;
    if (texture1_valid) enabled_count++;
    if (texture2_valid) enabled_count++;
    
    // Calculate texture blend weights
    float weight1 = 0.0;
    float weight2 = 0.0;
    
    if (enabled_count == 1) {
        // When only one valid texture, use that texture
        if (texture1_valid) weight1 = 1.0;
        else if (texture2_valid) weight2 = 1.0;
    } else if (enabled_count == 2) {
        // When both textures are valid, blend based on noise
        float smooth_noise = smoothstep(0.2, 0.8, combined_noise);
        // Apply blend sharpness
        smooth_noise = pow(smooth_noise, blend_sharpness);
        
        weight1 = 1.0 - smooth_noise;
        weight2 = smooth_noise;
    }
    
    // Ensure invalid texture weights are 0
    if (!texture1_valid) weight1 = 0.0;
    if (!texture2_valid) weight2 = 0.0;
    
    // Calculate total weight
    float total_weight = weight1 + weight2;
    
    // Weight normalization
    if (total_weight <= weight_threshold) {
        weight1 = 0.0;
        weight2 = 0.0;
        total_weight = 0.0;
    } else {
        // Normalize weights
        weight1 /= total_weight;
        weight2 /= total_weight;
    }
    

    // Initialize texture sampling results
    vec4 albedo1 = vec4(0.0);
    vec4 albedo2 = vec4(0.0);
    vec3 normal1 = vec3(0.5, 0.5, 1.0);  // Default normal
    vec3 normal2 = vec3(0.5, 0.5, 1.0);
    float roughness1 = 1.0;              // Default roughness
    float roughness2 = 1.0;
    float ao1 = 1.0;                     // Default AO
    float ao2 = 1.0;
    float height1 = 0.0;                 // Default height
    float height2 = 0.0;
    
    // Sample first texture set (if valid)
    if (texture1_valid) {
        albedo1 = vec4(textureNoTile(texture1_albedo, uv1, tile_offset), texture(texture1_albedo, uv1).a);
        normal1 = textureNoTile(texture1_normal, uv1, tile_offset);
        roughness1 = textureNoTileSingle(texture1_roughness, uv1, tile_offset);
        ao1 = textureNoTileSingle(texture1_ao, uv1, tile_offset);
        height1 = textureNoTileSingle(texture1_height, uv1, tile_offset);
    }
    
    // Sample second texture set (if valid)
    if (texture2_valid) {
        albedo2 = vec4(textureNoTile(texture2_albedo, uv2, tile_offset), texture(texture2_albedo, uv2).a);
        normal2 = textureNoTile(texture2_normal, uv2, tile_offset);
        roughness2 = textureNoTileSingle(texture2_roughness, uv2, tile_offset);
        ao2 = textureNoTileSingle(texture2_ao, uv2, tile_offset);
        height2 = textureNoTileSingle(texture2_height, uv2, tile_offset);
    }
    

    // Blend all material properties
    vec4 final_albedo = albedo1 * weight1 + albedo2 * weight2;
    vec3 final_normal = normal1 * weight1 + normal2 * weight2;
    float final_roughness = roughness1 * weight1 + roughness2 * weight2;
    float final_ao = ao1 * weight1 + ao2 * weight2;
    float final_height = height1 * weight1 + height2 * weight2;
    
    // Output final results
    if (enabled_count == 0 || total_weight <= weight_threshold) {
        // Use default values when no valid textures
        ALBEDO = vec3(0.0, 0.0, 0.0);
        ALPHA = 0.0;
        NORMAL_MAP = vec3(0.5, 0.5, 1.0);
        ROUGHNESS = 1.0;
        AO = 1.0;
    } else {
        // Validate and correct final values
        if (length(final_normal) < 0.1) {
            final_normal = vec3(0.5, 0.5, 1.0);
        }
        if (final_ao < 0.1) {
            final_ao = 1.0;
        }
        
        // Set material outputs
        ALBEDO = final_albedo.rgb;
        ALPHA = final_albedo.a;
        NORMAL_MAP = final_normal;
        ROUGHNESS = final_roughness;
        AO = final_ao;
    }
    
    // Set depth offset (parallax effect)
    DEPTH = final_height * depth_scale;
}
Live Preview
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Saurygiel
Saurygiel
3 months ago

Hey awesome shader, works 99% of the way for me, but I had two major issues with it that I can’t seem to resolve properly.

  1. The terrain mesh does not render in the proper order unless I comment out the depth offset code (Line 496), even with trying every combination of culling and depth checking and blending option.
  2. Even if I fix that, the terrain mesh also has zero ambient occlusion received from other meshes. The best result I got managed to get AO to be received by other meshes for intersecting the terrain mesh but the terrain mesh itself still does not.

I tried fixing this in my own project as well as the demo project with lots of trial and error and trying to do research online but I’m not that well versed in shader code so I’ve given up on it for now. The shader you took inspiration from works fine, so it has to do with the implementation of combining both textures causing a loss of information in the final result.

If anyone knows how to get this to work properly I would really appreciate the help.