Terrain-Malen: weiche, organische Kanten statt 1-m-Pixelblöcke
Sly 25.10.: „immer noch harte Kanten beim Malen“. Ursache: die Splat-Textur hatte beim feinen 1-m-Raster genau einen Texel je Materialpunkt und wird nearest gesampelt → jede Zelle ein harter Block. - Shader liest die Material-ID-Karte (R8, ein Texel je Punkt, texelFetch) und mischt PRO PIXEL bilinear über die 4 Nachbarpunkte. - Leichter Domain-Warp (Value-Noise in Rasterzellen, eine Oktave) verwackelt die Abtaststelle → keine Rastertreppen, natürliche Kante. - Malen aktualisiert nur noch die Bytes der Karte (kein Splat-Neuberechnen). - TerrainArrays.material_image (Spiegel-Datei, identisch im Client). - Render-Probe tests/probe_mal_weich_render.gd (vorher/nachher geprüft). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69668eb250
commit
40ca5ab6ba
4 changed files with 124 additions and 38 deletions
|
|
@ -206,3 +206,16 @@ static func decode_splat_texel(bytes: PackedByteArray, texel_ofs: int) -> Array:
|
||||||
int(bytes.decode_u8(texel_ofs + 2)),
|
int(bytes.decode_u8(texel_ofs + 2)),
|
||||||
float(bytes.decode_u8(texel_ofs + 3)) / 255.0,
|
float(bytes.decode_u8(texel_ofs + 3)) / 255.0,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Material-ID-Karte für den Shader (Karte 25.10., weiche Übergänge pro Pixel):
|
||||||
|
## R8, ein Texel je Materialpunkt (mres²) — der Shader liest per texelFetch die
|
||||||
|
## 4 umliegenden IDs und mischt bilinear. Ersetzt im Rendering die Splat-
|
||||||
|
## Ableitung (build_splat_bytes bleibt für Probes/Tests). Unpassende Schicht →
|
||||||
|
## 2×2 alles Gras (Shader bleibt nie ohne Textur).
|
||||||
|
static func material_image(mats: PackedByteArray, mres: int) -> Image:
|
||||||
|
if mres < 2 or mats.size() != mres * mres:
|
||||||
|
var leer := PackedByteArray()
|
||||||
|
leer.resize(4)
|
||||||
|
return Image.create_from_data(2, 2, false, Image.FORMAT_R8, leer)
|
||||||
|
return Image.create_from_data(mres, mres, false, Image.FORMAT_R8, mats)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,14 @@ uniform float texturen_an = 0.0; // 1.0 = Texturen aktiv (vom Mesh-Code gesetz
|
||||||
// B=id1, A=w1 (w0+w1=1). FILTER_NEAREST: IDs dürfen NICHT bilinear vermisch
|
// 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.
|
// werden — die Weichheit steckt IN den Gewichten, nicht in der Filterung.
|
||||||
// Ohne source_color-Hint: raw-Bytes, id = round(kanal × 255).
|
// Ohne source_color-Hint: raw-Bytes, id = round(kanal × 255).
|
||||||
uniform sampler2D splat_map : filter_nearest, repeat_disable;
|
// 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;
|
||||||
|
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_x = 1.0; // Terrain-Kantenlänge in Weltmetern
|
||||||
uniform float map_size_z = 1.0;
|
uniform float map_size_z = 1.0;
|
||||||
|
|
||||||
|
|
@ -48,6 +55,31 @@ void vertex() {
|
||||||
(world_pos.z + 0.5 * map_size_z) / max(map_size_z, 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.
|
||||||
|
float hash2(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
float mat_id(ivec2 p) {
|
||||||
|
return clamp(floor(texelFetch(material_map, p, 0).r * 255.0 + 0.5), 0.0, layer_count - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec3 col = COLOR.rgb;
|
vec3 col = COLOR.rgb;
|
||||||
// --- Textur (t_ab408127): Material-ID aus Vertex-Alpha, Worldspace-UV ---
|
// --- Textur (t_ab408127): Material-ID aus Vertex-Alpha, Worldspace-UV ---
|
||||||
|
|
@ -58,13 +90,19 @@ void fragment() {
|
||||||
// Top-2-Splat-Blend (t_7a0a6b79): die Weichheit steckt in den Gewich-
|
// Top-2-Splat-Blend (t_7a0a6b79): die Weichheit steckt in den Gewich-
|
||||||
// ten der Splat-Textur — an Zellgrenzen rampen die IDs weich (bilinear
|
// ten der Splat-Textur — an Zellgrenzen rampen die IDs weich (bilinear
|
||||||
// über die 4 umliegenden Zellen), im Flachbereich w0=1 (reines id0).
|
// über die 4 umliegenden Zellen), im Flachbereich w0=1 (reines id0).
|
||||||
vec4 sp = texture(splat_map, map_uv);
|
// Karte 25.10.: bilinear pro Pixel über die 4 Materialpunkte (Spiegel
|
||||||
float id0 = clamp(floor(sp.r * 255.0 + 0.5), 0.0, layer_count - 1.0);
|
// zum Client-Shader terrain_splat.gdshader).
|
||||||
float id1 = clamp(floor(sp.b * 255.0 + 0.5), 0.0, layer_count - 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 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);
|
||||||
vec2 uv = world_pos.xz / max(tile_size, 0.05);
|
vec2 uv = world_pos.xz / max(tile_size, 0.05);
|
||||||
vec4 alb0 = texture(albedo_array, vec3(uv, id0));
|
vec3 alb = texture(albedo_array, vec3(uv, mat_id(p))).rgb * (1.0 - f.x) * (1.0 - f.y)
|
||||||
vec4 alb1 = texture(albedo_array, vec3(uv, id1));
|
+ texture(albedo_array, vec3(uv, mat_id(p + ivec2(1, 0)))).rgb * f.x * (1.0 - f.y)
|
||||||
col = mix(alb1.rgb, alb0.rgb, sp.g) * col; // Tönung/Shading multipliziert (wie Client)
|
+ texture(albedo_array, vec3(uv, mat_id(p + ivec2(0, 1)))).rgb * (1.0 - f.x) * f.y
|
||||||
|
+ texture(albedo_array, vec3(uv, mat_id(p + ivec2(1, 1)))).rgb * f.x * f.y;
|
||||||
|
col = alb * col; // Tönung/Shading multipliziert (wie Client)
|
||||||
}
|
}
|
||||||
// --- Konturlinien (unverändert, Karte t_3ebe4086) ---
|
// --- Konturlinien (unverändert, Karte t_3ebe4086) ---
|
||||||
float h = UV2.x;
|
float h = UV2.x;
|
||||||
|
|
|
||||||
|
|
@ -318,9 +318,12 @@ func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0
|
||||||
var shm := mat as ShaderMaterial
|
var shm := mat as ShaderMaterial
|
||||||
shm.set_shader_parameter("map_size_x", size_x)
|
shm.set_shader_parameter("map_size_x", size_x)
|
||||||
shm.set_shader_parameter("map_size_z", size_z)
|
shm.set_shader_parameter("map_size_z", size_z)
|
||||||
if splat_rect_cells.size.x > 0 and _splat_tex != null and _splat_res == mres \
|
# Material-ID-Karte (Karte 25.10.): Malen = gleiche Größe → nur Daten
|
||||||
and _splat_side == TerrainArrays.splat_side_for(mres, maxf(size_x, size_z)):
|
# tauschen (1 Upload); sonst neu anlegen. splat_rect_cells bleibt als
|
||||||
_refresh_splat_region(splat_rect_cells)
|
# Parameter (Aufrufer-Vertrag), wird aber nicht mehr gebraucht.
|
||||||
|
if _splat_tex != null and _splat_res == mres and mats.size() == mres * mres:
|
||||||
|
_splat_image.set_data(mres, mres, false, Image.FORMAT_R8, mats)
|
||||||
|
_splat_tex.update(_splat_image)
|
||||||
else:
|
else:
|
||||||
_build_splat(shm, mats, mres, size_x, size_z)
|
_build_splat(shm, mats, mres, size_x, size_z)
|
||||||
|
|
||||||
|
|
@ -393,39 +396,18 @@ func resolution() -> int:
|
||||||
## size_x/size_z (t_5c94d098): Karten-Kantenlänge in Metern → Texel-Dichte der
|
## size_x/size_z (t_5c94d098): Karten-Kantenlänge in Metern → Texel-Dichte der
|
||||||
## Splat (1 m/Texel bei 1024 m; vorher 2 m/Texel = sichtbare Stufen).
|
## Splat (1 m/Texel bei 1024 m; vorher 2 m/Texel = sichtbare Stufen).
|
||||||
func _build_splat(mat: ShaderMaterial, mats: PackedByteArray, res: int,
|
func _build_splat(mat: ShaderMaterial, mats: PackedByteArray, res: int,
|
||||||
size_x: float = 0.0, size_z: float = 0.0) -> void:
|
_size_x: float = 0.0, _size_z: float = 0.0) -> void:
|
||||||
if res < 2:
|
if res < 2:
|
||||||
_splat_tex = null
|
_splat_tex = null
|
||||||
return
|
return
|
||||||
var side := TerrainArrays.splat_side_for(res, maxf(size_x, size_z))
|
# Karte 25.10.: Material-ID-Karte statt Splat-Gewichte (weiche Übergänge
|
||||||
var bytes := TerrainArrays.build_splat_bytes(mats, res, side)
|
# pro Pixel im Shader, Spiegel zum Client).
|
||||||
if bytes.size() != side * side * 4:
|
_splat_image = TerrainArrays.material_image(mats, res)
|
||||||
return
|
_splat_side = _splat_image.get_width()
|
||||||
_splat_image = Image.create_from_data(side, side, false, Image.FORMAT_RGBA8, bytes)
|
|
||||||
_splat_side = side
|
|
||||||
_splat_res = res
|
_splat_res = res
|
||||||
_splat_tex = ImageTexture.create_from_image(_splat_image)
|
_splat_tex = ImageTexture.create_from_image(_splat_image)
|
||||||
if mat != null:
|
if mat != null:
|
||||||
mat.set_shader_parameter("splat_map", _splat_tex)
|
mat.set_shader_parameter("material_map", _splat_tex)
|
||||||
|
mat.set_shader_parameter("material_res", float(_splat_side))
|
||||||
|
|
||||||
|
|
||||||
## Live-Pfad beim Malen (t_7a0a6b79): NUR die betroffene Region der Splat-
|
|
||||||
## Textur neu ableiten (dieselben Region-Grenzen wie apply_material_brush,
|
|
||||||
## Zellrechteck inkl. Spiegelzellen; hier in Texel umgerechnet). Die Gewichte
|
|
||||||
## brauchen den Kernel-Umfeld-Zugriff auf die GESAMTE Schicht (_mats_cache).
|
|
||||||
func _refresh_splat_region(cell_rect: Rect2i) -> void:
|
|
||||||
if _splat_image == null or _splat_res < 2:
|
|
||||||
return
|
|
||||||
var last := float(_splat_res - 1)
|
|
||||||
var tpc := float(_splat_side) / last # Texel je Zelle
|
|
||||||
var tx0 := clampi(int(floorf(float(cell_rect.position.x) * tpc)) - 1, 0, _splat_side - 1)
|
|
||||||
var ty0 := clampi(int(floorf(float(cell_rect.position.y) * tpc)) - 1, 0, _splat_side - 1)
|
|
||||||
var tx1 := clampi(int(ceilf(float(cell_rect.end.x) * tpc)) + 1, tx0 + 1, _splat_side)
|
|
||||||
var ty1 := clampi(int(ceilf(float(cell_rect.end.y) * tpc)) + 1, ty0 + 1, _splat_side)
|
|
||||||
var rect := Rect2i(tx0, ty0, tx1 - tx0, ty1 - ty0)
|
|
||||||
var bytes := TerrainArrays.build_splat_bytes(_mats_cache, _splat_res, _splat_side, rect)
|
|
||||||
if bytes.size() != rect.size.x * rect.size.y * 4:
|
|
||||||
return
|
|
||||||
var sub := Image.create_from_data(rect.size.x, rect.size.y, false, Image.FORMAT_RGBA8, bytes)
|
|
||||||
_splat_image.blit_rect(sub, Rect2i(0, 0, rect.size.x, rect.size.y), rect.position)
|
|
||||||
_splat_tex.update(_splat_image)
|
|
||||||
|
|
|
||||||
53
tests/probe_mal_weich_render.gd
Normal file
53
tests/probe_mal_weich_render.gd
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Render-Probe (Karte 25.10., Sly: „immer noch harte Kanten beim Malen“):
|
||||||
|
## 256-m-Karte (feines Raster 257 = 1 m), Sandpfad mit 2-m-Pinsel, top-down
|
||||||
|
## auf 40 m Ausschnitt. Misst die größte Helligkeits-Stufe zwischen
|
||||||
|
## Nachbarpixeln quer zur Kante — weich = kleine Stufen über mehrere Pixel.
|
||||||
|
## Start (BRAUCHT Rendering): godot --path . --rendering-driver opengl3 -s res://tests/probe_mal_weich_render.gd -- <out.png>
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var out := OS.get_cmdline_user_args()[0] if OS.get_cmdline_user_args().size() > 0 else "/tmp/mal_weich.png"
|
||||||
|
var size := float(OS.get_environment("PROBE_SIZE")) if OS.get_environment("PROBE_SIZE") != "" else 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)
|
||||||
|
for i in 60: # schräger 2-m-Pfad + Kurve
|
||||||
|
var t := float(i) / 59.0
|
||||||
|
TerrainField.apply_material_brush(mats, mres, size, size,
|
||||||
|
-15.0 + 30.0 * t, -10.0 + 8.0 * sin(t * 3.0), 2.0, 1)
|
||||||
|
var view := Node3D.new()
|
||||||
|
view.set_script(load("res://src/viewport/terrain_mesh.gd"))
|
||||||
|
root.add_child(view)
|
||||||
|
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 = 40.0
|
||||||
|
cam.position = Vector3(0.0, 50.0, -5.0 + 0.01)
|
||||||
|
cam.look_at(Vector3(0, 0, -5.0), Vector3(0, 0, -1))
|
||||||
|
cam.current = true
|
||||||
|
for i in 4:
|
||||||
|
await process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var img := root.get_texture().get_image()
|
||||||
|
img.save_png(out)
|
||||||
|
# Größte Nachbarpixel-Stufe (Luminanz) im Bild — harte Pixelkanten ≈ volle
|
||||||
|
# Sand↔Gras-Differenz in EINEM Pixel, weich = über ~Pixel/Meter verteilt.
|
||||||
|
var maxstep := 0.0
|
||||||
|
for y in range(0, img.get_height(), 2):
|
||||||
|
for x in range(1, img.get_width()):
|
||||||
|
var a := img.get_pixel(x - 1, y).get_luminance()
|
||||||
|
var b := img.get_pixel(x, y).get_luminance()
|
||||||
|
maxstep = maxf(maxstep, absf(a - b))
|
||||||
|
print("PROBE_MAXSTEP %.4f mres=%d px/m=%.1f -> %s" % [maxstep, mres, float(img.get_width()) / 40.0, out])
|
||||||
|
quit(0)
|
||||||
Loading…
Reference in a new issue