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

Editor terrain: level brush with height picker, faster orthophoto fit

This commit is contained in:
2026-10-10 01:27:48 +02:00
parent 8cd0bab27b
commit 243ce636a7
8 changed files with 233 additions and 34 deletions

View File

@@ -391,7 +391,9 @@ namespace
// ground triangles overlapping an XZ rectangle (Min/Max are x,z). shapes hold the (legacy) terrain,
// model instances at least ModelRadius large (if ModelsAsGround) the terrain tiles
void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector<world_triangle> &Out, bool const Skiproads = false)
// Heightmap: the heightmap terrain is given as triangles too (for the ones which can ask the terrain for heights, it's a waste)
void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector<world_triangle> &Out, bool const Skiproads = false,
bool const Heightmap = true)
{
auto const started = std::chrono::steady_clock::now();
++ground_question;
@@ -444,7 +446,8 @@ namespace
}
}
// the heightmap terrain answers for itself, with its full grid whatever is drawn of it
EditorTerrain.gather_triangles(Min, Max, Out);
if (Heightmap)
EditorTerrain.gather_triangles(Min, Max, Out);
trim_ground_meshes();
// going through the scenery is the one thing here which can take long enough to be felt; when it does it's worth knowing
auto const elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
@@ -533,9 +536,10 @@ editor_mode::editor_mode() {
road_select(nullptr);
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
// the roads built in the editor aren't ground: the imagery goes under them
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
gather_ground_triangles(Min, Max, true, 50.0f, Out, true);
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Heightmap, std::vector<world_triangle> &Out) {
gather_ground_triangles(Min, Max, true, 50.0f, Out, true, Heightmap);
});
m_orthophoto.height_source([](double const X, double const Z, double &Height) { return EditorTerrain.height_at(X, Z, Height); });
editor_track::observe(this);
}
@@ -1380,6 +1384,8 @@ bool editor_mode::update()
// the brush of the terrain works while the left mouse button is held
if (terrain_brush() && mouseHold)
handle_terrain_brush(deltarealtime);
else
m_terrain_stroke = false;
if (m_terrain_open)
draw_terrain_overlay();
@@ -1606,6 +1612,8 @@ void editor_mode::render_terrain_ui()
ImGui::SameLine();
radio(STR_C("Smooth"), terrain_tool::smooth, STR_C("LMB evens the terrain out under the brush"));
ImGui::SameLine();
radio(STR_C("Level"), terrain_tool::level, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"));
ImGui::SameLine();
radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"));
ImGui::SameLine();
radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"));
@@ -1650,7 +1658,23 @@ void editor_mode::render_terrain_ui()
ImGui::DragFloat(STR_C("Strength"), &m_terrain_brush_strength, 0.05f, 0.05f, 50.0f, "%.2f");
m_terrain_brush_strength = std::max(0.05f, m_terrain_brush_strength);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Sculpting: metres per second at the middle of the brush; smoothing and painting: how fast they work"));
ImGui::SetTooltip("%s", STR_C("Sculpting: metres per second at the middle of the brush; smoothing, levelling and painting: how fast they work"));
if (m_terrain_tool == terrain_tool::level)
{
ImGui::SetNextItemWidth(120.0f);
ImGui::DragFloat(STR_C("Target height (m)"), &m_terrain_target, 0.05f, -1000.0f, 5000.0f, "%.2f");
ImGui::SameLine();
if (ImGui::Button(m_terrain_picking ? STR_C("Click in the view...") : STR_C("Pick")))
m_terrain_picking = false == m_terrain_picking;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The next click takes the height of whatever is under the cursor: terrain, a track, a model.\nCtrl+LMB does the same at any time"));
char const *modes[]{STR_C("Raise and lower"), STR_C("Only raise"), STR_C("Only lower")};
ImGui::SetNextItemWidth(160.0f);
ImGui::Combo("##levelmode", &m_terrain_level_mode, modes, IM_ARRAYSIZE(modes));
ImGui::Checkbox(STR_C("Target where the stroke starts"), &m_terrain_target_from_stroke);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Each stroke takes the height of the terrain under the cursor where it starts as its target,\nand drags it along: the ground stroked over is levelled with that point"));
}
}
if (ImGui::CollapsingHeader(STR_C("Materials"), m_terrain_tool == terrain_tool::paint ? ImGuiTreeNodeFlags_DefaultOpen : 0))
@@ -1834,6 +1858,30 @@ void editor_mode::draw_terrain_overlay()
}
auto const color = m_terrain_tool == terrain_tool::paint ? IM_COL32(120, 230, 120, 220) : m_terrain_tool == terrain_tool::smooth ? IM_COL32(120, 200, 255, 220) : IM_COL32(255, 180, 80, 220);
polyline(circle, color, true, 2.0f);
if (m_terrain_tool == terrain_tool::level)
{
// the target: the rim of the brush at its height, joined with the ground, and the height by the cursor
auto const target = static_cast<double>(m_terrain_target);
std::vector<glm::dvec3> level;
for (auto const &point : circle)
level.emplace_back(point.x, target, point.z);
ImU32 const levelcolor = IM_COL32(255, 230, 90, 230);
polyline(level, levelcolor, true, 1.5f);
for (std::size_t i = 0; i < circle.size(); i += circle.size() / 8)
{
ImVec2 from, to;
if (projection.project(circle[i], from) && projection.project(level[i], to))
drawlist->AddLine(from, to, IM_COL32(255, 230, 90, 120), 1.0f);
}
ImVec2 label;
if (projection.project(glm::dvec3{cursor.x, target, cursor.z}, label))
{
double ground{cursor.y};
m_streamer.height_at(cursor.x, cursor.z, ground);
auto const text = format("%.2f m (%+.2f)", target, target - ground);
drawlist->AddText(ImVec2(label.x + 12.0f, label.y - 8.0f), levelcolor, text.c_str());
}
}
}
if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering)
{
@@ -2888,6 +2936,18 @@ void editor_mode::handle_terrain_brush(double Deltatime)
case terrain_tool::smooth:
shaped = m_streamer.smooth(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5));
break;
case terrain_tool::level:
{
if (false == m_terrain_stroke && m_terrain_target_from_stroke)
{
double height;
if (m_streamer.height_at(world.x, world.z, height))
m_terrain_target = static_cast<float>(height);
}
m_terrain_stroke = true;
shaped = m_streamer.level(world.x, world.z, m_terrain_brush_radius, m_terrain_target, std::min(1.0, rate * 0.5), m_terrain_level_mode);
break;
}
case terrain_tool::paint:
m_streamer.paint(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.25), Global.shiftState ? std::uint16_t{0} : static_cast<std::uint16_t>(m_terrain_layer));
break;
@@ -2902,6 +2962,16 @@ void editor_mode::handle_terrain_brush(double Deltatime)
}
}
bool editor_mode::pick_terrain_target()
{
glm::dvec3 const offset = GfxRenderer->Mouse_Position();
if (glm::length(offset) < 1e-3)
return false; // nothing under the cursor
m_terrain_target = static_cast<float>(Camera.Pos.y + offset.y);
m_terrain_status = format(STR_C("Target height: %.2f m"), m_terrain_target);
return true;
}
void editor_mode::capture_terrain()
{
TAnimModel *model = dynamic_cast<TAnimModel *>(m_node);
@@ -5153,6 +5223,15 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
// with a brush of the terrain the left button works the brush instead of picking nodes
if (terrain_brush() && Button == GLFW_MOUSE_BUTTON_LEFT)
{
if (m_terrain_tool == terrain_tool::level && is_press(Action) && (Global.ctrlState || m_terrain_picking))
{
// a click which picks the target height doesn't level anything
pick_terrain_target();
m_terrain_picking = false;
mouseHold = false;
m_input.mouse.button(Button, Action);
return;
}
mouseHold = is_press(Action);
m_input.mouse.button(Button, Action);
return;

View File

@@ -309,12 +309,15 @@ class editor_mode : public application_mode, private editor_track::observer
chunks, // adds a chunk, Shift: removes it
sculpt, // raises the ground, Shift: lowers it
smooth,
level, // leads the ground to the target height, Ctrl: picks the target height under the cursor
paint, // paints the chosen material, Shift: the first material of the palette
spacing, // gives the clicked chunk the chosen point spacing
water // outlines a body of water
};
terrain_tool m_terrain_tool{terrain_tool::none};
bool terrain_brush() const { return m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::paint; }
bool terrain_brush() const { return m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::level || m_terrain_tool == terrain_tool::paint; }
// takes the height of whatever is under the cursor (terrain, track, model) for the target of the level brush. returns: false if there's nothing
bool pick_terrain_target();
bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water; }
// the brush, the hovered chunk and the outlines of water drawn over the viewport
void draw_terrain_overlay();
@@ -410,6 +413,12 @@ class editor_mode : public application_mode, private editor_track::observer
int m_terrain_add_count{4}; // chunks along a side added around the camera
float m_terrain_brush_radius{12.0f};
float m_terrain_brush_strength{4.0f}; // metres per second while held; for the paint, how fast it covers
// the level brush
float m_terrain_target{0.0f}; // height the ground is led to
int m_terrain_level_mode{0}; // 0: up and down, 1: only raises, 2: only lowers
bool m_terrain_target_from_stroke{false}; // the target is the height where a stroke starts
bool m_terrain_stroke{false}; // a stroke of the brush is under way
bool m_terrain_picking{false}; // the next click picks the target height
int m_terrain_layer{0}; // entry of the palette painted
char m_terrain_material[128]{"grass"}; // material added to the palette
float m_terrain_material_size{0.0f}; // metres its textures repeat at, 0: given by the material

View File

@@ -73,9 +73,12 @@ http://mozilla.org/MPL/2.0/.
// newscenery the editor starts again with the wizard of a new scenery
// vehicles logs each vehicle: its head, facing, and what its couplers hold
// terrainmode the terrain field of work, with its texture browser
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry> as a stroke of the brush
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry>,
// terrainlevel <x> <z> <radius> <target height> [mode] as a stroke of the brush
// terrainadd <count> <spacing> chunks made around the camera, count x count, as the button of the chunks panel does it
// terrainprobe <x> <z> logs the height of the heightmap terrain and the materials of the chunk
// terrainconvert converts every material of the terrain files of the scenery
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
// screenshot
// save
// log <text>
@@ -1138,19 +1141,42 @@ void editor_mode::selftest_step()
}
}
}
else if (command == "orthophoto")
{
auto config{m_orthophoto.settings()};
int fit{1}, inscene{0};
words >> config.north >> config.east >> config.radius >> fit >> inscene;
config.drape = fit != 0;
config.in_scene = inscene != 0;
m_orthophoto.settings(config);
m_orthophoto.enabled(true);
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" : ""));
}
else if (command == "terrainmode")
{
m_terrain_open = true;
WriteLog("SELFTEST terrainmode");
}
else if (command == "terrainsculpt" || command == "terrainpaint")
else if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel")
{
double x{0.0}, z{0.0}, radius{10.0}, value{1.0};
words >> x >> z >> radius >> value;
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) : m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
int mode{0};
words >> x >> z >> radius >> value >> mode;
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value)
: command == "terrainlevel" ? m_streamer.level(x, z, radius, value, 1.0, mode)
: m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
forget_ground();
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
}
else if (command == "terrainadd")
{
int count{1};
float spacing{m_terrain_spacing};
words >> count >> spacing;
m_terrain_spacing = heightmap::valid_spacing(spacing);
add_chunks_around(std::clamp(count, 1, 8));
WriteLog("SELFTEST terrainadd: " + m_terrain_status);
}
else if (command == "terrainprobe")
{
double x{0.0}, z{0.0}, height{0.0};

View File

@@ -209,6 +209,8 @@ void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), chosen(terrain_tool::none), choose(terrain_tool::none)});
Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), chosen(terrain_tool::sculpt), choose(terrain_tool::sculpt)});
Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), chosen(terrain_tool::smooth), choose(terrain_tool::smooth)});
Buttons.push_back({icon::profile, STR_C("Level"), nullptr, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"),
chosen(terrain_tool::level), choose(terrain_tool::level)});
Buttons.push_back({icon::brush, STR_C("Paint"), nullptr, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"), chosen(terrain_tool::paint), choose(terrain_tool::paint)});
Buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB removes it"), chosen(terrain_tool::chunks), choose(terrain_tool::chunks)});
Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB gives the clicked chunk the point spacing chosen in the tool options"), chosen(terrain_tool::spacing), choose(terrain_tool::spacing)});