shader_type spatial; // Terrain-Konturlinien (EDITOR-ONLY, Karte t_3ebe4086): Höhenlinien alle 1 m // plus kräftigere 5-m-Indexlinie, per-pixel aus UV2.x (= Vertex-Höhe in m). // Der Quest-3-Perf-Vertrag (kein Custom-Shader) gilt fürs SPIEL — der Editor // darf teurer sein (DECISIONS.md-Konvention). // // Karte t_ab408127 (Terrain-Texturierung): der Editor rendert DIESELBEN // Texturen wie der Match-Client — Albedo/Normal-Arrays über die Vertex-Alpha // (Material-ID), Worldspace-Tiling. Konturlinien liegen OBENDRAUF (dunkle // Multiplikation auf der texturierten Fläche). Ohne Arrays (uniforms nicht // gesetzt, Fallback-Klon) bleibt es beim reinen Vertex-Farb-Look. // // Farbquelle = COLOR: RGB = gebackenes Relief-Shading (Höhen-Kurve × // Pseudo-Licht), A = Material-ID (m/N gepackt). Unshaded: das gebackene // Shading ist exakt sichtbar (deterministisch für die Render-Probe). render_mode unshaded, cull_back; // Linienstärken (0 = aus). Terrain-Mesh setzt linien_an auf 0, wenn die // Map flacher als 2 m ist (sonst wäre ALLES auf der 0-m-Linie dunkel). uniform float linien_an : hint_range(0.0, 1.0) = 1.0; uniform float linien_staerke : hint_range(0.0, 1.0) = 0.5; uniform float index_staerke : hint_range(0.0, 1.0) = 0.75; // Textur-Arrays (t_ab408127) — identische Semantik wie terrain_splat.gdshader // im Client. Nicht gesetzt → ausschalten (vertex_only-Modus). uniform sampler2DArray albedo_array : source_color, filter_linear_mipmap, repeat_enable; uniform float layer_count = 20.0; uniform float tile_size = 3.0; uniform float texturen_an = 0.0; // 1.0 = Texturen aktiv (vom Mesh-Code gesetzt) // Splat-Gewichtstextur (Karte t_7a0a6b79 — weiche Übergänge): je Texel die // Top-2-Zell-IDs + bilineare Gewichte, von TerrainArrays.build_splat_bytes // abgeleitet (Spiegel-Datei, identische Formel im Client). R=id0, G=w0, // B=id1, A=w1 (w0+w1=1). FILTER_NEAREST: IDs dürfen NICHT bilinear vermisch // werden — die Weichheit steckt IN den Gewichten, nicht in der Filterung. // Ohne source_color-Hint: raw-Bytes, id = round(kanal × 255). // Karte 25.10. (Sly: „immer noch harte Kanten“): statt der Splat-Textur (ein // Texel je Materialpunkt → beim feinen 1-m-Raster harte Pixelstufen) liest der // Shader die MATERIAL-ID-KARTE direkt (R8, ein Texel je Materialpunkt, // material_res²) und mischt PRO PIXEL bilinear zwischen den 4 umliegenden // Punkten — weiche Übergänge über genau einen Rasterabstand, egal wie fein. // 4 Albedo- + 4 Normal-Fetches + 4 texelFetch, keine Branches. uniform sampler2D material_map : filter_nearest, repeat_disable; // Misch-Schicht (Karte 25.10., weiche Pinselränder): je Punkt ein zweites // Material (b) und dessen Anteil (w, 0–255). Ohne Mischung: alles 0. uniform sampler2D material_b_map : filter_nearest, repeat_disable; uniform sampler2D material_w_map : filter_nearest, repeat_disable; uniform float material_res = 2.0; // Kantenlänge der Material-Karte (Punkte) uniform float map_size_x = 1.0; // Terrain-Kantenlänge in Weltmetern uniform float map_size_z = 1.0; varying vec3 world_pos; varying vec2 map_uv; void vertex() { world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; // UV 0..1 über die Map (Index 0 = −X/−Z-Ecke, konventionstreu zum Mesh-Bau). map_uv = vec2((world_pos.x + 0.5 * map_size_x) / max(map_size_x, 0.001), (world_pos.z + 0.5 * map_size_z) / max(map_size_z, 0.001)); } // Organische Kanten (Karte 25.10.): die Materialpunkte liegen auf einem // 1-m-Raster — ohne Störung zeichnet die bilineare Mischung dessen Treppen // nach. Ein leichter Domain-Warp (Value-Noise in Rasterzellen, ±0,45 Zelle) // verwackelt die Abtaststelle, die Kante wird unregelmäßig wie echter Boden. // Ganzzahl-Hash statt sin()-Hash (Karte 25.10.): sin() großer Argumente ist // auf der GPU ungenau — die Noise bekam gerade, rasterförmige Sprünge, die // als harte Kanten durch die Verläufe liefen (Sly-Befund, nah gezoomt). float hash2(vec2 p) { uvec2 q = uvec2(ivec2(p) + ivec2(4096)); uint h = q.x * 1597334677u ^ q.y * 3812015801u; h = (h ^ (h >> 16u)) * 2246822519u; h = h ^ (h >> 13u); return float(h & 65535u) / 65535.0; } float vnoise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash2(i), hash2(i + vec2(1.0, 0.0)), f.x), mix(hash2(i + vec2(0.0, 1.0)), hash2(i + vec2(1.0, 1.0)), f.x), f.y); } vec2 warp(vec2 g) { vec2 o = vec2(vnoise(g * 0.9), vnoise(g * 0.9 + vec2(17.3, 5.1))) - 0.5; return g + o * 0.9; // eine Oktave: Quest-Budget (8 Hash statt 16) } // Anti-Wiederholung v2 (Karte 25.10., Sly: Büschel sitzen im Raster, diagonale // Streifen): Hex-Tiling (Mikkelsen/Heitz-Neyret): die Ebene wird in ein // Dreiecksgitter geteilt, jede Ecke bekommt zufälligen Versatz + Drehung, die // 3 Abtastungen werden mit weichen Gewichten überblendet — kein Raster, keine // Achsen mehr, keine harten Nähte. Quest-Kosten: 3 Albedo-Fetches je Material // (nur 1 Material in ~allen Pixeln, die Mischzone nimmt 2). Danach Makro- // Variation: Helligkeit (~35 m) + Farbstich (~120 m), je 1 Value-Noise. uniform float detail_kontrast = 1.3; // >1 = Büschel/Steine kräftiger (Hex-Mischung glättet sonst) vec3 alb_hex(float id, vec2 wp, vec2 gx, vec2 gy) { float T = max(tile_size, 0.05); vec2 st = wp / T * 1.7320508; // sqrt(3): Dreieckskante ≈ 0,58 Kachel (größer = weniger Mischzone) vec2 sk = vec2(st.x - 0.57735027 * st.y, 1.15470054 * st.y); vec2 base = floor(sk); vec2 fr = fract(sk); float tz = 1.0 - fr.x - fr.y; float up = step(0.0, tz); // 1: unteres Dreieck, 0: oberes vec3 w = mix(vec3(-tz, 1.0 - fr.y, 1.0 - fr.x), vec3(tz, fr.y, fr.x), up); vec2 v1 = mix(base + vec2(1.0), base, up); vec2 v2 = mix(base + vec2(1.0, 0.0), base + vec2(0.0, 1.0), up); vec2 v3 = mix(base + vec2(0.0, 1.0), base + vec2(1.0, 0.0), up); w = w * w * w; // Mitte stärker, Nähte weicher w /= (w.x + w.y + w.z); float wn = sqrt(dot(w, w)); // für Kontrast-Erhalt (Varianz der Mischung) vec2 uv = wp / T; vec3 acc = vec3(0.0); for (int k = 0; k < 3; k++) { vec2 v = k == 0 ? v1 : (k == 1 ? v2 : v3); float wk = k == 0 ? w.x : (k == 1 ? w.y : w.z); float a = hash2(v + vec2(31.0, 7.0)) * 6.2831853; vec2 off = vec2(hash2(v), hash2(v + vec2(53.0, 11.0))); float c = cos(a); float s = sin(a); vec2 r = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y) + off; acc += wk * textureGrad(albedo_array, vec3(r, id), gx, gy).rgb; } // Kontrast-Erhalt: Mischen dreier Abtastungen glättet auf den Mittelwert → // Abweichung vom Mittelwert (grober Mip) durch ||w|| teilen. vec3 mean_c = textureLod(albedo_array, vec3(0.5, 0.5, id), 8.0).rgb; acc = mean_c + (acc - mean_c) * (detail_kontrast / max(wn, 0.577)); float bright = mix(0.86, 1.12, vnoise(wp * 0.028 + vec2(17.0, 3.0))); float tint = vnoise(wp * 0.0085 + vec2(5.0, 41.0)) - 0.5; vec3 col = acc * bright; return col * vec3(1.0 + 0.10 * tint, 1.0 + 0.03 * tint, 1.0 - 0.10 * tint); } float mat_id(ivec2 p) { return clamp(floor(texelFetch(material_map, p, 0).r * 255.0 + 0.5), 0.0, layer_count - 1.0); } float mat_b(ivec2 p) { return clamp(floor(texelFetch(material_b_map, p, 0).r * 255.0 + 0.5), 0.0, layer_count - 1.0); } // Top-2-Mischung (Karte 25.10.): 4 Nachbarpunkte × (a, b) = 8 Kandidaten mit // Gewicht (bilinear × Punkt-Anteil). Gleiche IDs werden aufsummiert, die // zwei stärksten Materialien gewinnen. Rückgabe: (id1, id2, Anteil id1). // Danach nur 2 Albedo- + 2 Normal-Fetches. vec3 top2(vec2 g) { vec2 gi = min(floor(g), vec2(max(material_res - 2.0, 0.0))); vec2 f = clamp(g - gi, 0.0, 1.0); ivec2 p = ivec2(gi); ivec2 q[4] = ivec2[4](p, p + ivec2(1, 0), p + ivec2(0, 1), p + ivec2(1, 1)); float cw[4] = float[4]((1.0 - f.x) * (1.0 - f.y), f.x * (1.0 - f.y), (1.0 - f.x) * f.y, f.x * f.y); float ids[8]; float ws[8]; for (int k = 0; k < 4; k++) { float wb = texelFetch(material_w_map, q[k], 0).r; ids[2 * k] = mat_id(q[k]); ws[2 * k] = cw[k] * (1.0 - wb); ids[2 * k + 1] = mat_b(q[k]); ws[2 * k + 1] = cw[k] * wb; } float id1 = ids[0]; float s1 = -1.0; for (int i = 0; i < 8; i++) { float s = 0.0; for (int j = 0; j < 8; j++) { s += ids[j] == ids[i] ? ws[j] : 0.0; } if (s > s1) { s1 = s; id1 = ids[i]; } } float id2 = id1; float s2 = 0.0; for (int i = 0; i < 8; i++) { if (ids[i] == id1) { continue; } float s = 0.0; for (int j = 0; j < 8; j++) { s += ids[j] == ids[i] ? ws[j] : 0.0; } if (s > s2) { s2 = s; id2 = ids[i]; } } return vec3(id1, id2, s1 / max(s1 + s2, 0.00001)); } void fragment() { vec3 col = COLOR.rgb; // --- Textur (t_ab408127): Material-ID aus Vertex-Alpha, Worldspace-UV --- // Editor-Vorschau ALBEDO-ONLY (der Editor-Viewport ist unshaded; eine // Normal-Map würde ohne Licht ohnehin nichts zeigen — die Relief-Optik // kommt aus dem gebackenen Hillshade in COLOR.rgb). if (texturen_an > 0.5) { // Top-2-Splat-Blend (t_7a0a6b79): die Weichheit steckt in den Gewich- // ten der Splat-Textur — an Zellgrenzen rampen die IDs weich (bilinear // über die 4 umliegenden Zellen), im Flachbereich w0=1 (reines id0). // Karte 25.10.: Top-2 aus 4 Nachbarpunkten inkl. Misch-Schicht // (Spiegel zum Client-Shader terrain_splat.gdshader). float m1 = max(material_res - 1.0, 1.0); vec2 g = clamp(warp(clamp(map_uv, 0.0, 1.0) * m1), vec2(0.0), vec2(m1)); vec3 t2 = top2(g); // Ableitungen VOR dem Branch (Mip-Wahl bleibt gültig, textureGrad). vec2 guv = world_pos.xz / max(tile_size, 0.05); vec2 gx = dFdx(guv); vec2 gy = dFdy(guv); // Reiner Boden (Anteil ≥ 0,999, Normalfall): nur 3 statt 6 Fetches. vec3 alb = t2.z > 0.999 ? alb_hex(t2.x, world_pos.xz, gx, gy) : mix(alb_hex(t2.y, world_pos.xz, gx, gy), alb_hex(t2.x, world_pos.xz, gx, gy), t2.z); col = alb * col; // Tönung/Shading multipliziert (wie Client) } // --- Konturlinien (unverändert, Karte t_3ebe4086) --- float h = UV2.x; // Flachflächen-Schutz: liegt eine GANZE Platte exakt auf einer Meter-Marke // (z. B. Tal = 0.0 m, Plateau = 5.0 m), wäre jedes Pixel „auf der Linie“ // → die Fläche würde komplett dunkel übermalt. Deshalb: Linienstärke an // der lokalen Steigung dämpfen — exakt flach (dh = 0) → keine Linie, // sanfte bis steile Hänge → volle Linie. float dh = max(abs(dFdx(h)), abs(dFdy(h))); float relief = smoothstep(0.0, 0.0005, dh); // Abstand (in m) zur nächsten ganzen Meter-Marke: float d1 = abs(fract(h + 0.5) - 0.5); float w1 = fwidth(h) * 1.2; // Linie ~1–2 px breit, auflösungsstabil float linie = 1.0 - smoothstep(w1 * 0.5, w1 * 1.5, d1); // 5-m-Indexlinie (Abstand in m zur nächsten 5er-Marke): float d5 = abs(fract(h / 5.0 + 0.5) - 0.5) * 5.0; float w5 = fwidth(h) * 2.2; float linie5 = 1.0 - smoothstep(w5 * 0.5, w5 * 1.5, d5); col = mix(col, col * 0.55, linie * linien_an * linien_staerke * relief); col = mix(col, col * 0.32, linie5 * linien_an * index_staerke * relief); ALBEDO = col; }