mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-07 00:22:56 +01:00
Editor terrain: level brush with height picker, faster orthophoto fit
This commit is contained in:
@@ -391,7 +391,9 @@ namespace
|
|||||||
|
|
||||||
// ground triangles overlapping an XZ rectangle (Min/Max are x,z). shapes hold the (legacy) terrain,
|
// ground triangles overlapping an XZ rectangle (Min/Max are x,z). shapes hold the (legacy) terrain,
|
||||||
// model instances at least ModelRadius large (if ModelsAsGround) the terrain tiles
|
// model instances at least ModelRadius large (if ModelsAsGround) the terrain tiles
|
||||||
void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector<world_triangle> &Out, bool const Skiproads = false)
|
// Heightmap: the heightmap terrain is given as triangles too (for the ones which can ask the terrain for heights, it's a waste)
|
||||||
|
void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector<world_triangle> &Out, bool const Skiproads = false,
|
||||||
|
bool const Heightmap = true)
|
||||||
{
|
{
|
||||||
auto const started = std::chrono::steady_clock::now();
|
auto const started = std::chrono::steady_clock::now();
|
||||||
++ground_question;
|
++ground_question;
|
||||||
@@ -444,7 +446,8 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the heightmap terrain answers for itself, with its full grid whatever is drawn of it
|
// the heightmap terrain answers for itself, with its full grid whatever is drawn of it
|
||||||
EditorTerrain.gather_triangles(Min, Max, Out);
|
if (Heightmap)
|
||||||
|
EditorTerrain.gather_triangles(Min, Max, Out);
|
||||||
trim_ground_meshes();
|
trim_ground_meshes();
|
||||||
// going through the scenery is the one thing here which can take long enough to be felt; when it does it's worth knowing
|
// going through the scenery is the one thing here which can take long enough to be felt; when it does it's worth knowing
|
||||||
auto const elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
|
auto const elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
|
||||||
@@ -533,9 +536,10 @@ editor_mode::editor_mode() {
|
|||||||
road_select(nullptr);
|
road_select(nullptr);
|
||||||
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
|
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
|
||||||
// the roads built in the editor aren't ground: the imagery goes under them
|
// the roads built in the editor aren't ground: the imagery goes under them
|
||||||
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
|
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Heightmap, std::vector<world_triangle> &Out) {
|
||||||
gather_ground_triangles(Min, Max, true, 50.0f, Out, true);
|
gather_ground_triangles(Min, Max, true, 50.0f, Out, true, Heightmap);
|
||||||
});
|
});
|
||||||
|
m_orthophoto.height_source([](double const X, double const Z, double &Height) { return EditorTerrain.height_at(X, Z, Height); });
|
||||||
editor_track::observe(this);
|
editor_track::observe(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1380,6 +1384,8 @@ bool editor_mode::update()
|
|||||||
// the brush of the terrain works while the left mouse button is held
|
// the brush of the terrain works while the left mouse button is held
|
||||||
if (terrain_brush() && mouseHold)
|
if (terrain_brush() && mouseHold)
|
||||||
handle_terrain_brush(deltarealtime);
|
handle_terrain_brush(deltarealtime);
|
||||||
|
else
|
||||||
|
m_terrain_stroke = false;
|
||||||
if (m_terrain_open)
|
if (m_terrain_open)
|
||||||
draw_terrain_overlay();
|
draw_terrain_overlay();
|
||||||
|
|
||||||
@@ -1606,6 +1612,8 @@ void editor_mode::render_terrain_ui()
|
|||||||
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"));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
radio(STR_C("Level"), terrain_tool::level, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"));
|
||||||
|
ImGui::SameLine();
|
||||||
radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"));
|
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("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"));
|
||||||
@@ -1650,7 +1658,23 @@ void editor_mode::render_terrain_ui()
|
|||||||
ImGui::DragFloat(STR_C("Strength"), &m_terrain_brush_strength, 0.05f, 0.05f, 50.0f, "%.2f");
|
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);
|
m_terrain_brush_strength = std::max(0.05f, m_terrain_brush_strength);
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered())
|
||||||
ImGui::SetTooltip("%s", STR_C("Sculpting: metres per second at the middle of the brush; smoothing and painting: how fast they work"));
|
ImGui::SetTooltip("%s", STR_C("Sculpting: metres per second at the middle of the brush; smoothing, levelling and painting: how fast they work"));
|
||||||
|
if (m_terrain_tool == terrain_tool::level)
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemWidth(120.0f);
|
||||||
|
ImGui::DragFloat(STR_C("Target height (m)"), &m_terrain_target, 0.05f, -1000.0f, 5000.0f, "%.2f");
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(m_terrain_picking ? STR_C("Click in the view...") : STR_C("Pick")))
|
||||||
|
m_terrain_picking = false == m_terrain_picking;
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", STR_C("The next click takes the height of whatever is under the cursor: terrain, a track, a model.\nCtrl+LMB does the same at any time"));
|
||||||
|
char const *modes[]{STR_C("Raise and lower"), STR_C("Only raise"), STR_C("Only lower")};
|
||||||
|
ImGui::SetNextItemWidth(160.0f);
|
||||||
|
ImGui::Combo("##levelmode", &m_terrain_level_mode, modes, IM_ARRAYSIZE(modes));
|
||||||
|
ImGui::Checkbox(STR_C("Target where the stroke starts"), &m_terrain_target_from_stroke);
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", STR_C("Each stroke takes the height of the terrain under the cursor where it starts as its target,\nand drags it along: the ground stroked over is levelled with that point"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader(STR_C("Materials"), m_terrain_tool == terrain_tool::paint ? ImGuiTreeNodeFlags_DefaultOpen : 0))
|
if (ImGui::CollapsingHeader(STR_C("Materials"), m_terrain_tool == terrain_tool::paint ? ImGuiTreeNodeFlags_DefaultOpen : 0))
|
||||||
@@ -1834,6 +1858,30 @@ void editor_mode::draw_terrain_overlay()
|
|||||||
}
|
}
|
||||||
auto const color = m_terrain_tool == terrain_tool::paint ? IM_COL32(120, 230, 120, 220) : m_terrain_tool == terrain_tool::smooth ? IM_COL32(120, 200, 255, 220) : IM_COL32(255, 180, 80, 220);
|
auto const color = m_terrain_tool == terrain_tool::paint ? IM_COL32(120, 230, 120, 220) : m_terrain_tool == terrain_tool::smooth ? IM_COL32(120, 200, 255, 220) : IM_COL32(255, 180, 80, 220);
|
||||||
polyline(circle, color, true, 2.0f);
|
polyline(circle, color, true, 2.0f);
|
||||||
|
if (m_terrain_tool == terrain_tool::level)
|
||||||
|
{
|
||||||
|
// the target: the rim of the brush at its height, joined with the ground, and the height by the cursor
|
||||||
|
auto const target = static_cast<double>(m_terrain_target);
|
||||||
|
std::vector<glm::dvec3> level;
|
||||||
|
for (auto const &point : circle)
|
||||||
|
level.emplace_back(point.x, target, point.z);
|
||||||
|
ImU32 const levelcolor = IM_COL32(255, 230, 90, 230);
|
||||||
|
polyline(level, levelcolor, true, 1.5f);
|
||||||
|
for (std::size_t i = 0; i < circle.size(); i += circle.size() / 8)
|
||||||
|
{
|
||||||
|
ImVec2 from, to;
|
||||||
|
if (projection.project(circle[i], from) && projection.project(level[i], to))
|
||||||
|
drawlist->AddLine(from, to, IM_COL32(255, 230, 90, 120), 1.0f);
|
||||||
|
}
|
||||||
|
ImVec2 label;
|
||||||
|
if (projection.project(glm::dvec3{cursor.x, target, cursor.z}, label))
|
||||||
|
{
|
||||||
|
double ground{cursor.y};
|
||||||
|
m_streamer.height_at(cursor.x, cursor.z, ground);
|
||||||
|
auto const text = format("%.2f m (%+.2f)", target, target - ground);
|
||||||
|
drawlist->AddText(ImVec2(label.x + 12.0f, label.y - 8.0f), levelcolor, text.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering)
|
if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering)
|
||||||
{
|
{
|
||||||
@@ -2888,6 +2936,18 @@ void editor_mode::handle_terrain_brush(double Deltatime)
|
|||||||
case terrain_tool::smooth:
|
case terrain_tool::smooth:
|
||||||
shaped = m_streamer.smooth(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5));
|
shaped = m_streamer.smooth(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5));
|
||||||
break;
|
break;
|
||||||
|
case terrain_tool::level:
|
||||||
|
{
|
||||||
|
if (false == m_terrain_stroke && m_terrain_target_from_stroke)
|
||||||
|
{
|
||||||
|
double height;
|
||||||
|
if (m_streamer.height_at(world.x, world.z, height))
|
||||||
|
m_terrain_target = static_cast<float>(height);
|
||||||
|
}
|
||||||
|
m_terrain_stroke = true;
|
||||||
|
shaped = m_streamer.level(world.x, world.z, m_terrain_brush_radius, m_terrain_target, std::min(1.0, rate * 0.5), m_terrain_level_mode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case terrain_tool::paint:
|
case terrain_tool::paint:
|
||||||
m_streamer.paint(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.25), Global.shiftState ? std::uint16_t{0} : static_cast<std::uint16_t>(m_terrain_layer));
|
m_streamer.paint(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.25), Global.shiftState ? std::uint16_t{0} : static_cast<std::uint16_t>(m_terrain_layer));
|
||||||
break;
|
break;
|
||||||
@@ -2902,6 +2962,16 @@ void editor_mode::handle_terrain_brush(double Deltatime)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool editor_mode::pick_terrain_target()
|
||||||
|
{
|
||||||
|
glm::dvec3 const offset = GfxRenderer->Mouse_Position();
|
||||||
|
if (glm::length(offset) < 1e-3)
|
||||||
|
return false; // nothing under the cursor
|
||||||
|
m_terrain_target = static_cast<float>(Camera.Pos.y + offset.y);
|
||||||
|
m_terrain_status = format(STR_C("Target height: %.2f m"), m_terrain_target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void editor_mode::capture_terrain()
|
void editor_mode::capture_terrain()
|
||||||
{
|
{
|
||||||
TAnimModel *model = dynamic_cast<TAnimModel *>(m_node);
|
TAnimModel *model = dynamic_cast<TAnimModel *>(m_node);
|
||||||
@@ -5153,6 +5223,15 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
|||||||
// with a brush of the terrain the left button works the brush instead of picking nodes
|
// with a brush of the terrain the left button works the brush instead of picking nodes
|
||||||
if (terrain_brush() && Button == GLFW_MOUSE_BUTTON_LEFT)
|
if (terrain_brush() && Button == GLFW_MOUSE_BUTTON_LEFT)
|
||||||
{
|
{
|
||||||
|
if (m_terrain_tool == terrain_tool::level && is_press(Action) && (Global.ctrlState || m_terrain_picking))
|
||||||
|
{
|
||||||
|
// a click which picks the target height doesn't level anything
|
||||||
|
pick_terrain_target();
|
||||||
|
m_terrain_picking = false;
|
||||||
|
mouseHold = false;
|
||||||
|
m_input.mouse.button(Button, Action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
mouseHold = is_press(Action);
|
mouseHold = is_press(Action);
|
||||||
m_input.mouse.button(Button, Action);
|
m_input.mouse.button(Button, Action);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -309,12 +309,15 @@ class editor_mode : public application_mode, private editor_track::observer
|
|||||||
chunks, // adds a chunk, Shift: removes it
|
chunks, // adds a chunk, Shift: removes it
|
||||||
sculpt, // raises the ground, Shift: lowers it
|
sculpt, // raises the ground, Shift: lowers it
|
||||||
smooth,
|
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
|
paint, // paints the chosen material, Shift: the first material of the palette
|
||||||
spacing, // gives the clicked chunk the chosen point spacing
|
spacing, // gives the clicked chunk the chosen point spacing
|
||||||
water // outlines a body of water
|
water // outlines a body of water
|
||||||
};
|
};
|
||||||
terrain_tool m_terrain_tool{terrain_tool::none};
|
terrain_tool m_terrain_tool{terrain_tool::none};
|
||||||
bool terrain_brush() const { return m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::paint; }
|
bool terrain_brush() const { return m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::level || m_terrain_tool == terrain_tool::paint; }
|
||||||
|
// takes the height of whatever is under the cursor (terrain, track, model) for the target of the level brush. returns: false if there's nothing
|
||||||
|
bool pick_terrain_target();
|
||||||
bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water; }
|
bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water; }
|
||||||
// the brush, the hovered chunk and the outlines of water drawn over the viewport
|
// the brush, the hovered chunk and the outlines of water drawn over the viewport
|
||||||
void draw_terrain_overlay();
|
void draw_terrain_overlay();
|
||||||
@@ -410,6 +413,12 @@ class editor_mode : public application_mode, private editor_track::observer
|
|||||||
int m_terrain_add_count{4}; // chunks along a side added around the camera
|
int m_terrain_add_count{4}; // chunks along a side added around the camera
|
||||||
float m_terrain_brush_radius{12.0f};
|
float m_terrain_brush_radius{12.0f};
|
||||||
float m_terrain_brush_strength{4.0f}; // metres per second while held; for the paint, how fast it covers
|
float m_terrain_brush_strength{4.0f}; // metres per second while held; for the paint, how fast it covers
|
||||||
|
// the level brush
|
||||||
|
float m_terrain_target{0.0f}; // height the ground is led to
|
||||||
|
int m_terrain_level_mode{0}; // 0: up and down, 1: only raises, 2: only lowers
|
||||||
|
bool m_terrain_target_from_stroke{false}; // the target is the height where a stroke starts
|
||||||
|
bool m_terrain_stroke{false}; // a stroke of the brush is under way
|
||||||
|
bool m_terrain_picking{false}; // the next click picks the target height
|
||||||
int m_terrain_layer{0}; // entry of the palette painted
|
int m_terrain_layer{0}; // entry of the palette painted
|
||||||
char m_terrain_material[128]{"grass"}; // material added to the palette
|
char m_terrain_material[128]{"grass"}; // material added to the palette
|
||||||
float m_terrain_material_size{0.0f}; // metres its textures repeat at, 0: given by the material
|
float m_terrain_material_size{0.0f}; // metres its textures repeat at, 0: given by the material
|
||||||
|
|||||||
@@ -73,9 +73,12 @@ http://mozilla.org/MPL/2.0/.
|
|||||||
// newscenery the editor starts again with the wizard of a new scenery
|
// newscenery the editor starts again with the wizard of a new scenery
|
||||||
// vehicles logs each vehicle: its head, facing, and what its couplers hold
|
// vehicles logs each vehicle: its head, facing, and what its couplers hold
|
||||||
// terrainmode the terrain field of work, with its texture browser
|
// terrainmode the terrain field of work, with its texture browser
|
||||||
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry> as a stroke of the brush
|
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry>,
|
||||||
|
// terrainlevel <x> <z> <radius> <target height> [mode] as a stroke of the brush
|
||||||
|
// terrainadd <count> <spacing> chunks made around the camera, count x count, as the button of the chunks panel does it
|
||||||
// terrainprobe <x> <z> logs the height of the heightmap terrain and the materials of the chunk
|
// terrainprobe <x> <z> logs the height of the heightmap terrain and the materials of the chunk
|
||||||
// terrainconvert converts every material of the terrain files of the scenery
|
// terrainconvert converts every material of the terrain files of the scenery
|
||||||
|
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
|
||||||
// screenshot
|
// screenshot
|
||||||
// save
|
// save
|
||||||
// log <text>
|
// log <text>
|
||||||
@@ -1138,19 +1141,42 @@ void editor_mode::selftest_step()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (command == "orthophoto")
|
||||||
|
{
|
||||||
|
auto config{m_orthophoto.settings()};
|
||||||
|
int fit{1}, inscene{0};
|
||||||
|
words >> config.north >> config.east >> config.radius >> fit >> inscene;
|
||||||
|
config.drape = fit != 0;
|
||||||
|
config.in_scene = inscene != 0;
|
||||||
|
m_orthophoto.settings(config);
|
||||||
|
m_orthophoto.enabled(true);
|
||||||
|
WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : ""));
|
||||||
|
}
|
||||||
else if (command == "terrainmode")
|
else if (command == "terrainmode")
|
||||||
{
|
{
|
||||||
m_terrain_open = true;
|
m_terrain_open = true;
|
||||||
WriteLog("SELFTEST terrainmode");
|
WriteLog("SELFTEST terrainmode");
|
||||||
}
|
}
|
||||||
else if (command == "terrainsculpt" || command == "terrainpaint")
|
else if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel")
|
||||||
{
|
{
|
||||||
double x{0.0}, z{0.0}, radius{10.0}, value{1.0};
|
double x{0.0}, z{0.0}, radius{10.0}, value{1.0};
|
||||||
words >> x >> z >> radius >> value;
|
int mode{0};
|
||||||
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) : m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
|
words >> x >> z >> radius >> value >> mode;
|
||||||
|
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value)
|
||||||
|
: command == "terrainlevel" ? m_streamer.level(x, z, radius, value, 1.0, mode)
|
||||||
|
: m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
|
||||||
forget_ground();
|
forget_ground();
|
||||||
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
|
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
|
||||||
}
|
}
|
||||||
|
else if (command == "terrainadd")
|
||||||
|
{
|
||||||
|
int count{1};
|
||||||
|
float spacing{m_terrain_spacing};
|
||||||
|
words >> count >> spacing;
|
||||||
|
m_terrain_spacing = heightmap::valid_spacing(spacing);
|
||||||
|
add_chunks_around(std::clamp(count, 1, 8));
|
||||||
|
WriteLog("SELFTEST terrainadd: " + m_terrain_status);
|
||||||
|
}
|
||||||
else if (command == "terrainprobe")
|
else if (command == "terrainprobe")
|
||||||
{
|
{
|
||||||
double x{0.0}, z{0.0}, height{0.0};
|
double x{0.0}, z{0.0}, height{0.0};
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
|
|||||||
Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), chosen(terrain_tool::none), choose(terrain_tool::none)});
|
Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), chosen(terrain_tool::none), choose(terrain_tool::none)});
|
||||||
Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), chosen(terrain_tool::sculpt), choose(terrain_tool::sculpt)});
|
Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), chosen(terrain_tool::sculpt), choose(terrain_tool::sculpt)});
|
||||||
Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), chosen(terrain_tool::smooth), choose(terrain_tool::smooth)});
|
Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), chosen(terrain_tool::smooth), choose(terrain_tool::smooth)});
|
||||||
|
Buttons.push_back({icon::profile, STR_C("Level"), nullptr, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"),
|
||||||
|
chosen(terrain_tool::level), choose(terrain_tool::level)});
|
||||||
Buttons.push_back({icon::brush, STR_C("Paint"), nullptr, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"), chosen(terrain_tool::paint), choose(terrain_tool::paint)});
|
Buttons.push_back({icon::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::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 gives the clicked chunk the point spacing chosen in the tool options"), chosen(terrain_tool::spacing), choose(terrain_tool::spacing)});
|
||||||
|
|||||||
@@ -698,7 +698,11 @@ void editor_orthophoto::settings(config const &Config)
|
|||||||
{
|
{
|
||||||
bool const sourcechanged = (Config.year != m_config.year || Config.hires != m_config.hires);
|
bool const sourcechanged = (Config.year != m_config.year || Config.hires != m_config.hires);
|
||||||
bool const shapechanged = (Config.height != m_config.height || Config.drape != m_config.drape || Config.in_scene != m_config.in_scene || Config.lift != m_config.lift);
|
bool const shapechanged = (Config.height != m_config.height || Config.drape != m_config.drape || Config.in_scene != m_config.in_scene || Config.lift != m_config.lift);
|
||||||
|
// the surface of the ground is gathered only for the geometry in the scene, the overlay makes do with the heights
|
||||||
|
bool const groundchanged = (Config.in_scene != m_config.in_scene);
|
||||||
m_config = Config;
|
m_config = Config;
|
||||||
|
if (groundchanged)
|
||||||
|
refit();
|
||||||
m_config.lift = std::clamp(m_config.lift, 0.0f, 50.0f);
|
m_config.lift = std::clamp(m_config.lift, 0.0f, 50.0f);
|
||||||
m_config.radius = std::clamp(m_config.radius, 0, max_radius);
|
m_config.radius = std::clamp(m_config.radius, 0, max_radius);
|
||||||
m_config.opacity = std::clamp(m_config.opacity, 0.0f, 1.0f);
|
m_config.opacity = std::clamp(m_config.opacity, 0.0f, 1.0f);
|
||||||
@@ -914,12 +918,23 @@ void editor_orthophoto::sample_ground(tile_key const &Key, tile &Tile)
|
|||||||
glm::dvec2 const corner = tile_corner(Key);
|
glm::dvec2 const corner = tile_corner(Key);
|
||||||
glm::dvec2 const min(corner.x - tile_size, corner.y - tile_size);
|
glm::dvec2 const min(corner.x - tile_size, corner.y - tile_size);
|
||||||
glm::dvec2 const max(corner.x, corner.y);
|
glm::dvec2 const max(corner.x, corner.y);
|
||||||
|
// the clipped surface is drawn only by the geometry in the scene. the overlay needs just the heights, and the heightmap
|
||||||
|
// terrain gives these straight away: its triangles (hundreds of thousands in a tile) aren't gathered for it
|
||||||
|
bool const surface = m_config.in_scene || !m_height;
|
||||||
std::vector<world_triangle> triangles;
|
std::vector<world_triangle> triangles;
|
||||||
m_ground(min, max, triangles);
|
m_ground(min, max, surface, triangles);
|
||||||
|
|
||||||
int constexpr samples{ground_grid + 1};
|
int constexpr samples{ground_grid + 1};
|
||||||
double constexpr step{tile_size / ground_grid};
|
double constexpr step{tile_size / ground_grid};
|
||||||
std::vector<float> heights(static_cast<std::size_t>(samples) * samples, no_height);
|
std::vector<float> heights(static_cast<std::size_t>(samples) * samples, no_height);
|
||||||
|
if (!surface)
|
||||||
|
for (int j = 0; j < samples; ++j)
|
||||||
|
for (int i = 0; i < samples; ++i)
|
||||||
|
{
|
||||||
|
double y;
|
||||||
|
if (m_height(corner.x - i * step, corner.y - j * step, y))
|
||||||
|
heights[static_cast<std::size_t>(j) * samples + i] = static_cast<float>(y);
|
||||||
|
}
|
||||||
|
|
||||||
for (auto const &triangle : triangles)
|
for (auto const &triangle : triangles)
|
||||||
{
|
{
|
||||||
@@ -935,17 +950,20 @@ void editor_orthophoto::sample_ground(tile_key const &Key, tile &Tile)
|
|||||||
if (length < 1e-9 || normal.y / length < steepest_ground)
|
if (length < 1e-9 || normal.y / length < steepest_ground)
|
||||||
continue; // walls and the like don't receive the imagery
|
continue; // walls and the like don't receive the imagery
|
||||||
|
|
||||||
glm::dvec3 polygon[8] = {a, b, c};
|
if (surface)
|
||||||
glm::dvec3 scratch[8];
|
|
||||||
int count = clip_xz(polygon, 3, scratch, 0, max.x, true);
|
|
||||||
count = clip_xz(scratch, count, polygon, 0, min.x, false);
|
|
||||||
count = clip_xz(polygon, count, scratch, 2, max.y, true);
|
|
||||||
count = clip_xz(scratch, count, polygon, 2, min.y, false);
|
|
||||||
for (int k = 1; k + 1 < count; ++k)
|
|
||||||
{
|
{
|
||||||
Tile.surface.push_back(polygon[0]);
|
glm::dvec3 polygon[8] = {a, b, c};
|
||||||
Tile.surface.push_back(polygon[k]);
|
glm::dvec3 scratch[8];
|
||||||
Tile.surface.push_back(polygon[k + 1]);
|
int count = clip_xz(polygon, 3, scratch, 0, max.x, true);
|
||||||
|
count = clip_xz(scratch, count, polygon, 0, min.x, false);
|
||||||
|
count = clip_xz(polygon, count, scratch, 2, max.y, true);
|
||||||
|
count = clip_xz(scratch, count, polygon, 2, min.y, false);
|
||||||
|
for (int k = 1; k + 1 < count; ++k)
|
||||||
|
{
|
||||||
|
Tile.surface.push_back(polygon[0]);
|
||||||
|
Tile.surface.push_back(polygon[k]);
|
||||||
|
Tile.surface.push_back(polygon[k + 1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// heightfield samples under the (whole) triangle, the highest surface wins
|
// heightfield samples under the (whole) triangle, the highest surface wins
|
||||||
@@ -1415,7 +1433,33 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const &
|
|||||||
if (draped)
|
if (draped)
|
||||||
segments = std::max(segments, distance < 300.0 ? ground_grid : distance < 800.0 ? ground_grid / 2 : ground_grid / 4);
|
segments = std::max(segments, distance < 300.0 ? ground_grid : distance < 800.0 ? ground_grid / 2 : ground_grid / 4);
|
||||||
float const segmentsize = 1.0f / static_cast<float>(segments);
|
float const segmentsize = 1.0f / static_cast<float>(segments);
|
||||||
auto const height_at = [&](float U, float V) { return draped ? sample_heights(t.heights, U, V) + lift : flat; };
|
// the grid points of the heights are taken as they are where the segments fall on them
|
||||||
|
int const stride = (draped && ground_grid % segments == 0) ? ground_grid / segments : 0;
|
||||||
|
auto const height_at = [&](int I, int J) {
|
||||||
|
if (!draped)
|
||||||
|
return flat;
|
||||||
|
if (stride > 0)
|
||||||
|
return static_cast<double>(t.heights[static_cast<std::size_t>(J * stride) * (ground_grid + 1) + I * stride]) + lift;
|
||||||
|
return sample_heights(t.heights, I * segmentsize, J * segmentsize) + lift;
|
||||||
|
};
|
||||||
|
|
||||||
|
// each point of the grid is projected once, with the sides of the view it's beyond: a quad wholly inside goes
|
||||||
|
// straight to the draw list, one wholly beyond a side is skipped, only the ones across an edge are clipped
|
||||||
|
int const side = segments + 1;
|
||||||
|
static std::vector<clip_vertex> points;
|
||||||
|
static std::vector<std::uint8_t> codes;
|
||||||
|
points.resize(static_cast<std::size_t>(side) * side);
|
||||||
|
codes.resize(points.size());
|
||||||
|
for (int j = 0; j < side; ++j)
|
||||||
|
for (int i = 0; i < side; ++i)
|
||||||
|
{
|
||||||
|
auto const index = static_cast<std::size_t>(j) * side + i;
|
||||||
|
float const u = i * segmentsize, v = j * segmentsize;
|
||||||
|
glm::vec4 const position = clip(u, v, height_at(i, j));
|
||||||
|
points[index] = {position, {u, v}};
|
||||||
|
codes[index] = static_cast<std::uint8_t>((position.w < near_w ? 1 : 0) | (position.x > position.w * guard ? 2 : 0) | (position.x < -position.w * guard ? 4 : 0) |
|
||||||
|
(position.y > position.w * guard ? 8 : 0) | (position.y < -position.w * guard ? 16 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
drawlist->PushTextureID((ImTextureID)(intptr_t)(textureid));
|
drawlist->PushTextureID((ImTextureID)(intptr_t)(textureid));
|
||||||
for (int j = 0; j < segments; ++j)
|
for (int j = 0; j < segments; ++j)
|
||||||
@@ -1426,19 +1470,29 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const &
|
|||||||
drawlist->PopTextureID();
|
drawlist->PopTextureID();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float const u0 = i * segmentsize, u1 = (i + 1) * segmentsize;
|
std::size_t const corners[4] = {static_cast<std::size_t>(j) * side + i, static_cast<std::size_t>(j) * side + i + 1, static_cast<std::size_t>(j + 1) * side + i + 1,
|
||||||
float const v0 = j * segmentsize, v1 = (j + 1) * segmentsize;
|
static_cast<std::size_t>(j + 1) * side + i};
|
||||||
clip_vertex quad[4] = {
|
auto const all = codes[corners[0]] & codes[corners[1]] & codes[corners[2]] & codes[corners[3]];
|
||||||
{clip(u0, v0, height_at(u0, v0)), {u0, v0}},
|
if (all != 0)
|
||||||
{clip(u1, v0, height_at(u1, v0)), {u1, v0}},
|
continue; // beyond one side of the view
|
||||||
{clip(u1, v1, height_at(u1, v1)), {u1, v1}},
|
auto const any = codes[corners[0]] | codes[corners[1]] | codes[corners[2]] | codes[corners[3]];
|
||||||
{clip(u0, v1, height_at(u0, v1)), {u0, v1}},
|
if (any == 0)
|
||||||
};
|
{
|
||||||
glm::vec4 const positions[4] = {quad[0].position, quad[1].position, quad[2].position, quad[3].position};
|
drawlist->PrimReserve(6, 4);
|
||||||
if (outside(positions, 4))
|
auto const base = static_cast<ImDrawIdx>(drawlist->_VtxCurrentIdx);
|
||||||
|
drawlist->PrimWriteIdx(base);
|
||||||
|
drawlist->PrimWriteIdx(static_cast<ImDrawIdx>(base + 1));
|
||||||
|
drawlist->PrimWriteIdx(static_cast<ImDrawIdx>(base + 2));
|
||||||
|
drawlist->PrimWriteIdx(base);
|
||||||
|
drawlist->PrimWriteIdx(static_cast<ImDrawIdx>(base + 2));
|
||||||
|
drawlist->PrimWriteIdx(static_cast<ImDrawIdx>(base + 3));
|
||||||
|
for (auto const corner : corners)
|
||||||
|
drawlist->PrimWriteVtx(toscreen(points[corner].position), ImVec2(points[corner].uv.x, points[corner].uv.y), color);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// clip against the near plane and the (slightly widened) sides of the view
|
// clip against the near plane and the (slightly widened) sides of the view
|
||||||
|
clip_vertex quad[4] = {points[corners[0]], points[corners[1]], points[corners[2]], points[corners[3]]};
|
||||||
clip_vertex buffer0[12], buffer1[12];
|
clip_vertex buffer0[12], buffer1[12];
|
||||||
int count = clip_polygon(quad, 4, buffer0, [](glm::vec4 const &v) { return v.w - near_w; });
|
int count = clip_polygon(quad, 4, buffer0, [](glm::vec4 const &v) { return v.w - near_w; });
|
||||||
count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.x; });
|
count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.x; });
|
||||||
|
|||||||
@@ -67,8 +67,11 @@ class editor_orthophoto
|
|||||||
};
|
};
|
||||||
|
|
||||||
using world_triangle = std::array<glm::dvec3, 3>;
|
using world_triangle = std::array<glm::dvec3, 3>;
|
||||||
// supplies the ground triangles overlapping an XZ rectangle (Min/Max are x,z); used to drape the imagery
|
// supplies the ground triangles overlapping an XZ rectangle (Min/Max are x,z); used to drape the imagery.
|
||||||
using ground_query = std::function<void(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out)>;
|
// Heightmap: the heightmap terrain as triangles too; without it the terrain is asked through the height query alone
|
||||||
|
using ground_query = std::function<void(glm::dvec2 const &Min, glm::dvec2 const &Max, bool Heightmap, std::vector<world_triangle> &Out)>;
|
||||||
|
// height of the heightmap terrain at a point, false where there's none
|
||||||
|
using height_query = std::function<bool(double X, double Z, double &Height)>;
|
||||||
|
|
||||||
struct statistics
|
struct statistics
|
||||||
{
|
{
|
||||||
@@ -93,6 +96,7 @@ class editor_orthophoto
|
|||||||
// changes of year / resolution invalidate resident tiles, the rest applies immediately
|
// changes of year / resolution invalidate resident tiles, the rest applies immediately
|
||||||
void settings(config const &Config);
|
void settings(config const &Config);
|
||||||
void ground_source(ground_query Query) { m_ground = std::move(Query); }
|
void ground_source(ground_query Query) { m_ground = std::move(Query); }
|
||||||
|
void height_source(height_query Query) { m_height = std::move(Query); }
|
||||||
// samples the ground again (after it was edited); tiles are refitted progressively
|
// samples the ground again (after it was edited); tiles are refitted progressively
|
||||||
void refit();
|
void refit();
|
||||||
// takes the scene geometry out of the region (e.g. when the editor is left); it's re-created on the next update
|
// takes the scene geometry out of the region (e.g. when the editor is left); it's re-created on the next update
|
||||||
@@ -202,6 +206,7 @@ class editor_orthophoto
|
|||||||
std::vector<texture_slot> m_free_slots; // pool of textures, reused between tiles
|
std::vector<texture_slot> m_free_slots; // pool of textures, reused between tiles
|
||||||
int m_texture_count{0};
|
int m_texture_count{0};
|
||||||
ground_query m_ground;
|
ground_query m_ground;
|
||||||
|
height_query m_height;
|
||||||
scene::basic_region *m_region{nullptr}; // region the scene geometry was put into
|
scene::basic_region *m_region{nullptr}; // region the scene geometry was put into
|
||||||
std::multimap<std::size_t, gfx::geometry_handle> m_free_chunks; // geometry chunks by capacity, reused between tiles
|
std::multimap<std::size_t, gfx::geometry_handle> m_free_chunks; // geometry chunks by capacity, reused between tiles
|
||||||
// the scene opacity is baked into the textures, which means reading the tiles again; applied once the value settles
|
// the scene opacity is baked into the textures, which means reading the tiles again; applied once the value settles
|
||||||
|
|||||||
@@ -1041,6 +1041,27 @@ bool terrain_streamer::smooth(double X, double Z, double Radius, double Amount)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool terrain_streamer::level(double X, double Z, double Radius, double Target, double Amount, int const Mode)
|
||||||
|
{
|
||||||
|
if (Radius <= 0.0 || Amount <= 0.0)
|
||||||
|
return false;
|
||||||
|
return reshape(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, [&](double const Vx, double const Vz, float &Height) {
|
||||||
|
auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))};
|
||||||
|
if (distance > Radius)
|
||||||
|
return false;
|
||||||
|
auto const own{static_cast<double>(Height)};
|
||||||
|
if ((Mode == 1 && own >= Target) || (Mode == 2 && own <= Target))
|
||||||
|
return false;
|
||||||
|
// the middle of the brush gets there, the rim follows less; what's within the step of the stored heights is there already
|
||||||
|
auto const levelled{own + (Target - own) * std::min(1.0, Amount * falloff(distance, Radius))};
|
||||||
|
auto const result{std::abs(Target - levelled) < heightmap::height_step ? Target : levelled};
|
||||||
|
if (std::abs(result - own) < 1e-5)
|
||||||
|
return false;
|
||||||
|
Height = static_cast<float>(result);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool terrain_streamer::paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer)
|
bool terrain_streamer::paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer)
|
||||||
{
|
{
|
||||||
if (Radius <= 0.0 || Strength <= 0.0)
|
if (Radius <= 0.0 || Strength <= 0.0)
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class terrain_streamer
|
|||||||
bool sculpt(double X, double Z, double Radius, double Strength);
|
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
|
// evens out the ground within Radius of (X,Z), Amount (0..1) of the way at the centre
|
||||||
bool smooth(double X, double Z, double Radius, double Amount);
|
bool smooth(double X, double Z, double Radius, double Amount);
|
||||||
|
// 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);
|
// 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
|
// Shaper returns true if it changed the height
|
||||||
bool reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function<bool(double X, double Z, float &Height)> const &Shaper);
|
bool reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function<bool(double X, double Z, float &Height)> const &Shaper);
|
||||||
|
|||||||
Reference in New Issue
Block a user