Terrain-Shader: Hex-Tiling + Makro-Variation statt Raster-Kachel (Büschel nicht mehr im Raster, keine Diagonalstreifen)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
slyphax 2026-10-25 09:42:40 +01:00
parent 99a06c6cb8
commit a1f386f5e6
2 changed files with 102 additions and 12 deletions

View file

@ -87,18 +87,48 @@ vec2 warp(vec2 g) {
return g + o * 0.9; // eine Oktave: Quest-Budget (8 Hash statt 16) return g + o * 0.9; // eine Oktave: Quest-Budget (8 Hash statt 16)
} }
// Anti-Wiederholung (Karte 25.10., Sly: „Texturen wiederholen sich zu krass“): // Anti-Wiederholung v2 (Karte 25.10., Sly: Büschel sitzen im Raster, diagonale
// Kachel 3 m statt 1 m; zweite Abtastung gedreht + 2,3× größer, ein // Streifen): Hex-Tiling (Mikkelsen/Heitz-Neyret): die Ebene wird in ein
// großflächiges Rauschen (~9 m) mischt beide, ein zweites (~30 m) variiert die // Dreiecksgitter geteilt, jede Ecke bekommt zufälligen Versatz + Drehung, die
// Helligkeit leicht. 2 Albedo-Fetches je Material statt 1. // 3 Abtastungen werden mit weichen Gewichten überblendet — kein Raster, keine
vec3 alb_anti(float id, vec2 wp) { // 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); float T = max(tile_size, 0.05);
vec2 uv1 = wp / T; vec2 st = wp / T * 1.7320508; // sqrt(3): Dreieckskante ≈ 0,58 Kachel (größer = weniger Mischzone)
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); vec2 sk = vec2(st.x - 0.57735027 * st.y, 1.15470054 * st.y);
vec3 a = texture(albedo_array, vec3(uv1, id)).rgb; vec2 base = floor(sk);
vec3 b = texture(albedo_array, vec3(uv2, id)).rgb; vec2 fr = fract(sk);
vec3 col = mix(a, b, smoothstep(0.35, 0.65, vnoise(wp * 0.11))); float tz = 1.0 - fr.x - fr.y;
return col * mix(0.88, 1.08, vnoise(wp * 0.035 + vec2(17.0, 3.0))); 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) { float mat_id(ivec2 p) {
@ -173,7 +203,13 @@ void fragment() {
float m1 = max(material_res - 1.0, 1.0); 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)); vec2 g = clamp(warp(clamp(map_uv, 0.0, 1.0) * m1), vec2(0.0), vec2(m1));
vec3 t2 = top2(g); 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) col = alb * col; // Tönung/Shading multipliziert (wie Client)
} }
// --- Konturlinien (unverändert, Karte t_3ebe4086) --- // --- Konturlinien (unverändert, Karte t_3ebe4086) ---

View file

@ -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 -- <prefix>
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)