608 lines
24 KiB
GDScript
608 lines
24 KiB
GDScript
extends Node3D
|
||
class_name TerrainMeshView
|
||
## Terrain-Ansicht: baut das Höhenfeld (svmap terrain-Block) als Mesh im
|
||
## Editor-Viewport (M2, Karte t_9aba04f1).
|
||
##
|
||
## Zwei Wege:
|
||
## rebuild_from_doc(data) — nach Laden/Neu/Undo: Base64 dekodieren + Mesh neu.
|
||
## update_heights(...) — während eines Pinsel-Strichs: nur Vertices neu
|
||
## schreiben (SOFT-war das Mesh live — kein Rebuild,
|
||
## kein Flackern, res 129 ≈ 16,6k Punkte = flott).
|
||
##
|
||
## Farben (Karte t_603ec1a8): Vertex-Farben aus der Material-Schicht
|
||
## (terrain.materials_r8_b64, v2) — Gras/Sand/Fels, plus Höhen-Faktor.
|
||
##
|
||
## Relief-Lesbarkeit (Karte t_3ebe4086, Sly-Befund „Erhöhungen/Vertiefungen
|
||
## kaum erkennbar"): TWO-PUNCH im Vertex-Farben-Pfad:
|
||
## 1. Höhen-Faktor deutlich kontrastreicher: steilere S-Kurve statt linear
|
||
## und breitere Faktor-Spanne (Senken dunkler, Hügel heller).
|
||
## 2. Pseudo-Licht: Terrain-Normale × feste Sonnenrichtung (Hillshade) —
|
||
## Sonnenabgewandte Hänge werden dunkler, zugewandte heller. Das ist der
|
||
## günstigste echte Relief-Eindruck für die mobile Vertex-Farb-Pipeline
|
||
## und identisch zum Match-Client (svmap_terrain.gd, gleiche Konstanten).
|
||
##
|
||
## Editor-only Extra (gleiche Karte): Höhenlinien alle 1 m + 5-m-Indexlinie
|
||
## per Custom-Shader (terrain_contour.gdshader, UV2.x = Höhe). Quest-Vertrag
|
||
## „kein Custom-Shader" gilt fürs Spiel — der EDITOR darf teurer sein
|
||
## (DECISIONS.md-Konvention). Der Match-Client bleibt StandardMaterial3D.
|
||
## Editor-Viewport material ist UNSHADED: das gebackene Shading ist exakt
|
||
## sichtbar (kein Doppel-Shading mit dem Viewport-Sonnenlicht, deterministisch
|
||
## für die Render-Probe).
|
||
|
||
const MATERIAL_COLORS: Array[Color] = [
|
||
Color(1.0, 1.0, 1.0), # 0 = Gras — Karte t_ab408127: Textur liefert die
|
||
Color(1.0, 1.0, 1.0), # 1 = Sand — Farbe; Vertex-RGB ist nur noch Shading-
|
||
Color(1.0, 1.0, 1.0), # 2 = Fels — Multiplikator (Höhe × Hillshade),
|
||
# 20-Slot-Palette: Weiß = neutral, Albedo-Textur trägt die Färbung.
|
||
Color(1.0, 1.0, 1.0), # 3
|
||
Color(1.0, 1.0, 1.0), # 4
|
||
Color(1.0, 1.0, 1.0), # 5
|
||
Color(1.0, 1.0, 1.0), # 6
|
||
Color(1.0, 1.0, 1.0), # 7
|
||
Color(1.0, 1.0, 1.0), # 8
|
||
Color(1.0, 1.0, 1.0), # 9
|
||
Color(1.0, 1.0, 1.0), # 10
|
||
Color(1.0, 1.0, 1.0), # 11
|
||
Color(1.0, 1.0, 1.0), # 12
|
||
Color(1.0, 1.0, 1.0), # 13
|
||
Color(1.0, 1.0, 1.0), # 14
|
||
Color(1.0, 1.0, 1.0), # 15
|
||
Color(1.0, 1.0, 1.0), # 16
|
||
Color(1.0, 1.0, 1.0), # 17
|
||
Color(1.0, 1.0, 1.0), # 18
|
||
Color(1.0, 1.0, 1.0), # 19
|
||
]
|
||
# Höhen-Faktor (Material-Farbe × Faktor) — KARTE t_3ebe4086: bewusst breiter
|
||
# und mit S-Kurve (t*t*(3-2t)) kalibriert als die alte Linear-Version
|
||
# (0.72 … 1.15): Senken deutlich dunkler, Hügel deutlich heller.
|
||
const SHADING_LOW := 0.58 # Tal-Faktor
|
||
const SHADING_HIGH := 1.28 # Hügel-Faktor
|
||
# Pseudo-Licht (Hillshade): feste Sonnenrichtung, editor-seitig so gewählt,
|
||
# dass beim Default-Blick (Kamera südlich der Map, Blick Richtung −Z) die
|
||
# Formen seitlich beleuchtet wirken (Licht von rechts-hinten +X/+Z).
|
||
const RELIEF_SUN := Vector3(0.45, 0.75, 0.5)
|
||
const LIGHT_LOW := 0.62 # Hang abgewandt von der Sonne
|
||
const LIGHT_HIGH := 1.22 # Hang zugewandt
|
||
const LIGHT_T0 := 0.15 # Smoothstep-Fenster der NDOT-Kurve
|
||
const LIGHT_T1 := 0.95
|
||
const SHADE_MIN := 0.40 # Gesamtklemme (Höhen × Licht), Schutz vor Schwarz
|
||
const SHADE_MAX := 1.35
|
||
# Höhenlinien (editor-only) nur ab 2 m Gesamtrelief — auf einer flachen Map
|
||
# läge sonst ALLES auf der 0-m-Linie und die Fläche würde dunkel übermalt.
|
||
const CONTOUR_MIN_SPAN := 2.0
|
||
|
||
const CONTOUR_SHADER := "res://core/viewport/terrain_contour.gdshader"
|
||
|
||
var _mi: MeshInstance3D
|
||
|
||
## Quest: Terrain in Stücken, ein Pinselschritt schreibt nur die getroffenen
|
||
## Punkte neu (Messung 31.10.: 3 ms statt 132 ms bei Raster 257). Am PC reicht
|
||
## das ganze Mesh, dort läuft außerdem der Kontur-Shader.
|
||
static var stuecke_an := OS.has_feature("android")
|
||
## Eigenes Terrain-Material der App (Quest: der leichte Spiel-Shader).
|
||
static var material_fabrik := Callable()
|
||
const STUECK_ZELLEN := 32
|
||
var _stuecke: Array[Dictionary] = []
|
||
var _stuecke_res := 0
|
||
var _stuecke_groesse := Vector2.ZERO
|
||
var _dirty: Array[Rect2i] = []
|
||
var _hmin := 0.0
|
||
var _hmax := 1.0
|
||
var _res := 0
|
||
var _size_x := 0.0
|
||
var _size_z := 0.0
|
||
var _mats_cache := PackedByteArray() # zuletzt gesehene Material-Schicht
|
||
# (res²) — Live-Pfad-Fallback, Karte
|
||
# t_55ed041a.
|
||
var _wasser_level := INF # Karte t_28fdef03: Wasserhöhe aus dem doc
|
||
# (INF = kein Wasser — Matsch-Ring aus).
|
||
var _triangles := 0
|
||
var _contours_pref := true # UI-Toggle „Höhenlinien“ (Default AN)
|
||
var _shader: Shader
|
||
|
||
# --- Splat-Gewichtstextur (Karte t_7a0a6b79 — weiche Übergänge) -------------
|
||
# CPU-Image + GPU-Textur der Top-2-Splat-Gewichte (RGBA8: R=id0 G=w0 B=id1
|
||
# A=w1, Formel TerrainArrays.build_splat_bytes — Spiegel-Datei). Beim Malen
|
||
# wird NUR die betroffene Region neu abgeleitet (Sub-Rect-Update, siehe
|
||
# _refresh_splat_region) — kein Voll-Rebuild pro Mal-Frame.
|
||
var _splat_image: Image
|
||
var _splat_tex: ImageTexture
|
||
var _splat_side := 0 # Texel-Kantenlänge (Zweierpotenz)
|
||
var _splat_res := 0 # Zellraster-Auflösung, für die die Splat gilt
|
||
# Misch-Schicht (Karte 25.10.): zweites Material + Anteil je Punkt — eigene
|
||
# R8-Texturen neben der Material-Karte (gleiche Größe, Shader liest alle drei).
|
||
var _mix_b_cache := PackedByteArray()
|
||
var _mix_w_cache := PackedByteArray()
|
||
var _mix_b_image: Image
|
||
var _mix_w_image: Image
|
||
var _mix_b_tex: ImageTexture
|
||
var _mix_w_tex: ImageTexture
|
||
|
||
|
||
func _ready() -> void:
|
||
_ensure_mesh_instance()
|
||
|
||
|
||
func _ensure_mesh_instance() -> void:
|
||
if _mi != null and is_instance_valid(_mi):
|
||
return
|
||
_mi = MeshInstance3D.new()
|
||
_mi.material_override = _make_material()
|
||
add_child(_mi)
|
||
|
||
|
||
## Material fürs Editor-Terrain: unshaded Kontur-Shader (Vertex-Farbe als
|
||
## Albedo + Höhenlinien). Karte t_ab408127: zusätzlich die Terrain-Texturen
|
||
## (TerrainArrays.res — spiegelgleich zum Client-Pfad, aber EDITOR-EIGENE
|
||
## Kopie der Resource-Klasse, Repos sind entkoppelt). Fällt auf unshaded
|
||
## StandardMaterial3D zurück, wenn der Shader nicht ladbar ist (Robustheit —
|
||
## Terrain bleibt immer sichtbar).
|
||
func _make_material() -> Material:
|
||
if material_fabrik.is_valid():
|
||
return material_fabrik.call()
|
||
_shader = load(CONTOUR_SHADER)
|
||
if _shader != null:
|
||
var mat := ShaderMaterial.new()
|
||
mat.shader = _shader
|
||
_apply_texture_uniforms(mat)
|
||
return mat
|
||
push_warning("TerrainMesh: Kontur-Shader fehlt — Fallback StandardMaterial3D (unshaded).")
|
||
var std := StandardMaterial3D.new()
|
||
std.vertex_color_use_as_albedo = true
|
||
std.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
std.roughness = 1.0
|
||
return std
|
||
|
||
|
||
## Terrain-Texturen an den Kontur-Shader binden (t_ab408127). Quelle =
|
||
## res://assets/textures/terrain/terrain_arrays.res — vom Sync-Skript
|
||
## (tools/sync_terrain_tex.sh) aus dem Client-Klon gespiegelt, analog
|
||
## tools/sync_models.sh (models/-Ordner-Muster). Fehlt die Resource
|
||
## (frischer Editor-Klon ohne Sync), bleibt texturen_an=0 → reiner
|
||
## Vertex-Farb-Look (Fallback, kein Crash).
|
||
func _apply_texture_uniforms(mat: ShaderMaterial) -> void:
|
||
const ARRAYS_PATH := "res://assets/textures/terrain/terrain_arrays.res"
|
||
if not ResourceLoader.exists(ARRAYS_PATH):
|
||
return # Fallback-Klon: texturen_an bleibt 0 (Vertex-Farb-Look)
|
||
var arrays = load(ARRAYS_PATH)
|
||
if arrays == null or not ("albedo_images" in arrays):
|
||
return
|
||
var imgs: Array = arrays.albedo_images
|
||
if imgs.size() < 3:
|
||
return
|
||
var albedo := Texture2DArray.new()
|
||
albedo.create_from_images(imgs)
|
||
mat.set_shader_parameter("albedo_array", albedo)
|
||
mat.set_shader_parameter("layer_count", float(imgs.size()))
|
||
mat.set_shader_parameter("tile_size", 3.0) # Spiegel Client TILE_SIZE (Karte 25.10.)
|
||
mat.set_shader_parameter("texturen_an", 1.0)
|
||
|
||
|
||
## UI-Toggle „Höhenlinien“ (main.gd): ohne Rebuild an/ab.
|
||
func set_contours(on: bool) -> void:
|
||
_contours_pref = on
|
||
_apply_contour_state(_last_span >= CONTOUR_MIN_SPAN)
|
||
|
||
|
||
func contours_enabled() -> bool:
|
||
return _contours_pref
|
||
|
||
|
||
var _last_span := 0.0
|
||
|
||
|
||
## Kompletter Neuaufbau aus dem svmap-terrain-Block (inkl. Material-Schicht).
|
||
func rebuild_from_doc(data: Dictionary) -> void:
|
||
var terrain: Dictionary = data.get("terrain", {})
|
||
if terrain.is_empty():
|
||
_triangles = 0
|
||
if _mi != null:
|
||
_mi.mesh = null
|
||
_stuecke_leeren()
|
||
return
|
||
# Splat-Textur verwerfen — sie wird im update_heights frisch aus der
|
||
# (nach Undo/Load evtl. geänderten) Zell-Schicht abgeleitet.
|
||
_splat_tex = null
|
||
var mix := SvmapIO.decode_mix(data.get("terrain", {}))
|
||
_mix_b_cache = mix[0]
|
||
_mix_w_cache = mix[1]
|
||
# Wasser (Karte t_28fdef03): Höhe merken — der Matsch-Ring (Ufer-Automatik)
|
||
# mischt sich in relief_color ein. INF = kein Wasser (Legacy unverändert).
|
||
_wasser_level = SvmapWasser.level(data)
|
||
var heights := SvmapIO.decode_heights(terrain)
|
||
var mats := SvmapIO.decode_materials(terrain)
|
||
update_heights(heights, int(terrain.get("resolution", 0)),
|
||
float(terrain.get("size_x_m", 1.0)), float(terrain.get("size_z_m", 1.0)), mats)
|
||
|
||
|
||
## Mesh aus einem (neuen) Höhenfeld bauen — auch live während des Strichs.
|
||
## res/size 0 = zuletzt verwendete Werte weiterverwenden (Live-Pfad).
|
||
## mats = Material-Schicht (res² Bytes, 0=Gras/1=Sand/2=Fels); leer → die
|
||
## zuletzt gesehene Schicht (Cache) wird weiterverwendet, damit Live-Updates
|
||
## ohne explizite Schicht (z. B. Heben/Senken-Stroke) die Material-Farben
|
||
## BEHALTEN statt live auf alles-Gras zurückzufallen (Karte t_55ed041a).
|
||
## Nur wenn NIE eine Schicht gesehen wurde (Legacy-v1-Map) gilt alles Gras.
|
||
func update_heights(heights: PackedFloat32Array, res: int = 0, size_x: float = 0.0,
|
||
size_z: float = 0.0, mats: PackedByteArray = PackedByteArray(),
|
||
splat_rect_cells: Rect2i = Rect2i()) -> void:
|
||
if res < 2:
|
||
res = _res
|
||
if size_x <= 0.0:
|
||
size_x = _size_x
|
||
if size_z <= 0.0:
|
||
size_z = _size_z
|
||
if _mi == null:
|
||
_ensure_mesh_instance()
|
||
if res < 2 or heights.size() < res * res or size_x <= 0.0 or size_z <= 0.0:
|
||
return
|
||
_res = res
|
||
_size_x = size_x
|
||
_size_z = size_z
|
||
# Material-Schicht hat ihr eigenes (feines) Raster (Karte 24.10.) — mres
|
||
# aus der Länge (quadratisch); alte Karten: mres == res.
|
||
if mats.is_empty() and not _mats_cache.is_empty():
|
||
mats = _mats_cache
|
||
elif not mats.is_empty():
|
||
_mats_cache = mats
|
||
var mres := mat_res_of(mats, res)
|
||
if stuecke_an:
|
||
# Mal-Schritt (Rechteck gesetzt): Vertices hängen nicht am Material.
|
||
if not splat_rect_cells.has_area():
|
||
_stuecke_schreiben(heights, res, size_x, size_z, mats, mres)
|
||
_splat_aktualisieren(size_x, size_z, mats, mres)
|
||
return
|
||
var last := float(res - 1)
|
||
var positions := PackedVector3Array()
|
||
var normals := PackedVector3Array()
|
||
var colors := PackedColorArray()
|
||
var heights_uv := PackedVector2Array()
|
||
var indices := PackedInt32Array()
|
||
positions.resize(res * res)
|
||
normals.resize(res * res)
|
||
colors.resize(res * res)
|
||
heights_uv.resize(res * res)
|
||
|
||
var hmin := INF
|
||
var hmax := -INF
|
||
for v in heights:
|
||
if v < hmin:
|
||
hmin = v
|
||
if v > hmax:
|
||
hmax = v
|
||
var span := maxf(hmax - hmin, 0.0001)
|
||
_last_span = span
|
||
var cell_x := size_x / last
|
||
var cell_z := size_z / last
|
||
_apply_contour_state(span >= CONTOUR_MIN_SPAN)
|
||
|
||
var z := 0
|
||
while z < res:
|
||
var x := 0
|
||
while x < res:
|
||
var idx := z * res + x
|
||
# Index→Welt-Konvention (Karte t_234490cf): Index 0 = −X/−Z-Ecke,
|
||
# x-Index läuft MIT +WeltX, z-Index MIT +WeltZ — IDENTISCH zu
|
||
# SvmapDocument.terrain_height_at, TerrainField.height_at und dem
|
||
# Match-Client (svmap_terrain.gd: −size*0.5 + index*cell).
|
||
# Vor b7b160d…+Fix stand hier eine SPIGELUNG (+0.5 − index·cell in
|
||
# beiden Achsen) — der Renderer war damit der einzige Verbraucher
|
||
# dieser Konvention: der Pinsel-Strahl traf das Mesh an der
|
||
# punktgespiegelten Stelle (Sly-Befund „Pinsel wirkt woanders“).
|
||
positions[idx] = Vector3(
|
||
-size_x * 0.5 + float(x) * cell_x,
|
||
heights[idx],
|
||
-size_z * 0.5 + float(z) * cell_z)
|
||
# Normale aus zentralen Differenzen (Rand: einseitig geklemmt):
|
||
# n ∝ (−∂h/∂x, 1, −∂h/∂z), skaliert mit 2·cell_x·cell_z.
|
||
var hx_l := heights[z * res + maxi(x - 1, 0)]
|
||
var hx_r := heights[z * res + mini(x + 1, res - 1)]
|
||
var hz_d := heights[maxi(z - 1, 0) * res + x]
|
||
var hz_u := heights[mini(z + 1, res - 1) * res + x]
|
||
normals[idx] = Vector3(
|
||
(hx_l - hx_r) * cell_z,
|
||
2.0 * cell_x * cell_z,
|
||
(hz_d - hz_u) * cell_x).normalized()
|
||
var mat_id := clampi(SvmapIO.material_at_height_point(mats, mres, res, x, z),
|
||
0, MATERIAL_COLORS.size() - 1)
|
||
# Material-Farbe × Höhen-Kurve × Pseudo-Licht (Relief lesbar).
|
||
var farbe := relief_color(mat_id, heights[idx], hmin, hmax, normals[idx])
|
||
# Ufer-Automatik (Karte t_28fdef03): Zellen knapp unter der Wasser-
|
||
# höhe bekommen den Matsch-Ring — IDENTISCHE Formel wie der Client
|
||
# (SvmapWasser.ufer_faktor/matsch_anwenden — dieselbe Spiegel-Datei).
|
||
# Alpha (gepackte Material-ID) bleibt unangetastet (Splat-Vertrag).
|
||
if _wasser_level < INF:
|
||
farbe = SvmapWasser.matsch_anwenden(farbe,
|
||
SvmapWasser.ufer_faktor(heights[idx], _wasser_level))
|
||
colors[idx] = farbe
|
||
heights_uv[idx] = Vector2(heights[idx], 0.0)
|
||
x += 1
|
||
z += 1
|
||
|
||
z = 0
|
||
while z < res - 1:
|
||
var x := 0
|
||
while x < res - 1:
|
||
var i := z * res + x
|
||
# Winding-Order: Godot betrachtet Clockwise (von der Vorderseite
|
||
# betrachtet) als Vorderseite. Von oben gelesen muss jedes Dreieck
|
||
# IM Uhrzeigersinn laufen, sonst cullt der Renderer die komplette
|
||
# Deckfläche als Backface (Karte t_7ac70014: „dunkle Platte“).
|
||
indices.append_array([i, i + 1, i + res, i + 1, i + res + 1, i + res])
|
||
x += 1
|
||
z += 1
|
||
|
||
var arrays := []
|
||
arrays.resize(Mesh.ARRAY_MAX)
|
||
arrays[Mesh.ARRAY_VERTEX] = positions
|
||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||
arrays[Mesh.ARRAY_COLOR] = colors
|
||
arrays[Mesh.ARRAY_TEX_UV2] = heights_uv # Höhe je Vertex → Kontur-Shader
|
||
arrays[Mesh.ARRAY_INDEX] = indices
|
||
var mesh := ArrayMesh.new()
|
||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||
_mi.mesh = mesh
|
||
_triangles = indices.size() / 3
|
||
|
||
_splat_aktualisieren(size_x, size_z, mats, mres)
|
||
|
||
|
||
func _splat_aktualisieren(size_x: float, size_z: float, mats: PackedByteArray, mres: int) -> void:
|
||
# --- Splat-Gewichtstextur (t_7a0a6b79): map_size + Sub-Rect-Update --------
|
||
# map_size_x/z in Weltmetern (Shader rechnet map_uv = Weltkoord. über Map).
|
||
# Mal-Stroke: splat_rect_cells = betroffenes Zellrechteck → NUR die Region
|
||
# neu ableiten. Voll-Rebuild (Load/Undo/Stroke-Ende): Textur frisch bauen.
|
||
var mat := _mi.material_override
|
||
if mat is ShaderMaterial:
|
||
var shm := mat as ShaderMaterial
|
||
shm.set_shader_parameter("map_size_x", size_x)
|
||
shm.set_shader_parameter("map_size_z", size_z)
|
||
# Material-ID-Karte (Karte 25.10.): Malen = gleiche Größe → nur Daten
|
||
# tauschen (1 Upload); sonst neu anlegen. splat_rect_cells bleibt als
|
||
# Parameter (Aufrufer-Vertrag), wird aber nicht mehr gebraucht.
|
||
if _splat_tex != null and _splat_res == mres and mats.size() == mres * mres \
|
||
and _mix_b_tex != null and _mix_b_cache.size() == mres * mres:
|
||
_splat_image.set_data(mres, mres, false, Image.FORMAT_R8, mats)
|
||
_splat_tex.update(_splat_image)
|
||
_mix_b_image.set_data(mres, mres, false, Image.FORMAT_R8, _mix_b_cache)
|
||
_mix_b_tex.update(_mix_b_image)
|
||
_mix_w_image.set_data(mres, mres, false, Image.FORMAT_R8, _mix_w_cache)
|
||
_mix_w_tex.update(_mix_w_image)
|
||
else:
|
||
_build_splat(shm, mats, mres, size_x, size_z)
|
||
|
||
|
||
## Pinsel meldet die geänderten Höhenpunkte, das nächste Update schreibt nur sie.
|
||
func markiere_hoehen(rect: Rect2i) -> void:
|
||
_dirty.append(rect)
|
||
|
||
|
||
func _stuecke_leeren() -> void:
|
||
for s in _stuecke:
|
||
(s["mi"] as Node).queue_free()
|
||
_stuecke.clear()
|
||
_stuecke_res = 0
|
||
_dirty.clear()
|
||
|
||
|
||
func _stuecke_schreiben(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
mats: PackedByteArray, mres: int) -> void:
|
||
var groesse := Vector2(size_x, size_z)
|
||
var neu_bauen := _stuecke_res != res or _stuecke_groesse != groesse
|
||
if neu_bauen:
|
||
_stuecke_leeren()
|
||
_stuecke_res = res
|
||
_stuecke_groesse = groesse
|
||
var je_seite := int(ceil(float(res - 1) / float(STUECK_ZELLEN)))
|
||
for cz in je_seite:
|
||
for cx in je_seite:
|
||
_stuecke.append(_stueck_anlegen(cx, cz, res))
|
||
_triangles = (res - 1) * (res - 1) * 2
|
||
var alles := neu_bauen or _dirty.is_empty()
|
||
if alles:
|
||
# Farbskala nur beim vollen Durchlauf neu, im Strich bleibt sie stehen.
|
||
_hmin = INF
|
||
_hmax = -INF
|
||
for v in heights:
|
||
_hmin = minf(_hmin, v)
|
||
_hmax = maxf(_hmax, v)
|
||
_last_span = maxf(_hmax - _hmin, 0.0001)
|
||
_apply_contour_state(_last_span >= CONTOUR_MIN_SPAN)
|
||
for s in _stuecke:
|
||
var ganz := Rect2i(s["x0"], s["z0"], s["x1"] - s["x0"] + 1, s["z1"] - s["z0"] + 1)
|
||
var getroffen := alles
|
||
if alles:
|
||
_stueck_punkte(s, ganz, heights, res, size_x, size_z, mats, mres)
|
||
else:
|
||
for r in _dirty:
|
||
# Normalen hängen an den Nachbarn: ein Punkt Rand mitnehmen.
|
||
var teil := r.grow(1).intersection(ganz)
|
||
if teil.has_area():
|
||
_stueck_punkte(s, teil, heights, res, size_x, size_z, mats, mres)
|
||
getroffen = true
|
||
if getroffen:
|
||
var mesh: ArrayMesh = (s["mi"] as MeshInstance3D).mesh
|
||
mesh.clear_surfaces()
|
||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, s["arrays"])
|
||
_dirty.clear()
|
||
|
||
|
||
func _stueck_anlegen(cx: int, cz: int, res: int) -> Dictionary:
|
||
var x0 := cx * STUECK_ZELLEN
|
||
var z0 := cz * STUECK_ZELLEN
|
||
var x1 := mini(x0 + STUECK_ZELLEN, res - 1)
|
||
var z1 := mini(z0 + STUECK_ZELLEN, res - 1)
|
||
var w := x1 - x0 + 1
|
||
var h := z1 - z0 + 1
|
||
var pos := PackedVector3Array()
|
||
var nor := PackedVector3Array()
|
||
var col := PackedColorArray()
|
||
var uv2 := PackedVector2Array()
|
||
var idx := PackedInt32Array()
|
||
pos.resize(w * h)
|
||
nor.resize(w * h)
|
||
col.resize(w * h)
|
||
uv2.resize(w * h)
|
||
idx.resize((w - 1) * (h - 1) * 6)
|
||
var n := 0
|
||
for z in h - 1:
|
||
for x in w - 1:
|
||
var i := z * w + x
|
||
# Wicklung wie im ganzen Mesh (update_heights).
|
||
idx[n] = i
|
||
idx[n + 1] = i + 1
|
||
idx[n + 2] = i + w
|
||
idx[n + 3] = i + 1
|
||
idx[n + 4] = i + w + 1
|
||
idx[n + 5] = i + w
|
||
n += 6
|
||
var arrays := []
|
||
arrays.resize(Mesh.ARRAY_MAX)
|
||
arrays[Mesh.ARRAY_VERTEX] = pos
|
||
arrays[Mesh.ARRAY_NORMAL] = nor
|
||
arrays[Mesh.ARRAY_COLOR] = col
|
||
arrays[Mesh.ARRAY_TEX_UV2] = uv2
|
||
arrays[Mesh.ARRAY_INDEX] = idx
|
||
var mi := MeshInstance3D.new()
|
||
mi.mesh = ArrayMesh.new()
|
||
mi.material_override = _mi.material_override
|
||
add_child(mi)
|
||
return {"x0": x0, "z0": z0, "x1": x1, "z1": z1, "w": w, "arrays": arrays, "mi": mi}
|
||
|
||
|
||
func _stueck_punkte(s: Dictionary, r: Rect2i, heights: PackedFloat32Array, res: int,
|
||
size_x: float, size_z: float, mats: PackedByteArray, mres: int) -> void:
|
||
var arrays: Array = s["arrays"]
|
||
var pos: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||
var nor: PackedVector3Array = arrays[Mesh.ARRAY_NORMAL]
|
||
var col: PackedColorArray = arrays[Mesh.ARRAY_COLOR]
|
||
var uv2: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV2]
|
||
var x0: int = s["x0"]
|
||
var z0: int = s["z0"]
|
||
var w: int = s["w"]
|
||
var cell_x := size_x / float(res - 1)
|
||
var cell_z := size_z / float(res - 1)
|
||
for z in range(r.position.y, r.end.y):
|
||
for x in range(r.position.x, r.end.x):
|
||
var i := z * res + x
|
||
var v := (z - z0) * w + (x - x0)
|
||
var hv := heights[i]
|
||
pos[v] = Vector3(-size_x * 0.5 + float(x) * cell_x, hv, -size_z * 0.5 + float(z) * cell_z)
|
||
var n := Vector3(
|
||
(heights[z * res + maxi(x - 1, 0)] - heights[z * res + mini(x + 1, res - 1)]) * cell_z,
|
||
2.0 * cell_x * cell_z,
|
||
(heights[maxi(z - 1, 0) * res + x] - heights[mini(z + 1, res - 1) * res + x]) * cell_x).normalized()
|
||
nor[v] = n
|
||
var mat_id := clampi(SvmapIO.material_at_height_point(mats, mres, res, x, z),
|
||
0, MATERIAL_COLORS.size() - 1)
|
||
var farbe := relief_color(mat_id, hv, _hmin, _hmax, n)
|
||
if _wasser_level < INF:
|
||
farbe = SvmapWasser.matsch_anwenden(farbe, SvmapWasser.ufer_faktor(hv, _wasser_level))
|
||
col[v] = farbe
|
||
uv2[v] = Vector2(hv, 0.0)
|
||
arrays[Mesh.ARRAY_VERTEX] = pos
|
||
arrays[Mesh.ARRAY_NORMAL] = nor
|
||
arrays[Mesh.ARRAY_COLOR] = col
|
||
arrays[Mesh.ARRAY_TEX_UV2] = uv2
|
||
|
||
|
||
## Misch-Schicht fürs nächste update_heights (Live-Malen, Karte 25.10.).
|
||
func set_mix(mix_b: PackedByteArray, mix_w: PackedByteArray) -> void:
|
||
_mix_b_cache = mix_b
|
||
_mix_w_cache = mix_w
|
||
|
||
|
||
## Raster einer Material-Schicht aus ihrer Länge (quadratisch). Unpassend/leer
|
||
## → Höhenraster (alte Karten, Legacy ohne Schicht).
|
||
static func mat_res_of(mats: PackedByteArray, res: int) -> int:
|
||
if mats.is_empty():
|
||
return res
|
||
var r := int(roundf(sqrt(float(mats.size()))))
|
||
return r if r >= 2 and r * r == mats.size() else res
|
||
|
||
|
||
## Höhenlinien-Uniform an die aktuelle Situation anpassen (Pref ∧ Relief ≥ 2 m).
|
||
func _apply_contour_state(span_ok: bool) -> void:
|
||
if _mi == null or not is_instance_valid(_mi):
|
||
return
|
||
var mat := _mi.material_override
|
||
if mat is ShaderMaterial:
|
||
mat.set_shader_parameter("linien_an", 1.0 if (_contours_pref and span_ok) else 0.0)
|
||
|
||
|
||
## Vertex-Farbe EINER Zelle: Material × Höhen-Kurve × Pseudo-Licht.
|
||
## Static (statt inline), damit Tests die Mathematik direkt prüfen können und
|
||
## Editor/Client die IDENTISCHE Formel teilen (Konstanten spiegeln — gleiche
|
||
## Kommentare/Konstanten wie scripts/world/svmap_terrain.gd im Client-Repo).
|
||
## Alpha = Material-ID (Karte t_ab408127): m/N gepackt — der Shader (Client
|
||
## UND Editor-Vorschau) sampelt die Texture2DArray-Layer über die Alpha.
|
||
## Der Shading-Faktor wirkt NUR auf RGB (Color × float skaliert auch Alpha —
|
||
## deshalb wird die ID erst im Return gezielt gesetzt).
|
||
static func relief_color(mat_id: int, h: float, hmin: float, hmax: float,
|
||
normal: Vector3) -> Color:
|
||
var m := clampi(mat_id, 0, MATERIAL_COLORS.size() - 1)
|
||
var span := maxf(hmax - hmin, 0.0001)
|
||
var t := clampf((h - hmin) / span, 0.0, 1.0)
|
||
# Steilere Kurve statt linear: S-Kurve hebt die Enden, senkt die Mitte —
|
||
# Hügel/Senken trennen sich stärker als bei t direkt.
|
||
var t_s := t * t * (3.0 - 2.0 * t)
|
||
var hf := lerpf(SHADING_LOW, SHADING_HIGH, t_s)
|
||
# Pseudo-Licht: Normale × feste Sonnenrichtung → Hillshade.
|
||
var nd := clampf(normal.normalized().dot(RELIEF_SUN.normalized()), 0.0, 1.0)
|
||
var x := clampf((nd - LIGHT_T0) / (LIGHT_T1 - LIGHT_T0), 0.0, 1.0)
|
||
var lf := lerpf(LIGHT_LOW, LIGHT_HIGH, x * x * (3.0 - 2.0 * x))
|
||
# Ausbrennen-Fix (t_7a0a6b79): der Faktor wird auf 1.0 normalisiert. Vorher
|
||
# lief hf·lf bis 1.35 und RGBA8 klemmte auf 1.0 — ALLE Relief-Unterschiede
|
||
# oberhalb 1.0 kollabierten zu flachem Weiß (Slys „ausgebrannte Weißflächen“
|
||
# auf Sand hell/Schnee). Relative Spanne bleibt identisch (0.40…1.35 →
|
||
# 0.296…1.0), Kontrast-Verhältnis unverändert, Shading bleibt sichtbar.
|
||
var f := clampf(hf * lf, SHADE_MIN, SHADE_MAX) / SHADE_MAX
|
||
var base := MATERIAL_COLORS[m] as Color
|
||
# Kanäle auf 0..1 klemmen: der Mesh-Speicher (ARRAY_COLOR, RGBA8) clamp't
|
||
# genauso — der Helfer muss die RENDER-Wahrheit liefern (Probe-Vergleich),
|
||
# nicht die unbehandelte Mathematik.
|
||
return Color(clampf(base.r * f, 0.0, 1.0), clampf(base.g * f, 0.0, 1.0),
|
||
clampf(base.b * f, 0.0, 1.0), float(m) / float(MATERIAL_COLORS.size()))
|
||
|
||
|
||
func triangle_count() -> int:
|
||
return _triangles
|
||
|
||
|
||
func resolution() -> int:
|
||
return _res
|
||
|
||
|
||
## Splat-Gewichtstextur komplett ableiten (t_7a0a6b79). Die Zell-Schicht
|
||
## (mats, res²) bleibt die Quelle der Wahrheit — die Textur ist reine
|
||
## Ableitung (Datenformat unverändert). Unbrauchbare Schicht → alte Textur
|
||
## bleibt (Robustheit, kein Crash).
|
||
## 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).
|
||
func _build_splat(mat: ShaderMaterial, mats: PackedByteArray, res: int,
|
||
_size_x: float = 0.0, _size_z: float = 0.0) -> void:
|
||
if res < 2:
|
||
_splat_tex = null
|
||
return
|
||
# Karte 25.10.: Material-ID-Karte statt Splat-Gewichte (weiche Übergänge
|
||
# pro Pixel im Shader, Spiegel zum Client).
|
||
_splat_image = TerrainArrays.material_image(mats, res)
|
||
_splat_side = _splat_image.get_width()
|
||
_splat_res = res
|
||
_splat_tex = ImageTexture.create_from_image(_splat_image)
|
||
# Misch-Schicht: passt sie nicht zum Raster (Legacy/leer) → Nullen.
|
||
if _mix_b_cache.size() != res * res or _mix_w_cache.size() != res * res:
|
||
_mix_b_cache = PackedByteArray()
|
||
_mix_b_cache.resize(res * res)
|
||
_mix_w_cache = PackedByteArray()
|
||
_mix_w_cache.resize(res * res)
|
||
_mix_b_image = TerrainArrays.material_image(_mix_b_cache, res)
|
||
_mix_w_image = TerrainArrays.material_image(_mix_w_cache, res)
|
||
_mix_b_tex = ImageTexture.create_from_image(_mix_b_image)
|
||
_mix_w_tex = ImageTexture.create_from_image(_mix_w_image)
|
||
if mat != null:
|
||
mat.set_shader_parameter("material_map", _splat_tex)
|
||
mat.set_shader_parameter("material_b_map", _mix_b_tex)
|
||
mat.set_shader_parameter("material_w_map", _mix_w_tex)
|
||
mat.set_shader_parameter("material_res", float(_splat_side))
|
||
|
||
|