17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-29 04:37:30 +01:00

feat(editor): heightmap terrain from the terrain model of GUGiK, fitted to the tracks of the scenery

- editorNmt: squares of 500 m of the NMT at 1 m from the WCS service of geoportal.gov.pl, as GeoTIFF, fetched in the
  background, kept in editor/cache/nmt and sampled in the coordinates of the scenery, placed as its orthophoto is
- the terrain panel's Terrain model section: Fit to the tracks moves the model up or down by the median of the
  differences to the formation of the tracks (Biała Podlaska, built at 0: -145.73 m, 0.28 m around it); Along the tracks
  and Around the camera make the chunks as the model comes in; each path gets its formation and slopes 1:1.5 as a modifier
  of the terrain, laid over each other into one corridor, bridges and tunnels keep the model; the orthophoto is turned
  on and laid onto the new ground
- the http client of the orthophoto in editorHttp, shared; fill_from_nearest in editormodeshared.h
- after the generator of Hirek (MaszynaTerrainGenerator): the WCS requests, the cache of squares, the corridor by envelopes
- self-tests nmtfit, nmtmake, nmtwait
This commit is contained in:
maj00r
2026-10-18 12:56:25 +02:00
parent 16348c655a
commit 5af699fba4
12 changed files with 1245 additions and 234 deletions

View File

@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
#include <fstream>
#include <regex>
#include "application/editormode.h"
#include "application/editormodeshared.h"
#include "scene/scenenodegroups.h"
#include "application/editoruilayer.h"
#ifndef _WIN32
@@ -1396,6 +1397,7 @@ bool editor_mode::update()
// --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays ---
m_orthophoto.update(Camera.Pos);
nmt_update();
draw_orthophoto();
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
@@ -1561,35 +1563,6 @@ namespace
return changed;
}
// grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid)
template <typename Value_> void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
{
std::deque<std::size_t> queue;
for (std::size_t index = 0; index < Found.size(); ++index)
if (Found[index] != 0)
queue.push_back(index);
while (false == queue.empty())
{
auto const index = queue.front();
queue.pop_front();
auto const ix = index % Side, iz = index / Side;
auto const visit = [&](std::size_t const Next) {
if (Found[Next] != 0)
return;
Found[Next] = 1;
Heights[Next] = Heights[index];
queue.push_back(Next);
};
if (ix > 0)
visit(index - 1);
if (ix + 1 < Side)
visit(index + 1);
if (iz > 0)
visit(index - Side);
if (iz + 1 < Side)
visit(index + Side);
}
}
} // namespace
void editor_mode::render_terrain_ui()
@@ -1665,6 +1638,9 @@ void editor_mode::render_terrain_ui()
}
}
if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen))
render_nmt_ui();
if (ImGui::CollapsingHeader(STR_C("Chunk resolution"), m_terrain_tool == terrain_tool::spacing ? ImGuiTreeNodeFlags_DefaultOpen : 0))
{
// chunks removed since are no longer selected

View File

@@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorTerrain.hpp"
#include "editor/editorTerrainStreamer.hpp"
#include "editor/editorOrthophoto.hpp"
#include "editor/editorNmt.hpp"
#include "editor/editorTrack.hpp"
#include "editor/editorConnections.hpp"
#include "editor/editorAlignment.hpp"
@@ -319,6 +320,48 @@ class editor_mode : public application_mode, private editor_track::observer
// adds a chunk of the terrain shaped as the ground under it; where there's no ground, carried on level with the nearest point
// of it, or at the base height. returns: false if there's a chunk already
bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0);
// the terrain from the digital terrain model of GUGiK (editormodenmt.cpp)
struct nmt_place
{
glm::dvec3 position{0.0}; // on the axis of the path
double formation{0.0}; // the height of the formation under it
double half_width{3.0}; // from the axis to the foot of the ballast
TTrack *track{nullptr};
int path{0};
bool shapes{true}; // not a bridge nor a tunnel
};
struct nmt_state
{
double offset{0.0}; // added to the heights of the model, so they meet the tracks of the scenery
bool fitted{false};
bool fitting{false}; // waiting for the model under the places of the tracks the offset is worked out from
std::vector<nmt_place> fit;
double spread{0.0}; // how much the tracks differ from the model about the offset, m
std::size_t samples{0};
double band{300.0}; // m from the tracks the chunks made along them reach
bool shape{true}; // the formation and the slopes of the tracks as modifiers of the terrain
double slope{1.5};
double reach{60.0};
std::vector<heightmap::chunk_key> pending; // chunks to make once their model is in
std::size_t made{0};
std::size_t shaped{0};
std::string status;
std::string error;
};
editor_nmt m_nmt;
nmt_state m_nmt_state;
std::vector<nmt_place> nmt_track_places(double const Step) const;
void nmt_locate();
void nmt_fit();
void nmt_finish_fit();
std::vector<heightmap::chunk_key> nmt_keys_around() const;
std::vector<heightmap::chunk_key> nmt_keys_along_tracks() const;
void nmt_make(std::vector<heightmap::chunk_key> const &Keys);
bool add_chunk_from_nmt(heightmap::chunk_key const &Key);
void nmt_shape_tracks();
void nmt_update();
void render_nmt_ui();
// the tool of the terrain the left mouse button works
enum class terrain_tool
{

View File

@@ -0,0 +1,337 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
// the heightmap terrain made from the digital terrain model of GUGiK: the scenery placed as its orthophoto is, the heights
// of the model moved up or down to meet the tracks it has already, the chunks around the camera or along the tracks made as
// the model comes in, and the formation of each track with its slopes kept as a modifier of the terrain
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "utilities/translation.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <cmath>
#include <set>
namespace
{
// how many places of the tracks the offset of the model is worked out from at most, each needing its square of the model
std::size_t constexpr fit_places{48};
bool rail(TTrack const &Track)
{
return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && false == Track.m_paths.empty();
}
// a bridge or a tunnel keeps the model under it as it is
bool shapes_ground(TTrack const &Track)
{
return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel;
}
} // namespace
std::vector<editor_mode::nmt_place> editor_mode::nmt_track_places(double const Step) const
{
std::vector<nmt_place> result;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track))
continue;
auto const ballast{profile_ballast(*track)};
for (std::size_t path = 0; path < track->m_paths.size(); ++path)
{
geometry::bezier const curve{track->m_paths[path]};
auto const &points{track->m_paths[path].points};
auto const chord{glm::distance(points[segment_data::point::start], points[segment_data::point::end])};
auto const count{std::max(1, static_cast<int>(std::ceil(chord * 1.1 / Step)))};
for (int i = 0; i <= count; ++i)
{
auto const at{curve.point(static_cast<double>(i) / count)};
result.push_back({at, at.y - ballast.first, ballast.second, track, static_cast<int>(path), shapes_ground(*track)});
}
}
}
return result;
}
void editor_mode::nmt_locate()
{
auto const &config{m_orthophoto.settings()};
m_nmt.place(config.north, config.east);
}
void editor_mode::nmt_fit()
{
nmt_locate();
auto &state{m_nmt_state};
state.fit.clear();
auto const places{nmt_track_places(25.0)};
std::vector<nmt_place> shaped;
std::copy_if(places.begin(), places.end(), std::back_inserter(shaped), [](nmt_place const &Place) { return Place.shapes; });
if (shaped.empty())
{
state.offset = 0.0;
state.fitted = true;
state.status = STR("No tracks to fit the terrain model to: its heights are taken as they are");
return;
}
// spread over the whole scenery, each place asking for its square of the model
auto const every{std::max<std::size_t>(1, shaped.size() / fit_places)};
for (std::size_t i = 0; i < shaped.size(); i += every)
state.fit.push_back(shaped[i]);
state.fitting = true;
state.status = STR("Fetching the terrain model under the tracks...");
}
void editor_mode::nmt_finish_fit()
{
auto &state{m_nmt_state};
std::vector<double> differences;
for (auto const &place : state.fit)
if (double height; m_nmt.height(place.position.x, place.position.z, height))
differences.push_back(place.formation - height);
state.fitting = false;
if (differences.empty())
{
state.status = STR("The terrain model has no data under the tracks: is the scenery placed where it is?");
return;
}
std::sort(differences.begin(), differences.end());
state.offset = differences[differences.size() / 2];
std::vector<double> deviations;
for (auto const difference : differences)
deviations.push_back(std::abs(difference - state.offset));
std::sort(deviations.begin(), deviations.end());
state.spread = deviations[deviations.size() / 2];
state.fitted = true;
state.samples = differences.size();
state.status = format(STR_C("The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)"), state.offset, state.samples, state.spread);
if (state.spread > 10.0)
state.status += STR(". That's far too much: the place of the scenery is likely wrong, check the orthophoto");
WriteLog("Editor: NMT - " + state.status, logtype::generic);
}
std::vector<heightmap::chunk_key> editor_mode::nmt_keys_around() const
{
std::vector<heightmap::chunk_key> result;
auto const centre{heightmap::chunk_at(Camera.Pos.x, Camera.Pos.z)};
auto const first{-(m_terrain_add_count - 1) / 2};
for (int dz = 0; dz < m_terrain_add_count; ++dz)
for (int dx = 0; dx < m_terrain_add_count; ++dx)
result.push_back({centre.first + first + dx, centre.second + first + dz});
return result;
}
std::vector<heightmap::chunk_key> editor_mode::nmt_keys_along_tracks() const
{
std::set<heightmap::chunk_key> keys;
auto const reach{static_cast<int>(std::ceil(m_nmt_state.band / heightmap::chunk_size))};
for (auto const &place : nmt_track_places(heightmap::chunk_size * 0.5))
{
auto const at{heightmap::chunk_at(place.position.x, place.position.z)};
for (int dz = -reach; dz <= reach; ++dz)
for (int dx = -reach; dx <= reach; ++dx)
{
heightmap::chunk_key const key{at.first + dx, at.second + dz};
auto const low{heightmap::chunk_corner(key)};
// the chunk reaches within the band of the track
glm::dvec2 const nearest{std::clamp(place.position.x, low.x, low.x + heightmap::chunk_size), std::clamp(place.position.z, low.y, low.y + heightmap::chunk_size)};
if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band)
keys.insert(key);
}
}
return {keys.begin(), keys.end()};
}
void editor_mode::nmt_make(std::vector<heightmap::chunk_key> const &Keys)
{
auto &state{m_nmt_state};
if (false == ensure_terrain())
{
state.status = STR("The heightmap terrain can't be opened");
return;
}
nmt_locate();
std::size_t queued{0};
for (auto const &key : Keys)
if (false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end())
{
state.pending.push_back(key);
++queued;
}
state.made = 0;
// the terrain comes with its photos
if (queued > 0 && false == m_orthophoto.enabled())
m_orthophoto.enabled(true);
state.status = queued > 0 ? format(STR_C("%zu chunks to make from the terrain model"), queued) : STR("There's terrain there already");
}
bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key)
{
auto const low{heightmap::chunk_corner(Key)};
auto const high{low + glm::dvec2{heightmap::chunk_size}};
if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0}))
return false;
auto const cells{heightmap::cells_for(m_terrain_spacing)};
auto const side{static_cast<std::size_t>(cells) + 1};
auto const step{heightmap::chunk_size / cells};
std::vector<double> heights(side * side, m_terrain_baseheight);
std::vector<char> found(side * side, 0);
bool any{false};
for (std::size_t iz = 0; iz < side; ++iz)
for (std::size_t ix = 0; ix < side; ++ix)
{
auto const index{iz * side + ix};
if (double height; m_nmt.height(low.x + ix * step, low.y + iz * step, height))
{
heights[index] = height + m_nmt_state.offset;
found[index] = 1;
any = true;
}
}
// where the model has no data the chunk goes on level with its nearest point
if (any)
fill_from_nearest(heights, found, side);
m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) {
auto const ix{static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / step), 0, cells))};
auto const iz{static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / step), 0, cells))};
return static_cast<float>(heights[iz * side + ix]);
});
return true;
}
void editor_mode::nmt_shape_tracks()
{
auto &state{m_nmt_state};
if (false == ensure_terrain())
return;
// one modifier per path: the formation under its axis and the slopes from its edges to the ground. laid over each other the
// modifiers keep the highest of the slopes going down and the lowest of those going up, so tracks side by side share one
// formation instead of a saw of their own ones
std::map<std::pair<TTrack *, int>, heightmap::modifier> modifiers;
double constexpr step{5.0};
for (auto const &place : nmt_track_places(step))
{
if (false == place.shapes)
continue;
auto &modifier{modifiers[{place.track, place.path}]};
if (modifier.name.empty())
{
modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{});
modifier.slope = state.slope;
modifier.reach = state.reach;
}
modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width});
}
for (auto &[path, modifier] : modifiers)
if (modifier.points.size() >= 2)
m_streamer.modifier(std::move(modifier));
forget_ground();
state.shaped = modifiers.size();
}
void editor_mode::nmt_update()
{
auto &state{m_nmt_state};
if (state.fitting)
{
bool all{true};
for (auto const &place : state.fit)
all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all;
if (all)
nmt_finish_fit();
return;
}
if (state.pending.empty())
return;
// a few chunks a frame, those whose model is in
int made{0};
for (auto it{state.pending.begin()}; it != state.pending.end() && made < 4;)
{
if (m_streamer.exists(*it))
{
it = state.pending.erase(it);
continue;
}
if (add_chunk_from_nmt(*it))
{
it = state.pending.erase(it);
++made;
++state.made;
continue;
}
++it;
}
if (made > 0)
{
forget_ground();
state.status = format(STR_C("Chunks made from the terrain model: %zu, waiting for it: %zu"), state.made, state.pending.size());
if (state.pending.empty())
{
if (state.shape)
{
nmt_shape_tracks();
state.status += format(STR_C("; the formation of %zu paths shaped"), state.shaped);
}
// the photos laid onto the new ground
m_orthophoto.refit();
}
}
if (auto const error{m_nmt.error()}; false == error.empty())
state.error = error;
}
void editor_mode::render_nmt_ui()
{
auto &state{m_nmt_state};
nmt_locate();
if (false == m_nmt.placed())
{
ImGui::TextWrapped("%s", STR_C("The scenery isn't placed anywhere: give its place in the settings of the orthophoto first"));
if (ImGui::Button(STR_C("Orthophoto settings...")))
m_orthophoto_window = true;
return;
}
auto const &config{m_orthophoto.settings()};
ImGui::TextDisabled(STR_C("Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto"), config.north, config.east);
if (ImGui::Button(STR_C("Fit to the tracks")))
nmt_fit();
item_tooltip("Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs");
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::InputDouble(STR_C("Height offset (m)"), &state.offset, 0.0, 0.0, "%.2f");
ImGui::SetNextItemWidth(110.0f);
ImGui::InputDouble(STR_C("Band along the tracks (m)"), &state.band, 0.0, 0.0, "%.0f");
state.band = std::clamp(state.band, 50.0, 3000.0);
ImGui::Checkbox(STR_C("Formation and slopes along the tracks"), &state.shape);
item_tooltip("Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them");
ImGui::BeginDisabled(state.fitting);
if (ImGui::Button(STR_C("Along the tracks")))
nmt_make(nmt_keys_along_tracks());
ImGui::SameLine();
if (ImGui::Button(STR_C("Around the camera")))
nmt_make(nmt_keys_around());
ImGui::EndDisabled();
if (false == state.pending.empty() || m_nmt.waiting() > 0)
ImGui::TextDisabled(STR_C("Squares of the model being fetched: %zu"), m_nmt.waiting());
if (false == state.status.empty())
ImGui::TextWrapped("%s", state.status.c_str());
if (false == state.error.empty())
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str());
ImGui::TextDisabled("%s", STR_C("Terrain model: GUGiK, geoportal.gov.pl"));
}

View File

@@ -63,6 +63,36 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co
WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : ""));
return selftest_result::handled;
}
if (command == "nmtfit")
{
// the heights of the terrain model fitted to the tracks, as the button of the terrain panel does
nmt_fit();
WriteLog("SELFTEST nmtfit: " + m_nmt_state.status);
return selftest_result::handled;
}
if (command == "nmtmake")
{
// chunks from the terrain model along the tracks (with the band in metres) or around the camera
std::string where;
double band{0.0};
words >> where >> band;
if (band > 0.0)
m_nmt_state.band = band;
nmt_make(where == "around" ? nmt_keys_around() : nmt_keys_along_tracks());
WriteLog("SELFTEST nmtmake " + where + ": " + m_nmt_state.status);
return selftest_result::handled;
}
if (command == "nmtwait")
{
// till the model is in and the chunks waiting for it are made
if (m_nmt_state.fitting || false == m_nmt_state.pending.empty())
{
--m_selftest.next;
return selftest_result::pause;
}
WriteLog("SELFTEST nmtwait: " + m_nmt_state.status + (m_nmt_state.error.empty() ? std::string{} : " (" + m_nmt_state.error + ")"));
return selftest_result::handled;
}
if (command == "terrainmode")
{
m_terrain_open = true;

View File

@@ -9,6 +9,9 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <deque>
#include <vector>
#include "imgui/imgui.h"
#include "utilities/translation.h"
@@ -19,3 +22,33 @@ inline void item_tooltip(char const *Text)
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C(Text));
}
// grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid)
template <typename Value_> inline void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
{
std::deque<std::size_t> queue;
for (std::size_t index = 0; index < Found.size(); ++index)
if (Found[index] != 0)
queue.push_back(index);
while (false == queue.empty())
{
auto const index = queue.front();
queue.pop_front();
auto const ix = index % Side, iz = index / Side;
auto const visit = [&](std::size_t const Next) {
if (Found[Next] != 0)
return;
Found[Next] = 1;
Heights[Next] = Heights[index];
queue.push_back(Next);
};
if (ix > 0)
visit(index - 1);
if (ix + 1 < Side)
visit(index + 1);
if (iz > 0)
visit(index - Side);
if (iz + 1 < Side)
visit(index + Side);
}
}