17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-05 21:49:10 +01:00

Editor terrain: area fill with blurred or sharp edge, texture rotation

This commit is contained in:
2026-10-10 17:35:52 +02:00
parent 63302c1ace
commit 7b9eaf1ccb
11 changed files with 255 additions and 27 deletions

View File

@@ -1604,13 +1604,16 @@ void editor_mode::render_terrain_ui()
}; };
// the tools are named like the headers below, which would give them the same IDs // the tools are named like the headers below, which would give them the same IDs
ImGui::PushID("terrain_tools"); ImGui::PushID("terrain_tools");
// the tools working on a click, then the brushes
radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings")); radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings"));
ImGui::SameLine(); 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")); 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(); ImGui::SameLine();
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")); 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(); 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("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"));
ImGui::SameLine();
radio(STR_C("Fill"), terrain_tool::fill, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back"));
radio(STR_C("Sculpt"), terrain_tool::sculpt, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it")); radio(STR_C("Sculpt"), terrain_tool::sculpt, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"));
ImGui::SameLine(); ImGui::SameLine();
radio(STR_C("Smooth"), terrain_tool::smooth, STR_C("LMB evens the terrain out under the brush")); radio(STR_C("Smooth"), terrain_tool::smooth, STR_C("LMB evens the terrain out under the brush"));
@@ -1619,7 +1622,7 @@ void editor_mode::render_terrain_ui()
ImGui::SameLine(); ImGui::SameLine();
radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette")); radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"));
ImGui::SameLine(); ImGui::SameLine();
radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back")); 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"));
ImGui::PopID(); ImGui::PopID();
m_terrain_tool = static_cast<terrain_tool>(tool); m_terrain_tool = static_cast<terrain_tool>(tool);
@@ -1747,7 +1750,9 @@ void editor_mode::render_terrain_ui()
m_terrain_layer = std::clamp(m_terrain_layer, 0, static_cast<int>(layers.size()) - 1); m_terrain_layer = std::clamp(m_terrain_layer, 0, static_cast<int>(layers.size()) - 1);
for (std::size_t i = 0; i < layers.size(); ++i) for (std::size_t i = 0; i < layers.size(); ++i)
{ {
auto const label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())}; auto label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())};
if (layers[i].rotation != 0.f)
label += format(STR_C(", turned %.0f deg"), layers[i].rotation);
if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == m_terrain_layer)) if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == m_terrain_layer))
m_terrain_layer = static_cast<int>(i); m_terrain_layer = static_cast<int>(i);
// a material dropped on an entry takes its place, on every chunk painted with it // a material dropped on an entry takes its place, on every chunk painted with it
@@ -1764,6 +1769,14 @@ void editor_mode::render_terrain_ui()
m_streamer.layer_size(static_cast<std::uint16_t>(m_terrain_layer), size); m_streamer.layer_size(static_cast<std::uint16_t>(m_terrain_layer), size);
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Metres the textures of the chosen material repeat at; 0: as the material gives it, or 8 m")); ImGui::SetTooltip("%s", STR_C("Metres the textures of the chosen material repeat at; 0: as the material gives it, or 8 m"));
auto rotation{layers[m_terrain_layer].rotation};
ImGui::SetNextItemWidth(120.0f);
ImGui::SliderFloat(STR_C("Turned by (deg)"), &rotation, -180.0f, 180.0f, "%.0f");
if (ImGui::IsItemDeactivatedAfterEdit())
m_streamer.layer_rotation(static_cast<std::uint16_t>(m_terrain_layer), rotation);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Angle the textures of the chosen material are turned by, on every chunk painted with it.\n"
"Ctrl+click to type it. To have the material turned in one place only, fill it there with an angle of its own."));
} }
ImGui::SetNextItemWidth(160.0f); ImGui::SetNextItemWidth(160.0f);
ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material)); ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material));
@@ -1775,6 +1788,35 @@ void editor_mode::render_terrain_ui()
"A texture dragged from the browser onto an entry of the palette takes its place.")); "A texture dragged from the browser onto an entry of the palette takes its place."));
} }
if (ImGui::CollapsingHeader(STR_C("Fill"), m_terrain_tool == terrain_tool::fill ? ImGuiTreeNodeFlags_DefaultOpen : 0))
{
auto const &layers{m_streamer.manifest().layers};
if (false == layers.empty())
{
auto const &chosen{layers[std::clamp(m_terrain_layer, 0, static_cast<int>(layers.size()) - 1)]};
ImGui::Text(STR_C("Material: %s (chosen in the palette below)"), chosen.material.c_str());
}
ImGui::Text(STR_C("Outline: %zu points"), m_paint_outline.size());
ImGui::SetNextItemWidth(120.0f);
ImGui::DragFloat(STR_C("Edge blur (m)"), &m_paint_feather, 0.1f, 0.0f, 100.0f, m_paint_feather > 0.0f ? "%.1f" : STR_C("sharp"));
m_paint_feather = std::max(0.0f, m_paint_feather);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Metres across the outline the material blends in over, half of them inside the area.\n"
"0: a sharp edge, as sharp as the paint samples go: every metre, or every 0.5 m in chunks converted to 0.5 m with fine paint"));
ImGui::SetNextItemWidth(120.0f);
ImGui::SliderFloat(STR_C("Texture turned by (deg)"), &m_paint_rotation, -180.0f, 180.0f, "%.0f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The area gets the material turned by this angle: an entry of the palette with the material at that angle\n"
"is used, or added. Ctrl+click to type it"));
ImGui::BeginDisabled(m_paint_outline.size() < 3 || layers.empty());
if (ImGui::Button(STR_C("Fill the area")))
fill_terrain_area();
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button(STR_C("Clear the outline##fill")))
m_paint_outline.clear();
}
if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0)) if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0))
{ {
if (m_terrain_tool == terrain_tool::water) if (m_terrain_tool == terrain_tool::water)
@@ -2072,6 +2114,32 @@ void editor_mode::draw_terrain_overlay()
outline.emplace_back(point.x, water[i].level, point.y); outline.emplace_back(point.x, water[i].level, point.y);
polyline(outline, static_cast<int>(i) == m_water_selected ? IM_COL32(120, 200, 255, 255) : IM_COL32(60, 120, 220, 160), true, static_cast<int>(i) == m_water_selected ? 2.5f : 1.5f); polyline(outline, static_cast<int>(i) == m_water_selected ? IM_COL32(120, 200, 255, 255) : IM_COL32(60, 120, 220, 160), true, static_cast<int>(i) == m_water_selected ? 2.5f : 1.5f);
} }
if (m_terrain_tool == terrain_tool::fill && false == m_paint_outline.empty())
{
// laid over the ground, the edges followed every few metres
std::vector<glm::dvec3> outline;
auto const closed = m_paint_outline.size() >= 3;
for (std::size_t i = 0; i + (closed ? 0 : 1) < m_paint_outline.size(); ++i)
{
auto const &from = m_paint_outline[i];
auto const &to = m_paint_outline[(i + 1) % m_paint_outline.size()];
auto const steps = std::clamp(static_cast<int>(glm::distance(glm::dvec2{from.x, from.z}, glm::dvec2{to.x, to.z}) / 4.0), 1, 200);
for (int k = 0; k < steps; ++k)
{
auto const point = from + (to - from) * (static_cast<double>(k) / steps);
outline.emplace_back(point.x, ground(point.x, point.z, point.y) + 0.15, point.z);
}
}
if (false == closed)
outline.push_back(m_paint_outline.back() + glm::dvec3{0.0, 0.15, 0.0});
polyline(outline, IM_COL32(250, 200, 90, 255), false, 2.0f);
for (auto const &point : m_paint_outline)
{
ImVec2 dot;
if (projection.project(point + glm::dvec3{0.0, 0.15, 0.0}, dot))
drawlist->AddCircleFilled(dot, 4.0f, IM_COL32(250, 200, 90, 255));
}
}
if (m_terrain_tool == terrain_tool::water && false == m_water_points.empty()) if (m_terrain_tool == terrain_tool::water && false == m_water_points.empty())
{ {
std::vector<glm::dvec3> outline; std::vector<glm::dvec3> outline;
@@ -2510,6 +2578,17 @@ void editor_mode::handle_terrain_click(bool const Shift)
m_terrain_status = format(STR_C("Chunks selected: %zu"), m_terrain_selection.size()); m_terrain_status = format(STR_C("Chunks selected: %zu"), m_terrain_selection.size());
break; break;
} }
case terrain_tool::fill:
{
if (Shift)
{
if (false == m_paint_outline.empty())
m_paint_outline.pop_back();
return;
}
m_paint_outline.push_back(placement_on_ground(world));
break;
}
case terrain_tool::water: case terrain_tool::water:
{ {
if (Shift) if (Shift)
@@ -3120,6 +3199,25 @@ void editor_mode::handle_terrain_brush(double Deltatime)
} }
} }
void editor_mode::fill_terrain_area()
{
auto const &layers = m_streamer.manifest().layers;
if (m_paint_outline.size() < 3 || layers.empty())
return;
auto const chosen = layers[std::clamp(m_terrain_layer, 0, static_cast<int>(layers.size()) - 1)];
// the material turned by its own angle is another entry of the palette
auto const rotation = static_cast<float>(std::remainder(static_cast<double>(m_paint_rotation), 360.0));
auto const layer = chosen.rotation == rotation ? static_cast<std::uint16_t>(std::clamp(m_terrain_layer, 0, static_cast<int>(layers.size()) - 1))
: m_streamer.layer(chosen.material, chosen.size, rotation);
std::vector<glm::dvec2> outline;
for (auto const &point : m_paint_outline)
outline.emplace_back(point.x, point.z);
auto const filled = m_streamer.fill(outline, m_paint_feather, layer);
m_terrain_status = filled ? format(STR_C("Area filled with %s"), chosen.material.c_str()) : std::string(STR_C("Nothing filled: no chunk under the outline, or no room for another material"));
if (filled)
m_paint_outline.clear();
}
void editor_mode::convert_selected_chunks() 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 // with 0.5 m spacing the paint can be as fine, the other spacings keep a sample per metre

View File

@@ -313,7 +313,8 @@ class editor_mode : public application_mode, private editor_track::observer
paint, // paints the chosen material, Shift: the first material of the palette paint, // paints the chosen material, Shift: the first material of the palette
spacing, // selects chunks (Ctrl: adds or takes away one), whose point spacing is then converted spacing, // selects chunks (Ctrl: adds or takes away one), whose point spacing is then converted
water, // outlines a body of water water, // outlines a body of water
restore // takes the touch-ups over the modifiers back restore, // takes the touch-ups over the modifiers back
fill // outlines an area filled with a material
}; };
terrain_tool m_terrain_tool{terrain_tool::none}; terrain_tool m_terrain_tool{terrain_tool::none};
bool terrain_brush() const bool terrain_brush() const
@@ -325,7 +326,9 @@ class editor_mode : public application_mode, private editor_track::observer
void convert_selected_chunks(); 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 // 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 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; } bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water || m_terrain_tool == terrain_tool::fill; }
// fills the outlined area with the material of the chosen palette entry, turned by m_paint_rotation
void fill_terrain_area();
// the brush, the hovered chunk and the outlines of water drawn over the viewport // the brush, the hovered chunk and the outlines of water drawn over the viewport
void draw_terrain_overlay(); void draw_terrain_overlay();
// forgets what's known about the ground, after the terrain changed // forgets what's known about the ground, after the terrain changed
@@ -442,6 +445,9 @@ class editor_mode : public application_mode, private editor_track::observer
std::set<heightmap::chunk_key> m_terrain_selection; // chunks chosen with the spacing tool 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 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 bool m_terrain_convert_paint{true}; // chunks converted to 0.5 m get paint samples every 0.5 m too
std::vector<glm::dvec3> m_paint_outline; // of the area filled with a material
float m_paint_feather{0.0f}; // metres the filled material blends in over across the outline, 0: sharp
float m_paint_rotation{0.0f}; // degrees the textures of the filled material are turned by
float m_water_selected_level{0.0f}; // level of the chosen water, while it's edited float m_water_selected_level{0.0f}; // level of the chosen water, while it's edited
bool m_water_level_dragged{false}; bool m_water_level_dragged{false};
std::string m_terrain_status; std::string m_terrain_status;

View File

@@ -84,6 +84,8 @@ http://mozilla.org/MPL/2.0/.
// terrainbake <name> the modifier baked into the ground // terrainbake <name> the modifier baked into the ground
// terrainselect <cx> <cz> [add] the chunk selected with the spacing tool (add: Ctrl) // 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 // terrainresolution <spacing> [paint 0|1] the selected chunks converted, as the button of the resolution panel does it
// terrainfill <palette entry> <blur> <rotation> <x> <z> <x> <z> <x> <z>... the area outlined filled, as the fill tool does it
// terrainturn <palette entry> <degrees> the textures of the palette entry turned
// terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material> // 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 // orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
// screenshot // screenshot
@@ -1188,6 +1190,29 @@ void editor_mode::selftest_step()
forget_ground(); forget_ground();
WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size())); WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size()));
} }
else if (command == "terrainfill")
{
int layer{0};
words >> layer >> m_paint_feather >> m_paint_rotation;
m_terrain_layer = layer;
m_paint_outline.clear();
double x, z;
while (words >> x >> z)
m_paint_outline.emplace_back(x, 0.0, z);
fill_terrain_area();
std::string palette;
for (auto const &entry : m_streamer.manifest().layers)
palette += format(" [%s %.1f m %.0f deg]", entry.material.c_str(), entry.size, entry.rotation);
WriteLog("SELFTEST terrainfill: " + m_terrain_status + ", palette" + palette);
}
else if (command == "terrainturn")
{
int layer{0};
float degrees{0.f};
words >> layer >> degrees;
m_streamer.layer_rotation(static_cast<std::uint16_t>(layer), degrees);
WriteLog(format("SELFTEST terrainturn %d: %.1f deg", layer, m_streamer.manifest().layers.at(layer).rotation));
}
else if (command == "terraintouchup") else if (command == "terraintouchup")
{ {
int state{1}; int state{1};
@@ -1292,6 +1317,16 @@ void editor_mode::selftest_step()
if (false == data.adjust.empty()) if (false == data.adjust.empty())
adjust = data.height_in(data.adjust, x, z); 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); grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust);
if (data.paint > 0)
{
// weights at the paint sample nearest the point
auto const corner{data.corner()};
auto const ix{static_cast<int>(std::lround((x - corner.x) / heightmap::chunk_size * data.paint))};
auto const iz{static_cast<int>(std::lround((z - corner.y) / heightmap::chunk_size * data.paint))};
grid += ", weights";
for (std::size_t slot = 0; slot < data.layer_count(); ++slot)
grid += format(" %d", data.weight(slot, std::clamp(ix, 0, data.paint), std::clamp(iz, 0, data.paint)));
}
} }
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())); 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()));
} }

View File

@@ -216,6 +216,8 @@ void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
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"), 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)}); 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::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::count, STR_C("Fill"), nullptr, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back"), chosen(terrain_tool::fill),
choose(terrain_tool::fill)});
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), 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)}); choose(terrain_tool::restore)});
auto const orthophoto{m_orthophoto.enabled()}; auto const orthophoto{m_orthophoto.enabled()};

View File

@@ -358,7 +358,8 @@ void terrain_streamer::update_material(editor_terrain &Chunk)
{ {
auto const layer{data.layers[slot] == heightmap::no_layer ? std::uint16_t{0} : data.layers[slot]}; auto const layer{data.layers[slot] == heightmap::no_layer ? std::uint16_t{0} : data.layers[slot]};
auto const size{layer < m_manifest.layers.size() ? m_manifest.layers[layer].size : 0.f}; auto const size{layer < m_manifest.layers.size() ? m_manifest.layers[layer].size : 0.f};
layers.push_back({palette_material(layer), size}); auto const rotation{layer < m_manifest.layers.size() ? m_manifest.layers[layer].rotation : 0.f};
layers.push_back({palette_material(layer), size, rotation});
} }
auto const corner{data.corner()}; auto const corner{data.corner()};
auto const packcorner{heightmap::pack_corner(heightmap::pack_of(data.key))}; auto const packcorner{heightmap::pack_corner(heightmap::pack_of(data.key))};
@@ -1166,15 +1167,62 @@ bool terrain_streamer::paint(double X, double Z, double Radius, double Strength,
{ {
if (Radius <= 0.0 || Strength <= 0.0) if (Radius <= 0.0 || Strength <= 0.0)
return false; return false;
return paint_area(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, Layer, false, [&](double const Px, double const Pz) {
auto const distance{std::sqrt((Px - X) * (Px - X) + (Pz - Z) * (Pz - Z))};
return distance > Radius ? 0.0 : Strength * falloff(distance, Radius);
});
}
bool terrain_streamer::fill(std::vector<glm::dvec2> const &Outline, double Feather, std::uint16_t Layer, double Strength)
{
if (Outline.size() < 3 || Strength <= 0.0)
return false;
Feather = std::max(0.0, Feather);
glm::dvec2 low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::max()};
for (auto const &point : Outline)
{
low = glm::min(low, point);
high = glm::max(high, point);
}
// the blend runs across the outline, half of it inside
auto const margin{Feather * 0.5};
return paint_area(low - glm::dvec2{margin}, high + glm::dvec2{margin}, Layer, true, [&](double const Px, double const Pz) {
glm::dvec2 const point{Px, Pz};
bool inside{false};
auto nearest{std::numeric_limits<double>::max()};
for (std::size_t i = 0, j = Outline.size() - 1; i < Outline.size(); j = i++)
{
auto const &a{Outline[i]};
auto const &b{Outline[j]};
if ((a.y > Pz) != (b.y > Pz) && Px < (b.x - a.x) * (Pz - a.y) / (b.y - a.y) + a.x)
inside = !inside;
if (Feather > 0.0)
{
auto const run{b - a};
auto const length{glm::dot(run, run)};
auto const t{length > 1e-12 ? std::clamp(glm::dot(point - a, run) / length, 0.0, 1.0) : 0.0};
nearest = std::min(nearest, glm::length(point - (a + run * t)));
}
}
if (Feather <= 0.0)
return inside ? Strength : 0.0;
// signed distance from the outline, inside positive, led smoothly through the blend
auto const t{std::clamp(((inside ? nearest : -nearest) + margin) / Feather, 0.0, 1.0)};
return Strength * t * t * (3.0 - 2.0 * t);
});
}
bool terrain_streamer::paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t const Layer, bool const Load, std::function<double(double X, double Z)> const &Amount)
{
bool changed{false}; bool changed{false};
for (auto *chunk : chunks_in(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, false)) for (auto *chunk : chunks_in(Min, Max, Load))
{ {
auto const &current{chunk->data()}; auto const &current{chunk->data()};
auto const samples{current.paint > 0 ? current.paint : heightmap::default_paint_samples}; auto const samples{current.paint > 0 ? current.paint : heightmap::default_paint_samples};
auto const step{heightmap::chunk_size / samples}; auto const step{heightmap::chunk_size / samples};
auto const low{current.corner()}; auto const low{current.corner()};
auto const xs{index_range(low.x, step, samples + 1, X - Radius, X + Radius)}; auto const xs{index_range(low.x, step, samples + 1, Min.x, Max.x)};
auto const zs{index_range(low.y, step, samples + 1, Z - Radius, Z + Radius)}; auto const zs{index_range(low.y, step, samples + 1, Min.y, Max.y)};
if (xs.first > xs.second || zs.first > zs.second) if (xs.first > xs.second || zs.first > zs.second)
continue; continue;
if (current.paint <= 0 && current.layers[0] == Layer) if (current.paint <= 0 && current.layers[0] == Layer)
@@ -1203,13 +1251,11 @@ bool terrain_streamer::paint(double X, double Z, double Radius, double Strength,
for (int iz = zs.first; iz <= zs.second; ++iz) for (int iz = zs.first; iz <= zs.second; ++iz)
for (int ix = xs.first; ix <= xs.second; ++ix) for (int ix = xs.first; ix <= xs.second; ++ix)
{ {
auto const px{low.x + ix * step}, pz{low.y + iz * step}; auto const amount{std::clamp(Amount(low.x + ix * step, low.y + iz * step), 0.0, 1.0)};
auto const distance{std::sqrt((px - X) * (px - X) + (pz - Z) * (pz - Z))}; if (amount <= 0.0)
if (distance > Radius)
continue; continue;
auto const sample{static_cast<std::size_t>(iz) * (samples + 1) + ix}; auto const sample{static_cast<std::size_t>(iz) * (samples + 1) + ix};
auto &own{data->weights[slot * plane + sample]}; auto &own{data->weights[slot * plane + sample]};
auto const amount{std::clamp(Strength * falloff(distance, Radius), 0.0, 1.0)};
auto const target{static_cast<int>(std::lround(own + (255 - own) * amount))}; auto const target{static_cast<int>(std::lround(own + (255 - own) * amount))};
if (target == own) if (target == own)
continue; continue;
@@ -1239,13 +1285,13 @@ bool terrain_streamer::paint(double X, double Z, double Radius, double Strength,
return changed; return changed;
} }
std::uint16_t terrain_streamer::layer(std::string const &Material, float const Size) std::uint16_t terrain_streamer::layer(std::string const &Material, float const Size, float const Rotation)
{ {
for (std::size_t i = 0; i < m_manifest.layers.size(); ++i) for (std::size_t i = 0; i < m_manifest.layers.size(); ++i)
if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size)) if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size) && m_manifest.layers[i].rotation == Rotation)
return static_cast<std::uint16_t>(i); return static_cast<std::uint16_t>(i);
remember_manifest(); remember_manifest();
m_manifest.layers.push_back({Material, std::max(0.f, Size)}); m_manifest.layers.push_back({Material, std::max(0.f, Size), Rotation});
m_manifestchanged = true; m_manifestchanged = true;
return static_cast<std::uint16_t>(m_manifest.layers.size() - 1); return static_cast<std::uint16_t>(m_manifest.layers.size() - 1);
} }
@@ -1263,6 +1309,19 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size)
entry.second->m_paintedversion = 0; entry.second->m_paintedversion = 0;
} }
void terrain_streamer::layer_rotation(std::uint16_t const Layer, float Rotation)
{
Rotation = static_cast<float>(std::remainder(static_cast<double>(Rotation), 360.0));
if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].rotation == Rotation)
return;
remember_manifest();
m_manifest.layers[Layer].rotation = Rotation;
m_manifestchanged = true;
for (auto &entry : m_chunks)
if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0))
entry.second->m_paintedversion = 0;
}
void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material) void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material)
{ {
if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material) if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material)
@@ -1529,7 +1588,7 @@ terrain_streamer::change terrain_streamer::restore(change const &Change)
opposite.water_material = m_manifest.water_material; opposite.water_material = m_manifest.water_material;
opposite.modifiers = m_manifest.modifiers; opposite.modifiers = m_manifest.modifiers;
auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(), 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; })}; [](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size && A.rotation == B.rotation; })};
m_manifest.layers = Change.layers; m_manifest.layers = Change.layers;
m_manifest.modifiers = Change.modifiers; m_manifest.modifiers = Change.modifiers;
m_manifest.water = Change.water; m_manifest.water = Change.water;

View File

@@ -112,9 +112,15 @@ class terrain_streamer
std::size_t touched_up() const; 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 // 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); 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 // fills the area within Outline (x, z) with the material of the palette entry Layer, Strength (0..1) of the way.
std::uint16_t layer(std::string const &Material, float Size = 0.f); // Feather: metres across the outline the material blends in over, half of them inside; 0: a sharp edge, as sharp as
// the paint samples of the chunks go. the chunks are loaded where needed
bool fill(std::vector<glm::dvec2> const &Outline, double Feather, std::uint16_t Layer, double Strength = 1.0);
// palette entry of the material (repeated at Size, 0: any, turned by Rotation degrees), added to the palette if needed
std::uint16_t layer(std::string const &Material, float Size = 0.f, float Rotation = 0.f);
void layer_size(std::uint16_t Layer, float Size); void layer_size(std::uint16_t Layer, float Size);
// turns the textures of the palette entry Layer by Rotation degrees, on every chunk painted with it
void layer_rotation(std::uint16_t Layer, float Rotation);
// gives the palette entry Layer another material; the chunks painted with it change with it // gives the palette entry Layer another material; the chunks painted with it change with it
void layer_material(std::uint16_t Layer, std::string const &Material); void layer_material(std::uint16_t Layer, std::string const &Material);
void compress(bool State); void compress(bool State);
@@ -234,6 +240,9 @@ class terrain_streamer
heightmap::chunk_ptr neighbour(chunk_key const &Key) const; heightmap::chunk_ptr neighbour(chunk_key const &Key) const;
void touch_neighbours(chunk_key const &Key); void touch_neighbours(chunk_key const &Key);
material_handle palette_material(std::uint16_t const Layer); material_handle palette_material(std::uint16_t const Layer);
// paints the material of the palette entry Layer over the paint samples within the rectangle, Amount (0..1) of the way
// at each, as Amount gives it for the place of the sample. Load: chunks not in memory are loaded
bool paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t Layer, bool Load, std::function<double(double X, double Z)> const &Amount);
// chunk loaded right away from its pack file, for editing far from the camera // chunk loaded right away from its pack file, for editing far from the camera
editor_terrain *load_now(chunk_key const &Key); editor_terrain *load_now(chunk_key const &Key);
// chunks in memory or in the files overlapping the rectangle, loaded if needed // chunks in memory or in the files overlapping the rectangle, loaded if needed

View File

@@ -289,6 +289,10 @@ material_handle opengl33_terrain_materials::make(material_handle const Reuse, st
table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f; table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f;
table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f; table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f;
table[(max_layers + i) * 4 + 0] = layer.reflection; table[(max_layers + i) * 4 + 0] = layer.reflection;
// the textures turned by the layer of the chunk (the same material can be turned differently in another layer)
auto const angle{glm::radians(static_cast<double>(Layers[i].rotation))};
table[(max_layers + i) * 4 + 1] = static_cast<float>(std::cos(angle));
table[(max_layers + i) * 4 + 2] = static_cast<float>(std::sin(angle));
} }
bind_for_upload(m_textures.texture(entry.table)); bind_for_upload(m_textures.texture(entry.table));
::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>(max_layers), 2, 0, GL_RGBA, GL_FLOAT, table.data()); ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>(max_layers), 2, 0, GL_RGBA, GL_FLOAT, table.data());

View File

@@ -28,6 +28,7 @@ struct terrain_layer
{ {
material_handle material{null_handle}; material_handle material{null_handle};
float size{0.f}; float size{0.f};
float rotation{0.f}; // degrees the textures are turned by
}; };
} // namespace gfx } // namespace gfx

View File

@@ -551,7 +551,7 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err
if (auto const node{document["layers"]}; node && node.IsSequence()) if (auto const node{document["layers"]}; node && node.IsSequence())
for (auto const &layer : node) for (auto const &layer : node)
{ {
// a material name, or a map with the material and the size its textures repeat at // a material name, or a map with the material, the size its textures repeat at and the angle they're turned by
if (layer.IsMap()) if (layer.IsMap())
{ {
layer_def entry; layer_def entry;
@@ -559,6 +559,8 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err
entry.material = material.as<std::string>(); entry.material = material.as<std::string>();
if (auto const size{layer["size"]}) if (auto const size{layer["size"]})
entry.size = std::max(0.f, size.as<float>()); entry.size = std::max(0.f, size.as<float>());
if (auto const rotation{layer["rotation"]})
entry.rotation = rotation.as<float>();
if (false == entry.material.empty()) if (false == entry.material.empty())
result.layers.push_back(entry); result.layers.push_back(entry);
} }
@@ -630,8 +632,13 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin
out << YAML::Key << "layers" << YAML::Value << YAML::BeginSeq; out << YAML::Key << "layers" << YAML::Value << YAML::BeginSeq;
for (auto const &layer : Manifest.layers) for (auto const &layer : Manifest.layers)
{ {
if (layer.size > 0.f) if (layer.size > 0.f || layer.rotation != 0.f)
out << YAML::Flow << YAML::BeginMap << YAML::Key << "material" << YAML::Value << layer.material << YAML::Key << "size" << YAML::Value << layer.size << YAML::EndMap; {
out << YAML::Flow << YAML::BeginMap << YAML::Key << "material" << YAML::Value << layer.material << YAML::Key << "size" << YAML::Value << layer.size;
if (layer.rotation != 0.f)
out << YAML::Key << "rotation" << YAML::Value << layer.rotation;
out << YAML::EndMap;
}
else else
out << layer.material; out << layer.material;
} }

View File

@@ -153,11 +153,13 @@ class modifier_shape
std::vector<std::pair<std::uint64_t, std::uint32_t>> m_grid; // (cell key, segment), sorted std::vector<std::pair<std::uint64_t, std::uint32_t>> m_grid; // (cell key, segment), sorted
}; };
// material of the palette, and the size (metres) its textures repeat at; 0: the size given by the material, or 8 m // material of the palette, and the size (metres) its textures repeat at; 0: the size given by the material, or 8 m.
// the same material can be in the palette more than once, e.g. turned by another angle
struct layer_def struct layer_def
{ {
std::string material; std::string material;
float size{0.f}; float size{0.f};
float rotation{0.f}; // degrees the textures are turned by, counterclockwise seen from above
}; };
// description of the terrain, terrain.yaml // description of the terrain, terrain.yaml

View File

@@ -16,7 +16,7 @@ layout(location = 1) out vec4 out_motion;
// heightmap terrain: up to 8 materials (layers) painted over a chunk, blended by their weights. // heightmap terrain: up to 8 materials (layers) painted over a chunk, blended by their weights.
// the textures of all layers of the terrain are kept in texture arrays; the table of the chunk tells which layers it has: // the textures of all layers of the terrain are kept in texture arrays; the table of the chunk tells which layers it has:
// row 0, per layer: index in the arrays, 1 / metres the textures repeat at, has normal map, has specular/gloss map // row 0, per layer: index in the arrays, 1 / metres the textures repeat at, has normal map, has specular/gloss map
// row 1, per layer: reflection // row 1, per layer: reflection, cosine and sine of the angle the textures are turned by
// f_coord holds metres from the corner of the pack of chunks, along x and -z // f_coord holds metres from the corner of the pack of chunks, along x and -z
#param (color, 0, 0, 4, diffuse) #param (color, 0, 0, 4, diffuse)
@@ -77,9 +77,12 @@ void main()
continue; continue;
vec4 info = texelFetch(layertable, ivec2(i, 0), 0); vec4 info = texelFetch(layertable, ivec2(i, 0), 0);
vec4 extra = texelFetch(layertable, ivec2(i, 1), 0); vec4 extra = texelFetch(layertable, ivec2(i, 1), 0);
vec3 coord = vec3(f_coord * info.y, info.x); // turned counterclockwise seen from above; f_coord runs along x and -z, so that's clockwise in it
vec2 dx = coorddx * info.y; vec2 turn = (extra.y == 0.0 && extra.z == 0.0) ? vec2(1.0, 0.0) : extra.yz;
vec2 dy = coorddy * info.y; mat2 rotation = mat2(turn.x, -turn.y, turn.y, turn.x);
vec3 coord = vec3(rotation * f_coord * info.y, info.x);
vec2 dx = rotation * coorddx * info.y;
vec2 dy = rotation * coorddy * info.y;
albedo += textureGrad(layerdiffuse, coord, dx, dy).rgb * weight; albedo += textureGrad(layerdiffuse, coord, dx, dy).rgb * weight;
vec3 layernormal_ts = vec3(0.0, 0.0, 1.0); vec3 layernormal_ts = vec3(0.0, 0.0, 1.0);
float layerreflection = extra.x; float layerreflection = extra.x;
@@ -88,6 +91,8 @@ void main()
vec4 normalmap = textureGrad(layernormal, coord, dx, dy); vec4 normalmap = textureGrad(layernormal, coord, dx, dy);
layernormal_ts.xy = normalmap.rg * 2.0 - 1.0; layernormal_ts.xy = normalmap.rg * 2.0 - 1.0;
layernormal_ts.z = sqrt(1.0 - clamp(dot(layernormal_ts.xy, layernormal_ts.xy), 0.0, 1.0)); layernormal_ts.z = sqrt(1.0 - clamp(dot(layernormal_ts.xy, layernormal_ts.xy), 0.0, 1.0));
// the normal map leans along the turned texture; turned back to the axes of the terrain
layernormal_ts.xy = layernormal_ts.xy * rotation;
layerreflection *= normalmap.a; layerreflection *= normalmap.a;
} }
normal += layernormal_ts * weight; normal += layernormal_ts * weight;