17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 11:29:20 +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();

View File

@@ -311,11 +311,18 @@ class editor_mode : public application_mode, private editor_track::observer
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
spacing, // selects chunks (Ctrl: adds or takes away one), whose point spacing is then converted
water, // outlines a body of water
restore // takes the touch-ups over the modifiers back
};
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::level || 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 ||
m_terrain_tool == terrain_tool::restore;
}
// converts the point spacing of the selected chunks to m_terrain_convert_spacing
void convert_selected_chunks();
// 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; }
@@ -427,6 +434,14 @@ class editor_mode : public application_mode, private editor_track::observer
bool m_water_level_edited{false}; // the level was given by hand, the points clicked don't change it
char m_water_material[128]{""};
int m_water_selected{-1};
char m_water_selected_material[128]{""}; // material of the chosen water, while it's edited
int m_water_material_of{-1}; // the water whose material is in the field above
float m_water_selected_size{0.0f}; // metres its texture repeats at, 0: given by the material
char m_water_default_material[128]{""}; // of the waters which don't name their own, while it's edited
bool m_water_default_edited{false};
std::set<heightmap::chunk_key> m_terrain_selection; // chunks chosen with the spacing tool
float m_terrain_convert_spacing{1.0f}; // point spacing the selected chunks are converted to
bool m_terrain_convert_paint{true}; // chunks converted to 0.5 m get paint samples every 0.5 m too
float m_water_selected_level{0.0f}; // level of the chosen water, while it's edited
bool m_water_level_dragged{false};
std::string m_terrain_status;

View File

@@ -78,6 +78,13 @@ http://mozilla.org/MPL/2.0/.
// 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
// terrainmodifier <name> <x0> <z0> <x1> <z1> <formation> <half width> a straight modifier, as the vertical profile makes them
// terraintouchup <0|1> the brushes touch up over the modifiers, or shape the ground under them
// terrainrestore <x> <z> <radius> a stroke of the restore brush
// terrainbake <name> the modifier baked into the ground
// terrainselect <cx> <cz> [add] the chunk selected with the spacing tool (add: Ctrl)
// terrainresolution <spacing> [paint 0|1] the selected chunks converted, as the button of the resolution panel does it
// terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material>
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
// screenshot
// save
@@ -1168,6 +1175,90 @@ void editor_mode::selftest_step()
forget_ground();
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
}
else if (command == "terrainmodifier")
{
heightmap::modifier modifier;
heightmap::corridor_point a, b;
double formation{0.0}, halfwidth{3.5};
words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth;
a.formation = b.formation = formation;
a.half_width = b.half_width = halfwidth;
modifier.points = {a, b};
m_streamer.modifier(std::move(modifier));
forget_ground();
WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size()));
}
else if (command == "terraintouchup")
{
int state{1};
words >> state;
m_streamer.touch_up(state != 0);
WriteLog(format("SELFTEST terraintouchup %d", state));
}
else if (command == "terrainrestore")
{
double x{0.0}, z{0.0}, radius{10.0};
words >> x >> z >> radius;
auto const changed{m_streamer.restore_stitching(x, z, radius, 1.0)};
forget_ground();
WriteLog(format("SELFTEST terrainrestore %.1f %.1f: %s, touch-ups left %zu", x, z, changed ? "changed" : "nothing changed", m_streamer.touched_up()));
}
else if (command == "terrainbake")
{
std::string name;
words >> name;
auto const baked{m_streamer.bake_modifier(name)};
forget_ground();
WriteLog(format("SELFTEST terrainbake %s: %s, %zu modifiers left", name.c_str(), baked ? "baked" : "not found", m_streamer.modifiers().size()));
}
else if (command == "terrainselect")
{
int cx{0}, cz{0};
std::string add;
words >> cx >> cz >> add;
if (add != "add")
m_terrain_selection.clear();
if (m_streamer.exists({cx, cz}))
m_terrain_selection.insert({cx, cz});
WriteLog(format("SELFTEST terrainselect: %zu selected", m_terrain_selection.size()));
}
else if (command == "terrainresolution")
{
float spacing{1.f};
int paint{1};
words >> spacing >> paint;
m_terrain_convert_spacing = heightmap::valid_spacing(spacing);
m_terrain_convert_paint = paint != 0;
convert_selected_chunks();
WriteLog("SELFTEST terrainresolution: " + m_terrain_status);
}
else if (command == "terrainwater" || command == "terrainwaterdefault")
{
if (command == "terrainwaterdefault")
{
std::string material;
words >> material;
m_streamer.water_material(material);
}
else
{
std::size_t index{0};
std::string material;
float size{0.f};
words >> index >> material >> size;
auto water{m_streamer.water()};
if (index < water.size())
{
water[index].material = material == "-" ? std::string{} : material;
water[index].size = size;
m_streamer.water(std::move(water));
}
}
std::string list;
for (auto const &body : m_streamer.water())
list += format(" [%s: %s, %.1f m]", body.name.c_str(), body.material.empty() ? "default" : body.material.c_str(), body.size);
WriteLog("SELFTEST " + command + ": default " + m_streamer.manifest().water_material + list);
}
else if (command == "terrainadd")
{
int count{1};
@@ -1191,7 +1282,18 @@ void editor_mode::selftest_step()
materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " ";
}
auto const found{m_streamer.height_at(x, z, height)};
WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str()));
std::string grid;
if (chunk != nullptr)
{
auto const &data{chunk->data()};
double base{0.0}, adjust{0.0};
if (false == data.base.empty())
base = data.height_in(data.base, x, z);
if (false == data.adjust.empty())
adjust = data.height_in(data.adjust, x, z);
grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust);
}
WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s%s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str(), grid.c_str()));
}
else if (command == "terrainconvert")
{

View File

@@ -213,8 +213,11 @@ void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
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)});
Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away; the tool options convert the point spacing of the selected"),
chosen(terrain_tool::spacing), choose(terrain_tool::spacing)});
Buttons.push_back({icon::area_fill, STR_C("Water"), nullptr, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"), chosen(terrain_tool::water), choose(terrain_tool::water)});
Buttons.push_back({icon::joints, STR_C("Restore"), nullptr, STR_C("LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it"), chosen(terrain_tool::restore),
choose(terrain_tool::restore)});
auto const orthophoto{m_orthophoto.enabled()};
Buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto,
[this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true});

View File

@@ -954,16 +954,21 @@ void terrain_streamer::remove_chunk(int Cx, int Cz)
touch_neighbours(key);
}
void terrain_streamer::spacing(int Cx, int Cz, float Spacing)
bool terrain_streamer::spacing(int Cx, int Cz, float Spacing, int Paint)
{
auto *chunk{load_now({Cx, Cz})};
if (chunk == nullptr || heightmap::cells_for(Spacing) == chunk->cells())
return;
if (chunk == nullptr)
return false;
auto const &current{chunk->data()};
auto const repaint{Paint > 0 && current.paint > 0 && Paint != current.paint};
if (heightmap::cells_for(Spacing) == current.cells && false == repaint)
return false;
remember(*chunk);
chunk->replace(chunk->data().resampled(Spacing), true, false);
chunk->replace(current.resampled(Spacing, Paint), true, repaint);
auto const low{chunk->data().corner()};
apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size});
touch_neighbours(chunk->key());
return true;
}
bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function<bool(double X, double Z, float &Height)> const &Shaper)
@@ -978,24 +983,119 @@ bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std
auto const zs{index_range(low.y, step, current.side(), Min.y, Max.y)};
if (xs.first > xs.second || zs.first > zs.second)
continue;
// a copy is shaped, and taken only if anything changed
auto heights{current.base.empty() ? current.heights : current.base};
bool shaped{false};
if (current.base.empty() || false == m_touchup)
{
// a copy is shaped, and taken only if anything changed
auto heights{current.base.empty() ? current.heights : current.base};
bool shaped{false};
for (int iz = zs.first; iz <= zs.second; ++iz)
for (int ix = xs.first; ix <= xs.second; ++ix)
shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]);
if (false == shaped)
continue;
remember(*chunk);
auto &data{chunk->edit(true, false)};
(data.base.empty() ? data.heights : data.base) = std::move(heights);
if (false == data.base.empty())
apply_modifiers(*chunk, Min, Max);
changed = true;
continue;
}
// the terrain as it's seen is shaped. where a modifier shapes the ground (or a touch-up is there already) the change
// is a touch-up over it, elsewhere it's the ground under the modifiers, which may then reach further
auto heights{current.heights};
auto base{current.base};
auto adjust{current.adjust.empty() ? std::vector<float>(heights.size(), 0.f) : current.adjust};
bool shaped{false}, touched{false};
for (int iz = zs.first; iz <= zs.second; ++iz)
for (int ix = xs.first; ix <= xs.second; ++ix)
shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]);
{
auto const index{static_cast<std::size_t>(iz) * current.side() + ix};
auto height{heights[index]};
if (false == Shaper(low.x + ix * step, low.y + iz * step, height))
continue;
shaped = true;
auto const change{height - heights[index]};
auto const shapedbymodifier{std::abs(heights[index] - adjust[index] - base[index]) > 0.0005f};
if (shapedbymodifier || adjust[index] != 0.f)
{
adjust[index] += change;
touched = true;
}
else
base[index] += change;
heights[index] = height;
}
if (false == shaped)
continue;
remember(*chunk);
auto &data{chunk->edit(true, false)};
(data.base.empty() ? data.heights : data.base) = std::move(heights);
if (false == data.base.empty())
apply_modifiers(*chunk, Min, Max);
data.base = std::move(base);
if (touched)
data.adjust = std::move(adjust);
data.heights = std::move(heights);
apply_modifiers(*chunk, Min, Max);
changed = true;
}
return changed;
}
bool terrain_streamer::restore_stitching(double X, double Z, double Radius, double Amount)
{
if (Radius <= 0.0 || Amount <= 0.0)
return false;
glm::dvec2 const low{X - Radius, Z - Radius}, high{X + Radius, Z + Radius};
bool changed{false};
for (auto *chunk : chunks_in(low, high, false))
{
auto const &current{chunk->data()};
if (current.adjust.empty())
continue;
auto const corner{current.corner()};
auto const step{current.spacing()};
auto const xs{index_range(corner.x, step, current.side(), low.x, high.x)};
auto const zs{index_range(corner.y, step, current.side(), low.y, high.y)};
auto adjust{current.adjust};
bool restored{false};
for (int iz = zs.first; iz <= zs.second; ++iz)
for (int ix = xs.first; ix <= xs.second; ++ix)
{
auto const vx{corner.x + ix * step}, vz{corner.y + iz * step};
auto const distance{std::sqrt((vx - X) * (vx - X) + (vz - Z) * (vz - Z))};
auto &value{adjust[static_cast<std::size_t>(iz) * current.side() + ix]};
if (distance > Radius || value == 0.f)
continue;
value *= static_cast<float>(1.0 - std::min(1.0, Amount * falloff(distance, Radius)));
// what's below the step of the stored heights is gone
if (std::abs(value) < heightmap::height_step * 0.5)
value = 0.f;
restored = true;
}
if (false == restored)
continue;
remember(*chunk);
auto &data{chunk->edit(true, false)};
if (std::all_of(adjust.begin(), adjust.end(), [](float const Value) { return Value == 0.f; }))
data.adjust.clear();
else
data.adjust = std::move(adjust);
apply_modifiers(*chunk, low, high);
changed = true;
}
return changed;
}
std::size_t terrain_streamer::touched_up() const
{
std::size_t count{0};
for (auto const &entry : m_chunks)
{
auto const &adjust{entry.second->data().adjust};
count += static_cast<std::size_t>(std::count_if(adjust.begin(), adjust.end(), [](float const Value) { return Value != 0.f; }));
}
return count;
}
bool terrain_streamer::sculpt(double X, double Z, double Radius, double Strength)
{
if (Radius <= 0.0)
@@ -1017,7 +1117,7 @@ bool terrain_streamer::smooth(double X, double Z, double Radius, double Amount)
// every chunk the brush touches is smoothed from the same ground, so the edges they share stay together
std::vector<std::pair<editor_terrain const *, std::vector<float>>> before;
for (auto *chunk : chunks_in(low - glm::dvec2{6.0}, high + glm::dvec2{6.0}, false))
before.emplace_back(chunk, chunk->data().base.empty() ? chunk->heights() : chunk->data().base);
before.emplace_back(chunk, (chunk->data().base.empty() || m_touchup) ? chunk->heights() : chunk->data().base);
auto const ground = [&](double const Gx, double const Gz, double const Fallback) {
for (auto const &entry : before)
if (entry.first->contains(Gx, Gz))
@@ -1201,6 +1301,16 @@ void terrain_streamer::water(std::vector<heightmap::water_body> Water)
m_waterchanged = true;
}
void terrain_streamer::water_material(std::string const &Material)
{
if (Material.empty() || Material == m_manifest.water_material)
return;
remember_manifest();
m_manifest.water_material = Material;
m_manifestchanged = true;
m_waterchanged = true;
}
void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max)
{
auto const &current{Chunk.data()};
@@ -1220,20 +1330,28 @@ void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &
}
if (false == reachesany)
{
// no modifier reaches the chunk (any more): its own heights are what's left
// no modifier reaches the chunk (any more): its own heights are what's left, with the touch-ups made over the modifiers
if (false == current.base.empty())
{
remember(Chunk);
auto &data{Chunk.edit(true, false)};
data.heights = std::move(data.base);
if (data.adjust.size() == data.heights.size())
for (std::size_t index = 0; index < data.heights.size(); ++index)
data.heights[index] += data.adjust[index];
data.base.clear();
data.adjust.clear();
}
return;
}
remember(Chunk);
auto &data{Chunk.edit(true, false)};
if (data.base.empty())
{
data.base = data.heights;
data.adjust.clear();
}
auto const touchups{data.adjust.size() == data.heights.size()};
auto const step{data.spacing()};
auto const xs{index_range(low.x, step, data.side(), std::max(Min.x, low.x), std::min(Max.x, high.x))};
auto const zs{index_range(low.y, step, data.side(), std::max(Min.y, low.y), std::min(Max.y, high.y))};
@@ -1245,7 +1363,7 @@ void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &
auto const x{low.x + ix * step}, z{low.y + iz * step};
for (auto const &shape : shapes)
shape.apply(x, z, height);
data.heights[index] = height;
data.heights[index] = touchups ? height + data.adjust[index] : height;
}
}
@@ -1287,6 +1405,41 @@ bool terrain_streamer::remove_modifier(std::string const &Name)
return true;
}
bool terrain_streamer::bake_modifier(std::string const &Name)
{
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })};
if (existing == m_manifest.modifiers.end())
return false;
remember_manifest();
auto const baked{*existing};
auto const bounds{baked.bounds()};
heightmap::modifier_shape const shape{baked};
// the ground under the modifiers takes the shape this one gives it; the other modifiers and the touch-ups go on top
for (auto *chunk : chunks_in(bounds.first, bounds.second, true))
{
auto const &current{chunk->data()};
if (current.base.empty())
continue;
auto const low{current.corner()};
auto const step{current.spacing()};
auto const xs{index_range(low.x, step, current.side(), bounds.first.x, bounds.second.x)};
auto const zs{index_range(low.y, step, current.side(), bounds.first.y, bounds.second.y)};
auto base{current.base};
bool shaped{false};
for (int iz = zs.first; iz <= zs.second; ++iz)
for (int ix = xs.first; ix <= xs.second; ++ix)
shaped |= shape.apply(low.x + ix * step, low.y + iz * step, base[static_cast<std::size_t>(iz) * current.side() + ix]);
if (false == shaped)
continue;
remember(*chunk);
chunk->edit(true, false).base = std::move(base);
}
m_manifest.modifiers.erase(std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; }));
m_manifestchanged = true;
apply_modifiers(bounds.first, bounds.second);
return true;
}
bool terrain_streamer::modified() const
{
if (m_manifestchanged || false == m_removed.empty())
@@ -1326,6 +1479,7 @@ void terrain_streamer::remember_manifest()
m_change.manifest = true;
m_change.layers = m_manifest.layers;
m_change.water = m_manifest.water;
m_change.water_material = m_manifest.water_material;
m_change.modifiers = m_manifest.modifiers;
}
@@ -1372,12 +1526,15 @@ terrain_streamer::change terrain_streamer::restore(change const &Change)
opposite.manifest = true;
opposite.layers = m_manifest.layers;
opposite.water = m_manifest.water;
opposite.water_material = m_manifest.water_material;
opposite.modifiers = m_manifest.modifiers;
auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(),
[](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size; })};
m_manifest.layers = Change.layers;
m_manifest.modifiers = Change.modifiers;
m_manifest.water = Change.water;
if (false == Change.water_material.empty())
m_manifest.water_material = Change.water_material;
m_manifestchanged = true;
m_waterchanged = true;
if (false == samelayers)
@@ -1523,7 +1680,7 @@ void terrain_streamer::build_water()
continue;
auto const material{GfxRenderer->Fetch_Material(body.material.empty() ? m_manifest.water_material : body.material)};
auto const *materialdata{GfxRenderer->Material(material)};
auto const repeat{(materialdata != nullptr && materialdata->GetSize().x > 0.f) ? static_cast<double>(materialdata->GetSize().x) : 8.0};
auto const repeat{body.size > 0.f ? static_cast<double>(body.size) : (materialdata != nullptr && materialdata->GetSize().x > 0.f) ? static_cast<double>(materialdata->GetSize().x) : 8.0};
glm::dvec3 const centre{centroid.x, body.level, centroid.y};
auto &section{simulation::Region->section(centre)};
// the section makes the geometry of its own shapes once, before it's drawn; done first, it leaves ours alone

View File

@@ -87,8 +87,9 @@ class terrain_streamer
// a chunk made elsewhere, at the place its key says; its layers are indices of the palette
bool add_chunk(std::shared_ptr<heightmap::chunk_data> Data);
void remove_chunk(int Cx, int Cz);
// changes the point spacing of the chunk at (Cx, Cz), heights resampled
void spacing(int Cx, int Cz, float Spacing);
// changes the point spacing of the chunk at (Cx, Cz), heights resampled; Paint: paint samples along a side less one
// the chunk gets, 0 keeps the ones it has. returns: true if the chunk changed
bool spacing(int Cx, int Cz, float Spacing, int Paint = 0);
// raises (Strength > 0) or lowers the ground within Radius of (X,Z), with a smooth falloff
bool sculpt(double X, double Z, double Radius, double Strength);
// evens out the ground within Radius of (X,Z), Amount (0..1) of the way at the centre
@@ -96,9 +97,19 @@ class terrain_streamer
// leads the ground within Radius of (X,Z) towards the height Target, Amount (0..1) of the way at the centre;
// Mode: 0 up and down, 1 only raises it, 2 only lowers it
bool level(double X, double Z, double Radius, double Target, double Amount, int Mode = 0);
// gives every grid point within the rectangle to Shaper, with its world position and height (before the modifiers);
// Shaper returns true if it changed the height
// gives every grid point within the rectangle to Shaper, with its world position and height; Shaper returns true if it
// changed the height. where the modifiers shape the ground the change is a touch-up kept over them (see touch_up()),
// elsewhere it changes the ground under them
bool reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function<bool(double X, double Z, float &Height)> const &Shaper);
// true: the brushes shape the terrain as it's seen, also where the modifiers (tracks) shape it, the changes kept there
// as touch-ups over the modifiers; false: the brushes shape only the ground under the modifiers, which keep their shape
void touch_up(bool const State) { m_touchup = State; }
bool touch_up() const { return m_touchup; }
// takes the touch-ups within Radius of (X,Z) back, Amount (0..1) of the way at the centre: the terrain goes back to the
// shape the modifiers give it
bool restore_stitching(double X, double Z, double Radius, double Amount);
// grid points with touch-ups over the modifiers, in the chunks in memory
std::size_t touched_up() const;
// paints the material of the palette entry Layer within Radius of (X,Z), Strength (0..1) of the way at the centre
bool paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer);
// palette entry of the material, added to the palette if needed
@@ -111,9 +122,14 @@ class terrain_streamer
// bodies of water
std::vector<heightmap::water_body> const &water() const { return m_manifest.water; }
void water(std::vector<heightmap::water_body> Water);
// material of the bodies of water which don't name their own
void water_material(std::string const &Material);
// modifiers: added or replaced by name
void modifier(heightmap::modifier Modifier);
bool remove_modifier(std::string const &Name);
// the ground takes the shape the modifier gives it for good, and the modifier is removed: from then on the
// terrain there is shaped by hand like anywhere else
bool bake_modifier(std::string const &Name);
std::vector<heightmap::modifier> const &modifiers() const { return m_manifest.modifiers; }
// changed since the last save
bool modified() const;
@@ -122,9 +138,10 @@ class terrain_streamer
struct change
{
std::map<chunk_key, heightmap::chunk_ptr> chunks; // the data the chunks had; null: there was no chunk
bool manifest{false}; // the palette, the water and the modifiers below were changed
bool manifest{false}; // the palette, the water (and its material) and the modifiers below were changed
std::vector<heightmap::layer_def> layers;
std::vector<heightmap::water_body> water;
std::string water_material;
std::vector<heightmap::modifier> modifiers;
bool empty() const { return chunks.empty() && false == manifest; }
@@ -238,6 +255,7 @@ class terrain_streamer
heightmap::manifest m_manifest;
bool m_manifestchanged{false};
float m_detail{120.f};
bool m_touchup{true};
int m_radius{2};
std::map<heightmap::pack_key, pack_state> m_packs;

View File

@@ -38,6 +38,7 @@ std::uint32_t constexpr tag_chunk{fourcc('C', 'H', 'N', 'K')};
std::uint32_t constexpr tag_heights{fourcc('H', 'G', 'H', 'T')};
std::uint32_t constexpr tag_base{fourcc('H', 'B', 'A', 'S')};
std::uint32_t constexpr tag_paint{fourcc('S', 'P', 'L', 'T')};
std::uint32_t constexpr tag_adjust{fourcc('H', 'A', 'D', 'J')};
std::uint32_t constexpr pack_magic{fourcc('T', 'C', 'H', '1')};
std::uint32_t constexpr pack_version{1};
@@ -262,23 +263,83 @@ std::shared_ptr<chunk_data> chunk_data::make_flat(chunk_key const &Key, float co
return chunk;
}
std::shared_ptr<chunk_data> chunk_data::resampled(float const Spacing) const
std::shared_ptr<chunk_data> chunk_data::resampled(float const Spacing, int const Paint) const
{
auto chunk{std::make_shared<chunk_data>(*this)};
chunk->cells = cells_for(Spacing);
if (chunk->cells == cells)
return chunk;
auto const low{corner()};
auto const step{chunk->spacing()};
auto const resample = [&](std::vector<float> const &Source, std::vector<float> &Target) {
Target.resize(static_cast<std::size_t>(chunk->side()) * chunk->side());
for (int iz = 0; iz <= chunk->cells; ++iz)
for (int ix = 0; ix <= chunk->cells; ++ix)
Target[static_cast<std::size_t>(iz) * chunk->side() + ix] = static_cast<float>(height_in(Source, low.x + ix * step, low.y + iz * step));
};
resample(heights, chunk->heights);
if (false == base.empty())
resample(base, chunk->base);
if (chunk->cells != cells)
{
auto const low{corner()};
auto const step{chunk->spacing()};
// a coarser grid would otherwise pick single points of the finer one: each point inside the chunk takes the average of
// the ground around it (a tent of the size of its cell). the points of the edges are the ground itself, as they're
// shared with the neighbours, which keep theirs
auto const reach{chunk->cells < cells ? step * 0.5 : 0.0};
auto const taps{reach > 0.0 ? std::max(1, static_cast<int>(std::ceil(reach / spacing()))) : 0};
auto const resample = [&](std::vector<float> const &Source, std::vector<float> &Target) {
Target.resize(static_cast<std::size_t>(chunk->side()) * chunk->side());
for (int iz = 0; iz <= chunk->cells; ++iz)
for (int ix = 0; ix <= chunk->cells; ++ix)
{
auto const x{low.x + ix * step}, z{low.y + iz * step};
auto value{height_in(Source, x, z)};
if (taps > 0 && ix > 0 && iz > 0 && ix < chunk->cells && iz < chunk->cells)
{
double sum{0.0}, weights{0.0};
for (int dz = -taps; dz <= taps; ++dz)
for (int dx = -taps; dx <= taps; ++dx)
{
auto const weight{static_cast<double>((taps + 1 - std::abs(dx)) * (taps + 1 - std::abs(dz)))};
sum += weight * height_in(Source, x + dx * reach / taps, z + dz * reach / taps);
weights += weight;
}
value = sum / weights;
}
Target[static_cast<std::size_t>(iz) * chunk->side() + ix] = static_cast<float>(value);
}
};
resample(heights, chunk->heights);
if (false == base.empty())
resample(base, chunk->base);
if (false == adjust.empty())
resample(adjust, chunk->adjust);
}
if (Paint > 0 && paint > 0 && Paint != paint)
{
// the weights of the samples between the old ones blended from them, and brought back to 255 a sample
auto const count{layer_count()};
auto const oldside{static_cast<std::size_t>(paint + 1)};
auto const newside{static_cast<std::size_t>(Paint + 1)};
chunk->paint = Paint;
chunk->weights.assign(newside * newside * count, 0);
std::vector<double> values(count);
for (std::size_t iz = 0; iz < newside; ++iz)
for (std::size_t ix = 0; ix < newside; ++ix)
{
auto const fx{static_cast<double>(ix) * paint / Paint}, fz{static_cast<double>(iz) * paint / Paint};
auto const x0{std::min(static_cast<std::size_t>(fx), oldside - 2)}, z0{std::min(static_cast<std::size_t>(fz), oldside - 2)};
auto const tx{fx - x0}, tz{fz - z0};
double total{0.0};
for (std::size_t slot = 0; slot < count; ++slot)
{
auto const at = [&](std::size_t const X, std::size_t const Z) { return static_cast<double>(weights[slot * oldside * oldside + Z * oldside + X]); };
values[slot] = (at(x0, z0) * (1.0 - tx) + at(x0 + 1, z0) * tx) * (1.0 - tz) + (at(x0, z0 + 1) * (1.0 - tx) + at(x0 + 1, z0 + 1) * tx) * tz;
total += values[slot];
}
// rounded down, what's left of 255 goes to the heaviest
int sum{0};
std::size_t heaviest{0};
for (std::size_t slot = 0; slot < count; ++slot)
{
auto const weight{total > 0.0 ? static_cast<int>(values[slot] * 255.0 / total) : (slot == 0 ? 255 : 0)};
chunk->weights[slot * newside * newside + iz * newside + ix] = static_cast<std::uint8_t>(weight);
sum += weight;
if (values[slot] > values[heaviest])
heaviest = slot;
}
chunk->weights[heaviest * newside * newside + iz * newside + ix] += static_cast<std::uint8_t>(255 - sum);
}
}
return chunk;
}
@@ -516,6 +577,8 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err
water.level = level.as<double>();
if (auto const material{entry["material"]})
water.material = material.as<std::string>();
if (auto const size{entry["size"]})
water.size = std::max(0.f, size.as<float>());
if (auto const outline{entry["outline"]}; outline && outline.IsSequence())
for (auto const &point : outline)
if (point.IsSequence() && point.size() >= 2)
@@ -583,6 +646,8 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin
out << YAML::Key << "level" << YAML::Value << water.level;
if (false == water.material.empty())
out << YAML::Key << "material" << YAML::Value << water.material;
if (water.size > 0.f)
out << YAML::Key << "size" << YAML::Value << water.size;
out << YAML::Key << "outline" << YAML::Value << YAML::BeginSeq;
for (auto const &point : water.outline)
out << YAML::Flow << YAML::BeginSeq << point.x << point.y << YAML::EndSeq;
@@ -764,7 +829,7 @@ std::shared_ptr<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
auto chunk{std::make_shared<chunk_data>()};
chunk->key = Key;
slice const *heights{nullptr}, *base{nullptr}, *paint{nullptr};
slice const *heights{nullptr}, *base{nullptr}, *adjust{nullptr}, *paint{nullptr};
bool header{false};
for (auto const &slice : slices)
{
@@ -787,6 +852,8 @@ std::shared_ptr<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
heights = &slice;
else if (slice.tag == tag_base)
base = &slice;
else if (slice.tag == tag_adjust)
adjust = &slice;
else if (slice.tag == tag_paint)
paint = &slice;
// slices of unknown kinds are left for newer versions of the simulator
@@ -801,6 +868,9 @@ std::shared_ptr<chunk_data> pack_file::read(chunk_key const &Key, std::string *E
return fail("damaged heights");
if (base != nullptr && (false == load(*base, data) || false == decode_heights(data, count, chunk->base)))
return fail("damaged base heights");
// touch-ups go with the base heights only
if (adjust != nullptr && false == chunk->base.empty() && (false == load(*adjust, data) || false == decode_heights(data, count, chunk->adjust)))
return fail("damaged touch-ups");
if (chunk->paint > 0)
{
auto const expected{static_cast<std::size_t>(chunk->paint + 1) * (chunk->paint + 1) * chunk->layer_count()};
@@ -864,6 +934,12 @@ bool write_pack(std::string const &Path, pack_key const &Key, std::vector<chunk_
data.clear();
encode_heights(chunk->base, data);
add(table, tag_base, data);
if (chunk->adjust.size() == chunk->heights.size())
{
data.clear();
encode_heights(chunk->adjust, data);
add(table, tag_adjust, data);
}
}
if (chunk->paint > 0 && chunk->layer_count() > 1)
add(table, tag_paint, chunk->weights);

View File

@@ -64,6 +64,9 @@ struct chunk_data
int cells{0}; // quads along a side; (cells + 1)^2 grid points
std::vector<float> heights; // world y of the grid points, rows of growing z, from the lowest x
std::vector<float> base; // the heights before the modifiers were applied; empty where no modifier reaches the chunk
// touch-ups made by hand over what the modifiers give, added to it: heights = modifiers(base) + adjust.
// empty when there are none, and always when base is empty
std::vector<float> adjust;
int paint{0}; // paint samples along a side, less one. 0: the chunk is covered with its first layer alone
std::array<std::uint16_t, max_chunk_layers> layers; // indices of the materials in the terrain palette, no_layer for unused
std::vector<std::uint8_t> weights; // planar, (paint + 1)^2 per used layer, rows like the heights; weights of a sample add up to 255
@@ -84,8 +87,10 @@ struct chunk_data
bool contains(double const X, double const Z) const;
// a flat chunk of specified spacing at specified height, covered with the first material of the palette
static std::shared_ptr<chunk_data> make_flat(chunk_key const &Key, float const Spacing, float const Height);
// the same chunk with the grid of another spacing, heights and paint taken over
std::shared_ptr<chunk_data> resampled(float const Spacing) const;
// the same chunk with the grid of another spacing, heights and paint taken over. a coarser grid takes the average
// of the ground around each of its points (the finer one only its own heights). Paint: paint samples along a side
// less one the chunk gets, 0 keeps the ones it has
std::shared_ptr<chunk_data> resampled(float const Spacing, int const Paint = 0) const;
// weight of specified layer slot at paint sample (Ix, Iz)
std::uint8_t weight(std::size_t const Slot, int const Ix, int const Iz) const;
// layer slot of specified palette index, or -1
@@ -106,6 +111,7 @@ struct water_body
std::string name;
double level{0.0};
std::string material; // empty: the default water material
float size{0.f}; // metres the texture repeats at; 0: the size given by the material, or 8 m
std::vector<glm::dvec2> outline; // x, z
};
@@ -183,6 +189,7 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin
// 'HGHT' heights: uint8 encoding (0: int32 base and uint16 steps of height_step above it, delta coded in row order;
// 1: float32), then the values
// 'HBAS' heights before the modifiers, as 'HGHT'
// 'HADJ' touch-ups over the modifiers, as 'HGHT' (only with 'HBAS'; older versions skip it and keep the heights)
// 'SPLT' paint weights: uint8 per sample per used layer, planar
class pack_file
{