17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-03 12:00:22 +01:00

fix(editor): ground lookups sorted once instead of walking every terrain model, orthophoto kept under the roads; feat(roads): junction sides, stop lines and junction options

This commit is contained in:
2026-10-03 23:13:01 +02:00
parent 502c609b13
commit d37dff11e1
7 changed files with 650 additions and 100 deletions

View File

@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorSettings.hpp" #include "editor/editorSettings.hpp"
#include "editor/editorModelSets.hpp" #include "editor/editorModelSets.hpp"
#include "editor/editorIncludeInfo.hpp" #include "editor/editorIncludeInfo.hpp"
#include "editor/editorGroundMesh.hpp"
#include "utilities/Globals.h" #include "utilities/Globals.h"
#include "simulation/simulation.h" #include "simulation/simulation.h"
#include "simulation/simulationtime.h" #include "simulation/simulationtime.h"
@@ -230,6 +231,69 @@ namespace
int m_rows{1}; int m_rows{1};
}; };
// meshes of the shapes and the models asked about as ground. the ground is asked about a tile at a time, by the orthophoto
// and by the road tools, and without these every question meant going through every triangle of every terrain model in reach
struct ground_record
{
ground_mesh mesh;
// what the mesh was made of, to tell when it has to be made again
std::size_t vertices{0};
std::size_t indices{0};
glm::dvec3 origin{0.0};
TModel3d const *model{nullptr};
glm::dmat4 matrix{0.0};
std::uint64_t used{0}; // number of the last question it served
};
std::unordered_map<std::uint64_t, ground_record> ground_shapes; // by the geometry of the shape
std::unordered_map<TAnimModel const *, ground_record> ground_models;
std::uint64_t ground_question{0};
std::size_t ground_held{0}; // triangles in the meshes
std::size_t constexpr ground_limit = 8000000; // triangles kept; some 400 MB with the grids
std::size_t constexpr ground_small = 256; // triangles; anything smaller is quicker to go through than to look up
// lets go of the meshes left alone the longest, once there's too much of them
void trim_ground_meshes()
{
if (ground_held <= ground_limit)
return;
std::vector<std::uint64_t> ages;
for (auto const &record : ground_shapes)
ages.emplace_back(record.second.used);
for (auto const &record : ground_models)
ages.emplace_back(record.second.used);
std::sort(ages.begin(), ages.end());
// the older half goes, except for what the current question needed
auto const threshold = std::min(ages[ages.size() / 2], ground_question - 1);
for (auto record = ground_shapes.begin(); record != ground_shapes.end();)
{
if (record->second.used <= threshold)
{
ground_held -= record->second.mesh.size();
record = ground_shapes.erase(record);
}
else
++record;
}
for (auto record = ground_models.begin(); record != ground_models.end();)
{
if (record->second.used <= threshold)
{
ground_held -= record->second.mesh.size();
record = ground_models.erase(record);
}
else
++record;
}
}
// forgets the meshes of the shapes, whose geometry can be changed in place (terrain being sculpted)
void forget_ground_shapes()
{
for (auto const &record : ground_shapes)
ground_held -= record.second.mesh.size();
ground_shapes.clear();
}
// appends triangles of a shape node which overlap specified XZ rectangle // appends triangles of a shape node which overlap specified XZ rectangle
void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out, bool const Skiproads = false) void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out, bool const Skiproads = false)
{ {
@@ -262,6 +326,36 @@ namespace
return; return;
auto const &verts = GfxRenderer->Vertices(data.geometry); auto const &verts = GfxRenderer->Vertices(data.geometry);
auto const &indices = GfxRenderer->Indices(data.geometry); auto const &indices = GfxRenderer->Indices(data.geometry);
auto const count = (indices.empty() ? verts.size() : indices.size()) / 3;
if (count >= ground_small)
{
// a large shape is sorted once, and asked for what's in the rectangle from then on
auto &record = ground_shapes[(static_cast<std::uint64_t>(data.geometry.bank) << 32) | data.geometry.chunk];
if (record.mesh.size() == 0 || record.vertices != verts.size() || record.indices != indices.size() || record.origin != data.origin)
{
std::vector<ground_mesh::triangle> triangles;
triangles.reserve(count);
if (false == indices.empty())
{
for (std::size_t i = 0; i + 2 < indices.size(); i += 3)
triangles.push_back({verts[indices[i]].position, verts[indices[i + 1]].position, verts[indices[i + 2]].position});
}
else
{
for (std::size_t i = 0; i + 2 < verts.size(); i += 3)
triangles.push_back({verts[i].position, verts[i + 1].position, verts[i + 2].position});
}
ground_held -= record.mesh.size();
record.mesh.assign(std::move(triangles), data.origin);
ground_held += record.mesh.size();
record.vertices = verts.size();
record.indices = indices.size();
record.origin = data.origin;
}
record.used = ground_question;
record.mesh.collect(Min, Max, Out);
return;
}
auto const to_world = [&](gfx::basic_vertex const &v) { return data.origin + glm::dvec3(v.position); }; auto const to_world = [&](gfx::basic_vertex const &v) { return data.origin + glm::dvec3(v.position); };
if (false == indices.empty()) if (false == indices.empty())
{ {
@@ -279,6 +373,8 @@ namespace
// 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) 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)
{ {
auto const started = std::chrono::steady_clock::now();
++ground_question;
glm::dvec2 const center = (Min + Max) * 0.5; glm::dvec2 const center = (Min + Max) * 0.5;
float const radius = static_cast<float>(glm::length(Max - Min) * 0.5); float const radius = static_cast<float>(glm::length(Max - Min) * 0.5);
auto const sections = simulation::Region->sections(glm::dvec3(center.x, 0.0, center.y), radius); // copy, the result is a scratchpad auto const sections = simulation::Region->sections(glm::dvec3(center.x, 0.0, center.y), radius); // copy, the result is a scratchpad
@@ -303,15 +399,35 @@ namespace
double const reach = instance->radius(); double const reach = instance->radius();
if (instance->location().x + reach < Min.x || instance->location().x - reach > Max.x || instance->location().z + reach < Min.y || instance->location().z - reach > Max.y) if (instance->location().x + reach < Min.x || instance->location().x - reach > Max.x || instance->location().z + reach < Min.y || instance->location().z - reach > Max.y)
continue; continue;
std::vector<world_triangle> modeltriangles; // the model is gone through once, and asked for what's in the rectangle from then on, for as long as it stays put
gather_submodel_triangles(instance->Model()->Root, instance_matrix(*instance), modeltriangles); auto const matrix = instance_matrix(*instance);
for (auto const &t : modeltriangles) auto &record = ground_models[instance];
if (std::max({t[0].x, t[1].x, t[2].x}) >= Min.x && std::min({t[0].x, t[1].x, t[2].x}) <= Max.x && std::max({t[0].z, t[1].z, t[2].z}) >= Min.y && if (record.model != instance->Model() || record.matrix != matrix)
std::min({t[0].z, t[1].z, t[2].z}) <= Max.y) {
Out.push_back(t); std::vector<world_triangle> modeltriangles;
gather_submodel_triangles(instance->Model()->Root, matrix, modeltriangles);
glm::dvec3 const origin = instance->location();
std::vector<ground_mesh::triangle> triangles;
triangles.reserve(modeltriangles.size());
for (auto const &t : modeltriangles)
triangles.push_back({glm::vec3(t[0] - origin), glm::vec3(t[1] - origin), glm::vec3(t[2] - origin)});
ground_held -= record.mesh.size();
record.mesh.assign(std::move(triangles), origin);
ground_held += record.mesh.size();
// a model which isn't loaded yet has nothing to show for itself, and is looked at again the next time
record.model = (record.mesh.size() > 0 ? instance->Model() : nullptr);
record.matrix = matrix;
}
record.used = ground_question;
record.mesh.collect(Min, Max, Out);
} }
} }
} }
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
auto const elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
if (elapsed > 250.0)
WriteLog("editor: gathering the ground took " + std::to_string(static_cast<int>(elapsed)) + " ms, for " + std::to_string(Out.size()) + " triangles; " + std::to_string(ground_held) + " are kept for the next time");
} }
// ground kept for the road tools, which ask about the same places frame after frame. it's gathered a tile at a time: // ground kept for the road tools, which ask about the same places frame after frame. it's gathered a tile at a time:
@@ -364,8 +480,9 @@ editor_mode::editor_mode() {
m_roadtool.settings.normalize(); m_roadtool.settings.normalize();
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
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, std::vector<world_triangle> &Out) {
gather_ground_triangles(Min, Max, true, 50.0f, Out); gather_ground_triangles(Min, Max, true, 50.0f, Out, true);
}); });
} }
@@ -1452,7 +1569,12 @@ void editor_mode::render_orthophoto_ui()
{ {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Refit")) if (ImGui::Button("Refit"))
{
// the ground was edited: what's kept of it has to go as well
ground_tiles.clear();
forget_ground_shapes();
m_orthophoto.refit(); m_orthophoto.refit();
}
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Samples the ground again, after it was edited"); ImGui::SetTooltip("Samples the ground again, after it was edited");
} }
@@ -1947,6 +2069,7 @@ void editor_mode::handle_terrain_sculpt(double Deltatime)
terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate); terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate);
// what the road tools know about the ground is no longer true // what the road tools know about the ground is no longer true
ground_tiles.clear(); ground_tiles.clear();
forget_ground_shapes();
} }
void editor_mode::capture_terrain() void editor_mode::capture_terrain()

View File

@@ -534,6 +534,9 @@ class editor_mode : public application_mode
void render_road_menu(); void render_road_menu();
void render_road_window(); void render_road_window();
bool render_road_layout(road_node::state &State); bool render_road_layout(road_node::state &State);
bool render_road_material(char const *Label, char *Buffer, std::size_t const Size, std::string &Value);
void render_junction_layout(junction_node &Junction);
void junction_apply(junction_node &Junction, junction_node::state const &State);
void draw_road_overlay() const; void draw_road_overlay() const;
void update_road_tool(); void update_road_tool();
void road_click(); void road_click();

View File

@@ -284,6 +284,139 @@ void editor_mode::junction_select(junction_node *Junction)
tool.status = "Selected: " + Junction->name(); tool.status = "Selected: " + Junction->name();
if (false == editor_road::can_edit(*Junction, &reason)) if (false == editor_road::can_edit(*Junction, &reason))
tool.status += ". It can't be changed: " + reason; tool.status += ". It can't be changed: " + reason;
// the fields for the names of materials are shared with the roads
copy_text(tool.surface, sizeof(tool.surface), Junction->definition().surface);
copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material);
}
// replaces the definition of a junction, as a step which can be taken back
void editor_mode::junction_apply(junction_node &Junction, junction_node::state const &State)
{
auto &tool{m_roadtool};
std::string reason;
if (false == editor_road::can_edit(Junction, &reason))
{
tool.status = "\"" + Junction.name() + "\" can't be changed: " + reason;
// what the window shows goes back to what the junction is like
junction_select(&Junction);
tool.status = "\"" + Junction.name() + "\" can't be changed: " + reason;
return;
}
editor_road::record record;
record.junctions.emplace_back(&Junction, Junction.definition());
editor_road::apply(std::vector<std::pair<junction_node *, junction_node::state>>{{&Junction, State}});
push_road_snapshot(std::move(record));
tool.status = "Changed";
}
// draws controls for what a junction is like, and makes the changes as they're made
void editor_mode::render_junction_layout(junction_node &Junction)
{
auto &tool{m_roadtool};
auto state{Junction.definition()};
bool changed{false};
// typed values are taken when confirmed, so the junction isn't made anew with each digit
auto const number = [](char const *Label, float &Value, float const Step, char const *Format) {
ImGui::SetNextItemWidth(120.0f);
return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue);
};
auto limit{state.velocity > 0.f ? state.velocity : 0.f};
if (number("Speed limit [km/h], 0: none", limit, 5.0f, "%.0f"))
{
state.velocity = (limit > 0.f ? limit : -1.f);
changed = true;
}
auto centre{static_cast<float>(state.centre.y)};
if (number("Height of the middle [m]", centre, 0.1f, "%.2f"))
{
state.centre.y = centre;
changed = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road.");
// appearance
ImGui::Separator();
char const *colours[]{"none", "white", "orange"};
int colour{state.markings == road_node::marking_colour::white ? 1 : state.markings == road_node::marking_colour::orange ? 2 : 0};
ImGui::SetNextItemWidth(120.0f);
if (ImGui::Combo("Markings", &colour, colours, 3))
{
state.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none);
changed = true;
}
if (render_road_material("Surface", tool.surface, sizeof(tool.surface), state.surface))
changed = true;
if (number("Texture length [m]", state.texturelength, 0.5f, "%.2f"))
{
state.texturelength = std::max(0.1f, state.texturelength);
changed = true;
}
// what the corners between the roads are lined with
char const *sidetypes[]{"none", "shoulder", "sidewalk"};
int type{state.side.type == road_node::side_type::shoulder ? 1 : state.side.type == road_node::side_type::sidewalk ? 2 : 0};
ImGui::SetNextItemWidth(120.0f);
if (ImGui::Combo("Sides of the corners", &type, sidetypes, 3))
{
state.side.type = (type == 1 ? road_node::side_type::shoulder : type == 2 ? road_node::side_type::sidewalk : road_node::side_type::none);
if (state.side.type != road_node::side_type::none && state.side.width <= 0.0f)
state.side.width = 1.5f;
changed = true;
}
if (state.side.type != road_node::side_type::none)
{
ImGui::Indent();
if (number("Width [m]", state.side.width, 0.25f, "%.2f"))
{
state.side.width = std::clamp(state.side.width, 0.1f, 20.0f);
changed = true;
}
if (render_road_material("Material", tool.sides[0], sizeof(tool.sides[0]), state.side.material))
changed = true;
if (state.side.material.empty() || state.side.material == "none")
ImGui::TextDisabled("Pick a material to have the sides drawn");
if (state.side.type == road_node::side_type::sidewalk)
{
if (number("Kerb height [m]", state.kerbheight, 0.01f, "%.2f"))
{
state.kerbheight = std::clamp(state.kerbheight, 0.0f, 1.0f);
changed = true;
}
}
else
{
if (number("Bank width [m]", state.slope.x, 0.25f, "%.2f"))
{
state.slope.x = std::clamp(state.slope.x, 0.0f, 20.0f);
changed = true;
}
if (number("Bank drop [m]", state.slope.y, 0.1f, "%.2f"))
{
state.slope.y = std::clamp(state.slope.y, 0.0f, 20.0f);
changed = true;
}
}
ImGui::Unindent();
}
// the roads
if (ImGui::CollapsingHeader("Roads of the junction", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::TextDisabled("The lanes are taken from the roads: change them on the road, the junction follows");
for (std::size_t arm = 0; arm < state.arms.size(); ++arm)
{
ImGui::PushID(static_cast<int>(arm));
auto &data{state.arms[arm]};
ImGui::Text("%d: %d in, %d out, %.2f m a lane", static_cast<int>(arm) + 1, data.incoming, data.outgoing, data.width);
ImGui::SameLine();
if (ImGui::Checkbox("Stop line", &data.stopline))
changed = true;
ImGui::PopID();
}
}
if (changed)
junction_apply(Junction, state);
} }
void editor_mode::road_cancel() void editor_mode::road_cancel()
@@ -969,8 +1102,62 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec
road_cancel(); road_cancel();
if (tool.selected != nullptr) if (tool.selected != nullptr)
road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); road_select(tool.selected->m_editorremoved ? nullptr : tool.selected);
if (tool.junction != nullptr && tool.junction->m_editorremoved) if (tool.junction != nullptr)
tool.junction = nullptr; junction_select(tool.junction->m_editorremoved ? nullptr : tool.junction);
}
// a field for the name of a material, with a list of the ones in the texture folder to pick from. returns: true if the material was changed
bool editor_mode::render_road_material(char const *Label, char *Buffer, std::size_t const Size, std::string &Value)
{
auto &tool{m_roadtool};
bool picked{false};
ImGui::PushID(Label);
ImGui::SetNextItemWidth(200.0f);
if (ImGui::InputText("##name", Buffer, Size, ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit())
{
auto const name{lower_case(Buffer)};
if (name != Value && name.find(' ') == std::string::npos)
{
Value = name.empty() ? std::string{"none"} : name;
picked = true;
}
copy_text(Buffer, Size, Value);
}
ImGui::SameLine();
if (ImGui::Button("..."))
ImGui::OpenPopup("pick");
ImGui::SameLine();
ImGui::TextUnformatted(Label);
if (ImGui::BeginPopup("pick"))
{
ImGui::TextDisabled("Materials and images found in the texture folder");
ImGui::SetNextItemWidth(200.0f);
ImGui::InputText("Filter", tool.filter, sizeof(tool.filter));
auto const filter{lower_case(tool.filter)};
std::vector<std::string const *> listed;
for (auto const &name : editor_road::materials())
if (filter.empty() || name.find(filter) != std::string::npos)
listed.emplace_back(&name);
ImGui::BeginChild("list", ImVec2(340.0f, 280.0f), true);
ImGuiListClipper clipper(static_cast<int>(listed.size()));
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
if (ImGui::Selectable(listed[i]->c_str(), *listed[i] == Value))
{
Value = *listed[i];
copy_text(Buffer, Size, Value);
picked = true;
ImGui::CloseCurrentPopup();
}
}
}
ImGui::EndChild();
ImGui::EndPopup();
}
ImGui::PopID();
return picked;
} }
// draws controls for the layout of a road. returns: true if the layout was changed in a way which calls for the road to be made anew // draws controls for the layout of a road. returns: true if the layout was changed in a way which calls for the road to be made anew
@@ -983,56 +1170,7 @@ bool editor_mode::render_road_layout(road_node::state &State)
ImGui::SetNextItemWidth(120.0f); ImGui::SetNextItemWidth(120.0f);
return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue); return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue);
}; };
auto const material = [&tool](char const *Label, char *Buffer, std::size_t const Size, std::string &Value) { auto const material = [this](char const *Label, char *Buffer, std::size_t const Size, std::string &Value) { return render_road_material(Label, Buffer, Size, Value); };
bool picked{false};
ImGui::PushID(Label);
ImGui::SetNextItemWidth(200.0f);
if (ImGui::InputText("##name", Buffer, Size, ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit())
{
auto const name{lower_case(Buffer)};
if (name != Value && name.find(' ') == std::string::npos)
{
Value = name.empty() ? std::string{"none"} : name;
picked = true;
}
copy_text(Buffer, Size, Value);
}
ImGui::SameLine();
if (ImGui::Button("..."))
ImGui::OpenPopup("pick");
ImGui::SameLine();
ImGui::TextUnformatted(Label);
if (ImGui::BeginPopup("pick"))
{
ImGui::TextDisabled("Materials and images found in the texture folder");
ImGui::SetNextItemWidth(200.0f);
ImGui::InputText("Filter", tool.filter, sizeof(tool.filter));
auto const filter{lower_case(tool.filter)};
std::vector<std::string const *> listed;
for (auto const &name : editor_road::materials())
if (filter.empty() || name.find(filter) != std::string::npos)
listed.emplace_back(&name);
ImGui::BeginChild("list", ImVec2(340.0f, 280.0f), true);
ImGuiListClipper clipper(static_cast<int>(listed.size()));
while (clipper.Step())
{
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
if (ImGui::Selectable(listed[i]->c_str(), *listed[i] == Value))
{
Value = *listed[i];
copy_text(Buffer, Size, Value);
picked = true;
ImGui::CloseCurrentPopup();
}
}
}
ImGui::EndChild();
ImGui::EndPopup();
}
ImGui::PopID();
return picked;
};
// lanes // lanes
ImGui::SetNextItemWidth(120.0f); ImGui::SetNextItemWidth(120.0f);
@@ -1200,9 +1338,10 @@ void editor_mode::render_road_window()
road_cancel(); road_cancel();
if (tool.tool != 0) if (tool.tool != 0)
{ {
tool.selected = nullptr;
tool.junction = nullptr; tool.junction = nullptr;
tool.points.clear(); tool.points.clear();
// the fields for the names of materials show the layout for the roads to be built again
road_select(nullptr);
} }
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -1296,44 +1435,7 @@ void editor_mode::render_road_window()
ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast<int>(junction.definition().arms.size()), static_cast<int>(junction.movements().size())); ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast<int>(junction.definition().arms.size()), static_cast<int>(junction.movements().size()));
ImGui::TextDisabled("The ways through are made for the roads attached at the moment."); ImGui::TextDisabled("The ways through are made for the roads attached at the moment.");
ImGui::TextDisabled("Build tool, LMB on the junction: lead another road out of it"); ImGui::TextDisabled("Build tool, LMB on the junction: lead another road out of it");
auto limit{junction.definition().velocity > 0.f ? junction.definition().velocity : 0.f}; render_junction_layout(junction);
if (number("Speed limit [km/h], 0: none", limit, 5.0f, "%.0f"))
{
if (editor_road::can_edit(junction, &reason))
{
editor_road::record record;
record.junctions.emplace_back(&junction, junction.definition());
auto state{junction.definition()};
state.velocity = (limit > 0.f ? limit : -1.f);
editor_road::apply(std::vector<std::pair<junction_node *, junction_node::state>>{{&junction, state}});
push_road_snapshot(std::move(record));
tool.status = "Changed";
}
else
{
tool.status = "\"" + junction.name() + "\" can't be changed: " + reason;
}
}
auto centre{static_cast<float>(junction.definition().centre.y)};
if (number("Height of the middle [m]", centre, 0.1f, "%.2f"))
{
if (editor_road::can_edit(junction, &reason))
{
editor_road::record record;
record.junctions.emplace_back(&junction, junction.definition());
auto state{junction.definition()};
state.centre.y = centre;
editor_road::apply(std::vector<std::pair<junction_node *, junction_node::state>>{{&junction, state}});
push_road_snapshot(std::move(record));
tool.status = "Changed";
}
else
{
tool.status = "\"" + junction.name() + "\" can't be changed: " + reason;
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road.");
if (ImGui::Button("Delete (Del)")) if (ImGui::Button("Delete (Del)"))
road_delete(); road_delete();
} }

148
editor/editorGroundMesh.hpp Normal file
View File

@@ -0,0 +1,148 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <vector>
#include <glm/glm.hpp>
// triangles of a single piece of the scenery - a shape or a model - sorted on a grid seen from above, so the ones
// overlapping an area can be picked without going through all of them.
// the editor asks about the ground a tile at a time, frame after frame, and a terrain model can hold a million
// triangles: walking it whole for every question is what used to bring the simulator down to a frame a second
class ground_mesh
{
public:
using triangle = std::array<glm::vec3, 3>; // relative to the origin of the mesh
using world_triangle = std::array<glm::dvec3, 3>;
// takes over provided triangles. Origin: the point their vertices are relative to
void assign(std::vector<triangle> Triangles, glm::dvec3 const &Origin)
{
m_triangles = std::move(Triangles);
m_origin = Origin;
m_starts.clear();
m_items.clear();
m_seen.assign(m_triangles.size(), 0);
m_query = 0;
if (m_triangles.empty())
return;
glm::vec2 low{m_triangles.front()[0].x, m_triangles.front()[0].z};
glm::vec2 high{low};
for (auto const &entry : m_triangles)
{
for (auto const &vertex : entry)
{
low = glm::min(low, glm::vec2{vertex.x, vertex.z});
high = glm::max(high, glm::vec2{vertex.x, vertex.z});
}
}
m_low = low;
auto const extent{glm::max(high - low, glm::vec2{1.f})};
// cells of a few metres, larger for a large mesh so the grid stays small
m_cellsize = std::max(8.f, std::max(extent.x, extent.y) / 96.f);
m_columns = static_cast<int>(extent.x / m_cellsize) + 1;
m_rows = static_cast<int>(extent.y / m_cellsize) + 1;
// a triangle is listed in every cell its bounds reach. the lists are kept one after another, with the place each starts at
std::vector<std::uint32_t> counts(static_cast<std::size_t>(m_columns) * m_rows + 1, 0);
for (auto const &entry : m_triangles)
{
auto const span{cells(entry)};
for (int row = span[1]; row <= span[3]; ++row)
for (int column = span[0]; column <= span[2]; ++column)
++counts[static_cast<std::size_t>(row) * m_columns + column + 1];
}
for (std::size_t cell = 1; cell < counts.size(); ++cell)
counts[cell] += counts[cell - 1];
m_starts = counts;
m_items.resize(m_starts.back());
for (std::uint32_t index = 0; index < m_triangles.size(); ++index)
{
auto const span{cells(m_triangles[index])};
for (int row = span[1]; row <= span[3]; ++row)
for (int column = span[0]; column <= span[2]; ++column)
m_items[counts[static_cast<std::size_t>(row) * m_columns + column]++] = index;
}
}
std::size_t size() const
{
return m_triangles.size();
}
// appends the triangles whose bounds overlap specified rectangle of the world, seen from above (x, z)
void collect(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) const
{
if (m_triangles.empty())
return;
// the rectangle as the mesh sees it, widened by what's lost turning the numbers into single precision
glm::vec2 const low{static_cast<float>(Min.x - m_origin.x) - 0.001f, static_cast<float>(Min.y - m_origin.z) - 0.001f};
glm::vec2 const high{static_cast<float>(Max.x - m_origin.x) + 0.001f, static_cast<float>(Max.y - m_origin.z) + 0.001f};
auto const firstcolumn{column(low.x)};
auto const lastcolumn{column(high.x)};
auto const firstrow{row(low.y)};
auto const lastrow{row(high.y)};
if (++m_query == 0)
{
// the counter went all the way round
std::fill(m_seen.begin(), m_seen.end(), 0);
m_query = 1;
}
for (int row = firstrow; row <= lastrow; ++row)
{
for (int column = firstcolumn; column <= lastcolumn; ++column)
{
auto const cell{static_cast<std::size_t>(row) * m_columns + column};
for (auto item = m_starts[cell]; item < m_starts[cell + 1]; ++item)
{
auto const index{m_items[item]};
if (m_seen[index] == m_query)
continue;
m_seen[index] = m_query;
auto const &entry{m_triangles[index]};
if (std::max({entry[0].x, entry[1].x, entry[2].x}) < low.x || std::min({entry[0].x, entry[1].x, entry[2].x}) > high.x || std::max({entry[0].z, entry[1].z, entry[2].z}) < low.y ||
std::min({entry[0].z, entry[1].z, entry[2].z}) > high.y)
continue;
Out.push_back({m_origin + glm::dvec3{entry[0]}, m_origin + glm::dvec3{entry[1]}, m_origin + glm::dvec3{entry[2]}});
}
}
}
}
private:
int column(float const X) const
{
return std::clamp(static_cast<int>(std::floor((X - m_low.x) / m_cellsize)), 0, m_columns - 1);
}
int row(float const Z) const
{
return std::clamp(static_cast<int>(std::floor((Z - m_low.y) / m_cellsize)), 0, m_rows - 1);
}
// cells the bounds of a triangle reach: first column, first row, last column, last row
std::array<int, 4> cells(triangle const &Triangle) const
{
return {column(std::min({Triangle[0].x, Triangle[1].x, Triangle[2].x})), row(std::min({Triangle[0].z, Triangle[1].z, Triangle[2].z})), column(std::max({Triangle[0].x, Triangle[1].x, Triangle[2].x})),
row(std::max({Triangle[0].z, Triangle[1].z, Triangle[2].z}))};
}
std::vector<triangle> m_triangles;
glm::dvec3 m_origin{0.0};
glm::vec2 m_low{0.f};
float m_cellsize{8.f};
int m_columns{1};
int m_rows{1};
std::vector<std::uint32_t> m_starts; // for each cell: where its list starts; one more entry closes the last list
std::vector<std::uint32_t> m_items; // numbers of the triangles, cell after cell
mutable std::vector<std::uint32_t> m_seen; // number of the last lookup each triangle was reported in
mutable std::uint32_t m_query{0};
};

View File

@@ -300,6 +300,10 @@ void dress(junction_node::state &Junction, road_node::state const &Road)
Junction.surface = Road.surface; Junction.surface = Road.surface;
Junction.texturelength = Road.texturelength; Junction.texturelength = Road.texturelength;
Junction.markings = Road.markings; Junction.markings = Road.markings;
// one kind of side goes around the whole junction; the right side of the road is the one facing the corner a road leaves by
Junction.side = (Road.sides[1].type != road_node::side_type::none ? Road.sides[1] : Road.sides[0]);
Junction.kerbheight = Road.kerbheight;
Junction.slope = Road.slope;
Junction.friction = Road.friction; Junction.friction = Road.friction;
Junction.sounddistance = Road.sounddistance; Junction.sounddistance = Road.sounddistance;
Junction.quality = Road.quality; Junction.quality = Road.quality;

View File

@@ -948,6 +948,8 @@ std::unordered_set<std::uint32_t> ownedbanks;
double const junction_reach{0.25}; // an end of a road this close to an arm is attached to it double const junction_reach{0.25}; // an end of a road this close to an arm is attached to it
double const junction_fit{0.05}; // a lane has to end this close to where the junction expects it double const junction_fit{0.05}; // a lane has to end this close to where the junction expects it
int const corner_steps{6}; // pieces a rounded corner is made of int const corner_steps{6}; // pieces a rounded corner is made of
double const stopline_width{0.5};
double const stopline_inset{0.2}; // distance of a stop line from the end of its road
// direction on the ground as a vector in space // direction on the ground as a vector in space
glm::dvec3 flat(glm::dvec2 const &Direction) glm::dvec3 flat(glm::dvec2 const &Direction)
@@ -1064,6 +1066,9 @@ void junction_node::state::normalize()
arm.width = std::max(1.f, arm.width); arm.width = std::max(1.f, arm.width);
arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0}); arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0});
} }
side.width = std::max(0.f, side.width);
kerbheight = std::max(0.f, kerbheight);
slope = glm::max(slope, glm::vec2{0.f});
if (texturelength < 0.01f) if (texturelength < 0.01f)
{ {
texturelength = 4.f; texturelength = 4.f;
@@ -1168,6 +1173,7 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset)
Input.getTokens(3); Input.getTokens(3);
Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z; Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z;
m_state.centre += Offset; m_state.centre += Offset;
std::vector<int> stoplines;
auto token{Input.getToken<std::string>()}; auto token{Input.getToken<std::string>()};
while (false == token.empty() && token != "endjunction") while (false == token.empty() && token != "endjunction")
@@ -1200,6 +1206,39 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset)
auto const colour{Input.getToken<std::string>()}; auto const colour{Input.getToken<std::string>()};
m_state.markings = (colour == "white" ? road_node::marking_colour::white : colour == "orange" ? road_node::marking_colour::orange : road_node::marking_colour::none); m_state.markings = (colour == "white" ? road_node::marking_colour::white : colour == "orange" ? road_node::marking_colour::orange : road_node::marking_colour::none);
} }
else if (token == "side")
{
// side <none|shoulder|sidewalk> <width> <material>
auto const type{Input.getToken<std::string>()};
Input.getTokens();
Input >> m_state.side.width;
m_state.side.material = Input.getToken<std::string>();
replace_slashes(m_state.side.material);
m_state.side.type = (type == "shoulder" ? road_node::side_type::shoulder : type == "sidewalk" ? road_node::side_type::sidewalk : road_node::side_type::none);
if (type != "shoulder" && type != "sidewalk" && type != "none")
{
ErrorLog("Bad junction: unknown kind of side \"" + type + "\" defined for " + label);
}
}
else if (token == "kerb")
{
Input.getTokens();
Input >> m_state.kerbheight;
}
else if (token == "slope")
{
// slope <width> <drop>
Input.getTokens(2);
Input >> m_state.slope.x >> m_state.slope.y;
}
else if (token == "stopline")
{
// stopline <number of an arm, counted from 1>; the arms it's for may come later, so the numbers wait
int number{0};
Input.getTokens();
Input >> number;
stoplines.emplace_back(number);
}
else if (token == "velocity") else if (token == "velocity")
{ {
Input.getTokens(); Input.getTokens();
@@ -1220,6 +1259,15 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset)
} }
token = Input.getToken<std::string>(); token = Input.getToken<std::string>();
} }
for (auto const number : stoplines)
{
if (number < 1 || number > static_cast<int>(m_state.arms.size()))
{
ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(number) + " to put a stop line at");
continue;
}
m_state.arms[number - 1].stopline = true;
}
if (m_state.arms.size() < 2 || m_state.arms.size() > 4) if (m_state.arms.size() < 2 || m_state.arms.size() > 4)
{ {
ErrorLog("Bad junction: " + label + " has " + std::to_string(m_state.arms.size()) + " arms, it takes 2 to 4"); ErrorLog("Bad junction: " + label + " has " + std::to_string(m_state.arms.size()) + " arms, it takes 2 to 4");
@@ -1580,6 +1628,87 @@ std::vector<scene::shape_node> junction_node::create_shapes() const
shapes.emplace_back(std::move(shape)); shapes.emplace_back(std::move(shape));
} }
} }
auto const &side{m_state.side};
if (side.type != road_node::side_type::none && side.width > 0.f && false == side.material.empty() && side.material != "none")
{
// the corners are lined the way the sides of a road are, so the shoulders and the sidewalks of the roads carry on around the junction
struct corner_point
{
double offset; // distance from the edge of the surface
double height;
float texture;
};
std::vector<corner_point> profile;
if (side.type == road_node::side_type::shoulder)
{
profile.push_back({0.0, 0.0, 1.f});
profile.push_back({side.width, 0.0, 0.5f});
if (m_state.slope.x > 0.f)
{
profile.push_back({static_cast<double>(side.width) + m_state.slope.x, -m_state.slope.y, 0.f});
}
}
else
{
auto const tile{static_cast<double>(texture_ratio(side.material)) * m_state.texturelength};
profile.push_back({0.0, 0.0, 0.f});
profile.push_back({0.0, m_state.kerbheight, static_cast<float>(m_state.kerbheight / tile)});
profile.push_back({side.width, m_state.kerbheight, static_cast<float>((m_state.kerbheight + side.width) / tile)});
}
auto const order{m_state.arm_order()};
std::vector<world_vertex> vertices;
for (std::size_t idx = 0; idx < corners.size() && idx < order.size(); ++idx)
{
auto const &corner{corners[idx]};
if (corner.size() < 2)
{
continue;
}
// the way out of the surface at each point of the corner. at its ends it's the way across the roads there,
// so the side meets what the roads have along their edges
std::vector<glm::dvec3> outwards(corner.size());
outwards.front() = flat(left_of(m_state.arms[order[idx]].direction));
outwards.back() = -flat(left_of(m_state.arms[order[(idx + 1) % order.size()]].direction));
for (std::size_t point = 1; point + 1 < corner.size(); ++point)
{
// going around the junction the surface is on the left, and the way out of it on the right
glm::dvec2 const along{corner[point + 1].x - corner[point - 1].x, corner[point + 1].z - corner[point - 1].z};
outwards[point] = (glm::length(along) > 1e-9 ? -flat(left_of(glm::normalize(along))) : outwards[point - 1]);
}
double station{0.0};
for (std::size_t point = 0; point + 1 < corner.size(); ++point)
{
auto const length{glm::distance(corner[point], corner[point + 1])};
auto const tangent{corner[point + 1] - corner[point]};
auto const texturestart{static_cast<float>(station / m_state.texturelength)};
auto const textureend{static_cast<float>((station + length) / m_state.texturelength)};
for (std::size_t band = 0; band + 1 < profile.size(); ++band)
{
auto const &inner{profile[band]};
auto const &outer{profile[band + 1]};
auto const leftstart{corner[point] + outwards[point] * inner.offset + up * inner.height};
auto const rightstart{corner[point] + outwards[point] * outer.offset + up * outer.height};
auto const leftend{corner[point + 1] + outwards[point + 1] * inner.offset + up * inner.height};
auto const rightend{corner[point + 1] + outwards[point + 1] * outer.offset + up * outer.height};
auto facing{glm::cross(tangent, leftstart - rightstart)};
glm::vec3 const normal{glm::length2(facing) < 1e-12 ? up : glm::normalize(facing)};
vertices.push_back({leftstart, normal, {inner.texture, texturestart}});
vertices.push_back({rightstart, normal, {outer.texture, texturestart}});
vertices.push_back({leftend, normal, {inner.texture, textureend}});
vertices.push_back({rightstart, normal, {outer.texture, texturestart}});
vertices.push_back({rightend, normal, {outer.texture, textureend}});
vertices.push_back({leftend, normal, {inner.texture, textureend}});
}
station += length;
}
}
if (false == vertices.empty())
{
scene::shape_node shape;
shape.make_terrain(GfxRenderer->Fetch_Material(side.material), std::move(vertices), glm::dvec3{0.0});
shapes.emplace_back(std::move(shape));
}
}
if (m_state.markings != road_node::marking_colour::none) if (m_state.markings != road_node::marking_colour::none)
{ {
// edge lines carried around the corners // edge lines carried around the corners
@@ -1587,6 +1716,29 @@ std::vector<scene::shape_node> junction_node::create_shapes() const
glm::dvec3 const lift{0.0, line_lift, 0.0}; glm::dvec3 const lift{0.0, line_lift, 0.0};
glm::vec3 const upwards{0.f, 1.f, 0.f}; glm::vec3 const upwards{0.f, 1.f, 0.f};
auto const &arms{m_state.arms}; auto const &arms{m_state.arms};
for (std::size_t arm = 0; arm < arms.size(); ++arm)
{
if (false == arms[arm].stopline || arms[arm].incoming < 1)
{
continue;
}
// a stop line: across the lanes leading into the junction, right where the road ends
auto const across{flat(left_of(arms[arm].direction))};
auto const inwards{-flat(arms[arm].direction)};
auto const halfwidth{0.5 * m_state.arm_width(arm)};
auto const from{arms[arm].position + across * (halfwidth - static_cast<double>(arms[arm].incoming) * arms[arm].width + 0.5 * line_width) + lift};
auto const to{arms[arm].position + across * (halfwidth - edge_inset - line_width) + lift};
auto const leftstart{from + inwards * (stopline_inset + stopline_width)};
auto const rightstart{from + inwards * stopline_inset};
auto const leftend{to + inwards * (stopline_inset + stopline_width)};
auto const rightend{to + inwards * stopline_inset};
vertices.push_back({leftstart, upwards, {0.f, 0.f}});
vertices.push_back({rightstart, upwards, {1.f, 0.f}});
vertices.push_back({leftend, upwards, {0.f, 1.f}});
vertices.push_back({rightstart, upwards, {1.f, 0.f}});
vertices.push_back({rightend, upwards, {1.f, 1.f}});
vertices.push_back({leftend, upwards, {0.f, 1.f}});
}
if (arms.size() == 2 && arms[0].incoming > 0 && arms[0].outgoing > 0 && arms[1].incoming > 0 && arms[1].outgoing > 0) if (arms.size() == 2 && arms[0].incoming > 0 && arms[0].outgoing > 0 && arms[1].incoming > 0 && arms[1].outgoing > 0)
{ {
// a junction of two roads is where a road changes its lanes. the double line keeping the directions apart is led through it // a junction of two roads is where a road changes its lanes. the double line keeping the directions apart is led through it
@@ -1725,8 +1877,20 @@ void junction_node::export_as_text_(std::ostream &Output) const
} }
Output.precision(precision); Output.precision(precision);
Output << "surface " << m_state.surface << ' ' << "texlength " << m_state.texturelength << ' ' << "markings " Output << "surface " << m_state.surface << ' ' << "texlength " << m_state.texturelength << ' ' << "markings "
<< (m_state.markings == road_node::marking_colour::white ? "white" : m_state.markings == road_node::marking_colour::orange ? "orange" : "none") << ' ' << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' ' << (m_state.markings == road_node::marking_colour::white ? "white" : m_state.markings == road_node::marking_colour::orange ? "orange" : "none") << ' ';
<< "environment " << m_state.environment << ' '; if (m_state.side.type != road_node::side_type::none)
{
Output << "side " << (m_state.side.type == road_node::side_type::shoulder ? "shoulder" : "sidewalk") << ' ' << m_state.side.width << ' ' << (m_state.side.material.empty() ? "none" : m_state.side.material) << ' ' << "kerb " << m_state.kerbheight << ' '
<< "slope " << m_state.slope.x << ' ' << m_state.slope.y << ' ';
}
for (std::size_t arm = 0; arm < m_state.arms.size(); ++arm)
{
if (m_state.arms[arm].stopline)
{
Output << "stopline " << arm + 1 << ' ';
}
}
Output << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' ' << "environment " << m_state.environment << ' ';
Output << "endjunction" Output << "endjunction"
<< "\n"; << "\n";
} }

View File

@@ -210,6 +210,8 @@ class road_node : public scene::basic_node
// gets a legacy crossroads path, which is what the vehicles and their drivers already know how to pick a way through. // gets a legacy crossroads path, which is what the vehicles and their drivers already know how to pick a way through.
// scenery entry: // scenery entry:
// node <max> <min> <name> junction <centre> arm <position> <direction x z> <lanes in> <lanes out> <lane width> ... [<property> <values>]... endjunction // node <max> <min> <name> junction <centre> arm <position> <direction x z> <lanes in> <lanes out> <lane width> ... [<property> <values>]... endjunction
// properties: surface <material>, texlength <m>, markings <white|orange|none>, side <none|shoulder|sidewalk> <width> <material>, kerb <height>,
// slope <width> <drop>, stopline <number of an arm, counted from 1>, velocity <km/h>, friction <value>, environment <name>
// the arms don't have to be level with each other: the surface is spanned between their ends and the centre // the arms don't have to be level with each other: the surface is spanned between their ends and the centre
class junction_node : public scene::basic_node class junction_node : public scene::basic_node
{ {
@@ -223,6 +225,7 @@ class junction_node : public scene::basic_node
int incoming{1}; // lanes leading into the junction int incoming{1}; // lanes leading into the junction
int outgoing{1}; // lanes leading out of it int outgoing{1}; // lanes leading out of it
float width{3.5f}; // width of a lane float width{3.5f}; // width of a lane
bool stopline{false}; // a line is painted across the lanes leading into the junction
}; };
struct state struct state
{ {
@@ -231,6 +234,9 @@ class junction_node : public scene::basic_node
std::string surface{"asphaltdark1"}; std::string surface{"asphaltdark1"};
float texturelength{4.f}; float texturelength{4.f};
road_node::marking_colour markings{road_node::marking_colour::white}; road_node::marking_colour markings{road_node::marking_colour::white};
road_node::side_data side; // what the corners between the roads are lined with, the way the sides of a road are
float kerbheight{0.12f};
glm::vec2 slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder
float velocity{30.f}; // speed limit on the way through float velocity{30.f}; // speed limit on the way through
float friction{0.85f}; float friction{0.85f};
float sounddistance{25.f}; float sounddistance{25.f};