From d37dff11e166d24f55349edf524e4f5add86b4be Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 3 Oct 2026 23:13:01 +0200 Subject: [PATCH] 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 --- application/editormode.cpp | 137 +++++++++++++++- application/editormode.h | 3 + application/editormoderoad.cpp | 284 ++++++++++++++++++++++----------- editor/editorGroundMesh.hpp | 148 +++++++++++++++++ editor/editorRoad.cpp | 4 + world/Road.cpp | 168 ++++++++++++++++++- world/Road.h | 6 + 7 files changed, 650 insertions(+), 100 deletions(-) create mode 100644 editor/editorGroundMesh.hpp diff --git a/application/editormode.cpp b/application/editormode.cpp index 1a947be5..d690a199 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorSettings.hpp" #include "editor/editorModelSets.hpp" #include "editor/editorIncludeInfo.hpp" +#include "editor/editorGroundMesh.hpp" #include "utilities/Globals.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" @@ -230,6 +231,69 @@ namespace 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 ground_shapes; // by the geometry of the shape + std::unordered_map 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 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 void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out, bool const Skiproads = false) { @@ -262,6 +326,36 @@ namespace return; auto const &verts = GfxRenderer->Vertices(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(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 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); }; if (false == indices.empty()) { @@ -279,6 +373,8 @@ namespace // 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 &Out, bool const Skiproads = false) { + auto const started = std::chrono::steady_clock::now(); + ++ground_question; glm::dvec2 const center = (Min + Max) * 0.5; float const radius = static_cast(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 @@ -303,15 +399,35 @@ namespace 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) continue; - std::vector modeltriangles; - gather_submodel_triangles(instance->Model()->Root, instance_matrix(*instance), modeltriangles); - for (auto const &t : modeltriangles) - 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 && - std::min({t[0].z, t[1].z, t[2].z}) <= Max.y) - Out.push_back(t); + // the model is gone through once, and asked for what's in the rectangle from then on, for as long as it stays put + auto const matrix = instance_matrix(*instance); + auto &record = ground_models[instance]; + if (record.model != instance->Model() || record.matrix != matrix) + { + std::vector modeltriangles; + gather_submodel_triangles(instance->Model()->Root, matrix, modeltriangles); + glm::dvec3 const origin = instance->location(); + std::vector 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(std::chrono::steady_clock::now() - started).count(); + if (elapsed > 250.0) + WriteLog("editor: gathering the ground took " + std::to_string(static_cast(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: @@ -364,8 +480,9 @@ editor_mode::editor_mode() { m_roadtool.settings.normalize(); road_select(nullptr); // 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 &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(); 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(); + } if (ImGui::IsItemHovered()) 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); // what the road tools know about the ground is no longer true ground_tiles.clear(); + forget_ground_shapes(); } void editor_mode::capture_terrain() diff --git a/application/editormode.h b/application/editormode.h index e34c6e2f..15d5de4d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -534,6 +534,9 @@ class editor_mode : public application_mode void render_road_menu(); void render_road_window(); 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 update_road_tool(); void road_click(); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 300899f4..59c78b98 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -284,6 +284,139 @@ void editor_mode::junction_select(junction_node *Junction) tool.status = "Selected: " + Junction->name(); if (false == editor_road::can_edit(*Junction, &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>{{&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(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(arm)); + auto &data{state.arms[arm]}; + ImGui::Text("%d: %d in, %d out, %.2f m a lane", static_cast(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() @@ -969,8 +1102,62 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec road_cancel(); if (tool.selected != nullptr) road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); - if (tool.junction != nullptr && tool.junction->m_editorremoved) - tool.junction = nullptr; + if (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 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(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 @@ -983,56 +1170,7 @@ bool editor_mode::render_road_layout(road_node::state &State) ImGui::SetNextItemWidth(120.0f); 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) { - 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 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(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; - }; + 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); }; // lanes ImGui::SetNextItemWidth(120.0f); @@ -1200,9 +1338,10 @@ void editor_mode::render_road_window() road_cancel(); if (tool.tool != 0) { - tool.selected = nullptr; tool.junction = nullptr; 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(); @@ -1296,44 +1435,7 @@ void editor_mode::render_road_window() ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(junction.movements().size())); 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"); - auto limit{junction.definition().velocity > 0.f ? junction.definition().velocity : 0.f}; - 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>{{&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(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>{{&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."); + render_junction_layout(junction); if (ImGui::Button("Delete (Del)")) road_delete(); } diff --git a/editor/editorGroundMesh.hpp b/editor/editorGroundMesh.hpp new file mode 100644 index 00000000..068b3fbb --- /dev/null +++ b/editor/editorGroundMesh.hpp @@ -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 +#include +#include +#include +#include +#include + +// 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; // relative to the origin of the mesh + using world_triangle = std::array; + + // takes over provided triangles. Origin: the point their vertices are relative to + void assign(std::vector 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(extent.x / m_cellsize) + 1; + m_rows = static_cast(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 counts(static_cast(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(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(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 &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(Min.x - m_origin.x) - 0.001f, static_cast(Min.y - m_origin.z) - 0.001f}; + glm::vec2 const high{static_cast(Max.x - m_origin.x) + 0.001f, static_cast(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(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(std::floor((X - m_low.x) / m_cellsize)), 0, m_columns - 1); + } + int row(float const Z) const + { + return std::clamp(static_cast(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 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 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 m_starts; // for each cell: where its list starts; one more entry closes the last list + std::vector m_items; // numbers of the triangles, cell after cell + mutable std::vector m_seen; // number of the last lookup each triangle was reported in + mutable std::uint32_t m_query{0}; +}; diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 65358eb0..d713bfc1 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -300,6 +300,10 @@ void dress(junction_node::state &Junction, road_node::state const &Road) Junction.surface = Road.surface; Junction.texturelength = Road.texturelength; 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.sounddistance = Road.sounddistance; Junction.quality = Road.quality; diff --git a/world/Road.cpp b/world/Road.cpp index c43a7516..22c65289 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -948,6 +948,8 @@ std::unordered_set ownedbanks; 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 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 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.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) { texturelength = 4.f; @@ -1168,6 +1173,7 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) Input.getTokens(3); Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z; m_state.centre += Offset; + std::vector stoplines; auto token{Input.getToken()}; 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()}; 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 + auto const type{Input.getToken()}; + Input.getTokens(); + Input >> m_state.side.width; + m_state.side.material = Input.getToken(); + 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 + Input.getTokens(2); + Input >> m_state.slope.x >> m_state.slope.y; + } + else if (token == "stopline") + { + // stopline ; 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") { Input.getTokens(); @@ -1220,6 +1259,15 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) } token = Input.getToken(); } + for (auto const number : stoplines) + { + if (number < 1 || number > static_cast(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) { ErrorLog("Bad junction: " + label + " has " + std::to_string(m_state.arms.size()) + " arms, it takes 2 to 4"); @@ -1580,6 +1628,87 @@ std::vector junction_node::create_shapes() const 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 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(side.width) + m_state.slope.x, -m_state.slope.y, 0.f}); + } + } + else + { + auto const tile{static_cast(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(m_state.kerbheight / tile)}); + profile.push_back({side.width, m_state.kerbheight, static_cast((m_state.kerbheight + side.width) / tile)}); + } + auto const order{m_state.arm_order()}; + std::vector 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 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(station / m_state.texturelength)}; + auto const textureend{static_cast((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) { // edge lines carried around the corners @@ -1587,6 +1716,29 @@ std::vector junction_node::create_shapes() const glm::dvec3 const lift{0.0, line_lift, 0.0}; glm::vec3 const upwards{0.f, 1.f, 0.f}; 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(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) { // 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 << "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 << ' ' - << "environment " << m_state.environment << ' '; + << (m_state.markings == road_node::marking_colour::white ? "white" : m_state.markings == road_node::marking_colour::orange ? "orange" : "none") << ' '; + 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" << "\n"; } diff --git a/world/Road.h b/world/Road.h index db4e8d75..11465b5b 100644 --- a/world/Road.h +++ b/world/Road.h @@ -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. // scenery entry: // node junction arm ... [ ]... endjunction +// properties: surface , texlength , markings , side , kerb , +// slope , stopline , velocity , friction , environment // 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 { @@ -223,6 +225,7 @@ class junction_node : public scene::basic_node int incoming{1}; // lanes leading into the junction int outgoing{1}; // lanes leading out of it float width{3.5f}; // width of a lane + bool stopline{false}; // a line is painted across the lanes leading into the junction }; struct state { @@ -231,6 +234,9 @@ class junction_node : public scene::basic_node std::string surface{"asphaltdark1"}; float texturelength{4.f}; 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 friction{0.85f}; float sounddistance{25.f};