237 lines
11 KiB
Text
237 lines
11 KiB
Text
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;
|
||
}
|