scopedvr-map-core/viewport/terrain_contour.gdshader
2026-11-01 19:31:33 +01:00

237 lines
11 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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