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

Editor terrain: soft stitching to tracks, chunk resolution conversion, water materials

This commit is contained in:
2026-10-10 16:08:12 +02:00
parent 243ce636a7
commit 63302c1ace
8 changed files with 613 additions and 61 deletions

View File

@@ -920,6 +920,7 @@ void editor_mode::restore_terrain(EditorSnapshot &Snapshot)
Snapshot.terrain = std::make_shared<terrain_streamer::change const>(std::move(opposite));
forget_ground();
m_water_selected = -1;
m_water_material_of = -1;
}
void editor_mode::undo_last()
@@ -1607,7 +1608,9 @@ void editor_mode::render_terrain_ui()
ImGui::SameLine();
radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk"));
ImGui::SameLine();
radio(STR_C("Spacing"), terrain_tool::spacing, STR_C("LMB gives the clicked chunk the point spacing chosen below, the ground resampled"));
radio(STR_C("Spacing"), terrain_tool::spacing, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds it to the selection or takes it away;\nthe point spacing of the selected chunks is converted in \"Chunk resolution\" below"));
ImGui::SameLine();
radio(STR_C("Restore"), terrain_tool::restore, STR_C("LMB takes back the touch-ups made over the tracks under the brush:\nthe terrain returns to the shape the modifiers (tracks) give it"));
radio(STR_C("Sculpt"), terrain_tool::sculpt, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"));
ImGui::SameLine();
radio(STR_C("Smooth"), terrain_tool::smooth, STR_C("LMB evens the terrain out under the brush"));
@@ -1625,7 +1628,7 @@ void editor_mode::render_terrain_ui()
if (spacing_combo(STR_C("Point spacing"), m_terrain_spacing))
m_streamer.default_spacing(m_terrain_spacing);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Spacing of the grid points of the chunks made, and the one the spacing tool gives the clicked chunk.\n"
ImGui::SetTooltip("%s", STR_C("Spacing of the grid points of the chunks made.\n"
"A chunk is 250 x 250 m; finer grids take more memory and time to draw."));
ImGui::SetNextItemWidth(120.0f);
ImGui::InputFloat(STR_C("Base height (m)"), &m_terrain_baseheight, 0.0f, 0.0f, "%.2f");
@@ -1649,6 +1652,56 @@ void editor_mode::render_terrain_ui()
}
}
if (ImGui::CollapsingHeader(STR_C("Chunk resolution"), m_terrain_tool == terrain_tool::spacing ? ImGuiTreeNodeFlags_DefaultOpen : 0))
{
// chunks removed since are no longer selected
for (auto it = m_terrain_selection.begin(); it != m_terrain_selection.end();)
it = m_streamer.exists(*it) ? std::next(it) : m_terrain_selection.erase(it);
if (m_terrain_selection.empty())
ImGui::TextWrapped("%s", STR_C("No chunk selected. With the Spacing tool LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away."));
else
{
bool coarser = false;
std::size_t points = 0, after = 0;
int const cells = heightmap::cells_for(m_terrain_convert_spacing);
ImGui::BeginChild("##chunkselection", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * std::min<float>(6.0f, static_cast<float>(m_terrain_selection.size()) + 0.5f)), true);
for (auto const &key : m_terrain_selection)
{
auto const centre = heightmap::chunk_corner(key) + glm::dvec2{heightmap::chunk_size * 0.5};
auto const *chunk = m_streamer.terrain_at(centre.x, centre.y);
if (chunk == nullptr)
{
ImGui::TextDisabled(STR_C("%d,%d: not loaded"), key.first, key.second);
continue;
}
auto const &data = chunk->data();
coarser |= cells < data.cells;
points += static_cast<std::size_t>(data.side()) * data.side();
after += static_cast<std::size_t>(cells + 1) * (cells + 1);
auto const paint = data.paint > 0 ? format(STR_C(", paint every %.2f m"), heightmap::chunk_size / data.paint) : std::string(STR_C(", one material"));
auto const touchups = data.adjust.empty() ? std::string() : std::string(STR_C(", touch-ups"));
ImGui::Text("%d,%d: %s%s%s", key.first, key.second, spacing_label(static_cast<float>(data.spacing())), paint.c_str(), touchups.c_str());
}
ImGui::EndChild();
spacing_combo(STR_C("Convert to"), m_terrain_convert_spacing);
ImGui::SameLine();
if (ImGui::Button(STR_C("Convert")))
convert_selected_chunks();
ImGui::SameLine();
if (ImGui::Button(STR_C("Clear the selection")))
m_terrain_selection.clear();
ImGui::Checkbox(STR_C("Paint every 0.5 m with 0.5 m spacing"), &m_terrain_convert_paint);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Chunks converted to 0.5 m get their paint samples every 0.5 m as well (every metre otherwise),\n"
"so the materials can be painted as finely as the ground is shaped"));
if (points > 0)
ImGui::TextDisabled(STR_C("Grid points: %zu -> %zu"), points, after);
if (coarser)
ImGui::TextWrapped("%s", STR_C("A coarser grid can't hold the shapes smaller than its spacing: they're averaged out. "
"The edges of the chunks keep their heights, so the neighbours still meet them. Ctrl+Z takes it back."));
}
}
if (terrain_brush() && ImGui::CollapsingHeader(STR_C("Brush"), ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SetNextItemWidth(120.0f);
@@ -1658,7 +1711,18 @@ 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, levelling and painting: how fast they work"));
ImGui::SetTooltip("%s", STR_C("Sculpting: metres per second at the middle of the brush; smoothing, levelling, painting and restoring: how fast they work"));
if (m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::level)
{
auto touchup = m_streamer.touch_up();
if (ImGui::Checkbox(STR_C("Touch up over the tracks"), &touchup))
m_streamer.touch_up(touchup);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("On: the brush shapes the terrain as it's seen, also where the tracks (modifiers) shape it;\n"
"the changes there are kept as touch-ups over the modifiers, and stay when the track changes.\n"
"The Restore tool takes them back.\n"
"Off: the brush shapes only the ground under the modifiers, the tracks keep their formation and slopes."));
}
if (m_terrain_tool == terrain_tool::level)
{
ImGui::SetNextItemWidth(120.0f);
@@ -1750,13 +1814,27 @@ void editor_mode::render_terrain_ui()
}
auto const &water{m_streamer.water()};
std::optional<std::size_t> removed;
std::optional<std::pair<std::size_t, std::string>> dropped;
for (std::size_t i = 0; i < water.size(); ++i)
{
ImGui::PushID(static_cast<int>(i));
if (ImGui::Selectable(format("%s, %.2f m, %zu points", water[i].name.c_str(), water[i].level, water[i].outline.size()).c_str(), static_cast<int>(i) == m_water_selected))
auto const material = water[i].material.empty() ? std::string(STR_C("default material")) : water[i].material;
if (ImGui::Selectable(format("%s, %.2f m, %zu points, %s", water[i].name.c_str(), water[i].level, water[i].outline.size(), material.c_str()).c_str(), static_cast<int>(i) == m_water_selected))
m_water_selected = static_cast<int>(i);
// a texture dropped on an entry becomes the material of that water
std::string name;
if (material_drop(name))
dropped = std::make_pair(i, name);
ImGui::PopID();
}
if (dropped)
{
auto changed{water};
changed[dropped->first].material = dropped->second;
m_streamer.water(std::move(changed));
m_water_selected = static_cast<int>(dropped->first);
m_water_material_of = -1;
}
if (m_water_selected >= 0 && m_water_selected < static_cast<int>(water.size()))
{
// the level is changed in the water once the dragging ends
@@ -1771,6 +1849,33 @@ void editor_mode::render_terrain_ui()
changed[m_water_selected].level = m_water_selected_level;
m_streamer.water(std::move(changed));
}
// the material and the repeat of its texture, changed once the field is left (or a texture dropped on it)
if (m_water_material_of != m_water_selected)
{
std::snprintf(m_water_selected_material, sizeof(m_water_selected_material), "%s", water[m_water_selected].material.c_str());
m_water_selected_size = water[m_water_selected].size;
m_water_material_of = m_water_selected;
}
bool retexture = false;
ImGui::SetNextItemWidth(160.0f);
ImGui::InputText(STR_C("Material of the chosen"), m_water_selected_material, IM_ARRAYSIZE(m_water_selected_material));
retexture |= ImGui::IsItemDeactivatedAfterEdit();
retexture |= material_drop(m_water_selected_material, IM_ARRAYSIZE(m_water_selected_material));
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("A material or a texture (drag one from the texture browser).\nEmpty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str());
ImGui::SetNextItemWidth(120.0f);
ImGui::DragFloat(STR_C("Texture repeats every (m)##water"), &m_water_selected_size, 0.1f, 0.0f, 500.0f, "%.1f");
m_water_selected_size = std::max(0.0f, m_water_selected_size);
retexture |= ImGui::IsItemDeactivatedAfterEdit();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Metres the texture of the water repeats at; 0: as the material gives it, or 8 m"));
if (retexture)
{
auto changed{water};
changed[m_water_selected].material = m_water_selected_material;
changed[m_water_selected].size = m_water_selected_size;
m_streamer.water(std::move(changed));
}
if (ImGui::Button(STR_C("Remove the chosen water")))
removed = static_cast<std::size_t>(m_water_selected);
}
@@ -1780,7 +1885,20 @@ void editor_mode::render_terrain_ui()
changed.erase(changed.begin() + *removed);
m_streamer.water(std::move(changed));
m_water_selected = -1;
m_water_material_of = -1;
}
// the material of the bodies of water which don't name their own
if (false == m_water_default_edited)
std::snprintf(m_water_default_material, sizeof(m_water_default_material), "%s", m_streamer.manifest().water_material.c_str());
ImGui::SetNextItemWidth(160.0f);
ImGui::InputText(STR_C("Default water material"), m_water_default_material, IM_ARRAYSIZE(m_water_default_material));
m_water_default_edited = ImGui::IsItemActive();
bool redefault = ImGui::IsItemDeactivatedAfterEdit();
redefault |= material_drop(m_water_default_material, IM_ARRAYSIZE(m_water_default_material));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Material of the bodies of water which don't name their own"));
if (redefault)
m_streamer.water_material(m_water_default_material);
}
if (ImGui::CollapsingHeader(STR_C("Modifiers")))
@@ -1788,16 +1906,30 @@ void editor_mode::render_terrain_ui()
auto const &modifiers{m_streamer.modifiers()};
if (modifiers.empty())
ImGui::TextWrapped("%s", STR_C("None. The vertical profile of the tracks makes them: the terrain follows the formation and the slopes of the route."));
std::optional<std::string> removed;
std::optional<std::string> removed, baked;
for (auto const &modifier : modifiers)
{
ImGui::PushID(modifier.name.c_str());
ImGui::TextUnformatted(format("%s, %zu points", modifier.name.c_str(), modifier.points.size()).c_str());
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Bake")))
baked = modifier.name;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The ground keeps the shape the modifier gives it, and the modifier is removed:\n"
"from then on the terrain there is shaped by hand like anywhere else, and no longer follows the track"));
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Remove")))
removed = modifier.name;
ImGui::PopID();
}
if (auto const touchups = m_streamer.touched_up(); touchups > 0)
ImGui::TextDisabled(STR_C("Touch-ups over the modifiers: %zu points (of the chunks in memory)"), touchups);
if (baked)
{
m_streamer.bake_modifier(*baked);
ground_tiles.clear();
forget_ground_shapes();
}
if (removed)
{
m_streamer.remove_modifier(*removed);
@@ -1856,7 +1988,10 @@ void editor_mode::draw_terrain_overlay()
auto const z = cursor.z + std::sin(angle) * m_terrain_brush_radius;
circle.emplace_back(x, ground(x, z, cursor.y) + 0.1, z);
}
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);
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)
: m_terrain_tool == terrain_tool::restore ? IM_COL32(230, 120, 230, 220)
: IM_COL32(255, 180, 80, 220);
polyline(circle, color, true, 2.0f);
if (m_terrain_tool == terrain_tool::level)
{
@@ -1883,17 +2018,15 @@ void editor_mode::draw_terrain_overlay()
}
}
}
if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering)
{
auto const key = heightmap::chunk_at(cursor.x, cursor.z);
auto const exists = m_streamer.exists(key);
auto const low = heightmap::chunk_corner(key);
// outline of a chunk, laid over the ground
auto const chunk_outline = [&](heightmap::chunk_key const &Key, double const Fallback, ImU32 const Color, float const Thickness) {
auto const low = heightmap::chunk_corner(Key);
std::vector<glm::dvec3> square;
auto const edge = [&](glm::dvec2 const &From, glm::dvec2 const &To) {
for (int i = 0; i < 25; ++i)
{
auto const point = From + (To - From) * (i / 25.0);
square.emplace_back(point.x, ground(point.x, point.y, cursor.y) + 0.2, point.y);
square.emplace_back(point.x, ground(point.x, point.y, Fallback) + 0.2, point.y);
}
};
auto const size = heightmap::chunk_size;
@@ -1901,15 +2034,31 @@ void editor_mode::draw_terrain_overlay()
edge(low + glm::dvec2{size, 0.0}, low + glm::dvec2{size});
edge(low + glm::dvec2{size}, low + glm::dvec2{0.0, size});
edge(low + glm::dvec2{0.0, size}, low);
polyline(square, exists ? IM_COL32(255, 255, 255, 200) : IM_COL32(255, 230, 90, 200), true, 2.0f);
polyline(square, Color, true, Thickness);
};
if (m_terrain_tool == terrain_tool::spacing)
for (auto const &key : m_terrain_selection)
chunk_outline(key, cursor.y, IM_COL32(90, 220, 255, 230), 3.0f);
if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering)
{
auto const key = heightmap::chunk_at(cursor.x, cursor.z);
auto const exists = m_streamer.exists(key);
chunk_outline(key, cursor.y, exists ? IM_COL32(255, 255, 255, 200) : IM_COL32(255, 230, 90, 200), 2.0f);
ImVec2 label;
if (exists && projection.project(cursor, label))
{
auto const *chunk = m_streamer.terrain_at(cursor.x, cursor.z);
if (chunk != nullptr)
{
auto const text = (m_terrain_tool == terrain_tool::spacing ? format("%s -> %s", spacing_label(static_cast<float>(chunk->data().spacing())), spacing_label(m_terrain_spacing))
: std::string(spacing_label(static_cast<float>(chunk->data().spacing()))));
auto const &data = chunk->data();
auto text = std::string(spacing_label(static_cast<float>(data.spacing())));
if (m_terrain_tool == terrain_tool::spacing)
{
if (data.paint > 0)
text += format(STR_C(", paint every %.2f m"), heightmap::chunk_size / data.paint);
if (m_terrain_selection.count(key) != 0)
text += STR(" (selected)");
}
drawlist->AddText(ImVec2(label.x + 14.0f, label.y - 14.0f), IM_COL32(255, 255, 255, 230), text.c_str());
}
}
@@ -2346,13 +2495,19 @@ void editor_mode::handle_terrain_click(bool const Shift)
}
case terrain_tool::spacing:
{
// a click selects the chunk, Ctrl adds it to the selection or takes it away
auto const key = heightmap::chunk_at(world.x, world.z);
if (false == m_streamer.exists(key))
{
if (false == Global.ctrlState)
m_terrain_selection.clear();
return;
m_streamer.spacing(key.first, key.second, m_terrain_spacing);
ground_tiles.clear();
forget_ground_shapes();
m_terrain_status = format(STR_C("Chunk %d,%d has points every %s now"), key.first, key.second, spacing_label(m_terrain_spacing));
}
if (false == Global.ctrlState)
m_terrain_selection = {key};
else if (m_terrain_selection.erase(key) == 0)
m_terrain_selection.insert(key);
m_terrain_status = format(STR_C("Chunks selected: %zu"), m_terrain_selection.size());
break;
}
case terrain_tool::water:
@@ -2951,6 +3106,9 @@ void editor_mode::handle_terrain_brush(double Deltatime)
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;
case terrain_tool::restore:
shaped = m_streamer.restore_stitching(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5));
break;
default:
break;
}
@@ -2962,6 +3120,22 @@ void editor_mode::handle_terrain_brush(double Deltatime)
}
}
void editor_mode::convert_selected_chunks()
{
// with 0.5 m spacing the paint can be as fine, the other spacings keep a sample per metre
auto const paint = m_terrain_convert_paint ? (m_terrain_convert_spacing < 0.75f ? heightmap::default_paint_samples * 2 : heightmap::default_paint_samples) : 0;
std::size_t converted = 0;
for (auto const &key : m_terrain_selection)
if (m_streamer.spacing(key.first, key.second, m_terrain_convert_spacing, paint))
++converted;
if (converted > 0)
{
ground_tiles.clear();
forget_ground_shapes();
}
m_terrain_status = format(STR_C("Chunks converted to %s: %zu of %zu"), spacing_label(m_terrain_convert_spacing), converted, m_terrain_selection.size());
}
bool editor_mode::pick_terrain_target()
{
glm::dvec3 const offset = GfxRenderer->Mouse_Position();