From 69f5f551a179fffb0aa47b15c56841f78632e5c7 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 1 Oct 2026 22:17:12 +0200 Subject: [PATCH] feat(editor): orthophoto lift above the ground and opacity in the scene --- application/editormode.cpp | 19 ++++++++-- editor/editorOrthophoto.cpp | 72 ++++++++++++++++++++++++++++--------- editor/editorOrthophoto.hpp | 23 +++++++++--- editor/editorSettings.cpp | 3 ++ editor/editorSettings.hpp | 1 + 5 files changed, 94 insertions(+), 24 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index f4e1e2d2..74594118 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1174,6 +1174,7 @@ void editor_mode::load_orthophoto_settings() config.year = stored.year; config.hires = stored.hires; config.drape = stored.drape; + config.lift = stored.lift; config.in_scene = stored.in_scene; config.north = 0.0; config.east = 0.0; @@ -1192,6 +1193,7 @@ void editor_mode::save_orthophoto_settings() stored.year = config.year; stored.hires = config.hires; stored.drape = config.drape; + stored.lift = config.lift; stored.in_scene = config.in_scene; if (config.north != 0.0 || config.east != 0.0) EditorSettings.orthophoto_origin(m_orthophoto_scenery, config.north, config.east); @@ -1267,6 +1269,15 @@ void editor_mode::render_orthophoto_ui() ImGui::SetTooltip("Samples the ground again, after it was edited"); } + begin_disabled(!config.drape); + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::DragFloat("Lift above ground (m)", &config.lift, 0.01f, 0.0f, 50.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + end_disabled(!config.drape); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Raises the fitted imagery above the ground. Keep it below the objects you want to see:\n" + "in the scene everything lower than this ends up under the imagery."); + begin_disabled(config.drape); ImGui::SetNextItemWidth(160.0f); changed |= ImGui::DragFloat("Height (m)", &config.height, 0.1f, -1000.0f, 3000.0f, "%.2f"); @@ -1277,14 +1288,16 @@ void editor_mode::render_orthophoto_ui() changed = persist = true; if (ImGui::IsItemHovered()) ImGui::SetTooltip("Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\n" - "It is opaque then and lit like the scene; mouse placement lands on it as on any surface.\n" + "It is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\n" + "translucent, it doesn't catch the mouse.\n" "Not available with the Better Renderer, which doesn't pick up geometry added in the editor."); - begin_disabled(config.in_scene); ImGui::SetNextItemWidth(160.0f); changed |= ImGui::SliderFloat("Opacity", &config.opacity, 0.0f, 1.0f, "%.2f"); persist |= ImGui::IsItemDeactivatedAfterEdit(); - end_disabled(config.in_scene); + if (config.in_scene && ImGui::IsItemHovered()) + ImGui::SetTooltip("In the scene the opacity is part of the textures: the tiles are read again from the cache\n" + "shortly after the slider stops, which takes a moment."); // newest imagery, or the newest imagery taken up to the end of the selected year int const thisyear = static_cast(std::chrono::year_month_day{std::chrono::floor(std::chrono::system_clock::now())}.year()); diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index 3d478eb9..935fd825 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -554,7 +554,6 @@ template int clip_polygon(clip_vertex const *In, int Count, // ground fit and in-scene geometry constexpr int ground_grid{32}; // ground samples per tile side (8 m) for the draped overlay -constexpr double surface_offset{0.05}; // lift of the in-scene imagery above the ground it covers, against z-fighting constexpr double steepest_ground{0.3}; // minimal normal.y of ground triangles receiving the imagery constexpr float no_height{-std::numeric_limits::max()}; @@ -684,8 +683,9 @@ void editor_orthophoto::enabled(bool State) void editor_orthophoto::settings(config const &Config) { bool const sourcechanged = (Config.year != m_config.year || Config.hires != m_config.hires); - bool const shapechanged = (Config.height != m_config.height || Config.drape != m_config.drape || Config.in_scene != m_config.in_scene); + bool const shapechanged = (Config.height != m_config.height || Config.drape != m_config.drape || Config.in_scene != m_config.in_scene || Config.lift != m_config.lift); m_config = Config; + m_config.lift = std::clamp(m_config.lift, 0.0f, 50.0f); m_config.radius = std::clamp(m_config.radius, 0, max_radius); m_config.opacity = std::clamp(m_config.opacity, 0.0f, 1.0f); if (shapechanged) @@ -812,17 +812,24 @@ void editor_orthophoto::release_tile(tile &Tile) Tile.slot = texture_slot{}; } -void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level) +void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha) { { std::lock_guard lock(m_mutex); m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key; }), m_queue.end()); - m_queue.push_back(job{Key, Level, source{m_config.year, m_config.hires}, m_generation.load()}); + m_queue.push_back(job{Key, Level, Alpha, source{m_config.year, m_config.hires}, m_generation.load()}); } Tile.requested = Level; + Tile.requested_alpha = Alpha; m_wakeup.notify_one(); } +std::uint8_t editor_orthophoto::texture_alpha() const +{ + // the overlay applies its opacity when drawing, the scene shapes can only take it from the texture + return m_config.in_scene ? static_cast(std::lround(m_config.opacity * 255.0f)) : 255; +} + std::size_t editor_orthophoto::apply(result &Result, double Now) { if (Result.generation != m_generation.load()) @@ -831,7 +838,7 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) if (lookup == m_tiles.end()) return 0; // left the area in the meantime; the imagery stays in the disk cache tile &t = lookup->second; - bool const current = (t.requested == Result.level); + bool const current = (t.requested == Result.level && t.requested_alpha == Result.alpha); if (current) t.requested = 0; @@ -847,6 +854,9 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) t.failures = 0; t.level = Result.level; + if ((t.alpha < 255) != (Result.alpha < 255)) + t.scene_dirty = true; // the shape moves between the opaque and the translucent pass + t.alpha = Result.alpha; t.empty = Result.empty; if (Result.empty) { @@ -974,7 +984,7 @@ void editor_orthophoto::remove_shape(tile &Tile) return; if (m_region == simulation::Region) { - auto &shapes = Tile.section->m_shapes; + auto &shapes = (Tile.cell != nullptr ? Tile.cell->m_shapestranslucent : Tile.section->m_shapes); for (auto it = shapes.begin(); it != shapes.end(); ++it) { auto const &geometry = it->data().geometry; @@ -986,6 +996,7 @@ void editor_orthophoto::remove_shape(tile &Tile) } } Tile.section = nullptr; + Tile.cell = nullptr; } void editor_orthophoto::release_geometry(tile &Tile) @@ -1024,9 +1035,9 @@ void editor_orthophoto::build_shape(tile_key const &Key, tile &Tile) { vertices.reserve(Tile.surface.size()); for (auto const &point : Tile.surface) - add(point, surface_offset); + add(point, m_config.lift); ylo = Tile.height_min; - yhi = Tile.height_max + surface_offset; + yhi = Tile.height_max + m_config.lift; } else { @@ -1042,7 +1053,11 @@ void editor_orthophoto::build_shape(tile_key const &Key, tile &Tile) glm::dvec3 const centre(corner.x - tile_size * 0.5, (ylo + yhi) * 0.5, corner.y - tile_size * 0.5); scene::basic_section §ion = simulation::Region->section(centre); section.create_geometry(); // existing section geometry has to be in place before we add ours (idempotent) - glm::dvec3 const origin = section.m_area.center; + // opaque imagery goes with the section geometry; translucent one has to be drawn in the translucent pass, + // which only covers the cells. shapes are drawn relative to the centre of their container + bool const translucent = (Tile.alpha < 255); + scene::basic_cell *cell = translucent ? §ion.cell(centre) : nullptr; + glm::dvec3 const origin = translucent ? cell->m_area.center : section.m_area.center; // chunks are padded to a few fixed sizes (whole triangles), so ones freed by other tiles can be refilled in place std::size_t capacity{6}; @@ -1096,12 +1111,22 @@ void editor_orthophoto::build_shape(tile_key const &Key, tile &Tile) scene::shape_node shape; shape.make_terrain(Tile.slot.material, std::move(bounds), origin); shape.geometry(Tile.geometry); - section.m_shapes.emplace_back(std::move(shape)); - // extend the section bounds so the tile isn't culled at its edges + // extend the bounds of the containers so the tile isn't culled at its edges double const reach = glm::length(glm::dvec3(tile_size * 0.5, (yhi - ylo) * 0.5, tile_size * 0.5)); section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - centre) + reach)); + if (cell != nullptr) + { + cell->m_shapestranslucent.emplace_back(std::move(shape)); + cell->m_area.radius = std::max(cell->m_area.radius, static_cast(glm::length(cell->m_area.center - centre) + reach)); + cell->m_active = true; // the renderer skips cells which held nothing so far + } + else + { + section.m_shapes.emplace_back(std::move(shape)); + } Tile.section = §ion; + Tile.cell = cell; m_region = simulation::Region; } @@ -1171,6 +1196,16 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) int const radius = m_config.radius; auto const distance = [&](tile_key const &Key) { return std::max(std::abs(Key.first - centre.first), std::abs(Key.second - centre.second)); }; + // opacity of the scene shapes: the tiles are read again with the new alpha once the slider stops moving + std::uint8_t const alpha = texture_alpha(); + if (alpha != m_alpha_pending) + { + m_alpha_pending = alpha; + m_alpha_changed = now; + } + if (m_alpha != m_alpha_pending && now - m_alpha_changed > 0.3) + m_alpha = m_alpha_pending; + // tiles which left the area bool evicted{false}; for (auto it = m_tiles.begin(); it != m_tiles.end();) @@ -1199,9 +1234,9 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) if (!in_service_area(key.first, key.second)) t.empty = true; int const level = level_for(std::max(std::abs(dx), std::abs(dy)), m_config.hires); - if (t.empty || t.level == level || t.requested == level || now < t.retry_at) + if (t.empty || (t.level == level && t.alpha == m_alpha) || (t.requested == level && t.requested_alpha == m_alpha) || now < t.retry_at) continue; - schedule(key, t, level); + schedule(key, t, level, m_alpha); } // ground fit, one tile per frame (it walks the scene geometry), nearest first @@ -1265,6 +1300,7 @@ void editor_orthophoto::worker() out.key = current.key; out.level = current.level; out.generation = current.generation; + out.alpha = current.alpha; image picture; std::string error; @@ -1274,6 +1310,9 @@ void editor_orthophoto::worker() return; if (!out.ok) ErrorLog("Editor orthophoto: tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); + if (out.ok && current.alpha < 255) + for (std::size_t i = 3; i < picture.rgba.size(); i += 4) + picture.rgba[i] = static_cast((picture.rgba[i] * current.alpha + 127u) / 255u); out.width = picture.width; out.height = picture.height; out.pixels = std::move(picture.rgba); @@ -1320,8 +1359,9 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const & bool const draped = m_config.drape && !t.heights.empty(); double const flat = m_config.height; - double const ylo = draped ? t.height_min : flat; - double const yhi = draped ? t.height_max : flat; + double const lift = m_config.lift; + double const ylo = draped ? t.height_min + lift : flat; + double const yhi = draped ? t.height_max + lift : flat; // world extent of the tile; u runs west->east (-x), v runs north->south (-z), matching the image rows glm::dvec2 const corner = tile_corner(entry.first); @@ -1346,7 +1386,7 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const & if (draped) segments = std::max(segments, distance < 300.0 ? ground_grid : distance < 800.0 ? ground_grid / 2 : ground_grid / 4); float const segmentsize = 1.0f / static_cast(segments); - auto const height_at = [&](float U, float V) { return draped ? sample_heights(t.heights, U, V) : flat; }; + auto const height_at = [&](float U, float V) { return draped ? sample_heights(t.heights, U, V) + lift : flat; }; drawlist->PushTextureID(reinterpret_cast(textureid)); for (int j = 0; j < segments; ++j) diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp index 5f246db4..cfaf925a 100644 --- a/editor/editorOrthophoto.hpp +++ b/editor/editorOrthophoto.hpp @@ -31,6 +31,7 @@ namespace scene { class basic_region; class basic_section; +class basic_cell; } // namespace scene // Editor aid: aerial orthophoto tiles from geoportal.gov.pl (GUGiK WMS), shown around the camera either @@ -43,8 +44,9 @@ class basic_section; // Downloading, cache I/O and image decoding run on worker threads; the main thread only uploads // finished images (a bounded amount per frame) and emits the geometry. By default the layer goes into // the ImGui background draw list, like the other editor viewport overlays: translucent and drawn over -// everything. Optionally it is put into the scene instead (opaque shapes in the region sections, like the -// editor terrain), so tracks, models and terrain in front of it cover it. +// everything. Optionally it is put into the scene instead (shapes in the region sections, like the editor +// terrain, or translucent shapes in the cells when it isn't fully opaque), so tracks, models and terrain in +// front of it cover it. class editor_orthophoto { public: @@ -54,10 +56,11 @@ class editor_orthophoto double east{0.0}; // PUWG 1992 Y (easting) of the scenery origin int radius{2}; // tiles loaded around the camera tile (Chebyshev distance) float height{0.0f}; // world Y of the layer (with drape: only where no ground was found) - float opacity{0.6f}; // 0..1, overlay only + float opacity{0.6f}; // 0..1 int year{0}; // 0 = newest available imagery, otherwise newest imagery taken up to the end of that year bool hires{false}; // 4096 px tiles from the high-resolution product next to the camera, where it exists bool drape{true}; // fit the imagery onto the ground geometry instead of a flat plane + float lift{0.05f}; // with drape: distance above the ground (in the scene it has to clear the ground to be seen) bool in_scene{false}; // render as opaque scene geometry, covered by whatever is in front of it }; @@ -122,7 +125,8 @@ class editor_orthophoto struct job { tile_key key; - int level{0}; // requested texture size in pixels + int level{0}; // requested texture size in pixels + std::uint8_t alpha{255}; // baked into the texture, for translucency in the scene source src; std::uint32_t generation{0}; }; @@ -131,6 +135,7 @@ class editor_orthophoto { tile_key key; int level{0}; + std::uint8_t alpha{255}; std::uint32_t generation{0}; bool ok{false}; bool empty{false}; // no imagery for this place/year: nothing to draw @@ -151,6 +156,8 @@ class editor_orthophoto texture_slot slot; int level{0}; // size of the imagery currently held (0 = nothing yet) int requested{0}; // level of the job scheduled for this tile (0 = none) + std::uint8_t alpha{255}; // alpha of the imagery currently held + std::uint8_t requested_alpha{255}; // alpha of the scheduled job bool empty{false}; int failures{0}; double retry_at{0.0}; // steady clock seconds @@ -162,6 +169,7 @@ class editor_orthophoto std::vector surface; // upward facing ground triangles clipped to the tile (3 vertices each) // scene geometry scene::basic_section *section{nullptr}; // section holding the shape (nullptr = not in the scene) + scene::basic_cell *cell{nullptr}; // set instead of the section's shape list when the shape is translucent gfx::geometry_handle geometry{0, 0}; std::size_t capacity{0}; // vertex count of the geometry chunk bool scene_dirty{true}; // the shape needs to be rebuilt @@ -170,7 +178,8 @@ class editor_orthophoto static int level_for(int Distance, bool Hires); tile_key camera_tile(glm::dvec3 const &Camera) const; glm::dvec2 tile_corner(tile_key const &Key) const; // world (x,z) of the north-west corner - void schedule(tile_key const &Key, tile &Tile, int Level); + void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha); + std::uint8_t texture_alpha() const; // alpha the textures should carry with the current settings std::size_t apply(result &Result, double Now); // returns bytes uploaded void release_tile(tile &Tile); texture_slot acquire_slot(); @@ -191,6 +200,10 @@ class editor_orthophoto ground_query m_ground; scene::basic_region *m_region{nullptr}; // region the scene geometry was put into std::multimap m_free_chunks; // geometry chunks by capacity, reused between tiles + // the scene opacity is baked into the textures, which means reading the tiles again; applied once the value settles + std::uint8_t m_alpha{255}; // in effect + std::uint8_t m_alpha_pending{255}; // latest requested + double m_alpha_changed{0.0}; // when the requested value last changed // shared with the workers, guarded by m_mutex mutable std::mutex m_mutex; diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 3e7516c1..f9725818 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -88,6 +88,8 @@ bool editorSettings::load() m_orthophoto.hires = (value == "1"); else if (key == "ortho_drape") m_orthophoto.drape = (value == "1"); + else if (key == "ortho_lift") + parse(value, m_orthophoto.lift); else if (key == "ortho_in_scene") m_orthophoto.in_scene = (value == "1"); else if (key.rfind(origin_prefix, 0) == 0) @@ -126,6 +128,7 @@ bool editorSettings::save() stream << "ortho_year " << m_orthophoto.year << "\n"; stream << "ortho_4k " << (m_orthophoto.hires ? 1 : 0) << "\n"; stream << "ortho_drape " << (m_orthophoto.drape ? 1 : 0) << "\n"; + stream << "ortho_lift " << m_orthophoto.lift << "\n"; stream << "ortho_in_scene " << (m_orthophoto.in_scene ? 1 : 0) << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index 9d46a000..b43410fc 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -31,6 +31,7 @@ public: int year{0}; bool hires{false}; bool drape{true}; + float lift{0.05f}; bool in_scene{false}; }; orthophoto_settings &orthophoto() { return m_orthophoto; }