shader_type spatial; render_mode diffuse_burley, specular_schlick_ggx; // Luneraria STONE: moonstone ashlar and basalt for the strategic buildings (G2), meant to be // reused by G4/G5. Limestone texture sampled triplanar in object space, procedural block // courses with per-block tint, baked occlusion from COLOR (vertex colour Col), bump from the // texture and the joints, lichen on up-facing ledges, damage cracks that can leak team energy, // and a dithered occlusion fade above a height so units behind a building stay readable. uniform vec4 base_color : source_color = vec4(.78, .74, .66, 1.0); uniform sampler2D stone_map : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform float course_height = .42; uniform float block_length = .78; uniform float joint_depth = .55; uniform float tint_variation = .09; uniform float lichen = .25; uniform float roughness_base = .86; uniform float texture_scale = .9; uniform bool ashlar = true; // Per building (instance) state. instance uniform float damage : instance_index(0) = 0.0; instance uniform vec4 tint : source_color, instance_index(4) = vec4(.5, .75, 1.0, 1.0); instance uniform float crack_glow : instance_index(8) = 0.0; instance uniform float fade : instance_index(1) = 0.0; instance uniform float fade_floor : instance_index(2) = 2.2; instance uniform float seed : instance_index(3) = 0.0; instance uniform float scorch : instance_index(9) = 0.0; varying vec3 local_pos; varying vec3 local_normal; varying vec3 world_pos; #include "res://scripts/luneraria/view/materials/lune_common.gdshaderinc" // Distance to the nearest Voronoi edge in 2D (for cracks) and the cell id. vec2 voronoi_edge(vec2 x) { vec2 n = floor(x), f = fract(x); vec2 mg, mr; float md = 8.0; for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) { vec2 g = vec2(float(i), float(j)); vec2 o = vec2(hash12(n + g), hash12(n + g + 17.3)); vec2 r = g + o - f; float d = dot(r, r); if (d < md) { md = d; mr = r; mg = g; } } md = 8.0; for (int j = -2; j <= 2; j++) for (int i = -2; i <= 2; i++) { vec2 g = mg + vec2(float(i), float(j)); vec2 o = vec2(hash12(n + g), hash12(n + g + 17.3)); vec2 r = g + o - f; if (dot(mr - r, mr - r) > .00001) md = min(md, dot(.5 * (mr + r), normalize(r - mr))); } return vec2(md, hash12(n + mg)); } void vertex() { local_pos = VERTEX; local_normal = NORMAL; world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; } // Block pattern on the face that dominates the normal: returns joint mask (1 in a joint) and a block id. vec2 blocks(vec3 p, vec3 n) { vec3 w = abs(n); float row = floor(p.y / course_height); float along; if (w.y > .72) { // Horizontal faces: paving slabs. vec2 cell = p.xz / vec2(block_length * .9, block_length * .7); cell.x += .5 * mod(floor(cell.y), 2.0); vec2 f = abs(fract(cell) - .5); float joint = smoothstep(.43, .49, max(f.x, f.y)); return vec2(joint, hash12(floor(cell) + seed)); } // Vertical faces: courses; run along the tangent (angle around the local axis for towers). float radius = length(p.xz); along = radius > .15 ? atan(p.z, p.x) * radius : (w.x > w.z ? p.z : p.x); float shifted = along / block_length + .5 * mod(row, 2.0) + hash12(vec2(row, seed)) * .3; float fy = abs(fract(p.y / course_height) - .5); float fx = abs(fract(shifted) - .5); float joint = max(smoothstep(.44, .49, fy), smoothstep(.465, .495, fx)); return vec2(joint, hash12(vec2(floor(shifted), row) + seed * 1.7)); } void fragment() { float occlusion = COLOR.r; // Occlusion fade: screen-door dithering above the fade floor (world height). if (PROJECTION_MATRIX[3][3] < .5 && faded(fade, fade_floor, world_pos.y, FRAGCOORD.xy)) discard; vec3 n = normalize(local_normal); vec3 w = pow(abs(n), vec3(4.0)); w /= max(dot(w, vec3(1.0)), .0001); vec3 p = local_pos * texture_scale; vec3 tex = texture(stone_map, p.zy).rgb * w.x + texture(stone_map, p.xz).rgb * w.y + texture(stone_map, p.xy).rgb * w.z; float lum = dot(tex, vec3(.333)); vec2 b = ashlar ? blocks(local_pos, n) : vec2(0.0, hash13(floor(local_pos * 1.3) + seed)); float joint = b.x * joint_depth; float block_tint = (b.y - .5) * 2.0 * tint_variation; float broad = noise3(local_pos * .9 + seed); vec3 color = base_color.rgb * (.72 + lum * .62) * (1.0 + block_tint) * mix(.92, 1.06, broad); // Warm/cool drift per block keeps large faces from looking like plastic. color *= mix(vec3(1.02, 1.0, .96), vec3(.97, .99, 1.03), b.y); color *= 1.0 - joint * .55; // Lichen and dust gather on ledges that face up. float up = smoothstep(.55, .95, n.y); float moss = up * lichen * smoothstep(.45, .75, noise3(local_pos * 2.3 + seed * 3.1)); color = mix(color, vec3(.33, .37, .22) * (.7 + lum * .5), moss * .55); // Soot from fire and damage. color *= 1.0 - scorch * smoothstep(.3, .8, noise3(local_pos * 1.7 + 4.0)) * .55; // Cracks: Voronoi edges in the dominant plane, growing with damage. float crack = 0.0; if (damage > .01) { vec2 cp = (w.y > .6 ? local_pos.xz : (w.x > w.z ? local_pos.zy : local_pos.xy)) * 1.35 + seed * 3.7; vec2 v = voronoi_edge(cp); float width = mix(.006, .032, damage) * (.5 + noise3(local_pos * 3.0 + seed)); // Only some Voronoi edges crack; more of them as damage grows. float reach = step(1.0 - damage * (w.y > .6 ? .45 : .75), fract(v.y * 7.13 + seed)) * smoothstep(.35, .6, noise3(local_pos * .8 + seed * 2.0) + damage * .3); crack = (1.0 - smoothstep(width * .4, width, v.x)) * reach; color *= 1.0 - crack * .62; color *= 1.0 - damage * .1; } ALBEDO = color * mix(.55, 1.0, occlusion); ROUGHNESS = clamp(roughness_base + (1.0 - lum) * .08 + joint * .08 - moss * .05, .45, .98); SPECULAR = .35; AO = occlusion; AO_LIGHT_AFFECT = .35; EMISSION = tint.rgb * crack * crack_glow * (.6 + .4 * sin(TIME * 3.0 + local_pos.y * 2.0)); // Bump from texture luminance and the joints. float height = lum * .6 - joint * .9 - crack * .8; vec3 dpdx = dFdx(VERTEX), dpdy = dFdy(VERTEX); vec3 rx = cross(dpdy, NORMAL), ry = cross(NORMAL, dpdx); float det = dot(dpdx, rx); vec3 grad = dFdx(height) * rx + dFdy(height) * ry; NORMAL = normalize(abs(det) * NORMAL - sign(det) * grad * .022); }