From ceae93eec58a19b78df8e70fae0e23ed91f17a3c Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 4 Oct 2026 03:25:32 +0200 Subject: [PATCH] fix(roads): lane transitions drawn as road with an island or a closed zone, end faces and rounded island ends, closed zone stripes on curves, banks applied to the whole road; junction banks to the ground and pedestrian crossings --- application/editormode.h | 5 + application/editormoderoad.cpp | 228 +++++++-- editor/editorRoad.cpp | 9 + world/Road.cpp | 863 +++++++++++++++++++++++++++++---- world/Road.h | 22 +- 5 files changed, 995 insertions(+), 132 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 104e260a..50cd8111 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -562,6 +562,11 @@ class editor_mode : public application_mode 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); + // leads the banks of the corners of a junction to the ground beside it + void junction_bank_to_ground(junction_node &Junction); + // how far from specified point, and how far down, a slope of the grade set for the banks gets to the ground. + // Outwards: the way the slope leads, seen from above + std::pair bank_reach(glm::dvec3 const &Edge, glm::dvec3 const &Outwards); void draw_road_overlay() const; void update_road_tool(); void road_click(); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index e588afe6..d453b47e 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -336,6 +336,8 @@ void editor_mode::junction_select(junction_node *Junction) copy_text(tool.surface, sizeof(tool.surface), Junction->definition().surface); copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material); copy_text(tool.kerbtext, sizeof(tool.kerbtext), Junction->definition().kerbmaterial); + copy_text(tool.banktext, sizeof(tool.banktext), Junction->definition().bankmaterial); + copy_text(tool.mediantext, sizeof(tool.mediantext), Junction->definition().medianmaterial); } void editor_mode::roadpoint_select(roadpoint_node *Point) @@ -771,6 +773,59 @@ void editor_mode::render_junction_layout(junction_node &Junction) ImGui::Unindent(); } + // banks leading from the corners to the ground + if (ImGui::CollapsingHeader("Banks to the ground")) + { + if (render_road_material("Bank material", tool.banktext, sizeof(tool.banktext), state.bankmaterial)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "What the banks are covered with next to a sidewalk, a kerb or the bare edge.\nA bank of a shoulder is covered with the material of the shoulder."); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f")) + tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f); + if (ImGui::Button("Lead the banks to the ground")) + { + if (changed) + junction_apply(Junction, state); + junction_bank_to_ground(Junction); + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Looks for the ground past each corner, at both roads the corner runs between,\n" + "and sets the bank of the corner to reach it. Done for the roads too, the banks meet at the ends of the roads."); + auto banked{false}; + for (auto const &arm : state.arms) + banked = banked || arm.bank.set; + if (banked) + { + ImGui::SameLine(); + if (ImGui::Button("Remove them")) + { + for (auto &arm : state.arms) + arm.bank = road_node::bank_data{}; + changed = true; + } + } + } + if (state.arms.size() == 2) + { + // where a road changes its lanes: what's between its two directions, if the roads keep them apart + ImGui::Separator(); + char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"}; + int median{state.median == road_node::median_type::gap ? 0 : state.median == road_node::median_type::island ? 2 : 1}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("Between the directions", &median, mediantypes, 3)) + { + state.median = (median == 0 ? road_node::median_type::gap : median == 2 ? road_node::median_type::island : road_node::median_type::painted); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Shows where a road which keeps its two directions apart meets this stretch:\n" + "what's between the directions is led on to the other road, where it closes or carries on."); + if (state.median == road_node::median_type::island && render_road_material("Island material", tool.mediantext, sizeof(tool.mediantext), state.medianmaterial)) + changed = true; + } + // the roads if (ImGui::CollapsingHeader("Roads of the junction", ImGuiTreeNodeFlags_DefaultOpen)) { @@ -782,9 +837,30 @@ void editor_mode::render_junction_layout(junction_node &Junction) 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; + if (state.arms.size() > 2) + { + ImGui::SameLine(); + if (ImGui::Checkbox("Stop line", &data.stopline)) + changed = true; + // stripes for the pedestrians, across the whole road where it meets the junction + ImGui::SameLine(); + bool zebra{data.crosswalk > 0.f}; + if (ImGui::Checkbox("Pedestrian crossing", &zebra)) + { + data.crosswalk = (zebra ? 4.f : 0.f); + changed = true; + } + if (zebra) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + if (ImGui::InputFloat("m##crosswalk", &data.crosswalk, 0.5f, 1.0f, "%.1f", ImGuiInputTextFlags_EnterReturnsTrue)) + { + data.crosswalk = std::clamp(data.crosswalk, 1.0f, 20.0f); + changed = true; + } + } + } if (data.incoming > 0 && state.arms.size() > 2) { ImGui::Indent(); @@ -1231,10 +1307,31 @@ void editor_mode::road_click() editor_road::carry_out(plan.end, record); } std::vector states; + // what changes from the start of a piece to its end is spread over the whole stretch being built, + // so the pieces don't each go through all of it + std::vector stations{0.0}; for (auto const &piece : pieces) + stations.emplace_back(stations.back() + editor_road::planar_length(piece)); + auto const along = [&stations](std::size_t const Index) { + auto const fraction{stations.back() > 0.0 ? stations[Index] / stations.back() : 0.0}; + return static_cast(fraction * fraction * (3.0 - 2.0 * fraction)); + }; + for (std::size_t index = 0; index < pieces.size(); ++index) { auto state{tool.settings}; - state.axis = piece; + state.axis = pieces[index]; + auto const from{along(index)}; + auto const to{along(index + 1)}; + auto const &median{tool.settings.median}; + state.median.width = {glm::mix(median.width[0], median.width[1], from), glm::mix(median.width[0], median.width[1], to)}; + // an island is rounded where the stretch starts and ends + state.median.round = {index == 0 ? median.round[0] : 0.f, index + 1 == pieces.size() ? median.round[1] : 0.f}; + for (std::size_t side = 0; side < state.banks.size(); ++side) + { + auto const &bank{tool.settings.banks[side]}; + state.banks[side].width = {glm::mix(bank.width[0], bank.width[1], from), glm::mix(bank.width[0], bank.width[1], to)}; + state.banks[side].drop = {glm::mix(bank.drop[0], bank.drop[1], from), glm::mix(bank.drop[0], bank.drop[1], to)}; + } states.emplace_back(state); } auto const created{editor_road::create(states)}; @@ -1279,6 +1376,8 @@ void editor_mode::road_apply() auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; editor_road::record record; std::vector> changes; + // the banks are kept for each piece as they are, unless it's the banks which were just changed + auto const banked{tool.settings.banks != tool.selected->definition().banks}; for (auto *road : pieces) { std::string reason; @@ -1295,9 +1394,12 @@ void editor_mode::road_apply() state.taper = road->definition().taper; if (road != tool.selected) { - // and so are the banks, and how far apart the two directions are led + // and so is how far apart the two directions are led, and the rounded ends of what's between them state.median.width = road->definition().median.width; - state.banks = road->definition().banks; + state.median.round = road->definition().median.round; + // banks led to the ground differ from piece to piece, so a change of something else leaves them alone + if (false == banked) + state.banks = road->definition().banks; } if (road != tool.selected && road->definition().changes.size() == state.changes.size()) { @@ -1417,6 +1519,69 @@ void editor_mode::road_roundabout(glm::dvec3 const &Ground) tool.status = "Roundabout made. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around."; } +// how far from specified point, and how far down, a slope of the grade set for the banks gets to the ground +std::pair editor_mode::bank_reach(glm::dvec3 const &Edge, glm::dvec3 const &Outwards) +{ + auto const grade{std::clamp(static_cast(m_roadtool.bankgrade), 0.25, 10.0)}; + auto const steps{static_cast(kBankReach / kBankStep)}; + // the ground further and further from the edge, without the roads themselves + std::vector probes; + for (int step = 1; step <= steps; ++step) + probes.emplace_back(Edge + Outwards * (step * kBankStep)); + auto const ground{ground_heights(probes, true)}; + // the bank ends where a slope of the set grade, going down or up, gets to the ground + auto const rising{ground.front() > Edge.y}; + auto width{kBankReach}; + auto drop{Edge.y - ground.back()}; + for (int step = 1; step <= steps; ++step) + { + auto const reach{step * kBankStep}; + auto const height{ground[step - 1]}; + if (rising ? height <= Edge.y + reach / grade : height >= Edge.y - reach / grade) + { + width = reach; + drop = Edge.y - height; + break; + } + } + return {static_cast(width), static_cast(drop)}; +} + +// leads the banks of the corners of a junction to the ground beside it +void editor_mode::junction_bank_to_ground(junction_node &Junction) +{ + auto state{Junction.definition()}; + auto const order{state.arm_order()}; + auto const sided{state.side.type != road_node::side_type::none && state.side.width > 0.f}; + // where the banks start: past the kerb and whatever lines the corners + auto const lining{(state.kerbs ? state.kerbwidth : 0.f) + (sided ? state.side.width : 0.f)}; + auto const raised{sided && state.side.type == road_node::side_type::sidewalk ? state.kerbheight : 0.f}; + for (std::size_t idx = 0; idx < order.size(); ++idx) + { + // a corner starts at the left edge of a road, seen from the junction, and ends at the right edge of the next one + auto &arm{state.arms[order[idx]]}; + auto const &next{state.arms[order[(idx + 1) % order.size()]]}; + glm::dvec3 const outwards[2]{glm::dvec3{arm.direction.y, 0.0, -arm.direction.x}, -glm::dvec3{next.direction.y, 0.0, -next.direction.x}}; + glm::dvec3 const edges[2]{arm.position + outwards[0] * (0.5 * state.arm_width(order[idx]) + lining) + glm::dvec3{0.0, raised, 0.0}, + next.position + outwards[1] * (0.5 * state.arm_width(order[(idx + 1) % order.size()]) + lining) + glm::dvec3{0.0, raised, 0.0}}; + arm.bank.set = true; + for (int end = 0; end < 2; ++end) + { + auto const reach{bank_reach(edges[end], outwards[end])}; + arm.bank.width[end] = reach.first; + arm.bank.drop[end] = reach.second; + } + } + junction_apply(Junction, state); + auto &tool{m_roadtool}; + if (tool.status == "Changed") + { + tool.status = "Banks of the junction led to the ground."; + if ((false == sided || state.side.type != road_node::side_type::shoulder) && (state.bankmaterial.empty() || state.bankmaterial == "none")) + tool.status += " Pick a bank material to have them drawn."; + } +} + // leads the banks of the selected piece, or of the whole road, to the ground beside it void editor_mode::road_bank_to_ground() { @@ -1433,8 +1598,6 @@ void editor_mode::road_bank_to_ground() return; } } - auto const grade{std::clamp(static_cast(tool.bankgrade), 0.25, 10.0)}; - auto const steps{static_cast(kBankReach / kBankStep)}; std::vector> changes; for (auto *road : pieces) { @@ -1455,29 +1618,10 @@ void editor_mode::road_bank_to_ground() auto edge{state.point(t) + across * (0.5 * state.span(t) + (state.kerbs[side] ? state.kerbwidth : 0.f) + (sided ? data.width : 0.f))}; if (sided && data.type == road_node::side_type::sidewalk) edge.y += state.kerbheight; - // the ground further and further from the road, without the roads themselves - std::vector probes; - for (int step = 1; step <= steps; ++step) - probes.emplace_back(edge + across * (step * kBankStep)); - auto const ground{ground_heights(probes, true)}; - // the bank ends where a slope of the set grade, going down or up, gets to the ground - auto const rising{ground.front() > edge.y}; - auto width{kBankReach}; - auto drop{edge.y - ground.back()}; - for (int step = 1; step <= steps; ++step) - { - auto const reach{step * kBankStep}; - auto const height{ground[step - 1]}; - if (rising ? height <= edge.y + reach / grade : height >= edge.y - reach / grade) - { - width = reach; - drop = edge.y - height; - break; - } - } + auto const reach{bank_reach(edge, across)}; state.banks[side].set = true; - state.banks[side].width[end] = static_cast(width); - state.banks[side].drop[end] = static_cast(drop); + state.banks[side].width[end] = reach.first; + state.banks[side].drop[end] = reach.second; } } changes.emplace_back(road, state); @@ -1914,7 +2058,8 @@ bool editor_mode::render_road_layout(road_node::state &State) if (ImGui::CollapsingHeader("Banks to the ground")) { ImGui::TextDisabled("A slope from the edge of the road down, or up, to the ground."); - ImGui::TextDisabled("Set for each piece on its own, at its start and at its end."); + ImGui::TextDisabled("Set here it goes to every piece the changes go to; the button below"); + ImGui::TextDisabled("works out each piece on its own."); char const *banknames[]{"Bank on the left", "Bank on the right"}; for (int side = 0; side < 2; ++side) { @@ -1991,6 +2136,13 @@ bool editor_mode::render_road_layout(road_node::state &State) if (material("Island material", tool.mediantext, sizeof(tool.mediantext), State.median.material)) changed = true; ImGui::TextDisabled("The island is as high as the kerbs; with a kerb material set it gets kerbs of its own."); + if (number("Rounded at the start [m]", State.median.round[0], 0.5f, "%.2f")) + changed = true; + if (number("Rounded at the end [m]", State.median.round[1], 0.5f, "%.2f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Length of the rounded end of the island at that end of the piece; half of the width of the island makes it a half circle.\n" + "0: the island is cut straight, with a face. Use it where the next piece has no island."); } } @@ -2240,9 +2392,17 @@ void editor_mode::render_road_window() else if (tool.junction != nullptr) { auto &junction{*tool.junction}; - 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"); + if (junction.definition().arms.size() == 2) + { + ImGui::Text("%s, a stretch where the road changes its lanes", junction.name().c_str()); + ImGui::TextDisabled("It's drawn as a part of the road, and leads the lanes of one side to the lanes of the other."); + } + else + { + 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"); + } render_junction_layout(junction); if (ImGui::Button("Delete (Del)")) road_delete(); diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 1f483e55..3fe9da2c 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -141,6 +141,8 @@ road_node::state part(road_node::state const &State, double const From, double c result.taper = {static_cast(State.scale(From)), static_cast(State.scale(To))}; if (State.divided()) result.median.width = {static_cast(State.median_width(From)), static_cast(State.median_width(To))}; + // an island is rounded at the ends of the piece, not where the piece is cut + result.median.round = {From <= 0.0 ? State.median.round[0] : 0.f, To >= 1.0 ? State.median.round[1] : 0.f}; for (std::size_t side = 0; side < result.banks.size(); ++side) { auto const &bank{State.banks[side]}; @@ -337,6 +339,9 @@ void dress(junction_node::state &Junction, road_node::state const &Road) Junction.kerbs = (Road.kerbs[0] || Road.kerbs[1]); Junction.kerbwidth = Road.kerbwidth; Junction.kerbmaterial = Road.kerbmaterial; + Junction.bankmaterial = Road.bankmaterial; + Junction.median = Road.median.type; + Junction.medianmaterial = Road.median.material; Junction.slope = Road.slope; Junction.friction = Road.friction; Junction.sounddistance = Road.sounddistance; @@ -409,6 +414,7 @@ road_node::state flipped(road_node::state const &State) std::swap(result.sides[0], result.sides[1]); std::swap(result.taper[0], result.taper[1]); std::swap(result.median.width[0], result.median.width[1]); + std::swap(result.median.round[0], result.median.round[1]); std::swap(result.kerbs[0], result.kerbs[1]); std::swap(result.banks[0], result.banks[1]); for (auto &bank : result.banks) @@ -1110,6 +1116,7 @@ bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string } merged.median.width[1] = (after.divided() ? after.median.width[1] : 0.f); } + merged.median.round[1] = after.median.round[1]; for (std::size_t side = 0; side < merged.banks.size(); ++side) { if (merged.banks[side].set && after.banks[side].set) @@ -1171,6 +1178,8 @@ road_node *editor_road::split(road_node &Road, double const T) first.median.width[1] = gap; second.median.width[0] = gap; } + first.median.round[1] = 0.f; + second.median.round[0] = 0.f; for (std::size_t side = 0; side < first.banks.size(); ++side) { auto const &bank{Road.definition().banks[side]}; diff --git a/world/Road.cpp b/world/Road.cpp index 9a804933..f0e6193e 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -273,6 +273,90 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< } } +// corner of a cross-section which changes along the road +struct section_corner +{ + double offset; // distance from the axis, positive to the left + double height; + float texture; +}; + +// stretches a profile whose corners move from one cross-section to the next along the axis. +// Shape: fills in the corners, left to right, for specified distance from the start of the axis; it has to give the same number of them each time +template +void sweep(std::vector &Output, road_axis const &Axis, std::vector const &Stations, double const Texturelength, Shape_ Shape) +{ + auto const normal = [](glm::dvec3 const &Tangent, glm::dvec3 const &Across) { + auto const result{glm::cross(Tangent, Across)}; + return glm::vec3{glm::length2(result) < 1e-12 ? up : glm::normalize(result)}; + }; + std::vector corners, previouscorners; + std::vector previous, current; + axis_frame previousframe; + double previousstation{0.0}; + for (std::size_t section = 0; section < Stations.size(); ++section) + { + auto const station{Stations[section]}; + auto const frame{Axis.at(station)}; + corners.clear(); + Shape(station, corners); + current.resize(corners.size()); + for (std::size_t idx = 0; idx < corners.size(); ++idx) + { + current[idx] = frame.position + frame.left * corners[idx].offset + up * corners[idx].height; + } + if (section > 0 && previous.size() == current.size()) + { + auto const texturestart{static_cast(previousstation / Texturelength)}; + auto const textureend{static_cast(station / Texturelength)}; + for (std::size_t idx = 0; idx + 1 < current.size(); ++idx) + { + auto const normalstart{normal(previousframe.tangent, previous[idx] - previous[idx + 1])}; + auto const normalend{normal(frame.tangent, current[idx] - current[idx + 1])}; + world_vertex const leftstart{previous[idx], normalstart, {previouscorners[idx].texture, texturestart}}; + world_vertex const rightstart{previous[idx + 1], normalstart, {previouscorners[idx + 1].texture, texturestart}}; + world_vertex const leftend{current[idx], normalend, {corners[idx].texture, textureend}}; + world_vertex const rightend{current[idx + 1], normalend, {corners[idx + 1].texture, textureend}}; + Output.push_back(leftstart); + Output.push_back(rightstart); + Output.push_back(leftend); + + Output.push_back(rightstart); + Output.push_back(rightend); + Output.push_back(leftend); + } + } + previous.swap(current); + previouscorners = corners; + previousframe = frame; + previousstation = station; + } +} + +// closes the end of something raised over the surface with a flat face. +// Corners: outline of the face; Forward: the face looks the way the axis goes rather than back along it +void cap(std::vector &Output, axis_frame const &Frame, std::vector const &Corners, bool const Forward, double const Tile) +{ + if (Corners.size() < 3) + { + return; + } + auto const facing{Forward ? Frame.tangent : -Frame.tangent}; + auto const point = [&Frame](section_corner const &Corner) { return Frame.position + Frame.left * Corner.offset + up * Corner.height; }; + for (std::size_t idx = 1; idx + 1 < Corners.size(); ++idx) + { + std::array order{0, idx, idx + 1}; + if (glm::dot(glm::cross(point(Corners[idx]) - point(Corners[0]), point(Corners[idx + 1]) - point(Corners[0])), facing) < 0.0) + { + std::swap(order[1], order[2]); + } + for (auto const corner : order) + { + Output.push_back({point(Corners[corner]), glm::vec3{facing}, {static_cast(Corners[corner].offset / Tile), static_cast(Corners[corner].height / Tile)}}); + } + } +} + // proportions of the image specified material is painted with float texture_ratio(std::string const &Material) { @@ -311,6 +395,10 @@ void road_node::state::normalize() { median.material = "none"; } + for (auto &round : median.round) + { + round = std::clamp(round, 0.f, 100.f); + } kerbwidth = std::clamp(kerbwidth, 0.05f, 2.f); if (kerbmaterial.empty()) { @@ -698,6 +786,12 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) m_state.median.material = Input.getToken(); replace_slashes(m_state.median.material); } + else if (token == "medianround") + { + // medianround + Input.getTokens(2); + Input >> m_state.median.round[0] >> m_state.median.round[1]; + } else if (token == "kerbs") { // kerbs @@ -959,9 +1053,9 @@ std::vector road_node::create_shapes() const else { loft(surface, axis, {corner(halfwidth, 0.5), corner(divide, 0.5)}, 0.0, length, step, texturelength, road); - if (road.median.type == median_type::painted) + if (road.median.type != median_type::gap) { - // the surface carries on between the roadways + // the surface carries on between the roadways; an island stands on it, so there's something under its rounded ends loft(surface, axis, {corner(divide, 0.5), corner(divide, -0.5)}, 0.0, length, step, texturelength, road); } loft(surface, axis, {corner(divide, -0.5), corner(-halfwidth, -0.5)}, 0.0, length, step, texturelength, road); @@ -969,37 +1063,98 @@ std::vector road_node::create_shapes() const } // an island between the roadways: a kerb facing each of them, and what's between the kerbs - if (divided && road.median.type == median_type::island) + if (divided && road.median.type == median_type::island && length > 0.0) { auto const kerbed{usable(road.kerbmaterial)}; auto const covered{usable(road.median.material)}; - double const kerbwidth{kerbed ? road.kerbwidth : 0.0}; + // the ends of the island can be rounded; it gets narrower there along a quarter of an ellipse + std::array const nose{std::min(static_cast(road.median.round[0]), 0.5 * length), std::min(static_cast(road.median.round[1]), 0.5 * length)}; + int const nosesteps{8}; + double const quarter{1.5707963267948966}; + std::vector stations; + for (int idx = 0; idx < nosesteps && nose[0] > 0.0; ++idx) + { + stations.emplace_back(nose[0] * (1.0 - std::cos(quarter * idx / nosesteps))); + } + auto const body{length - nose[0] - nose[1]}; + auto const bodysteps{std::max(1, static_cast(std::ceil(body / step - 0.001)))}; + for (int idx = 0; idx <= bodysteps; ++idx) + { + stations.emplace_back(nose[0] + body * idx / bodysteps); + } + for (int idx = 1; idx <= nosesteps && nose[1] > 0.0; ++idx) + { + stations.emplace_back(length - nose[1] * (1.0 - std::sin(quarter * idx / nosesteps))); + } + // middle of the island and half of its width, for specified distance from the start of the axis + auto const middle = [&](double const Station) { return divide * glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(Station / length)); }; + auto const half = [&](double const Station) { + auto result{0.5 * glm::mix(static_cast(road.median.width[0]), static_cast(road.median.width[1]), taper_progress(Station / length))}; + if (Station < nose[0]) + { + auto const along{(nose[0] - Station) / nose[0]}; + result *= std::sqrt(std::max(0.0, 1.0 - along * along)); + } + else if (Station > length - nose[1]) + { + auto const along{(Station - (length - nose[1])) / nose[1]}; + result *= std::sqrt(std::max(0.0, 1.0 - along * along)); + } + return result; + }; if (kerbed) { auto const tile{static_cast(texture_ratio(road.kerbmaterial)) * texturelength}; - auto const foot{0.f}; auto const top{static_cast(height / tile)}; - auto const inner{static_cast((height + kerbwidth) / tile)}; + auto const inner{static_cast((height + road.kerbwidth) / tile)}; auto &kerbs{vertices(road.kerbmaterial, lighting_data{})}; - loft(kerbs, axis, {{divide, 0.0, foot, 0.0, 0.5}, {divide, height, top, 0.0, 0.5}, {divide, height, inner, -kerbwidth, 0.5}}, 0.0, length, step, texturelength, road); - loft(kerbs, axis, {{divide, height, inner, kerbwidth, -0.5}, {divide, height, top, 0.0, -0.5}, {divide, 0.0, foot, 0.0, -0.5}}, 0.0, length, step, texturelength, road); + sweep(kerbs, axis, stations, texturelength, [&](double const Station, std::vector &Corners) { + auto const edge{middle(Station) + half(Station)}; + Corners = {{edge, 0.0, 0.f}, {edge, height, top}, {edge - std::min(static_cast(road.kerbwidth), half(Station)), height, inner}}; + }); + sweep(kerbs, axis, stations, texturelength, [&](double const Station, std::vector &Corners) { + auto const edge{middle(Station) - half(Station)}; + Corners = {{edge + std::min(static_cast(road.kerbwidth), half(Station)), height, inner}, {edge, height, top}, {edge, 0.0, 0.f}}; + }); } if (covered) { auto const tile{static_cast(texture_ratio(road.median.material)) * texturelength}; - std::vector profile; - if (false == kerbed) + sweep(vertices(road.median.material, lighting_data{}), axis, stations, texturelength, [&](double const Station, std::vector &Corners) { + auto const centre{middle(Station)}; + auto const reach{half(Station)}; + auto const inset{kerbed ? std::min(static_cast(road.kerbwidth), reach) : 0.0}; + Corners.clear(); + if (false == kerbed) + { + // without kerbs of their own material the sides of the island are covered with what its top is + Corners.push_back({centre + reach, 0.0, 0.f}); + } + Corners.push_back({centre + reach - inset, height, static_cast(height / tile)}); + Corners.push_back({centre - reach + inset, height, static_cast((height + 2.0 * reach) / tile)}); + if (false == kerbed) + { + Corners.push_back({centre - reach, 0.0, static_cast((2.0 * height + 2.0 * reach) / tile)}); + } + }); + } + // an end which isn't rounded gets a face, so the island isn't hollow where the next piece has none + if (kerbed || covered) + { + auto const &material{covered ? road.median.material : road.kerbmaterial}; + auto const tile{static_cast(texture_ratio(material)) * texturelength}; + for (int end = 0; end < 2; ++end) { - // without kerbs of their own material the sides of the island are covered with what its top is - profile.push_back({divide, 0.0, 0.f, 0.0, 0.5}); + auto const station{end == 0 ? 0.0 : length}; + auto const reach{half(station)}; + if (nose[end] > 0.0 || reach < 0.025) + { + continue; + } + auto const centre{middle(station)}; + std::vector const face{{centre + reach, 0.0, 0.f}, {centre + reach, height, 0.f}, {centre - reach, height, 0.f}, {centre - reach, 0.0, 0.f}}; + cap(vertices(material, lighting_data{}), axis.at(station), face, end == 1, tile); } - profile.push_back({divide, height, static_cast(height / tile), -kerbwidth, 0.5}); - profile.push_back({divide, height, static_cast((height + widest) / tile), kerbwidth, -0.5}); - if (false == kerbed) - { - profile.push_back({divide, 0.0, static_cast((2.0 * height + widest) / tile), 0.0, -0.5}); - } - loft(vertices(road.median.material, lighting_data{}), axis, profile, 0.0, length, step, texturelength, road); } } @@ -1026,12 +1181,24 @@ std::vector road_node::create_shapes() const } loft(vertices(Material, lighting_data{}), axis, Profile, 0.0, length, step, texturelength, road); }; + // faces closing what's raised over the surface at both ends of the piece, so it isn't hollow where the next piece has none. + // From, To: distances from the edge of the surface + auto const close = [&](std::string const &Material, double const From, double const To) { + auto const tile{static_cast(texture_ratio(Material)) * texturelength}; + for (int end = 0; end < 2; ++end) + { + auto const edge{direction * (halfwidth * road.taper[end] + (divided ? 0.5 * road.median.width[end] : 0.0))}; + cap(vertices(Material, lighting_data{}), axis.at(end == 0 ? 0.0 : length), + {{edge + direction * From, 0.0, 0.f}, {edge + direction * From, height, 0.f}, {edge + direction * To, height, 0.f}, {edge + direction * To, 0.0, 0.f}}, end == 1, tile); + } + }; double reach{0.0}; // how far from the edge of the surface the next thing starts double level{0.0}; // and how high if (kerbed) { auto const tile{static_cast(texture_ratio(road.kerbmaterial)) * texturelength}; double const width{road.kerbwidth}; + close(road.kerbmaterial, 0.0, width); std::vector kerb{{0.0, 0.0, 0.f, 0.0}, {0.0, height, static_cast(height / tile), 0.0}, {0.0, height, static_cast((height + width) / tile), width}}; if (false == sided || data.type != side_type::sidewalk) { @@ -1067,6 +1234,7 @@ std::vector road_node::create_shapes() const profile.push_back({0.0, height, static_cast(height / tile), reach}); profile.push_back({0.0, height, static_cast((height + data.width) / tile), reach + data.width}); emit(data.material, profile); + close(data.material, reach, reach + data.width); reach += data.width; level = height; } @@ -1179,18 +1347,24 @@ std::vector road_node::create_shapes() const auto const scale{glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(station / length))}; return frame.position + frame.left * (divide * scale + Side * std::max(0.0, 0.5 * gap_at(station) - 2.0 * inset)) + up * line_lift; }; - for (auto station{0.5 * period}; station < length; station += period) + // the stripes are spread evenly over the piece, and none of them reaches past its ends + auto const count{std::max(1, static_cast(std::lround(length / period)))}; + auto const spacing{length / count}; + for (int index = 0; index < count; ++index) { - auto const gap{gap_at(station)}; + auto const centre{(index + 0.5) * spacing}; + auto const gap{gap_at(centre)}; if (gap < 1.0) { continue; } - // a stripe gets as far along the road as it gets across it + // a stripe gets as far along the road as it gets across it, as long as that leaves room for the next one + auto const slant{std::clamp(gap, 0.0, std::max(0.0, spacing - stripe - 0.25))}; + auto const station{centre - 0.5 * (slant + stripe)}; auto const leftstart{edge(station, 1.0)}; auto const leftend{edge(station + stripe, 1.0)}; - auto const rightstart{edge(station + gap, -1.0)}; - auto const rightend{edge(station + gap + stripe, -1.0)}; + auto const rightstart{edge(station + slant, -1.0)}; + auto const rightend{edge(station + slant + stripe, -1.0)}; lines.push_back({leftstart, upwards, {0.f, 0.f}}); lines.push_back({rightstart, upwards, {1.f, 0.f}}); lines.push_back({leftend, upwards, {0.f, 1.f}}); @@ -1235,6 +1409,94 @@ double const gate_commit{8.0}; // a vehicle let through this close to the juncti double const gate_patience{6.0}; // seconds a vehicle let through can stand still before the others stop waiting for it double const way_clearance{2.2}; // ways through a junction which pass this close to each other can't be taken at the same time +double const crosswalk_inset{1.0}; // distance of a pedestrian crossing from the end of its road, which leaves room for a stop line + +// corner of the cross-section of what lines the edge of a junction +struct edge_point +{ + double offset; // distance from the edge of the surface + double height; + float texture; + double grow{0.0}; // what the offset changes by along the edge, evenly + double sink{0.0}; // what the height changes by along the edge, evenly +}; + +// something which lines the edge of a junction: a kerb, a shoulder with its bank, a sidewalk, a bank +struct edge_lining +{ + std::string material; + std::vector profile; +}; + +// what the edge of a junction is lined with, from the edge outwards. Bank: slope leading to the ground from that edge +std::vector edge_linings(junction_node::state const &State, road_node::bank_data const &Bank) +{ + std::vector linings; + auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; + auto const &side{State.side}; + auto const sided{side.type != road_node::side_type::none && side.width > 0.f && usable(side.material)}; + auto const kerbed{State.kerbs && usable(State.kerbmaterial)}; + double const height{State.kerbheight}; + double reach{0.0}; // how far from the edge of the surface the next thing starts + double level{0.0}; // and how high + if (kerbed) + { + auto const tile{static_cast(texture_ratio(State.kerbmaterial)) * State.texturelength}; + double const width{State.kerbwidth}; + edge_lining kerb; + kerb.material = State.kerbmaterial; + kerb.profile = {{0.0, 0.0, 0.f}, {0.0, height, static_cast(height / tile)}, {width, height, static_cast((height + width) / tile)}}; + if (false == sided || side.type != road_node::side_type::sidewalk) + { + // with nothing raised behind it the kerb has a back + kerb.profile.push_back({width, 0.0, static_cast((2.0 * height + width) / tile)}); + } + linings.emplace_back(kerb); + reach = width; + } + if (sided && side.type == road_node::side_type::shoulder) + { + edge_lining shoulder; + shoulder.material = side.material; + shoulder.profile = {{reach, 0.0, 1.f}, {reach + side.width, 0.0, 0.5f}}; + reach += side.width; + glm::dvec2 const start{Bank.set ? Bank.width[0] : State.slope.x, Bank.set ? Bank.drop[0] : State.slope.y}; + glm::dvec2 const end{Bank.set ? Bank.width[1] : State.slope.x, Bank.set ? Bank.drop[1] : State.slope.y}; + if (start.x > 0.0 || end.x > 0.0) + { + shoulder.profile.push_back({reach + start.x, -start.y, 0.f, end.x - start.x, start.y - end.y}); + } + linings.emplace_back(shoulder); + } + else if (sided) + { + auto const tile{static_cast(texture_ratio(side.material)) * State.texturelength}; + edge_lining sidewalk; + sidewalk.material = side.material; + if (false == kerbed) + { + sidewalk.profile.push_back({0.0, 0.0, 0.f}); + } + sidewalk.profile.push_back({reach, height, static_cast(height / tile)}); + sidewalk.profile.push_back({reach + side.width, height, static_cast((height + side.width) / tile)}); + linings.emplace_back(sidewalk); + reach += side.width; + level = height; + } + // a bank next to something other than a shoulder is covered with a material of its own + if ((false == sided || side.type != road_node::side_type::shoulder) && Bank.set && usable(State.bankmaterial) && (Bank.width[0] > 0.f || Bank.width[1] > 0.f)) + { + auto const tile{static_cast(texture_ratio(State.bankmaterial)) * State.texturelength}; + edge_lining bank; + bank.material = State.bankmaterial; + bank.profile = {{reach, level, 0.f}, {reach + Bank.width[0], level - Bank.drop[0], static_cast(std::max(Bank.width[0], Bank.width[1]) / tile)}}; + bank.profile.back().grow = static_cast(Bank.width[1]) - Bank.width[0]; + bank.profile.back().sink = static_cast(Bank.drop[0]) - Bank.drop[1]; + linings.emplace_back(bank); + } + return linings; +} + // direction on the ground as a vector in space glm::dvec3 flat(glm::dvec2 const &Direction) { @@ -1349,6 +1611,12 @@ void junction_node::state::normalize() arm.outgoing = std::clamp(arm.outgoing, 0, 8); arm.width = std::max(1.f, arm.width); arm.median = (arm.incoming > 0 && arm.outgoing > 0 ? std::clamp(arm.median, 0.f, 100.f) : 0.f); + arm.crosswalk = std::clamp(arm.crosswalk, 0.f, 20.f); + for (int end = 0; end < 2; ++end) + { + arm.bank.width[end] = std::clamp(arm.bank.width[end], 0.f, 100.f); + arm.bank.drop[end] = std::clamp(arm.bank.drop[end], -100.f, 100.f); + } arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0}); if (arm.turns.size() > static_cast(arm.incoming)) { @@ -1366,6 +1634,14 @@ void junction_node::state::normalize() { kerbmaterial = "none"; } + if (bankmaterial.empty()) + { + bankmaterial = "none"; + } + if (medianmaterial.empty()) + { + medianmaterial = "none"; + } slope = glm::max(slope, glm::vec2{0.f}); if (texturelength < 0.01f) { @@ -1476,6 +1752,8 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) // what's said about the arms may come ahead of them, so it waits until all is read std::vector> priorities; std::vector> medians; + std::vector> crosswalks; + std::vector> banks; struct turn_property { int arm; @@ -1565,6 +1843,38 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) Input >> number; priorities.emplace_back(number, Input.getToken()); } + else if (token == "crosswalk") + { + // crosswalk + int number{0}; + float length{0.f}; + Input.getTokens(2); + Input >> number >> length; + crosswalks.emplace_back(number, length); + } + else if (token == "cornerbank") + { + // cornerbank + int number{0}; + road_node::bank_data bank; + bank.set = true; + Input.getTokens(5); + Input >> number >> bank.width[0] >> bank.drop[0] >> bank.width[1] >> bank.drop[1]; + banks.emplace_back(number, bank); + } + else if (token == "bankmaterial") + { + m_state.bankmaterial = Input.getToken(); + replace_slashes(m_state.bankmaterial); + } + else if (token == "median") + { + // median + auto const type{Input.getToken()}; + m_state.median = (type == "gap" ? road_node::median_type::gap : type == "island" ? road_node::median_type::island : road_node::median_type::painted); + m_state.medianmaterial = Input.getToken(); + replace_slashes(m_state.medianmaterial); + } else if (token == "armmedian") { // armmedian @@ -1623,6 +1933,24 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) } m_state.arms[entry.first - 1].median = entry.second; } + for (auto const &entry : crosswalks) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to paint a pedestrian crossing at"); + continue; + } + m_state.arms[entry.first - 1].crosswalk = entry.second; + } + for (auto const &entry : banks) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to set the bank for"); + continue; + } + m_state.arms[entry.first - 1].bank = entry.second; + } for (auto const &entry : priorities) { if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) @@ -2336,9 +2664,318 @@ bool junction_node::occupied() const return std::any_of(m_links.begin(), m_links.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->Dynamics.empty(); }); } +// geometry of a junction of two roads: a stretch of road, on which the lanes of one are led to the lanes of the other +std::vector junction_node::create_transition_shapes() const +{ + std::vector shapes; + auto const &arms{m_state.arms}; + auto const &from{arms[0]}; + auto const &to{arms[1]}; + // the way from the first road to the second, which leaves and joins them the way they go + auto const p0{from.position}; + auto const p3{to.position}; + auto const span{glm::distance(p0, p3)}; + if (span < 0.1) + { + return shapes; + } + auto const p1{p0 - flat(from.direction) * (0.39 * span)}; + auto const p2{p3 - flat(to.direction) * (0.39 * span)}; + // edges of the two roadways as distances from that way, positive to the left of it. at the first road the lanes + // leading into the junction go the way the stretch does, at the second one it's the lanes leading out of it + struct edges + { + double right; + double forward; // edge of the roadway going along the way, on the side of the other one + double backward; // edge of the roadway going against it, on the side of the other one + double left; + double lanewidth; + }; + auto const width0{m_state.arm_width(0)}; + auto const width1{m_state.arm_width(1)}; + edges const start{-0.5 * width0, -0.5 * width0 + from.incoming * from.width, 0.5 * width0 - from.outgoing * from.width, 0.5 * width0, from.width}; + edges const end{-0.5 * width1, -0.5 * width1 + to.outgoing * to.width, 0.5 * width1 - to.incoming * to.width, 0.5 * width1, to.width}; + struct section + { + glm::dvec3 position; + glm::dvec3 left; + glm::dvec3 tangent; + double station; + double fraction; + edges offsets; + }; + auto const at = [&](double const T) { + section result; + auto const u{1.0 - T}; + result.position = u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + auto const direction{3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2)}; + glm::dvec2 const heading{direction.x, direction.z}; + result.tangent = (glm::length2(direction) > 1e-12 ? glm::normalize(direction) : flat(to.direction)); + result.left = flat(left_of(glm::length(heading) > 1e-9 ? glm::normalize(heading) : to.direction)); + // the edges get from where they are at one road to where they are at the other the way the edges of a road which changes its width do + auto const blend{taper_progress(T)}; + result.offsets = {glm::mix(start.right, end.right, blend), glm::mix(start.forward, end.forward, blend), glm::mix(start.backward, end.backward, blend), glm::mix(start.left, end.left, blend), + glm::mix(start.lanewidth, end.lanewidth, blend)}; + result.station = 0.0; + result.fraction = T; + return result; + }; + auto const steps{std::clamp(static_cast(span / 2.0), 8, 32)}; + std::vector
sections; + for (int idx = 0; idx <= steps; ++idx) + { + sections.emplace_back(at(static_cast(idx) / steps)); + if (idx > 0) + { + sections[idx].station = sections[idx - 1].station + glm::distance(sections[idx - 1].position, sections[idx].position); + } + } + auto const length{sections.back().station}; + + // triangles are gathered separately for each look + struct batch + { + std::string material; + lighting_data lighting; + std::vector vertices; + }; + std::vector batches; + auto const vertices = [&batches](std::string const &Material, lighting_data const &Lighting) -> std::vector & { + for (auto &entry : batches) + { + if (entry.material == Material && entry.lighting == Lighting) + { + return entry.vertices; + } + } + batches.emplace_back(); + batches.back().material = Material; + batches.back().lighting = Lighting; + return batches.back().vertices; + }; + auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; + auto const place = [](section const &Section, section_corner const &Corner) { return Section.position + Section.left * Corner.offset + up * Corner.height; }; + // a band of triangles between two lines running along the stretch, from a section to another. + // Left, Right: give the distance from the way, the height and the texture coordinate of each line at a section + auto const strip = [&](std::vector &Output, auto const &Left, auto const &Right, std::size_t const First, std::size_t const Last) { + for (auto idx{First}; idx < Last && idx + 1 < sections.size(); ++idx) + { + auto const &here{sections[idx]}; + auto const &there{sections[idx + 1]}; + section_corner const left0{Left(here)}; + section_corner const right0{Right(here)}; + section_corner const left1{Left(there)}; + section_corner const right1{Right(there)}; + auto const leftstart{place(here, left0)}; + auto const rightstart{place(here, right0)}; + auto const leftend{place(there, left1)}; + auto const rightend{place(there, right1)}; + auto const facing{glm::cross(here.tangent, leftstart - rightstart)}; + glm::vec3 const normal{glm::length2(facing) < 1e-12 ? up : glm::normalize(facing)}; + auto const texturestart{static_cast(here.station / m_state.texturelength)}; + auto const textureend{static_cast(there.station / m_state.texturelength)}; + Output.push_back({leftstart, normal, {left0.texture, texturestart}}); + Output.push_back({rightstart, normal, {right0.texture, texturestart}}); + Output.push_back({leftend, normal, {left1.texture, textureend}}); + Output.push_back({rightstart, normal, {right0.texture, texturestart}}); + Output.push_back({rightend, normal, {right1.texture, textureend}}); + Output.push_back({leftend, normal, {left1.texture, textureend}}); + } + }; + auto const whole{sections.size() - 1}; + // the two directions are apart at either road + auto const apart{from.median > 0.f || to.median > 0.f}; + auto const forth{from.incoming > 0 || to.outgoing > 0}; + auto const back{from.outgoing > 0 || to.incoming > 0}; + double const height{m_state.kerbheight}; + + // surface. the image is centered on the way, like it is on a road + if (m_state.surface != "none") + { + auto const tile{static_cast(texture_ratio(m_state.surface)) * m_state.texturelength}; + auto const along = [tile](double edges::*Edge) { + return [tile, Edge](section const &Section) { return section_corner{Section.offsets.*Edge, 0.0, static_cast(0.5 + Section.offsets.*Edge / tile)}; }; + }; + auto &surface{vertices(m_state.surface, lighting_data{})}; + if (apart && m_state.median == road_node::median_type::gap) + { + // a roadway for each direction, with the ground showing between them + strip(surface, along(&edges::forward), along(&edges::right), 0, whole); + strip(surface, along(&edges::left), along(&edges::backward), 0, whole); + } + else + { + strip(surface, along(&edges::left), along(&edges::right), 0, whole); + } + } + // an island where the two directions part + if (apart && m_state.median == road_node::median_type::island && (usable(m_state.medianmaterial) || (m_state.kerbs && usable(m_state.kerbmaterial)))) + { + auto const &material{usable(m_state.medianmaterial) ? m_state.medianmaterial : m_state.kerbmaterial}; + auto const tile{static_cast(texture_ratio(material)) * m_state.texturelength}; + auto const widest{std::max(from.median, to.median)}; + auto &island{vertices(material, lighting_data{})}; + auto const corner = [](double edges::*Edge, double const Height, float const Texture) { + return [Edge, Height, Texture](section const &Section) { return section_corner{Section.offsets.*Edge, Height, Texture}; }; + }; + auto const top{static_cast(height / tile)}; + auto const across{static_cast((height + widest) / tile)}; + strip(island, corner(&edges::backward, 0.0, 0.f), corner(&edges::backward, height, top), 0, whole); + strip(island, corner(&edges::backward, height, top), corner(&edges::forward, height, across), 0, whole); + strip(island, corner(&edges::forward, height, across), corner(&edges::forward, 0.0, static_cast((2.0 * height + widest) / tile)), 0, whole); + } + // what lines the edges: the same the corners of a junction of more roads get. the right edge is the corner which starts + // at the first road, the left one is the corner which starts at the second road, so it's gone along backwards + for (int side = 0; side < 2; ++side) + { + auto const sign{side == 0 ? -1.0 : 1.0}; + for (auto const &lining : edge_linings(m_state, arms[side].bank)) + { + auto &output{vertices(lining.material, lighting_data{})}; + for (std::size_t band = 0; band + 1 < lining.profile.size(); ++band) + { + auto const corner = [side, sign](edge_point const &Point) { + return [sign, &Point, side](section const &Section) { + auto const fraction{side == 0 ? Section.fraction : 1.0 - Section.fraction}; + auto const edge{side == 0 ? Section.offsets.right : Section.offsets.left}; + return section_corner{edge + sign * (Point.offset + Point.grow * fraction), Point.height + Point.sink * fraction, Point.texture}; + }; + }; + auto const inner{corner(lining.profile[band])}; + auto const outer{corner(lining.profile[band + 1])}; + if (side == 0) + { + strip(output, inner, outer, 0, whole); + } + else + { + strip(output, outer, inner, 0, whole); + } + } + } + } + + // markings + if (m_state.markings != road_node::marking_colour::none) + { + lighting_data paint; + paint.diffuse = (m_state.markings == road_node::marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); + paint.ambient = paint.diffuse; + auto &lines{vertices("colored", paint)}; + // a line running at some distance from one of the edges + auto const line = [&](double edges::*Edge, double const Shift, double const Lanes, std::size_t const First, std::size_t const Last) { + auto const side = [Edge, Shift, Lanes](double const Half, float const Texture) { + return [Edge, Shift, Lanes, Half, Texture](section const &Section) { + return section_corner{Section.offsets.*Edge + Shift + Lanes * Section.offsets.lanewidth + Half, line_lift, Texture}; + }; + }; + strip(lines, side(0.5 * line_width, 0.f), side(-0.5 * line_width, 1.f), First, Last); + }; + auto const inset{edge_inset + 0.5 * line_width}; + line(&edges::right, inset, 0.0, 0, whole); + line(&edges::left, -inset, 0.0, 0, whole); + if (forth && back) + { + if (apart) + { + // each of the two roadways gets its edge line + line(&edges::forward, -inset, 0.0, 0, whole); + line(&edges::backward, inset, 0.0, 0, whole); + } + else + { + // traffic going opposite ways is kept apart with a double line + auto const pair{0.5 * (line_width + line_spacing)}; + line(&edges::forward, pair, 0.0, 0, whole); + line(&edges::forward, -pair, 0.0, 0, whole); + } + } + // lines between the lanes of each direction, dashed. the lanes are counted from the middle of the road, so the ones + // both roads have run all the way; a line of a lane only one of them has starts where the roadway got wide enough for the lane + for (int direction = 0; direction < 2; ++direction) + { + auto const lanes{direction == 0 ? std::max(from.incoming, to.outgoing) : std::max(from.outgoing, to.incoming)}; + for (int lane = 1; lane < lanes; ++lane) + { + for (std::size_t idx = 0; idx < whole; ++idx) + { + if (std::fmod(sections[idx].station, dash_period) >= dash_period / 3.0) + { + continue; + } + auto const room = [&](section const &Section) { + auto const &offsets{Section.offsets}; + return (direction == 0 ? offsets.forward - offsets.right : offsets.left - offsets.backward) >= (lane + 0.5) * offsets.lanewidth; + }; + if (room(sections[idx]) && room(sections[idx + 1])) + { + line(direction == 0 ? &edges::forward : &edges::backward, 0.0, direction == 0 ? -lane : lane, idx, idx + 1); + } + } + } + } + if (apart && m_state.median == road_node::median_type::painted && length > 0.0) + { + // what's between the roadways is closed to the traffic: slanted stripes are led from one edge line to the other + double const period{3.0}; + double const stripe{0.5}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + // point of the edge of one of the roadways, a bit into what's between them + auto const edge = [&](double const Station, bool const Left) { + auto const where{at(std::clamp(Station / length, 0.0, 1.0))}; + auto const gap{where.offsets.backward - where.offsets.forward}; + auto const margin{std::min(2.0 * inset, 0.5 * gap)}; + return where.position + where.left * (Left ? where.offsets.backward - margin : where.offsets.forward + margin) + up * line_lift; + }; + auto const count{std::max(1, static_cast(std::lround(length / period)))}; + auto const spacing{length / count}; + for (int index = 0; index < count; ++index) + { + auto const centre{(index + 0.5) * spacing}; + auto const middle{at(std::clamp(centre / length, 0.0, 1.0))}; + auto const gap{middle.offsets.backward - middle.offsets.forward}; + if (gap < 1.0) + { + continue; + } + auto const slant{std::clamp(gap, 0.0, std::max(0.0, spacing - stripe - 0.25))}; + auto const station{centre - 0.5 * (slant + stripe)}; + auto const leftstart{edge(station, true)}; + auto const leftend{edge(station + stripe, true)}; + auto const rightstart{edge(station + slant, false)}; + auto const rightend{edge(station + slant + stripe, false)}; + lines.push_back({leftstart, upwards, {0.f, 0.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({rightend, upwards, {1.f, 1.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + } + } + } + + for (auto &entry : batches) + { + if (entry.vertices.empty()) + { + continue; + } + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material(entry.material), std::move(entry.vertices), glm::dvec3{0.0}); + shape.lighting(entry.lighting); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + // generates geometry of the surface std::vector junction_node::create_shapes() const { + if (m_state.arms.size() == 2) + { + // where a road changes its lanes it's drawn as a stretch of that road + return create_transition_shapes(); + } std::vector shapes; std::vector> corners; auto const points{m_state.outline(&corners)}; @@ -2376,16 +3013,22 @@ std::vector junction_node::create_shapes() const shapes.emplace_back(std::move(shape)); } } - // the corners are lined the way the edges of a road are, so the kerbs, the shoulders and the sidewalks of the roads carry on around the junction - struct corner_point + // the corners are lined the way the edges of a road are, so the kerbs, the shoulders, the sidewalks and the banks of the roads carry on around the junction { - double offset; // distance from the edge of the surface - double height; - float texture; - }; - auto const line_corners = [&](std::string const &Material, std::vector const &Profile) { auto const order{m_state.arm_order()}; - std::vector vertices; + // triangles are gathered for each material, in the order the materials come up + std::vector>> lined; + auto const batch = [&lined](std::string const &Material) -> std::vector & { + for (auto &entry : lined) + { + if (entry.first == Material) + { + return entry.second; + } + } + lined.emplace_back(Material, std::vector{}); + return lined.back().second; + }; for (std::size_t idx = 0; idx < corners.size() && idx < order.size(); ++idx) { auto const &corner{corners[idx]}; @@ -2404,81 +3047,56 @@ std::vector junction_node::create_shapes() const 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}; + double total{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) + total += glm::distance(corner[point], corner[point + 1]); + } + // the bank of the corner is the one set for the road the corner starts at + for (auto const &lining : edge_linings(m_state, m_state.arms[order[idx]].bank)) + { + auto &vertices{batch(lining.material)}; + auto const &profile{lining.profile}; + double station{0.0}; + for (std::size_t point = 0; point + 1 < corner.size(); ++point) { - 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}}); + 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)}; + auto const fractionstart{total > 0.0 ? station / total : 0.0}; + auto const fractionend{total > 0.0 ? (station + length) / total : 0.0}; + 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 + inner.grow * fractionstart) + up * (inner.height + inner.sink * fractionstart)}; + auto const rightstart{corner[point] + outwards[point] * (outer.offset + outer.grow * fractionstart) + up * (outer.height + outer.sink * fractionstart)}; + auto const leftend{corner[point + 1] + outwards[point + 1] * (inner.offset + inner.grow * fractionend) + up * (inner.height + inner.sink * fractionend)}; + auto const rightend{corner[point + 1] + outwards[point + 1] * (outer.offset + outer.grow * fractionend) + up * (outer.height + outer.sink * fractionend)}; + 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; } - station += length; } } - if (false == vertices.empty()) + for (auto &entry : lined) { + if (entry.second.empty()) + { + continue; + } scene::shape_node shape; - shape.make_terrain(GfxRenderer->Fetch_Material(Material), std::move(vertices), glm::dvec3{0.0}); + shape.make_terrain(GfxRenderer->Fetch_Material(entry.first), std::move(entry.second), glm::dvec3{0.0}); shapes.emplace_back(std::move(shape)); } - }; - auto const &side{m_state.side}; - auto const sided{side.type != road_node::side_type::none && side.width > 0.f && false == side.material.empty() && side.material != "none"}; - auto const kerbed{m_state.kerbs && false == m_state.kerbmaterial.empty() && m_state.kerbmaterial != "none"}; - double const height{m_state.kerbheight}; - double reach{0.0}; // how far from the edge of the surface the side starts - if (kerbed) - { - auto const tile{static_cast(texture_ratio(m_state.kerbmaterial)) * m_state.texturelength}; - double const width{m_state.kerbwidth}; - std::vector kerb{{0.0, 0.0, 0.f}, {0.0, height, static_cast(height / tile)}, {width, height, static_cast((height + width) / tile)}}; - if (false == sided || side.type != road_node::side_type::sidewalk) - { - // with nothing raised behind it the kerb has a back - kerb.push_back({width, 0.0, static_cast((2.0 * height + width) / tile)}); - } - line_corners(m_state.kerbmaterial, kerb); - reach = width; - } - if (sided) - { - std::vector profile; - if (side.type == road_node::side_type::shoulder) - { - profile.push_back({reach, 0.0, 1.f}); - profile.push_back({reach + side.width, 0.0, 0.5f}); - if (m_state.slope.x > 0.f) - { - profile.push_back({reach + 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}; - if (false == kerbed) - { - profile.push_back({0.0, 0.0, 0.f}); - } - profile.push_back({reach, height, static_cast(height / tile)}); - profile.push_back({reach + side.width, height, static_cast((height + side.width) / tile)}); - } - line_corners(side.material, profile); } if (m_state.markings != road_node::marking_colour::none) { @@ -2510,6 +3128,35 @@ std::vector junction_node::create_shapes() const vertices.push_back({rightend, upwards, {1.f, 1.f}}); vertices.push_back({leftend, upwards, {0.f, 1.f}}); } + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + if (arms[arm].crosswalk <= 0.f) + { + continue; + } + // a pedestrian crossing: stripes laid the way the road goes, side by side across it, past the place for a stop line + auto const across{flat(left_of(arms[arm].direction))}; + auto const inwards{-flat(arms[arm].direction)}; + auto const reach{0.5 * m_state.arm_width(arm) - edge_inset}; + auto const stripes{std::max(1, static_cast(std::floor((2.0 * reach + stopline_width) / (2.0 * stopline_width))))}; + auto const first{-0.5 * (stripes * 2 - 1) * stopline_width}; + for (int stripe = 0; stripe < stripes; ++stripe) + { + auto const offset{first + stripe * 2.0 * stopline_width}; + auto const from{arms[arm].position + across * offset + lift}; + auto const to{arms[arm].position + across * (offset + stopline_width) + lift}; + auto const leftstart{from + inwards * (crosswalk_inset + arms[arm].crosswalk)}; + auto const rightstart{from + inwards * crosswalk_inset}; + auto const leftend{to + inwards * (crosswalk_inset + arms[arm].crosswalk)}; + auto const rightend{to + inwards * crosswalk_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 @@ -2672,6 +3319,15 @@ void junction_node::export_as_text_(std::ostream &Output) const { Output << "armmedian " << arm + 1 << ' ' << m_state.arms[arm].median << ' '; } + if (m_state.arms[arm].crosswalk > 0.f) + { + Output << "crosswalk " << arm + 1 << ' ' << m_state.arms[arm].crosswalk << ' '; + } + auto const &bank{m_state.arms[arm].bank}; + if (bank.set) + { + Output << "cornerbank " << arm + 1 << ' ' << bank.width[0] << ' ' << bank.drop[0] << ' ' << bank.width[1] << ' ' << bank.drop[1] << ' '; + } auto const priority{m_state.arms[arm].priority}; if (priority != right_of_way::none) { @@ -2687,6 +3343,15 @@ void junction_node::export_as_text_(std::ostream &Output) const } } } + if (m_state.bankmaterial != "none") + { + Output << "bankmaterial " << m_state.bankmaterial << ' '; + } + if (m_state.arms.size() == 2 && (m_state.arms[0].median > 0.f || m_state.arms[1].median > 0.f)) + { + Output << "median " << (m_state.median == road_node::median_type::gap ? "gap" : m_state.median == road_node::median_type::island ? "island" : "painted") << ' '; + Output << m_state.medianmaterial << ' '; + } Output << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' ' << "environment " << m_state.environment << ' '; Output << "endjunction" << "\n"; @@ -2821,6 +3486,10 @@ void road_node::export_as_text_(std::ostream &Output) const { Output << "median " << (road.median.type == median_type::gap ? "gap" : road.median.type == median_type::island ? "island" : "painted") << ' '; Output << road.median.width[0] << ' ' << road.median.width[1] << ' ' << road.median.material << ' '; + if (road.median.round[0] > 0.f || road.median.round[1] > 0.f) + { + Output << "medianround " << road.median.round[0] << ' ' << road.median.round[1] << ' '; + } } if (road.kerbs[0] || road.kerbs[1]) { diff --git a/world/Road.h b/world/Road.h index 99e85fcd..de83bf4d 100644 --- a/world/Road.h +++ b/world/Road.h @@ -56,6 +56,7 @@ class owned_shapes // scenery entry: // node road [ ]... endroad // properties, past the ones of the lanes and the look: median , +// medianround , // kerbs , bank , // bankmaterial , weight , roundabout class road_node : public scene::basic_node @@ -106,6 +107,7 @@ class road_node : public scene::basic_node median_type type{median_type::painted}; std::array width{0.f, 0.f}; // at the start and at the end of the axis std::string material{"none"}; // what an island is covered with + std::array round{0.f, 0.f}; // length of the rounded end of an island, at the start and at the end of the axis. 0: the island is cut straight }; // slope leading from the edge of the road down, or up, to the ground struct bank_data @@ -113,6 +115,11 @@ class road_node : public scene::basic_node bool set{false}; // false: a shoulder gets the bank the road gives to all its shoulders, anything else gets none std::array width{0.f, 0.f}; // at the start and at the end of the axis std::array drop{0.f, 0.f}; // how far below the edge of the road it ends; negative if it rises + + bool operator==(bank_data const &Other) const + { + return set == Other.set && width == Other.width && drop == Other.drop; + } }; // everything the scenery says about the road. the lanes go left to right when facing along the axis: // the ones going against it, outermost first, then the ones going along it @@ -253,7 +260,10 @@ class road_node : public scene::basic_node // properties: surface , texlength , markings , side , kerb , // slope , stopline , velocity , friction , environment , // priority , turns , -// armmedian , kerbs +// armmedian , kerbs , +// cornerbank (at that road, then at the next one), bankmaterial , +// crosswalk , median +// a junction of two roads is drawn as a stretch of road, on which the lanes of one are led to the lanes of the other // the arms don't have to be level with each other: the surface is spanned between their ends and the centre. // the junction also tells the vehicles when to wait: the ones which have to give way wait for the ones with the right of way, // and where neither has it, for the ones coming from their right, or from the opposite side when turning left across their way @@ -286,6 +296,10 @@ class junction_node : public scene::basic_node float width{3.5f}; // width of a lane bool stopline{false}; // a line is painted across the lanes leading into the junction float median{0.f}; // how far apart the two directions of the road are there + float crosswalk{0.f}; // length of the pedestrian crossing painted across the road where it meets the junction; 0: none + // slope leading to the ground from the corner which starts at the left edge of this road, seen from the junction, and goes on + // to the next road. it starts at this road and ends at the next one + road_node::bank_data bank; right_of_way priority{right_of_way::none}; // ways each lane leading into the junction can be left by, a sum of turn_flags; the lanes are counted from the middle of the road. // a lane with no entry, or with 0, gets what the junction works out: the inner lane takes the left turns, the outer one the right turns @@ -339,6 +353,10 @@ class junction_node : public scene::basic_node std::string kerbmaterial{"none"}; float kerbheight{0.12f}; glm::vec2 slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder + std::string bankmaterial{"none"}; // what a bank of a corner is covered with, other than one of a shoulder + // a junction of two roads, where a road changes its lanes: what's between the two directions where they're apart + road_node::median_type median{road_node::median_type::painted}; + std::string medianmaterial{"none"}; // what an island is covered with float velocity{30.f}; // speed limit on the way through float friction{0.85f}; float sounddistance{25.f}; @@ -413,6 +431,8 @@ class junction_node : public scene::basic_node void export_as_text_(std::ostream &Output) const override; // creates a path through the junction from provided definition and registers it with the simulation TTrack *create_track(std::string const &Definition, std::size_t const Index); + // geometry of a junction of two roads + std::vector create_transition_shapes() const; // sets up what the traffic of each lane leading in is told, once the ways through are made void create_gates(); // members