saumwacht-godot/scripts/luneraria/view/materials/lune_stone.gdshader
Martin Stickelbroeck 9f62e30377 G2: Ancients, Türme, Bossgrotte und Superboss; macOS- und Windows-ARM64-Pakete
Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone,
Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem
Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik.
Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion
(Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit
Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen,
Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation.
Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen
und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle
braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone
mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe,
dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein,
Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade
ohne dithernde Schatten. Keine Regeländerung.

Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73,
Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38,
verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463.
21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung,
Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten).
Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64
und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11
ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:12:49 +02:00

135 lines
6 KiB
Text

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);
}