shader_type spatial; render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx; uniform float blend_factor : hint_range(0.1, 50.0) = 5.0; uniform float fade_range = 5.0; uniform float height_dirt = 5.0; uniform float height_grass = 15.0; uniform float height_mountain = 30.0; //uniform float roughness : hint_range(0.0, 1.0) = 1.0; //uniform float ao_light_affect : hint_range(0.0, 1.0); group_uniforms Dirt; uniform sampler2D dirt_albedo : source_color, repeat_enable, filter_linear_mipmap, hint_default_white; //uniform sampler2D dirt_orm : source_color; uniform float dirt_blend_sharpness = 1.0; uniform float dirt_uv_scale : hint_range(0.01, 10.0, 0.01) = 1.0; uniform float dirt_stochastic_strength : hint_range(0.0, 1.0) = 0; uniform float dirt_stochastic_rotation_max_angle : hint_range(0.0, 3.14159) = 0; group_uniforms Grass; uniform sampler2D grass_albedo : source_color, repeat_enable, filter_linear_mipmap, hint_default_white; //uniform sampler2D grass_orm : source_color; uniform float grass_blend_sharpness = 1.0; uniform float grass_uv_scale : hint_range(0.01, 10.0, 0.01) = 1.0; uniform float grass_stochastic_strength : hint_range(0.0, 1.0) = 0; uniform float grass_stochastic_rotation_max_angle : hint_range(0.0, 3.14159) = 0; group_uniforms Mountain; uniform sampler2D mountain_albedo : source_color, repeat_enable, filter_linear_mipmap, hint_default_white; //uniform sampler2D mountain_orm : source_color; uniform float mountain_blend_sharpness = 1.0; uniform float mountain_uv_scale : hint_range(0.01, 10.0, 0.01) = 1.0; uniform float mountain_stochastic_strength : hint_range(0.0, 1.0) = 0; uniform float mountain_stochastic_rotation_max_angle : hint_range(0.0, 3.14159) = 0; // ================================================================ // Varyings // Data passed from Vertex Shader to Fragment Shader // ================================================================ varying vec3 world_pos; // Original vertex position in world space varying vec3 triplanar_normal_abs; // Absolute world-space normal for triplanar blending weights varying mat3 tangent_2_local; // Simple hash function to generate pseudo-random numbers based on a 2D input. // Used for consistent random offsets per "tile" in stochastic sampling. float hash21(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); } mat2 rotate2d(float angle) { float s = sin(angle); float c = cos(angle); return mat2(vec2(c, s), vec2(-s, c)); } // Helper function to unpack a normal from 0-1 range (texture) to -1 to 1 range (vector). vec3 unpack_normal(vec3 packed_normal) { return packed_normal * 2.0 - 1.0; } // Helper function to pack a normal from -1 to 1 range (vector) to 0-1 range (for NORMAL_MAP output). vec3 pack_normal(vec3 unpacked_normal) { return unpacked_normal * 0.5 + 0.5; } vec2 rotate_uv(vec2 uv, float angle) { float s = sin(angle); float c = cos(angle); return mat2(vec2(c, -s), vec2(s, c)) * uv; } vec2 stochastic_uv(vec2 uv, float strength, float max_rotation_angle) { // Get the tile index based on floor of UV vec2 tile_index = floor(uv); // Use tile index to generate rotation and offset float angle = hash21(tile_index) * max_rotation_angle; vec2 offset = vec2(hash21(tile_index + 1.3), hash21(tile_index + 2.1)) - 0.5; // Apply rotation and offset within each tile vec2 local_uv = fract(uv) + offset * strength; local_uv = rotate_uv(local_uv - 0.5, angle) + 0.5; return local_uv; } // Triplanar texture sampling with 4-sample stochastic anti-tiling for RGB textures. // Includes random per-tile rotation and offset. // tex: The texture to sample. // p: Scaled world position (uv_scale * world_pos). // normal_abs: Absolute world normal for blending between X, Y, Z projections. // k: Triplanar blend factor (blend_factor). // strength: Stochastic displacement strength (stochastic_strength). // max_rotation_angle: Maximum random rotation angle (stochastic_rotation_max_angle). vec3 apply_triplanar_texture_stochastic_rgb(sampler2D tex, vec3 p, vec3 normal_abs, float k, float strength, float max_rotation_angle) { // Calculate base UVs for each projection plane vec2 uv_xy = p.xy; // For Z-axis projection (top-down) vec2 uv_yz = p.yz; // For X-axis projection (side) vec2 uv_xz = p.xz; // For Y-axis projection (side) // --- XY Plane Projection (Z-axis) --- // Get the integer part of UV for a consistent random seed per tile vec2 tile_id_xy = floor(uv_xy); // Generate random rotation angle for this tile float random_angle_xy = hash21(tile_id_xy) * max_rotation_angle; mat2 rot_mat_xy = rotate2d(random_angle_xy); // Generate two random offsets for displacement (ensure distinct hashes for x,y components) vec2 rand_offset_xy = vec2(hash21(tile_id_xy + vec2(0.1, 0.0)), hash21(tile_id_xy + vec2(0.0, 0.1))) * strength; // Calculate the 'base' sampling UV by applying rotation around tile center // and then adding the random offset. // This 'base_sampling_uv' is the reference point for the 4 samples and the blending. vec2 base_sampling_uv_xy = (uv_xy - tile_id_xy - vec2(0.5)) * rot_mat_xy + tile_id_xy + vec2(0.5) + rand_offset_xy; // Use the fractional part of this base sampling UV for the blending weights vec2 frac_uv_for_blend_xy = fract(base_sampling_uv_xy); float blend_x_xy = smoothstep(0.4, 0.6, frac_uv_for_blend_xy.x); float blend_y_xy = smoothstep(0.4, 0.6, frac_uv_for_blend_xy.y); // Sample the texture 4 times. The (0,0), (1,0), etc. offsets are relative to the // potentially rotated and offset grid. vec3 s00_xy = texture(tex, base_sampling_uv_xy).rgb; vec3 s10_xy = texture(tex, base_sampling_uv_xy + vec2(1.0, 0.0)).rgb; vec3 s01_xy = texture(tex, base_sampling_uv_xy + vec2(0.0, 1.0)).rgb; vec3 s11_xy = texture(tex, base_sampling_uv_xy + vec2(1.0, 1.0)).rgb; // Blend the 4 samples using bilinear interpolation based on fractional UV vec3 color_xy = mix(mix(s00_xy, s10_xy, blend_x_xy), mix(s01_xy, s11_xy, blend_x_xy), blend_y_xy); // --- YZ Plane Projection (X-axis) --- vec2 tile_id_yz = floor(uv_yz); float random_angle_yz = hash21(tile_id_yz) * max_rotation_angle; mat2 rot_mat_yz = rotate2d(random_angle_yz); vec2 rand_offset_yz = vec2(hash21(tile_id_yz + vec2(0.1, 0.0)), hash21(tile_id_yz + vec2(0.0, 0.1))) * strength; vec2 base_sampling_uv_yz = (uv_yz - tile_id_yz - vec2(0.5)) * rot_mat_yz + tile_id_yz + vec2(0.5) + rand_offset_yz; vec2 frac_uv_for_blend_yz = fract(base_sampling_uv_yz); float blend_x_yz = smoothstep(0.4, 0.6, frac_uv_for_blend_yz.x); float blend_y_yz = smoothstep(0.4, 0.6, frac_uv_for_blend_yz.y); vec3 s00_yz = texture(tex, base_sampling_uv_yz).rgb; vec3 s10_yz = texture(tex, base_sampling_uv_yz + vec2(1.0, 0.0)).rgb; vec3 s01_yz = texture(tex, base_sampling_uv_yz + vec2(0.0, 1.0)).rgb; vec3 s11_yz = texture(tex, base_sampling_uv_yz + vec2(1.0, 1.0)).rgb; vec3 color_yz = mix(mix(s00_yz, s10_yz, blend_x_yz), mix(s01_yz, s11_yz, blend_x_yz), blend_y_yz); // --- XZ Plane Projection (Y-axis) --- vec2 tile_id_xz = floor(uv_xz); float random_angle_xz = hash21(tile_id_xz) * max_rotation_angle; mat2 rot_mat_xz = rotate2d(random_angle_xz); vec2 rand_offset_xz = vec2(hash21(tile_id_xz + vec2(0.1, 0.0)), hash21(tile_id_xz + vec2(0.0, 0.1))) * strength; vec2 base_sampling_uv_xz = (uv_xz - tile_id_xz - vec2(0.5)) * rot_mat_xz + tile_id_xz + vec2(0.5) + rand_offset_xz; vec2 frac_uv_for_blend_xz = fract(base_sampling_uv_xz); float blend_x_xz = smoothstep(0.4, 0.6, frac_uv_for_blend_xz.x); float blend_y_xz = smoothstep(0.4, 0.6, frac_uv_for_blend_xz.y); vec3 s00_xz = texture(tex, base_sampling_uv_xz).rgb; vec3 s10_xz = texture(tex, base_sampling_uv_xz + vec2(1.0, 0.0)).rgb; vec3 s01_xz = texture(tex, base_sampling_uv_xz + vec2(0.0, 1.0)).rgb; vec3 s11_xz = texture(tex, base_sampling_uv_xz + vec2(1.0, 1.0)).rgb; vec3 color_xz = mix(mix(s00_xz, s10_xz, blend_x_xz), mix(s01_xz, s11_xz, blend_x_xz), blend_y_xz); // Calculate blend weights for triplanar projection itself (based on normal direction) vec3 blend_weights = pow(normal_abs, vec3(k)); blend_weights /= dot(blend_weights, vec3(1.0)); // Normalize weights to sum to 1 // Final triplanar blend: combine the three projected colors using the calculated weights return color_yz * blend_weights.x + color_xz * blend_weights.y + color_xy * blend_weights.z; } vec4 apply_triplanar_texture_stochastic_rgba( sampler2D tex, vec3 p, vec3 normal_abs, float k, float strength, float max_rotation_angle ) { vec2 uv_xy = p.xy; vec2 uv_yz = p.yz; vec2 uv_xz = p.xz; // XY vec2 tile_id_xy = floor(uv_xy); float random_angle_xy = hash21(tile_id_xy) * max_rotation_angle; mat2 rot_mat_xy = rotate2d(random_angle_xy); vec2 rand_offset_xy = vec2(hash21(tile_id_xy + vec2(0.1, 0.0)), hash21(tile_id_xy + vec2(0.0, 0.1))) * strength; vec2 base_uv_xy = (uv_xy - tile_id_xy - vec2(0.5)) * rot_mat_xy + tile_id_xy + vec2(0.5) + rand_offset_xy; vec2 frac_uv_xy = fract(base_uv_xy); float blend_x_xy = smoothstep(0.4, 0.6, frac_uv_xy.x); float blend_y_xy = smoothstep(0.4, 0.6, frac_uv_xy.y); vec4 s00_xy = texture(tex, base_uv_xy); vec4 s10_xy = texture(tex, base_uv_xy + vec2(1.0, 0.0)); vec4 s01_xy = texture(tex, base_uv_xy + vec2(0.0, 1.0)); vec4 s11_xy = texture(tex, base_uv_xy + vec2(1.0, 1.0)); vec4 color_xy = mix(mix(s00_xy, s10_xy, blend_x_xy), mix(s01_xy, s11_xy, blend_x_xy), blend_y_xy); // YZ vec2 tile_id_yz = floor(uv_yz); float random_angle_yz = hash21(tile_id_yz) * max_rotation_angle; mat2 rot_mat_yz = rotate2d(random_angle_yz); vec2 rand_offset_yz = vec2(hash21(tile_id_yz + vec2(0.1, 0.0)), hash21(tile_id_yz + vec2(0.0, 0.1))) * strength; vec2 base_uv_yz = (uv_yz - tile_id_yz - vec2(0.5)) * rot_mat_yz + tile_id_yz + vec2(0.5) + rand_offset_yz; vec2 frac_uv_yz = fract(base_uv_yz); float blend_x_yz = smoothstep(0.4, 0.6, frac_uv_yz.x); float blend_y_yz = smoothstep(0.4, 0.6, frac_uv_yz.y); vec4 s00_yz = texture(tex, base_uv_yz); vec4 s10_yz = texture(tex, base_uv_yz + vec2(1.0, 0.0)); vec4 s01_yz = texture(tex, base_uv_yz + vec2(0.0, 1.0)); vec4 s11_yz = texture(tex, base_uv_yz + vec2(1.0, 1.0)); vec4 color_yz = mix(mix(s00_yz, s10_yz, blend_x_yz), mix(s01_yz, s11_yz, blend_x_yz), blend_y_yz); // XZ vec2 tile_id_xz = floor(uv_xz); float random_angle_xz = hash21(tile_id_xz) * max_rotation_angle; mat2 rot_mat_xz = rotate2d(random_angle_xz); vec2 rand_offset_xz = vec2(hash21(tile_id_xz + vec2(0.1, 0.0)), hash21(tile_id_xz + vec2(0.0, 0.1))) * strength; vec2 base_uv_xz = (uv_xz - tile_id_xz - vec2(0.5)) * rot_mat_xz + tile_id_xz + vec2(0.5) + rand_offset_xz; vec2 frac_uv_xz = fract(base_uv_xz); float blend_x_xz = smoothstep(0.4, 0.6, frac_uv_xz.x); float blend_y_xz = smoothstep(0.4, 0.6, frac_uv_xz.y); vec4 s00_xz = texture(tex, base_uv_xz); vec4 s10_xz = texture(tex, base_uv_xz + vec2(1.0, 0.0)); vec4 s01_xz = texture(tex, base_uv_xz + vec2(0.0, 1.0)); vec4 s11_xz = texture(tex, base_uv_xz + vec2(1.0, 1.0)); vec4 color_xz = mix(mix(s00_xz, s10_xz, blend_x_xz), mix(s01_xz, s11_xz, blend_x_xz), blend_y_xz); // Triplanar blending with control over blending strength vec3 weights = pow(normal_abs, vec3(k)); weights /= max(dot(weights, vec3(1.0)), 0.0001); return color_yz * weights.x + color_xz * weights.y + color_xy * weights.z; } vec3 apply_triplanar_texture_stochastic_normal( sampler2D normal_map, vec3 pos, vec3 normal_abs, float blend_sharpness, float strength, float max_rotation_angle ) { // === Weights === vec3 weights = pow(normal_abs, vec3(blend_sharpness)); weights /= max(dot(weights, vec3(1.0)), 0.0001); // === Projected stochastic UVs === vec2 uv_yz = stochastic_uv(pos.yz, strength, max_rotation_angle); vec2 uv_xz = stochastic_uv(pos.xz, strength, max_rotation_angle); vec2 uv_xy = stochastic_uv(pos.xy, strength, max_rotation_angle); // === Sample and unpack normal maps === vec3 n_yz = unpack_normal(texture(normal_map, uv_yz).rgb); vec3 n_xz = unpack_normal(texture(normal_map, uv_xz).rgb); vec3 n_xy = unpack_normal(texture(normal_map, uv_xy).rgb); n_yz *= tangent_2_local; n_xz *= tangent_2_local; n_xy *= tangent_2_local; // === Reorient normals from tangent space to world-space projection axis === // YZ projection (X facing) → local Z = +X vec3 world_n_yz = vec3(n_yz.z, n_yz.x, n_yz.y); // XZ projection (Y facing) → local Z = +Y vec3 world_n_xz = vec3(n_xz.x, n_xz.z, n_xz.y); // XY projection (Z facing) → local Z = +Z vec3 world_n_xy = vec3(n_xy.x, n_xy.y, n_xy.z); // === Blend and normalize === vec3 blended = normalize( world_n_yz * weights.x + world_n_xz * weights.y + world_n_xy * weights.z ); return blended; } // ================================================================ // Vertex Shader // Prepares world-space position and absolute normal for triplanar mapping // ================================================================ void vertex() { world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; triplanar_normal_abs = abs(NORMAL); TANGENT = vec3(0.0,0.0,-1.0) * abs(triplanar_normal_abs.x); TANGENT += vec3(1.0,0.0,0.0) * abs(triplanar_normal_abs.y); TANGENT += vec3(1.0,0.0,0.0) * abs(triplanar_normal_abs.z); TANGENT = normalize(TANGENT); BINORMAL = vec3(0.0,1.0,0.0) * abs(triplanar_normal_abs.x); BINORMAL += vec3(0.0,0.0,-1.0) * abs(triplanar_normal_abs.y); BINORMAL += vec3(0.0,1.0,0.0) * abs(triplanar_normal_abs.z); BINORMAL = normalize(BINORMAL); tangent_2_local = MODEL_NORMAL_MATRIX * mat3(TANGENT, BINORMAL, NORMAL); //normalize all components, so the UVs don't get scaled when scaling mesh tangent_2_local[0] = normalize(tangent_2_local[0]); tangent_2_local[1] = normalize(tangent_2_local[1]); tangent_2_local[2] = normalize(tangent_2_local[2]); } // ================================================================ // Fragment Shader // Calculates final material properties // ================================================================ void fragment() { float height = world_pos.y; float blend_dirt = 1.0 - smoothstep(height_dirt, height_dirt + fade_range, height); float blend_grass = smoothstep(height_dirt, height_grass, height) * (1.0 - smoothstep(height_grass, height_mountain, height)); float blend_mountain = smoothstep(height_grass, height_mountain, height); //Apply blend sharpness blend_dirt = pow(blend_dirt, dirt_blend_sharpness); blend_grass = pow(blend_grass, grass_blend_sharpness); blend_mountain = pow(blend_mountain, mountain_blend_sharpness); // Normalize weights (if necessary — this makes sure they add up to 1.0) float total = blend_dirt + blend_grass + blend_mountain; blend_dirt /= total; blend_grass /= total; blend_mountain /= total; vec3 albedo_dirt = apply_triplanar_texture_stochastic_rgb( dirt_albedo, world_pos * dirt_uv_scale, triplanar_normal_abs, blend_factor, dirt_stochastic_strength, dirt_stochastic_rotation_max_angle ); vec3 albedo_grass = apply_triplanar_texture_stochastic_rgb( grass_albedo, world_pos * grass_uv_scale, triplanar_normal_abs, blend_factor, grass_stochastic_strength, grass_stochastic_rotation_max_angle ); vec3 albedo_mountain = apply_triplanar_texture_stochastic_rgb( mountain_albedo, world_pos * mountain_uv_scale, triplanar_normal_abs, blend_factor, mountain_stochastic_strength, mountain_stochastic_rotation_max_angle ); //vec3 orm_dirt = texture(dirt_orm, UV * dirt_uv_scale).rgb; //vec3 orm_grass = texture(grass_orm, UV * grass_uv_scale).rgb; //vec3 orm_mountain = texture(mountain_orm, UV * mountain_uv_scale).rgb; // //vec3 mixed_orm = mix(orm_dirt, orm_grass, blend_factor); //mixed_orm = mix(mixed_orm, orm_mountain, blend_factor); // ALBEDO = albedo_dirt * blend_dirt + albedo_grass * blend_grass + albedo_mountain * blend_mountain; //AO = mixed_orm.r; //AO_LIGHT_AFFECT = ao_light_affect; //ROUGHNESS = mixed_orm.g * roughness; //METALLIC = mixed_orm.b; }