diff --git a/application/editormode.cpp b/application/editormode.cpp index 5f92461b..653563a1 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -920,6 +920,7 @@ void editor_mode::restore_terrain(EditorSnapshot &Snapshot) Snapshot.terrain = std::make_shared(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(6.0f, static_cast(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(data.side()) * data.side(); + after += static_cast(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(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 removed; + std::optional> dropped; for (std::size_t i = 0; i < water.size(); ++i) { ImGui::PushID(static_cast(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(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(i) == m_water_selected)) m_water_selected = static_cast(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(dropped->first); + m_water_material_of = -1; + } if (m_water_selected >= 0 && m_water_selected < static_cast(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(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 removed; + std::optional 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 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(chunk->data().spacing())), spacing_label(m_terrain_spacing)) - : std::string(spacing_label(static_cast(chunk->data().spacing())))); + auto const &data = chunk->data(); + auto text = std::string(spacing_label(static_cast(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(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(); diff --git a/application/editormode.h b/application/editormode.h index e6258fee..6b21786c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -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 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; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 5cb2fe20..cd138fcc 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -78,6 +78,13 @@ http://mozilla.org/MPL/2.0/. // terrainadd chunks made around the camera, count x count, as the button of the chunks panel does it // terrainprobe 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 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 a stroke of the restore brush +// terrainbake the modifier baked into the ground +// terrainselect [add] the chunk selected with the spacing tool (add: Ctrl) +// terrainresolution [paint 0|1] the selected chunks converted, as the button of the resolution panel does it +// terrainwater [size] material and repeat of a body of water; terrainwaterdefault // orthophoto [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") { diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 847ba8e4..8a9052d9 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -213,8 +213,11 @@ void editor_mode::work_area_tools(std::vector &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}); diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 27d75a95..7a6dbab2 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -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 ¤t{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 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(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(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(iz) * current.side() + ix]); + { + auto const index{static_cast(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 ¤t{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(iz) * current.side() + ix]}; + if (distance > Radius || value == 0.f) + continue; + value *= static_cast(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::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>> 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 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 ¤t{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 ¤t{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(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(materialdata->GetSize().x) : 8.0}; + auto const repeat{body.size > 0.f ? static_cast(body.size) : (materialdata != nullptr && materialdata->GetSize().x > 0.f) ? static_cast(materialdata->GetSize().x) : 8.0}; glm::dvec3 const centre{centroid.x, body.level, centroid.y}; auto §ion{simulation::Region->section(centre)}; // the section makes the geometry of its own shapes once, before it's drawn; done first, it leaves ours alone diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index 62e3cd9e..7fc44813 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -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 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 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 const &water() const { return m_manifest.water; } void water(std::vector 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 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 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 layers; std::vector water; + std::string water_material; std::vector 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 m_packs; diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index ea2bde06..e2e508ac 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -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::make_flat(chunk_key const &Key, float co return chunk; } -std::shared_ptr chunk_data::resampled(float const Spacing) const +std::shared_ptr chunk_data::resampled(float const Spacing, int const Paint) const { auto chunk{std::make_shared(*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 const &Source, std::vector &Target) { - Target.resize(static_cast(chunk->side()) * chunk->side()); - for (int iz = 0; iz <= chunk->cells; ++iz) - for (int ix = 0; ix <= chunk->cells; ++ix) - Target[static_cast(iz) * chunk->side() + ix] = static_cast(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(std::ceil(reach / spacing()))) : 0}; + auto const resample = [&](std::vector const &Source, std::vector &Target) { + Target.resize(static_cast(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((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(iz) * chunk->side() + ix] = static_cast(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(paint + 1)}; + auto const newside{static_cast(Paint + 1)}; + chunk->paint = Paint; + chunk->weights.assign(newside * newside * count, 0); + std::vector 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(ix) * paint / Paint}, fz{static_cast(iz) * paint / Paint}; + auto const x0{std::min(static_cast(fx), oldside - 2)}, z0{std::min(static_cast(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(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(values[slot] * 255.0 / total) : (slot == 0 ? 255 : 0)}; + chunk->weights[slot * newside * newside + iz * newside + ix] = static_cast(weight); + sum += weight; + if (values[slot] > values[heaviest]) + heaviest = slot; + } + chunk->weights[heaviest * newside * newside + iz * newside + ix] += static_cast(255 - sum); + } + } return chunk; } @@ -516,6 +577,8 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err water.level = level.as(); if (auto const material{entry["material"]}) water.material = material.as(); + if (auto const size{entry["size"]}) + water.size = std::max(0.f, size.as()); 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 pack_file::read(chunk_key const &Key, std::string *E auto chunk{std::make_shared()}; 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 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 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(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::vectorbase, 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); diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index cc938b3a..8b2c17b9 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -64,6 +64,9 @@ struct chunk_data int cells{0}; // quads along a side; (cells + 1)^2 grid points std::vector heights; // world y of the grid points, rows of growing z, from the lowest x std::vector 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 adjust; int paint{0}; // paint samples along a side, less one. 0: the chunk is covered with its first layer alone std::array layers; // indices of the materials in the terrain palette, no_layer for unused std::vector 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 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 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 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 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 {