17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 02:49:19 +02:00

feat(editor): orthophoto lift above the ground and opacity in the scene

This commit is contained in:
2026-10-01 22:17:12 +02:00
parent adc6328a09
commit 69f5f551a1
5 changed files with 94 additions and 24 deletions

View File

@@ -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<int>(std::chrono::year_month_day{std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now())}.year());

View File

@@ -554,7 +554,6 @@ template <typename Distance_> 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<float>::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<std::mutex> 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::uint8_t>(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 &section = 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 ? &section.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<float>(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<float>(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 = &section;
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<std::uint8_t>((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<float>(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<ImTextureID>(textureid));
for (int j = 0; j < segments; ++j)

View File

@@ -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<glm::dvec3> 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<std::size_t, gfx::geometry_handle> 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;

View File

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

View File

@@ -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; }