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:
@@ -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());
|
||||
|
||||
@@ -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 §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<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 = §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<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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user