diff --git a/src/viewport/terrain_contour.gdshader b/src/viewport/terrain_contour.gdshader index 909061e..ae8feb3 100644 --- a/src/viewport/terrain_contour.gdshader +++ b/src/viewport/terrain_contour.gdshader @@ -87,18 +87,48 @@ vec2 warp(vec2 g) { return g + o * 0.9; // eine Oktave: Quest-Budget (8 Hash statt 16) } -// Anti-Wiederholung (Karte 25.10., Sly: „Texturen wiederholen sich zu krass“): -// Kachel 3 m statt 1 m; zweite Abtastung gedreht + 2,3× größer, ein -// großflächiges Rauschen (~9 m) mischt beide, ein zweites (~30 m) variiert die -// Helligkeit leicht. 2 Albedo-Fetches je Material statt 1. -vec3 alb_anti(float id, vec2 wp) { +// 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. +vec3 alb_hex(float id, vec2 wp, vec2 gx, vec2 gy) { float T = max(tile_size, 0.05); - vec2 uv1 = wp / T; - vec2 uv2 = vec2(0.8196 * wp.x - 0.5729 * wp.y, 0.5729 * wp.x + 0.8196 * wp.y) / (T * 2.3) + vec2(0.37, 0.71); - vec3 a = texture(albedo_array, vec3(uv1, id)).rgb; - vec3 b = texture(albedo_array, vec3(uv2, id)).rgb; - vec3 col = mix(a, b, smoothstep(0.35, 0.65, vnoise(wp * 0.11))); - return col * mix(0.88, 1.08, vnoise(wp * 0.035 + vec2(17.0, 3.0))); + 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) / 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) { @@ -173,7 +203,13 @@ void fragment() { 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); - vec3 alb = mix(alb_anti(t2.y, world_pos.xz), alb_anti(t2.x, world_pos.xz), t2.z); + // 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) --- diff --git a/tests/probe_steppe_tiling.gd b/tests/probe_steppe_tiling.gd new file mode 100644 index 0000000..124fc96 --- /dev/null +++ b/tests/probe_steppe_tiling.gd @@ -0,0 +1,54 @@ +extends SceneTree +## Render-Probe Boden-Wiederholung (Steppe, 256 m flach, Sandpfad): Top-Down +## 60 m + flache Perspektive. Start (BRAUCHT Rendering): +## godot --path . --rendering-driver opengl3 -s res://tests/probe_steppe_tiling.gd -- + +func _initialize() -> void: + _run() + + +func _run() -> void: + await process_frame + await process_frame + var prefix := OS.get_cmdline_user_args()[0] if OS.get_cmdline_user_args().size() > 0 else "/tmp/steppe" + var size := 256.0 + var res := 129 + var mres := SvmapIO.material_resolution_for(res, size, size) + var heights := PackedFloat32Array() + heights.resize(res * res) + var mats := PackedByteArray() + mats.resize(mres * mres) + var mb := PackedByteArray() + mb.resize(mres * mres) + var mw := PackedByteArray() + mw.resize(mres * mres) + for i in 60: + var t := float(i) / 59.0 + TerrainField.apply_material_brush_mix(mats, mb, mw, mres, size, size, + -25.0 + 50.0 * t, -10.0 + 8.0 * sin(t * 3.0), 3.0, 1, 0.0) + var view := Node3D.new() + view.set_script(load("res://src/viewport/terrain_mesh.gd")) + root.add_child(view) + view.call("set_mix", mb, mw) + view.call("update_heights", heights, res, size, size, mats) + view.call("set_contours", false) + var cam := Camera3D.new() + root.add_child(cam) + cam.projection = Camera3D.PROJECTION_ORTHOGONAL + cam.size = 60.0 + cam.position = Vector3(0.0, 50.0, 10.01) + cam.look_at(Vector3(0, 0, 10.0), Vector3(0, 0, -1)) + cam.current = true + for i in 4: + await process_frame + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png(prefix + "_top.png") + cam.projection = Camera3D.PROJECTION_PERSPECTIVE + cam.fov = 70.0 + cam.position = Vector3(0.0, 6.0, 60.0) + cam.look_at(Vector3(0, 0, 20.0), Vector3.UP) + for i in 3: + await process_frame + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png(prefix + "_persp.png") + quit(0)