extends SceneTree ## Render-Probe M2 (Karte t_f9eff526): Terrain spiegeln im Symmetrie-Modus. ## Zwei Striche (Heben bei −260/−140, Malen Sand bei +150/+180) mit ## Symmetrie=Punkt über den ECHTEN terrain_tool-Pfad (demo_stroke) — ## draufsichtig gerendert müssen BOTHSEITIG Hügel + Sand erscheinen: ## - Hügel-Fenster und Zwilling-Hügel-Fenster nahezu gleich hell ## - Sand-Fenster und Zwilling-Sand-Fenster nahezu gleich farbig ## - beide deutlich verschieden zur unberührten Mitte ## PNGs landen im Profil-Scratch + /opt/data/scopedvr/work/m2_bilder/. ## Start (BRAUCHT Rendering — NICHT --headless): ## xvfb-run godot --path . --rendering-driver opengl3 -s res://tests/probe_terrain_mirror_render.gd const RES := 65 const SIZE := 1024.0 var failures := 0 var checks := 0 var out_dir := "/opt/data/profiles/scopedvr-dev/cache/scratch" class ToolMain: var doc: Dictionary = {} var sym_mode: int = SymmetryCore.Mode.POINT var history: ActionHistory = ActionHistory.new() func _set_status(_text: String, _err := false) -> void: pass func _on_terrain_stroke_committed() -> void: pass func _initialize() -> void: await process_frame await process_frame # --- Doc + Terrain-Tool (echter Strich-Pfad inkl. Spiegel) --- var doc := SvmapDocument.create_new("MirrorProbe", SIZE, "mittag") var terrain: Dictionary = doc["terrain"] terrain["resolution"] = RES terrain["size_x_m"] = SIZE terrain["size_z_m"] = SIZE terrain["min_height_m"] = 0.0 terrain["max_height_m"] = 40.0 var flat := PackedFloat32Array() flat.resize(RES * RES) terrain["heightmap_r16_b64"] = TerrainField.encode(flat, 0.0, 40.0) var mats := PackedByteArray() mats.resize(RES * RES) terrain["materials_r8_b64"] = SvmapIO.encode_materials(mats) var tmain := ToolMain.new() tmain.doc = doc tmain.history.begin(doc) var ttool := Node3D.new() ttool.set_script(load("res://src/editor/terrain_tool.gd")) ttool.setup(tmain, Camera3D.new(), null, null) ttool.call("activate") ttool.call("set_radius", 100.0) ttool.call("set_paint_material", 1) ttool.call("demo_stroke", -260.0, -140.0, TerrainField.Mode.RAISE, 2.0) ttool.call("demo_stroke", 150.0, 180.0, TerrainField.Mode.PAINT, 1.0) var heights := SvmapIO.decode_heights(terrain) var got_mats := SvmapIO.decode_materials(terrain) # Zellgleiche Hälften numerisch gegenprüfen (Punkt-Modus): var bad_h := 0 var bad_m := 0 for z in RES: for x in RES: var mc: Vector2i = SymmetryCore.mirror_cell(SymmetryCore.Mode.POINT, x, z, RES) if heights[z * RES + x] != heights[mc.y * RES + mc.x]: bad_h += 1 if int(got_mats[z * RES + x]) != int(got_mats[mc.y * RES + mc.x]): bad_m += 1 print("PROBE_MIRROR_ASYM hoehe=%d material=%d (0 = exakt zellgleich)" % [bad_h, bad_m]) _check(bad_h == 0, "Höhen-Hälften exakt zellgleich (0 Abweichungen)") _check(bad_m == 0, "Material-Hälften exakt zellgleich (0 Abweichungen)") # --- Draufsicht rendern (unshaded Vertex-Farben + Höhenlinien) --- 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, got_mats) var cam := Camera3D.new() root.add_child(cam) cam.projection = Camera3D.PROJECTION_ORTHOGONAL var vp := root.size cam.size = SIZE * float(vp.y) / float(vp.x) cam.position = Vector3(0.0, SIZE, 0.01) cam.look_at(Vector3.ZERO, Vector3(0, 0, -1)) cam.current = true await process_frame await process_frame await RenderingServer.frame_post_draw var img := root.get_texture().get_image() img.save_png(out_dir + "/terrain_mirror_beweis.png") print("PROBE_SAVED %s/terrain_mirror_beweis.png" % out_dir) # --- Fenster-Messungen --- # Ortho-Kamera füllt exakt die BildBREITE: horizontale Ausdehnung = # SIZE, vertikale = SIZE × (Höhe/Breite des Viewports) (KEEP_HEIGHT). # Welt → Bild: sx = 0.5 + x/SIZE, sy = 0.5 + z/vert_extent. var w := img.get_width() var h := img.get_height() var vert := SIZE * float(h) / float(w) var hill_a := _window_mean(img, Vector2(w * (0.5 - 260.0 / SIZE), h * (0.5 - 140.0 / vert)), int(w * 0.04)) var hill_b := _window_mean(img, Vector2(w * (0.5 + 260.0 / SIZE), h * (0.5 + 140.0 / vert)), int(w * 0.04)) var sand_a := _window_mean(img, Vector2(w * (0.5 + 150.0 / SIZE), h * (0.5 + 180.0 / vert)), int(w * 0.04)) var sand_b := _window_mean(img, Vector2(w * (0.5 - 150.0 / SIZE), h * (0.5 - 180.0 / vert)), int(w * 0.04)) var mitte := _window_mean(img, Vector2(w * 0.5, h * 0.5), int(w * 0.03)) print("PROBE_HUEGEL a=%s b=%s" % [hill_a, hill_b]) print("PROBE_SAND a=%s b=%s" % [sand_a, sand_b]) print("PROBE_MITTE %s (unberührtes Gras)" % mitte) var dh := _v3dist(hill_a, hill_b) var ds := _v3dist(sand_a, sand_b) print("PROBE_SYMDIFF huegel=%.4f sand=%.4f" % [dh, ds]) _check(dh < 0.02, "Hügel und Zwilling-Hügel im Bild gleich (Δ %.4f < 0.02)" % dh) _check(ds < 0.02, "Sand und Zwilling-Sand im Bild gleich (Δ %.4f < 0.02)" % ds) _check(_v3dist(hill_a, mitte) > 0.05, "Hügel deutlich sichtbar vs. Mitte (Δ %.4f)" % _v3dist(hill_a, mitte)) _check(_v3dist(sand_a, mitte) > 0.05, "Sand deutlich sichtbar vs. Gras-Mitte (Δ %.4f)" % _v3dist(sand_a, mitte)) if failures == 0: print("ALL OK: %d checks" % checks) quit(0) else: print("FAILED: %d/%d checks" % [failures, checks]) quit(1) func _window_mean(img: Image, center: Vector2, radius_px: int) -> Vector3: var s := Vector3.ZERO var n := 0 var cx := int(center.x) var cy := int(center.y) for y in range(maxi(0, cy - radius_px), mini(img.get_height(), cy + radius_px)): for x in range(maxi(0, cx - radius_px), mini(img.get_width(), cx + radius_px)): var c := img.get_pixel(x, y) s += Vector3(c.r, c.g, c.b) n += 1 return s / float(maxi(n, 1)) func _v3dist(a: Vector3, b: Vector3) -> float: return (a - b).length() func _check(cond: bool, label: String) -> void: checks += 1 if cond: print(" ok: %s" % label) else: failures += 1 print(" FAIL: %s" % label)