diff --git a/CMakeLists.txt b/CMakeLists.txt index fe5b1f2b..58b3e4ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/Road.cpp" "world/Traction.cpp" "world/TractionPower.cpp" "vehicle/Train.cpp" diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 73ebb580..9c2bb100 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -173,6 +173,9 @@ bool editor_track::is_supported(TTrack const &Track) { if ((Track.iCategoryFlag & 0x80) != 0) return false; + // lanes are generated from their road, and change only along with it + if (Track.m_road != nullptr) + return false; if (Track.eType == tt_Normal) return Track.Segment != nullptr; if (Track.eType == tt_Switch) diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index df077aa7..c45e24e8 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -747,7 +747,7 @@ save_result node_layers::save(std::vector const &Rootstatements) }; for (auto *path : simulation::Paths.sequence()) { - if (path == nullptr || (path->iCategoryFlag & 0x80) != 0 || path->from_template()) + if (path == nullptr || (path->iCategoryFlag & 0x80) != 0 || path->m_road != nullptr || path->from_template()) { continue; } diff --git a/scene/scenenode.h b/scene/scenenode.h index df60187f..7d1db424 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -145,6 +145,9 @@ public: // replaces the renderable geometry handle (used by the editor when it re-uploads terrain geometry) void geometry( gfx::geometry_handle const &Handle ); + // set lighting (used by generated geometry painted with plain colour) + void + lighting( lighting_data const &Lighting ); // data access shapenode_data const & data() const; @@ -176,6 +179,12 @@ void shape_node::geometry( gfx::geometry_handle const &Handle ) { m_data.geometry = Handle; } +// set lighting +inline +void +shape_node::lighting( lighting_data const &Lighting ) { + m_data.lighting = Lighting; +} // data access inline shape_node::shapenode_data const & diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 5f04c611..e8e4542f 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "world/Event.h" #include "world/MemCell.h" #include "world/Track.h" +#include "world/Road.h" #include "world/Traction.h" #include "world/TractionPower.h" #include "audio/sound.h" @@ -36,6 +37,7 @@ state_manager State; event_manager Events; memory_table Memory; path_table Paths; +road_table Roads; traction_table Traction; powergridsource_table Powergrid; instance_table Instances; diff --git a/simulation/simulation.h b/simulation/simulation.h index b75056ad..03448007 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -82,6 +82,7 @@ extern state_manager State; extern event_manager Events; extern memory_table Memory; extern path_table Paths; +extern road_table Roads; extern traction_table Traction; extern powergridsource_table Powergrid; extern instance_table Instances; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 18cd07af..e6f60934 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -25,6 +25,7 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "rendering/lightarray.h" #include "world/TractionPower.h" +#include "world/Road.h" #include "application/application.h" #include "rendering/renderer.h" #include "utilities/Logs.h" @@ -177,6 +178,10 @@ state_serializer::deserialize_continue(std::shared_ptr state Region->serialize( state->scenariofile ); } + // geometry of the roads is generated on each load instead of being kept in the binary terrain file, + // so it's inserted in the region only after that file had its chance to be written + simulation::Roads.create_geometry( Scratchpad ); + return false; } @@ -393,6 +398,7 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc } simulation::Paths.InitTracks(); + simulation::Roads.InitRoads(); simulation::Traction.InitTraction(); simulation::Events.InitEvents(); simulation::Events.InitLaunchers(); @@ -499,6 +505,21 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch simulation::Region->insert_and_register( path ); scene::Layers.track( path, { sourcebegin, Input.TokenEnd() } ); } + else if( nodedata.type == "road" ) { + + auto *road { new road_node( nodedata ) }; + road->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Roads.insert( road ) ) { + ErrorLog( "Bad scenario: duplicate road name \"" + road->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), road ); + // the lanes are regular paths, registered right away so they get joined with their neighbours along with the tracks + road->create_lanes(); + } else if( nodedata.type == "traction" ) { auto *traction { deserialize_traction( Input, Scratchpad, nodedata ) }; @@ -1366,10 +1387,21 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons if( path == nullptr || path->m_editorremoved ) { continue; } + if( path->m_road != nullptr ) { + // lanes are generated anew from their road + continue; + } if( path->dirty() == Dirty && path->group() == null_handle ) { path->export_as_text( scmfile ); } } + // roads + scmfile << "// roads\n"; + for( auto const *road : Roads.sequence() ) { + if( road != nullptr && road->dirty() == Dirty && road->group() == null_handle ) { + road->export_as_text( scmfile ); + } + } // traction scmfile << "// traction\n"; for( auto const *traction : Traction.sequence() ) { @@ -1425,7 +1457,7 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { // statements which take more than a single token, with the tokens ending them static std::unordered_map const nodeends { - { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, + { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, { "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" }, { "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } }; static std::unordered_map const statementends { diff --git a/utilities/Classes.h b/utilities/Classes.h index 1e91bc96..7622a57c 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -43,6 +43,8 @@ class vehicle_table; class train_table; struct light_array; class particle_manager; +class road_node; // kawałek drogi, z którego powstają pasy ruchu +class road_table; struct dictionary_source; class trainset_desc; class scenery_desc; diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 873cc4d3..0bf8815b 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -2275,7 +2275,8 @@ TDynamicObject::Init(std::string Name, // nazwa pojazdu, np. "EU07-424" if (MoverParameters->CategoryFlag & 2) // jeśli samochód { // ustawianie samochodow na poboczu albo na środku drogi - if( Track->fTrackWidth < 3.5 ) // jeśli droga wąska + if( Track->fTrackWidth < 3.5 // jeśli droga wąska + || Track->m_road != nullptr ) // a lane of a road node is one-way, the vehicle keeps to its middle MoverParameters->OffsetTrackH = 0.0; // to stawiamy na środku, niezależnie od stanu // ruchu else if( driveractive ) {// od 3.5m do 8.0m jedzie po środku pasa, dla diff --git a/world/Road.cpp b/world/Road.cpp new file mode 100644 index 00000000..f52a4e51 --- /dev/null +++ b/world/Road.cpp @@ -0,0 +1,958 @@ +/* +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/. +*/ + +#include "stdafx.h" +#include "world/Road.h" + +#include "world/Track.h" +#include "simulation/simulation.h" +#include "scene/scene.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" + +namespace +{ + +// dimensions of the markings, in metres +// NOTE: these give a layout resembling an ordinary polish road, they weren't taken from the regulations +constexpr double line_width{0.12}; +constexpr double line_spacing{0.12}; // gap between two lines painted side by side +constexpr double line_lift{0.02}; // the markings are raised over the surface, so the two don't fight for depth +constexpr double edge_inset{0.15}; // distance between the edge of the surface and the line running along it +constexpr double dash_period{12.0}; // a third of it is painted, the rest is the gap +constexpr double dash_step{2.0}; // longest straight piece of a painted line +// speed and reach of the paths made to turn the vehicles back at a loose end of the road +constexpr float turn_velocity{10.f}; +constexpr double turn_reach{6.0}; + +glm::dvec3 const up{0.0, 1.0, 0.0}; + +// placement of a cross-section of the road +struct axis_frame +{ + glm::dvec3 position; + glm::dvec3 tangent; // direction of the axis + glm::dvec3 left; // direction across the road, tilted by the roll + double roll{0.0}; +}; + +// the axis of a road piece; a cubic bezier curve, the same thing the paths of the tracks are made of +class road_axis +{ + + public: + explicit road_axis(segment_data const &Path) + { + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + m_curved = (control1 != glm::dvec3{0.0}) || (control2 != glm::dvec3{0.0}); + m_points[0] = start; + m_points[3] = end; + if (m_curved) + { + m_points[1] = start + control1; + m_points[2] = end + control2; + } + else + { + // a straight gets its control points in thirds of the length, the way the tracks do it + m_points[1] = start + (end - start) / 3.0; + m_points[2] = end - (end - start) / 3.0; + } + m_rolls = {glm::radians(static_cast(Path.rolls[0])), glm::radians(static_cast(Path.rolls[1]))}; + m_radius = std::abs(Path.radius); + // distances along the curve for evenly spaced values of its parameter, to convert the former into the latter + m_stations.resize(samples + 1); + m_stations[0] = 0.0; + auto previous{m_points[0]}; + for (int idx = 1; idx <= samples; ++idx) + { + auto const current{point(static_cast(idx) / samples)}; + m_stations[idx] = m_stations[idx - 1] + glm::distance(previous, current); + previous = current; + } + } + bool curved() const + { + return m_curved; + } + double length() const + { + return m_stations.back(); + } + // control vector of specified end of the curve, relative to that end + glm::dvec3 control(int const End) const + { + return End == 0 ? m_points[1] - m_points[0] : m_points[2] - m_points[3]; + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * m_points[0] + 3.0 * u * u * T * m_points[1] + 3.0 * u * T * T * m_points[2] + T * T * T * m_points[3]; + } + // curvature of the axis seen from above, positive when it bends to the left + double curvature(double const T) const + { + auto const first{derivative(T)}; + auto const second{6.0 * (1.0 - T) * (m_points[2] - 2.0 * m_points[1] + m_points[0]) + 6.0 * T * (m_points[3] - 2.0 * m_points[2] + m_points[1])}; + auto const speedsquared{first.x * first.x + first.z * first.z}; + if (speedsquared < 1e-9) + { + return 0.0; + } + return (second.x * first.z - second.z * first.x) / (speedsquared * std::sqrt(speedsquared)); + } + // cross-section at specified value of the curve parameter + axis_frame frame(double const T) const + { + axis_frame frame; + frame.position = point(T); + auto direction{derivative(T)}; + if (glm::length2(direction) < 1e-9) + { + // a control point placed right at the end of the curve + direction = derivative(std::clamp(T, 0.01, 0.99)); + } + if (glm::length2(direction) < 1e-9) + { + direction = m_points[3] - m_points[0]; + } + if (glm::length2(direction) < 1e-9) + { + direction = glm::dvec3{0.0, 0.0, 1.0}; + } + frame.tangent = glm::normalize(direction); + // with the y axis up and the vehicle facing along z its left side is at positive x + glm::dvec3 across{frame.tangent.z, 0.0, -frame.tangent.x}; + across = (glm::length2(across) < 1e-9 ? glm::dvec3{1.0, 0.0, 0.0} : glm::normalize(across)); + // positive roll lowers the right side of the road, same as it does for the tracks + frame.roll = glm::mix(m_rolls[0], m_rolls[1], T); + frame.left = across * std::cos(frame.roll) + up * std::sin(frame.roll); + return frame; + } + // cross-section at specified distance from the start of the axis + axis_frame at(double const Station) const + { + return frame(parameter(Station)); + } + // length of pieces the road is cut into along the axis; follows the rules the tracks use + double step() const + { + if (false == m_curved) + { + return 10.0; + } + auto const fidelity{std::max(1.0, static_cast(Global.SplineFidelity))}; + return std::clamp(m_radius != 0.0 ? m_radius * 0.02 / fidelity : length() * 0.1, 2.0 / fidelity, 10.0 / fidelity); + } + + private: + static constexpr int samples{64}; + + glm::dvec3 derivative(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (m_points[1] - m_points[0]) + 6.0 * u * T * (m_points[2] - m_points[1]) + 3.0 * T * T * (m_points[3] - m_points[2]); + } + double parameter(double const Station) const + { + if (Station <= 0.0) + { + return 0.0; + } + if (Station >= length()) + { + return 1.0; + } + // first of the sampled distances past the requested one; it's never the very first sample + auto const idx{std::distance(m_stations.begin(), std::upper_bound(m_stations.begin(), m_stations.end(), Station))}; + auto const span{m_stations[idx] - m_stations[idx - 1]}; + auto const fraction{span > 0.0 ? (Station - m_stations[idx - 1]) / span : 0.0}; + return (static_cast(idx - 1) + fraction) / samples; + } + + std::array m_points; + std::array m_rolls{}; + double m_radius{0.0}; + bool m_curved{false}; + std::vector m_stations; +}; + +// corner of the cross-section of something laid along the road +struct profile_point +{ + double offset; // distance from the axis, positive to the left + double height; // above the surface + float texture; // texture coordinate across the road +}; + +// stretches provided profile along a part of the axis. the profile points go left to right, each two neighbours make a band of triangles. +void loft(std::vector &Output, road_axis const &Axis, std::vector const &Profile, double const From, double const To, double const Step, double const Texturelength) +{ + if (Profile.size() < 2 || To <= From || Step <= 0.0) + { + return; + } + auto const count{std::max(1, static_cast(std::ceil((To - From) / Step - 0.001)))}; + auto const pointcount{Profile.size()}; + // each band is flat across the road, and follows its bends along it + 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 previous(pointcount), current(pointcount); + axis_frame previousframe; + double previousstation{From}; + + for (int section = 0; section <= count; ++section) + { + auto const station{From + (To - From) * section / count}; + auto const frame{Axis.at(station)}; + for (std::size_t idx = 0; idx < pointcount; ++idx) + { + current[idx] = frame.position + frame.left * Profile[idx].offset + up * Profile[idx].height; + } + if (section > 0) + { + auto const texturestart{static_cast(previousstation / Texturelength)}; + auto const textureend{static_cast(station / Texturelength)}; + for (std::size_t idx = 0; idx + 1 < pointcount; ++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, {Profile[idx].texture, texturestart}}; + world_vertex const rightstart{previous[idx + 1], normalstart, {Profile[idx + 1].texture, texturestart}}; + world_vertex const leftend{current[idx], normalend, {Profile[idx].texture, textureend}}; + world_vertex const rightend{current[idx + 1], normalend, {Profile[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); + previousframe = frame; + previousstation = station; + } +} + +// proportions of the image specified material is painted with +float texture_ratio(std::string const &Material) +{ + auto const material{GfxRenderer->Fetch_Material(Material)}; + if (material == null_handle) + { + return 1.f; + } + auto const texture{GfxRenderer->Material(material)->GetTexture(0)}; + if (texture == null_handle) + { + return 1.f; + } + auto const &image{GfxRenderer->Texture(texture)}; + return image.get_height() > 0 ? static_cast(image.get_width()) / static_cast(image.get_height()) : 1.f; +} + +} // namespace + +road_node::road_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +// restores content of the node from provided input stream; reads up to and including the closing 'endroad' +void road_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + auto const label{m_name.empty() ? std::string{"unnamed road"} : "road \"" + m_name + "\""}; + + m_axis.deserialize(Input, Offset); + + // properties of the lanes can precede the statement which sets how many of them there are, so they wait until everything is read + struct lane_property + { + std::string lane; + std::string property; + float value; + }; + struct change_property + { + std::string first; + std::string second; + std::string mode; + }; + std::vector laneproperties; + std::vector changeproperties; + + auto token{Input.getToken()}; + while (false == token.empty() && token != "endroad") + { + if (token == "lanes") + { + // lanes + Input.getTokens(2); + Input >> m_forward >> m_backward; + } + else if (token == "width") + { + Input.getTokens(); + Input >> m_lanewidth; + } + else if (token == "velocity") + { + Input.getTokens(); + Input >> m_velocity; + } + else if (token == "lane") + { + // lane + lane_property property; + property.lane = Input.getToken(); + property.property = Input.getToken(); + property.value = 0.f; + Input.getTokens(); + Input >> property.value; + laneproperties.emplace_back(property); + } + else if (token == "change") + { + // change + change_property property; + property.first = Input.getToken(); + property.second = Input.getToken(); + property.mode = Input.getToken(); + changeproperties.emplace_back(property); + } + else if (token == "surface") + { + m_surface = Input.getToken(); + replace_slashes(m_surface); + } + else if (token == "texlength") + { + Input.getTokens(); + Input >> m_texturelength; + } + else if (token == "side") + { + // side + auto const where{Input.getToken()}; + auto const type{Input.getToken()}; + side_data side; + Input.getTokens(); + Input >> side.width; + side.material = Input.getToken(); + replace_slashes(side.material); + side.type = (type == "shoulder" ? side_type::shoulder : type == "sidewalk" ? side_type::sidewalk : side_type::none); + if (type != "shoulder" && type != "sidewalk" && type != "none") + { + ErrorLog("Bad road: unknown kind of side \"" + type + "\" defined for " + label); + } + if (where == "left" || where == "both") + { + m_sides[0] = side; + } + if (where == "right" || where == "both") + { + m_sides[1] = side; + } + if (where != "left" && where != "right" && where != "both") + { + ErrorLog("Bad road: unknown side \"" + where + "\" defined for " + label); + } + } + else if (token == "kerb") + { + Input.getTokens(); + Input >> m_kerbheight; + } + else if (token == "slope") + { + // slope + Input.getTokens(2); + Input >> m_slope.x >> m_slope.y; + } + else if (token == "markings") + { + auto const colour{Input.getToken()}; + m_markings = (colour == "white" ? marking_colour::white : colour == "orange" ? marking_colour::orange : marking_colour::none); + if (colour != "white" && colour != "orange" && colour != "none") + { + ErrorLog("Bad road: unknown colour of markings \"" + colour + "\" defined for " + label); + } + } + else if (token == "friction") + { + Input.getTokens(); + Input >> m_friction; + } + else if (token == "environment") + { + m_environment = Input.getToken(); + } + else + { + ErrorLog("Bad road: unknown property: \"" + token + "\" defined for " + label); + } + token = Input.getToken(); + } + + // the paths of the lanes are loaded from text, so what goes into it has to be something they understand + static std::array const environments{"flat", "mountains", "mountain", "canyon", "tunnel", "bridge", "bank"}; + if (std::find(environments.begin(), environments.end(), m_environment) == environments.end()) + { + ErrorLog("Bad road: unknown environment \"" + m_environment + "\" defined for " + label); + m_environment = "flat"; + } + m_forward = std::clamp(m_forward, 0, 8); + m_backward = std::clamp(m_backward, 0, 8); + if (m_forward + m_backward == 0) + { + ErrorLog("Bad road: " + label + " has no lanes, a single one is used instead"); + m_forward = 1; + } + m_lanewidth = std::max(1.f, m_lanewidth); + if (m_texturelength < 0.01f) + { + m_texturelength = 4.f; + } + if (m_surface.empty()) + { + m_surface = "none"; + } + + lane_data lanetemplate; + lanetemplate.width = m_lanewidth; + lanetemplate.velocity = m_velocity; + m_lanes.assign(static_cast(m_forward + m_backward), lanetemplate); + for (auto const &property : laneproperties) + { + auto const lane{lane_index(property.lane)}; + if (lane < 0) + { + ErrorLog("Bad road: " + label + " has no lane \"" + property.lane + "\""); + continue; + } + if (property.property == "width") + { + m_lanes[lane].width = std::max(1.f, property.value); + } + else if (property.property == "velocity") + { + m_lanes[lane].velocity = property.value; + } + else + { + ErrorLog("Bad road: unknown lane property: \"" + property.property + "\" defined for " + label); + } + } + + m_changes.resize(m_lanes.size() - 1); + for (std::size_t boundary = 0; boundary < m_changes.size(); ++boundary) + { + m_changes[boundary] = default_change(boundary); + } + for (auto const &property : changeproperties) + { + auto const first{lane_index(property.first)}; + auto const second{lane_index(property.second)}; + if (first < 0 || second < 0 || std::abs(first - second) != 1) + { + ErrorLog("Bad road: lanes \"" + property.first + "\" and \"" + property.second + "\" aren't neighbours on " + label); + continue; + } + // the permissions are stored for the pair taken left to right + auto const firstonleft{first < second}; + auto &change{m_changes[std::min(first, second)]}; + if (property.mode == "both") + { + change = change_both; + } + else if (property.mode == "none") + { + change = change_none; + } + else if (property.mode == "ab") + { + change = (firstonleft ? change_toright : change_toleft); + } + else if (property.mode == "ba") + { + change = (firstonleft ? change_toleft : change_toright); + } + else + { + ErrorLog("Bad road: unknown lane change mode: \"" + property.mode + "\" defined for " + label); + } + } + + location(road_axis{m_axis}.frame(0.5).position); +} + +// creates paths of the lanes and hands them over to the simulation +void road_node::create_lanes() +{ + for (std::size_t lane = 0; lane < m_lanes.size(); ++lane) + { + m_lanes[lane].track = create_track(m_name.empty() ? std::string{} : m_name + ":" + lane_id(lane), lane_path(lane), m_lanes[lane].width, m_lanes[lane].velocity); + } +} + +// links lanes of opposite directions at an end of the road which leads nowhere, so the vehicles can turn back +void road_node::close_ends() +{ + // NOTE: without the link a vehicle reaching the end would be sent back on the lane it came by, against its direction + road_axis const axis{m_axis}; + for (int end = 0; end < 2; ++end) + { + // the lanes going along the axis leave the road at its end, the ones going against it leave at its start + auto const outwards{end == 1 ? axis.frame(1.0).tangent : -axis.frame(0.0).tangent}; + for (int pair = 1; pair <= std::min(m_forward, m_backward); ++pair) + { + auto const forward{static_cast(m_backward + pair - 1)}; + auto const backward{static_cast(m_backward - pair)}; + auto *exit{m_lanes[end == 1 ? forward : backward].track}; + auto *entry{m_lanes[end == 1 ? backward : forward].track}; + if (exit == nullptr || entry == nullptr || exit->trNext != nullptr || entry->trPrev != nullptr) + { + continue; + } + segment_data path; + path.points[segment_data::point::start] = exit->CurrentSegment()->FastGetPoint_1(); + path.points[segment_data::point::end] = entry->CurrentSegment()->FastGetPoint_0(); + auto const span{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + path.points[segment_data::point::control1] = outwards * std::max(turn_reach, 1.5 * span); + path.points[segment_data::point::control2] = outwards * std::max(turn_reach, 1.5 * span); + path.radius = static_cast(std::max(2.0, 0.5 * span)); + auto *turn{create_track(m_name.empty() ? std::string{} : m_name + ":" + (end == 1 ? "endturn" : "startturn") + std::to_string(pair), path, m_lanes[forward].width, turn_velocity)}; + exit->ConnectNextPrev(turn, 0); + turn->ConnectNextPrev(entry, 0); + m_turns.emplace_back(turn); + } + } +} + +// generates geometry of the surface, the sides and the markings +std::vector road_node::create_shapes() const +{ + // triangles are gathered separately for each look, so the lot can be drawn with as few shapes as possible + 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; + }; + + road_axis const axis{m_axis}; + auto const length{axis.length()}; + auto const step{axis.step()}; + auto const halfwidth{0.5 * width()}; + + // surface. the image is centered on the axis, and repeated if the road is wider + if (m_surface != "none") + { + auto const tile{static_cast(texture_ratio(m_surface) * m_texturelength)}; + loft(vertices(m_surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast(0.5 - halfwidth / tile)}}, 0.0, length, step, m_texturelength); + } + + // sides + for (int side = 0; side < 2; ++side) + { + auto const &data{m_sides[side]}; + if (data.type == side_type::none || data.width <= 0.f || data.material.empty() || data.material == "none") + { + continue; + } + auto const direction{side == 0 ? 1.0 : -1.0}; + // corners of the profile, from the edge of the surface outwards + std::vector profile; + if (data.type == side_type::shoulder) + { + // the image is laid out like the one of the legacy roads: the edge of the surface on its right, the bank on its left half + profile.push_back({direction * halfwidth, 0.0, 1.f}); + profile.push_back({direction * (halfwidth + data.width), 0.0, 0.5f}); + if (m_slope.x > 0.f) + { + profile.push_back({direction * (halfwidth + data.width + m_slope.x), -m_slope.y, 0.f}); + } + } + else + { + // the image starts at the foot of the kerb, and is laid out in its own proportions + auto const tile{static_cast(texture_ratio(data.material) * m_texturelength)}; + profile.push_back({direction * halfwidth, 0.0, 0.f}); + profile.push_back({direction * halfwidth, m_kerbheight, static_cast(m_kerbheight / tile)}); + profile.push_back({direction * (halfwidth + data.width), m_kerbheight, static_cast((m_kerbheight + data.width) / tile)}); + } + if (side == 0) + { + // the loft takes the corners left to right + std::reverse(profile.begin(), profile.end()); + } + loft(vertices(data.material, lighting_data{}), axis, profile, 0.0, length, step, m_texturelength); + } + + // markings. these are painted with plain colour instead of an image + if (m_markings != marking_colour::none) + { + lighting_data paint; + paint.diffuse = (m_markings == 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)}; + + auto const solid = [&](double const Offset) { + loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, 0.0, length, std::min(step, dash_step), m_texturelength); + }; + // the dashes are fitted so the gaps at both ends of the road are a half of the regular one, which makes them match the next piece + auto const dashed = [&](double const Offset) { + auto const count{std::max(1, static_cast(std::lround(length / dash_period)))}; + auto const period{length / count}; + for (int dash = 0; dash < count; ++dash) + { + auto const from{(dash + 1.0 / 3.0) * period}; + loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, from, from + period / 3.0, dash_step, m_texturelength); + } + }; + + // edges of the surface + auto const edge{halfwidth - edge_inset - 0.5 * line_width}; + if (edge > 0.5) + { + solid(edge); + solid(-edge); + } + // lines between the lanes. a lane can be left across a dashed line, or across a pair of lines if the dashed one is on its side + auto offset{halfwidth}; + for (std::size_t boundary = 0; boundary < m_changes.size(); ++boundary) + { + offset -= m_lanes[boundary].width; + auto const opposite{m_backward > 0 && boundary + 1 == static_cast(m_backward)}; + auto const pair{0.5 * (line_width + line_spacing)}; + switch (m_changes[boundary]) + { + case change_both: + { + dashed(offset); + break; + } + case change_toright: + { + dashed(offset + pair); + solid(offset - pair); + break; + } + case change_toleft: + { + solid(offset + pair); + dashed(offset - pair); + break; + } + default: + { + if (opposite) + { + // traffic going opposite ways is kept apart with a double line + solid(offset + pair); + solid(offset - pair); + } + else + { + solid(offset); + } + break; + } + } + } + } + + std::vector shapes; + 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; +} + +// identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet +std::string road_node::lane_id(std::size_t const Lane) const +{ + auto const backward{static_cast(m_backward)}; + return Lane < backward ? "b" + std::to_string(backward - Lane) : "f" + std::to_string(Lane - backward + 1); +} + +// number of a lane in the left to right order, -1 if the road has no lane with such identifier +int road_node::lane_index(std::string const &Id) const +{ + if (Id.size() < 2 || Id.size() > 3 || (Id[0] != 'f' && Id[0] != 'b')) + { + return -1; + } + int number{0}; + for (std::size_t idx = 1; idx < Id.size(); ++idx) + { + if (Id[idx] < '0' || Id[idx] > '9') + { + return -1; + } + number = number * 10 + (Id[idx] - '0'); + } + if (number < 1 || number > (Id[0] == 'f' ? m_forward : m_backward)) + { + return -1; + } + return Id[0] == 'f' ? m_backward + number - 1 : m_backward - number; +} + +// permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise +int road_node::default_change(std::size_t const Boundary) const +{ + auto const opposite{m_backward > 0 && m_forward > 0 && Boundary + 1 == static_cast(m_backward)}; + if (false == opposite) + { + return change_both; + } + // overtaking across the oncoming lane is left for the simple two lane roads + return (m_forward == 1 && m_backward == 1) ? change_both : change_none; +} + +// distance of the middle of a lane from the axis, positive to the left when facing along the axis +double road_node::lane_offset(std::size_t const Lane) const +{ + auto offset{0.5 * width()}; + for (std::size_t idx = 0; idx < Lane && idx < m_lanes.size(); ++idx) + { + offset -= m_lanes[idx].width; + } + return Lane < m_lanes.size() ? offset - 0.5 * m_lanes[Lane].width : offset; +} + +// combined width of the lanes +double road_node::width() const +{ + double width{0.0}; + for (auto const &lane : m_lanes) + { + width += lane.width; + } + return width; +} + +// shape of the middle of a lane, laid out in the direction of travel +segment_data road_node::lane_path(std::size_t const Lane) const +{ + road_axis const axis{m_axis}; + auto const offset{lane_offset(Lane)}; + auto const begin{axis.frame(0.0)}; + auto const end{axis.frame(1.0)}; + + segment_data path; + path.points[segment_data::point::start] = begin.position + begin.left * offset; + path.points[segment_data::point::end] = end.position + end.left * offset; + if (axis.curved()) + { + // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors + path.points[segment_data::point::control1] = axis.control(0) * std::max(0.05, 1.0 - offset * std::cos(begin.roll) * axis.curvature(0.0)); + path.points[segment_data::point::control2] = axis.control(1) * std::max(0.05, 1.0 - offset * std::cos(end.roll) * axis.curvature(1.0)); + // if the roll changes along the road its sides rise or fall relative to the axis + auto const climb{offset * (std::sin(end.roll) - std::sin(begin.roll)) / 3.0}; + path.points[segment_data::point::control1].y += climb; + path.points[segment_data::point::control2].y -= climb; + } + path.rolls = m_axis.rolls; + if (m_axis.radius != 0.f) + { + // the lanes on the inner side of the bend have it tighter + auto const turn{axis.curvature(0.5) >= 0.0 ? 1.0 : -1.0}; + path.radius = static_cast(std::max(1.0, std::abs(m_axis.radius) - turn * offset)); + } + if (Lane < static_cast(m_backward)) + { + // the lane goes against the axis + std::swap(path.points[segment_data::point::start], path.points[segment_data::point::end]); + std::swap(path.points[segment_data::point::control1], path.points[segment_data::point::control2]); + path.rolls = {-m_axis.rolls[1], -m_axis.rolls[0]}; + } + if (Global.bRollFix) + { + // with this setting on a rolled path gets raised when it's set up, by an amount fit for a railway track. + // the surface of the road stays where it is, so the lane is lowered beforehand to make up for it + path.points[segment_data::point::start].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[0])))) * 0.75; + path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[1])))) * 0.75; + } + return path; +} + +// creates a path for the vehicles and registers it with the simulation +TTrack *road_node::create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + auto const &end{Path.points[segment_data::point::end]}; + // the path is put together the way the scenery defines an invisible road, to have it set up by the code which loads these + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + text << "road " << glm::distance(start, end) << ' ' << Width << ' ' << m_friction << ' ' << m_sounddistance << ' ' << m_quality << " 0 " << m_environment << " unvis " << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' + << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; + if (Velocity > 0.f) + { + text << "velocity " << Velocity << ' '; + } + text << "endtrack"; + + cParser parser(text.str(), cParser::buffer_TEXT); + scene::node_data nodedata; + nodedata.range_max = -1.0; + nodedata.name = Name; + nodedata.type = "track"; + auto *track{new TTrack(nodedata)}; + track->Load(&parser, glm::dvec3{0.0}); + track->m_road = this; + if (false == simulation::Paths.insert(track)) + { + ErrorLog("Bad scenario: duplicate track name \"" + track->name() + "\" generated for a road"); + } + simulation::Region->insert_and_register(track); + return track; +} + +// radius() subclass details, calculates node's bounding radius +float road_node::radius_() +{ + return static_cast(0.5 * road_axis{m_axis}.length() + 0.5 * width() + std::max(m_sides[0].width, m_sides[1].width) + m_slope.x); +} + +// serialize() subclass details, sends content of the subclass to provided stream +void road_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +// deserialize() subclass details, restores content of the subclass from provided stream +void road_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +void road_node::export_as_text_(std::ostream &Output) const +{ + Output << "road "; + // axis + auto const precision{Output.precision(std::numeric_limits::digits10)}; + Output << m_axis.points[segment_data::point::start].x << ' ' << m_axis.points[segment_data::point::start].y << ' ' << m_axis.points[segment_data::point::start].z << ' ' << m_axis.rolls[0] << ' ' + << m_axis.points[segment_data::point::control1].x << ' ' << m_axis.points[segment_data::point::control1].y << ' ' << m_axis.points[segment_data::point::control1].z << ' ' + << m_axis.points[segment_data::point::control2].x << ' ' << m_axis.points[segment_data::point::control2].y << ' ' << m_axis.points[segment_data::point::control2].z << ' ' + << m_axis.points[segment_data::point::end].x << ' ' << m_axis.points[segment_data::point::end].y << ' ' << m_axis.points[segment_data::point::end].z << ' ' << m_axis.rolls[1] << ' ' + << m_axis.radius << ' '; + Output.precision(precision); + // lanes + Output << "lanes " << m_forward << ' ' << m_backward << ' ' << "width " << m_lanewidth << ' '; + if (m_velocity > 0.f) + { + Output << "velocity " << m_velocity << ' '; + } + for (std::size_t lane = 0; lane < m_lanes.size(); ++lane) + { + if (m_lanes[lane].width != m_lanewidth) + { + Output << "lane " << lane_id(lane) << " width " << m_lanes[lane].width << ' '; + } + if (m_lanes[lane].velocity != m_velocity) + { + Output << "lane " << lane_id(lane) << " velocity " << m_lanes[lane].velocity << ' '; + } + } + for (std::size_t boundary = 0; boundary < m_changes.size(); ++boundary) + { + if (m_changes[boundary] == default_change(boundary)) + { + continue; + } + Output << "change " << lane_id(boundary) << ' ' << lane_id(boundary + 1) << ' ' << (m_changes[boundary] == change_both ? "both" : m_changes[boundary] == change_toright ? "ab" : m_changes[boundary] == change_toleft ? "ba" : "none") << ' '; + } + // appearance + Output << "surface " << m_surface << ' ' << "texlength " << m_texturelength << ' '; + char const *sidenames[]{"left", "right"}; + for (int side = 0; side < 2; ++side) + { + auto const &data{m_sides[side]}; + if (data.type == side_type::none) + { + continue; + } + Output << "side " << sidenames[side] << ' ' << (data.type == side_type::shoulder ? "shoulder" : "sidewalk") << ' ' << data.width << ' ' << (data.material.empty() ? "none" : data.material) << ' '; + } + Output << "kerb " << m_kerbheight << ' ' << "slope " << m_slope.x << ' ' << m_slope.y << ' ' << "markings " << (m_markings == marking_colour::white ? "white" : m_markings == marking_colour::orange ? "orange" : "none") << ' '; + // properties passed on to the lanes + Output << "friction " << m_friction << ' ' << "environment " << m_environment << ' '; + // footer + Output << "endroad" + << "\n"; +} + +// legacy style initialization, to be performed when the tracks are already joined +void road_table::InitRoads() +{ + for (auto *road : m_items) + { + if (road != nullptr) + { + road->close_ends(); + } + } +} + +// generates geometry of the roads and inserts it in the region +void road_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *road : m_items) + { + if (road == nullptr) + { + continue; + } + for (auto &shape : road->create_shapes()) + { + // the geometry is already where it belongs, so the placement set by the scenery isn't to be applied to it + simulation::Region->insert(shape, Scratchpad, false); + } + } +} + +//--------------------------------------------------------------------------- diff --git a/world/Road.h b/world/Road.h new file mode 100644 index 00000000..5d4184c1 --- /dev/null +++ b/world/Road.h @@ -0,0 +1,153 @@ +/* +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 "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Segment.h" + +namespace scene +{ +struct scratch_data; +} + +// a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it. +// the node itself is neither driven on nor drawn. it produces the lanes, which are ordinary invisible one-way tracks, +// and the surface, the sides and the markings, which are ordinary shapes. this way the vehicles, the ai and the renderers +// deal with a road without knowing about it. +// scenery entry: +// node road [ ]... endroad +class road_node : public scene::basic_node +{ + + public: + // types + enum class side_type + { + none, + shoulder, // level with the surface, closed with a bank + sidewalk // raised on a kerb + }; + enum class marking_colour + { + none, + white, + orange + }; + // which of two neighbouring lanes can be left across the line they share; the lanes are taken left to right, facing along the axis + enum lane_change : int + { + change_none = 0, + change_toright = 1, // from the left lane to the right one + change_toleft = 2, // from the right lane to the left one + change_both = 3 + }; + struct lane_data + { + float width{3.5f}; + float velocity{-1.f}; // speed limit, none if negative + TTrack *track{nullptr}; // the generated path; owned by the path table, like every other track + }; + struct side_data + { + side_type type{side_type::none}; + float width{0.f}; + std::string material; + }; + // constructors + explicit road_node(scene::node_data const &Nodedata); + // methods + // restores content of the node from provided input stream; reads up to and including the closing 'endroad' + void import(cParser &Input, glm::dvec3 const &Offset); + // creates paths of the lanes and hands them over to the simulation + void create_lanes(); + // links lanes of opposite directions at an end of the road which leads nowhere, so the vehicles can turn back + void close_ends(); + // generates geometry of the surface, the sides and the markings + std::vector create_shapes() const; + // identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet + std::string lane_id(std::size_t const Lane) const; + // distance of the middle of a lane from the axis, positive to the left when facing along the axis + double lane_offset(std::size_t const Lane) const; + // combined width of the lanes + double width() const; + // lanes, left to right when facing along the axis: the ones going against it, outermost first, then the ones going along it + std::vector const &lanes() const + { + return m_lanes; + } + // permissions to change the lane, one for each pair of neighbouring lanes + std::vector const &lane_changes() const + { + return m_changes; + } + segment_data const &axis() const + { + return m_axis; + } + + private: + // methods + // radius() subclass details, calculates node's bounding radius + float radius_() override; + // serialize() subclass details, sends content of the subclass to provided stream + void serialize_(std::ostream &Output) const override; + // deserialize() subclass details, restores content of the subclass from provided stream + void deserialize_(std::istream &Input) override; + // export() subclass details, sends basic content of the class in legacy (text) format to provided stream + void export_as_text_(std::ostream &Output) const override; + // number of a lane in the left to right order, -1 if the road has no lane with such identifier + int lane_index(std::string const &Id) const; + // permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise + int default_change(std::size_t const Boundary) const; + // shape of the middle of a lane, laid out in the direction of travel + segment_data lane_path(std::size_t const Lane) const; + // creates a path for the vehicles and registers it with the simulation + TTrack *create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity); + // members + segment_data m_axis; + int m_forward{1}; // number of lanes going along the axis + int m_backward{1}; // number of lanes going against the axis + float m_lanewidth{3.5f}; + float m_velocity{-1.f}; + std::vector m_lanes; + std::vector m_changes; + std::string m_surface{"asphaltdark1"}; + float m_texturelength{4.f}; + std::array m_sides; // left, right + float m_kerbheight{0.12f}; + glm::vec2 m_slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder + marking_colour m_markings{marking_colour::white}; + float m_friction{0.85f}; + float m_sounddistance{25.f}; + int m_quality{15}; + std::string m_environment{"flat"}; + std::vector m_turns; // paths made to close loose ends of the road +}; + +// collection of roads present in the scene +class road_table : public basic_table +{ + + public: + // legacy style initialization, to be performed when the tracks are already joined + void InitRoads(); + // generates geometry of the roads and inserts it in the region + void create_geometry(scene::scratch_data &Scratchpad); +}; + +//--------------------------------------------------------------------------- diff --git a/world/Track.h b/world/Track.h index ea974cbd..2fb9b0ce 100644 --- a/world/Track.h +++ b/world/Track.h @@ -186,6 +186,7 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; + road_node *m_road { nullptr }; // road which generated this path as one of its lanes. such path is one-way, and isn't saved nor edited on its own int iterate_stamp = 0; // sleepermodel optional parameter -------------------------------------------------