diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 55eb9e18..9e98c813 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -703,8 +703,36 @@ std::vector make_properties() // ride and sound add_float(list, "friction", section::ride, group::ride_sound, "Friction", "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip", &TTrack::fFriction, 3, 0.f, false); list.back().applies = any; - add_float(list, "sounddistance", section::ride, group::ride_sound, "Sound distance (m)", "Distance between the rail joints, sets the rhythm of the wheel clatter; -1: the default", &TTrack::fSoundDistance, 1, -1.f, false); + // the dips at the rail joints of an uneven track go with it + add_float(list, "sounddistance", section::ride, group::ride_sound, "Sound distance (m)", "Distance between the rail joints, sets the rhythm of the wheel clatter; -1: the default", &TTrack::fSoundDistance, 1, -1.f, true); list.back().applies = any; + { + // the unevenness of the rails (world/TrackUneven.h), mm up to track_uneven::most; with Unset a negative value is -1, the + // component taken from the unevenness + auto const uneven = [&](char const *Key, char const *Label, char const *Tooltip, float TTrack::*Member, bool const Unset, bool const Switches) { + auto &property{add(list, Key, field::number, section::ride, group::ride_sound, Label, Tooltip)}; + property.rebuild = true; + property.applies = [Switches](TTrack const &Track) { + return Track.m_visible && (Track.iCategoryFlag & 15) == kRailCategory && (Track.eType == tt_Normal || (Switches && Track.eType == tt_Switch)); + }; + property.get = [Member](TTrack const &Track) { return decimal(Track.*Member, 1); }; + property.set = [Member, Unset](TTrack &Track, std::string const &Text) { + double value; + if (false == parse_number(Text, value)) + return false; + auto const clamped{(Unset && value < 0.0) ? -1.f : std::clamp(static_cast(value), 0.f, track_uneven::most)}; + if (decimal(clamped, 1) == decimal(Track.*Member, 1)) + return false; + Track.*Member = clamped; + return true; + }; + }; + uneven("uneven", "Unevenness (mm)", "Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values", &TTrack::m_uneven, false, true); + uneven("unevenup", "Up and down (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenup, true, true); + uneven("unevenside", "Sideways (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevenside, true, true); + uneven("unevencross", "Cross level (mm)", "Set apart from the unevenness, mm. Negative: the unevenness", &TTrack::m_unevencross, true, false); + uneven("jointdip", "Dip at rail joints (mm)", "Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness", &TTrack::m_jointdip, true, false); + } { auto &property{add(list, "quality", field::number, section::ride, group::ride_sound, "Quality", "Quality of the track, 20 is the nominal one; lower values give a rougher ride")}; property.applies = any; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index f2c7f8e7..a4e17a9b 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -248,6 +248,11 @@ editor_track::state editor_track::capture(TTrack const &Track) for (auto const *isolated : Track.Isolated) result.isolated.push_back(isolated->asName); result.overhead = Track.fOverhead; + result.uneven = Track.m_uneven; + result.unevenup = Track.m_unevenup; + result.unevenside = Track.m_unevenside; + result.unevencross = Track.m_unevencross; + result.jointdip = Track.m_jointdip; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; result.sleeper_model = Track.m_sleeper_model_name; @@ -355,6 +360,11 @@ void editor_track::apply(TTrack &Track, state const &State) Track.fTexSlope = State.texslope; isolated(Track, State.isolated); overhead(Track, State.overhead); + Track.m_uneven = State.uneven; + Track.m_unevenup = State.unevenup; + Track.m_unevenside = State.unevenside; + Track.m_unevencross = State.unevencross; + Track.m_jointdip = State.jointdip; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; Track.m_sleeper_model_name = State.sleeper_model; @@ -3225,6 +3235,16 @@ void write_ending(std::ostream &Text, TTrack const &Style) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; + if (Style.m_uneven > 0.f) + Text << "uneven " << Style.m_uneven << ' '; + if (Style.m_unevenup >= 0.f) + Text << "unevenup " << Style.m_unevenup << ' '; + if (Style.m_unevenside >= 0.f) + Text << "unevenside " << Style.m_unevenside << ' '; + if (Style.m_unevencross >= 0.f) + Text << "unevencross " << Style.m_unevencross << ' '; + if (Style.m_jointdip >= 0.f) + Text << "jointdip " << Style.m_jointdip << ' '; // a piece of the track made from another one stays in its track circuit and under the same overhead line for (auto const *isolated : Style.Isolated) Text << "isolated " << isolated->asName << ' '; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 09ffd4bd..9b93dc61 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -63,6 +63,11 @@ class editor_track float texslope{0.9f}; std::vector isolated; float overhead{-1.f}; + float uneven{0.f}; + float unevenup{-1.f}; + float unevenside{-1.f}; + float unevencross{-1.f}; + float jointdip{-1.f}; bool sleepers{false}; float sleeper_frequency{0.6f}; std::string sleeper_model; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index b13fa62a..8ca13b46 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -547,6 +547,30 @@ msgstr "Odstęp stuków" msgid "Quality" msgstr "Jakość" +msgid "Unevenness (mm)" +msgstr "Nierówności toru (mm)" + +msgid "Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values" +msgstr "Największa odchyłka toku szynowego na cięciwie 10 m. Limity Id-14: 6 mm przy 160 km/h, 10 przy 120, 18 przy 80, 35 przy 40.\nPrzy podanym odstępie styków szyny zapadają się też na stykach. Połączone rozjazdy mają najmniejszą ze swoich wartości" + +msgid "Up and down (mm)" +msgstr "W pionie (mm)" + +msgid "Sideways (mm)" +msgstr "W poziomie (mm)" + +msgid "Cross level (mm)" +msgstr "Przechyłka (mm)" + +msgid "Set apart from the unevenness, mm. Negative: the unevenness" +msgstr "Osobno od nierówności, mm. Ujemna: wartość nierówności" + +msgid "Dip at rail joints (mm)" +msgstr "Zapadnięcie na stykach szyn (mm)" + +msgid "Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness" +msgstr "Głębokość zapadnięcia na każdym styku szyn, co odstęp styków. Działa też przy nierównościach 0. Ujemna: 0,4 nierówności" + msgid "Damage flags" msgstr "Flagi uszkodzeń" diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp new file mode 100644 index 00000000..afea0b67 --- /dev/null +++ b/tests/unit/test_track_uneven.cpp @@ -0,0 +1,175 @@ +#include "stdafx.h" +#include "doctest.h" +#include "world/TrackUneven.h" + +#include +#include + +namespace { + +double deviation(std::vector const &Values) +{ + auto mean{0.0}; + for (auto const value : Values) + mean += value; + mean /= Values.size(); + auto sum{0.0}; + for (auto const value : Values) + sum += (value - mean) * (value - mean); + return std::sqrt(sum / Values.size()); +} + +double peak(std::vector Values) +{ + for (auto &value : Values) + value = std::abs(value); + std::sort(Values.begin(), Values.end()); + return Values[static_cast(0.999 * Values.size())]; +} + +void add_chord(std::vector &Output, std::vector const &Line, int const Half) +{ + for (int i = Half; i + Half < static_cast(Line.size()); ++i) + Output.push_back(Line[i] - 0.5 * (Line[i - Half] + Line[i + Half])); +} + +} // namespace + +TEST_CASE("uneven track: measures as the Id-14 limits for its value") +{ + // appendix 1, class C at 80 km/h: 18 mm vertically, 17 mm sideways, 16 mm of twist on 5 m, gauge +10/-8 mm, 20 mm of cross level, J = 5.3 + auto const amplitude{track_uneven::stray_per_mm * 18.0 * 1000.0}; // mm + double const gauge{1.435}; + double const step{0.5}; + int const steps{4000}; + int const base{10}; // 5 m + int const lines{36}; + double vertical{0.0}, horizontal{0.0}, twist{0.0}, wander{0.0}, level{0.0}, j{0.0}, away{0.0}; + for (int line = 0; line < lines; ++line) + { + auto const heading{glm::pi() * (line + 0.37) / lines}; + glm::dvec3 const start{7919.0 * std::sin(line * 1.7), 0.0, 6577.0 * std::cos(line * 2.3)}; + glm::dvec3 const along{std::cos(heading), 0.0, std::sin(heading)}; + glm::dvec3 const across{-along.z, 0.0, along.x}; + std::vector up[2], side[2], middle; + for (int i = 0; i < steps; ++i) + { + auto const point{start + along * (i * step)}; + for (int rail = 0; rail < 2; ++rail) + { + auto const sign{rail == 0 ? 1.0 : -1.0}; + auto const stray{track_uneven::field(point + across * (sign * 0.5 * gauge))}; + up[rail].push_back((stray.y - sign * 0.5 * track_uneven::level(point, across)) * amplitude); + side[rail].push_back(glm::dot(stray, across) * amplitude); + } + middle.push_back(track_uneven::field(point).y * amplitude); + } + std::vector verticals, horizontals, twists, gauges, levels; + for (int rail = 0; rail < 2; ++rail) + { + add_chord(verticals, up[rail], base); + add_chord(horizontals, side[rail], base); + } + for (int i = 0; i + base < steps; ++i) + twists.push_back((up[0][i + base] - up[1][i + base]) - (up[0][i] - up[1][i])); + for (int i = 0; i < steps; ++i) + { + gauges.push_back(side[0][i] - side[1][i]); + levels.push_back(up[0][i] - up[1][i]); + } + vertical += peak(verticals) / lines; + horizontal += peak(horizontals) / lines; + twist += peak(twists) / lines; + wander += peak(gauges) / lines; + level += peak(levels) / lines; + away += peak(middle) / lines; + j += (deviation(verticals) + deviation(horizontals) + deviation(twists) + 0.5 * deviation(gauges)) / 3.5 / lines; + } + CHECK(vertical == doctest::Approx(18.0).epsilon(0.1)); + CHECK(horizontal == doctest::Approx(17.0).epsilon(0.2)); + CHECK(twist == doctest::Approx(16.0).epsilon(0.2)); + CHECK(wander == doctest::Approx(10.0).epsilon(0.2)); + CHECK(level == doctest::Approx(20.0).epsilon(0.1)); + CHECK(j == doctest::Approx(5.3).epsilon(0.2)); + CHECK(away < 40.0); // the long waves go past the 15 mm off the survey marks of appendix 1a +} + +TEST_CASE("uneven track: a place strays the same every time") +{ + // no random numbers: changing these changes how every uneven track in every scenery looks + glm::dvec3 const place{1234.5, 0.0, -678.9}; + auto const stray{track_uneven::field(place)}; + CHECK(stray.x == doctest::Approx(0.309777548158).epsilon(1e-9)); + CHECK(stray.y == doctest::Approx(0.311350299745).epsilon(1e-9)); + CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); + CHECK(track_uneven::level(place, {1.0, 0.0, 0.0}) == doctest::Approx(0.53 * -0.178598957983).epsilon(1e-9)); + CHECK(track_uneven::level(place, {0.0, 0.0, 1.0}) == doctest::Approx(0.53 * -0.868291889902).epsilon(1e-9)); +} + +TEST_CASE("uneven track: tracks laid towards each other meet at one cant") +{ + // the same rail stays lower seen from either of them, and a path taken the other way sees the roll turned over + glm::dvec3 const place{812.25, 0.0, 3071.5}; + glm::dvec3 const across{0.6, 0.0, -0.8}; + CHECK(track_uneven::level(place, -across) == doctest::Approx(-track_uneven::level(place, across))); + CHECK(track_uneven::roll(place, -across, 1.435) == doctest::Approx(-track_uneven::roll(place, across, 1.435))); + // and there's no step along a curve turning through the headings + auto last{track_uneven::level(place, {1.0, 0.0, 0.0})}; + for (int step = 1; step <= 3600; ++step) + { + auto const heading{glm::two_pi() * step / 3600.0}; + auto const value{track_uneven::level(place, {std::cos(heading), 0.0, std::sin(heading)})}; + REQUIRE(std::abs(value - last) < 0.002); + last = value; + } +} + +TEST_CASE("uneven track: the field stays within its bound") +{ + auto most{0.0}; + auto highest{0.0}; + for (int i = 0; i < 20000; ++i) + { + auto const stray{track_uneven::field({i * 3.7 - 30000.0, 0.0, i * -2.9 + 41000.0})}; + most = std::max({most, std::abs(stray.x), std::abs(stray.z)}); + highest = std::max(highest, std::abs(stray.y)); + } + CHECK(most <= 1.0); + CHECK(most > 0.5); + CHECK(highest <= 1.69); + CHECK(highest > 0.5); +} + +TEST_CASE("uneven track: the value along a track meets its neighbours exactly") +{ + double const length{500.0}; + CHECK(track_uneven::along(20.0, 5.0, 7.0, 0.0, length) == doctest::Approx(5.0)); + CHECK(track_uneven::along(20.0, 5.0, 7.0, length, length) == doctest::Approx(7.0)); + CHECK(track_uneven::along(20.0, 5.0, 7.0, 0.5 * length, length) == doctest::Approx(20.0)); + // equal values stay constant + CHECK(track_uneven::along(20.0, 20.0, 20.0, 3.0, length) == doctest::Approx(20.0)); + // a short track between ends of no value hardly strays + CHECK(track_uneven::along(20.0, 0.0, 0.0, 2.5, 5.0) < 1.0); + // a switch of more than the track: the track starts at the value of the switch + CHECK(track_uneven::along(18.0, 35.0, 18.0, 0.0, length) == doctest::Approx(35.0)); + CHECK(track_uneven::along(18.0, 35.0, 18.0, track_uneven::fade, length) == doctest::Approx(18.0)); + // the short piece inside a slip goes with its switches + CHECK(track_uneven::along(18.0, 35.0, 35.0, 0.25, 0.5) == doctest::Approx(35.0).epsilon(0.01)); + // no jump anywhere + auto last{track_uneven::along(18.0, 35.0, 0.0, 0.0, length)}; + for (auto distance{0.25}; distance <= length; distance += 0.25) + { + auto const value{track_uneven::along(18.0, 35.0, 0.0, distance, length)}; + REQUIRE(std::abs(value - last) < 0.5); + last = value; + } +} + +TEST_CASE("uneven track: the rails dip around a joint") +{ + CHECK(track_uneven::dip(0.0) == doctest::Approx(1.0)); + CHECK(track_uneven::dip(1.0) == doctest::Approx(0.75).epsilon(0.01)); // the worst case for rings 2 m apart + CHECK(track_uneven::dip(track_uneven::dip_reach) == doctest::Approx(0.0)); + CHECK(track_uneven::rail_phase(26.0, 25.0) == doctest::Approx(1.0)); + CHECK(track_uneven::rail_phase(-1.0, 25.0) == doctest::Approx(24.0)); +} diff --git a/track_uneven_doc.md b/track_uneven_doc.md new file mode 100644 index 00000000..aca9ca32 --- /dev/null +++ b/track_uneven_doc.md @@ -0,0 +1,83 @@ +# Nierówności toru + +Opcjonalne atrybuty wpisu toru, przed `endtrack`: + +``` +node -1 0 tor1 track normal 100.0 1.435 0.15 25.0 20 0 flat vis +rail_screw_used1 6 1435mm/tpbps-new2 0.2 0.5 1.1 +0.0 0.2 0.0 0.0 +0.0 0.0 0.0 +0.0 0.0 0.0 +0.0 0.2 100.0 0.0 +0.0 +uneven 18 +unevencross 25 +jointdip 6 +endtrack +``` + +| atrybut | dotyczy | opis | +|-------------------|-----------------------|---------------------------------------------------------------------------------------| +| `uneven ` | `normal`, `switch` | największa odchyłka toku szynowego na cięciwie 10 m; 0-100, domyślnie 0 | +| `unevenup ` | `normal`, `switch` | składowa w pionie, osobno od `uneven` | +| `unevenside ` | `normal`, `switch` | składowa w poziomie, osobno od `uneven` | +| `unevencross `| `normal` | składowa przechyłki, osobno od `uneven`; rozjazd jej nie ma | +| `jointdip ` | `normal` | głębokość zapadnięcia na styku szyn; 0-100, domyślnie 0,4 składowej w pionie | + +Składowa niepodana przyjmuje wartość `uneven`. Na przykład sam bok to `unevenside 30`, a same zapadnięcia na stykach to `jointdip 6`. + +Wszystkie są też w edytorze, w panelu toru, w sekcji „Ride and sound”. Na drogach, rzekach i obrotnicach są ignorowane. + +## Jaką wartość wybrać + +`uneven` odpowiada kolumnie „nierówności pionowe” z instrukcji PLK Id-14, załącznik 1 (klasa C): + +| prędkość [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | +|-----------------|----|----|----|----|-----|-----|-----| +| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | + +Tor w dobrym stanie to według instrukcji połowa tej wartości (klasa A), w dostatecznym 3/4 (klasa B). + +Tor z `uneven 18` mierzy się tak (`tests/unit/test_track_uneven.cpp`), w nawiasie limit Id-14 dla 80 km/h: + +| parametr | wynik | Id-14 | +|---------------------------|---------|---------| +| nierówności pionowe | 18 mm | 18 mm | +| nierówności poziome | 19 mm | 17 mm | +| wichrowatość na bazie 5 m | 15 mm | 16 mm | +| szerokość toru | ±10 mm | +10/-8 mm | +| przechyłka | 20 mm | 20 mm | +| wskaźnik J | 6,1 | 5,3 | + +## Jak to działa + +Nierówność to pole przesunięć zależne od położenia na mapie, złożone z fal o długości 5-42 m. W pionie krótkich fal jest mniej, żeby częściej dochodziły blisko wartości szczytowej, a do tego są fale 47-83 m: na cięciwie 10 m prawie się nie liczą, pudło unosi się na nich i opada, a pantograf za nimi nadąża. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. + +Każda składowa łączy się osobno: + +- **Tor z torem:** na styku obowiązuje mniejsza z dwóch wartości, a każdy tor dochodzi do niej płynnie na 20 m. Wolny koniec toru leży w projekcie. +- **Rozjazd:** wszystkie jego drogi mają jedną wartość. Rozjazdy złączone bezpośrednio przyjmują najmniejszą ze swoich. Dołączony tor dochodzi do wartości rozjazdu, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. +- **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. +- **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. + +Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła i bujanie kabiny. + +Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. + +Pantograf podnosi się z szybkością ciśnienia, ok. 0,14 m/s, a szczelina do drutu większa niż 1 cm odcina napięcie i wybija wyłącznik szybki. Nierówność dokłada się do tego przy dużej prędkości. Na torach Bałtyku symulacja daje szczelinę 0,6 cm, gdy składowa w pionie razy prędkość w km/h wynosi 925, czyli np. 8 mm przy 120 km/h, 13 mm przy 70 km/h i 23 mm przy 40 km/h. Wartości z tabeli Id-14 są od tego większe. Sam drut też to potrafi: na Bałtyku przy `tor0290` wznosi się o ok. 1%, więc pantograf nadąża tam do ok. 46 km/h, a powyżej wyłącznik wybija także na prostym torze. + +Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². + +## Wydajność + +Tor z nierównością ma geometrię co 2 m zamiast co 10 m na prostych. Rozjazdy zachowują swój podział. + +Zmierzone (kamera w ruchu lub kabina, 1920×1080): + +| scena | bez flag | z flagami 18/35 | +|---------------------------------------|--------------------|--------------------| +| Bałtyk, 56 km torów: wierzchołki | 0,56 mln (ok. 27 MB) | 2,17 mln (ok. 104 MB) | +| Bałtyk: najdłuższa klatka w przelocie | 31,5 ms | 64 ms | +| `td.scn`, jazda 120 km/h: klatka | 2,73 ms | 2,93 ms | + +Koszt rośnie z liczbą oflagowanych torów, a najdłuższe klatki pojawiają się przy budowaniu geometrii kolejnych sekcji. Sama jazda po takim torze praktycznie nie kosztuje: ok. 0,3 µs na oś w kroku fizyki. diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index ca10bfcf..370b486b 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3642,6 +3642,7 @@ bool TDynamicObject::Update(double dt, double dt1) if( dRailLength > 0.0 ) { for( auto &axle : m_axlesounds ) { axle.distance = axle.offset; + axle.atjoints = false; } } dRailLength = MyTrack->fSoundDistance; @@ -3675,10 +3676,23 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - axle.distance += dDOMoveLen * directioninconsist; - if( axle.distance < 0 - || axle.distance > dRailLength ) { - axle.distance = clamp_circular( axle.distance, dRailLength ); + // on a track with dipping joints the clatter comes from them, elsewhere the axle counts a rail off itself + auto const railphase { Axle0.RailPhase( axle.offset - 0.5 * fAxleDist ) }; + auto crossed { false }; + if( railphase >= 0.0 ) { + // the rails under the axle, which aren't always those under the track the vehicle is on + crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * Axle0.GetTrack()->fSoundDistance; + axle.distance = railphase; + } + else { + axle.distance += dDOMoveLen * directioninconsist; + crossed = axle.distance < 0 || axle.distance > dRailLength; + if( crossed ) { + axle.distance = clamp_circular( axle.distance, dRailLength ); + } + } + axle.atjoints = ( railphase >= 0.0 ); + if( crossed ) { if( MoverParameters->Vel > 0.1 ) { // NOTE: for combined clatter sound we supply 1/100th of actual value, as the sound module converts does the opposite, converting received (typically) 0-1 values to 0-100 range auto const frequency = true == axle.clatter.is_combined() ? MoverParameters->Vel * 0.01 : 1.0; @@ -8050,6 +8064,12 @@ TDynamicObject::update_shake( double const Timedelta ) { -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway -MoverParameters->AccVert * Timedelta * Global.ShakingMultiplierUD, -MoverParameters->AccSVBased * Timedelta * 1.5 * Global.ShakingMultiplierBF); // accent acceleration/deceleration + // unevenness of the track under the outer axles + auto const ride { 0.5 * ( Axle0.UnevenBend() + Axle1.UnevenBend() ) * ( MoverParameters->V * MoverParameters->V ) }; + shakevector += glm::dvec3( + -glm::dot( ride, vLeft ) * Timedelta * 5.0 * Global.ShakingMultiplierRL, + -ride.y * Timedelta * Global.ShakingMultiplierUD, + 0.0 ); } auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 9bbb8f97..e0238c77 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -401,6 +401,7 @@ private: double distance; // distance to rail joint double offset; // axle offset from centre of the vehicle sound_source clatter; // clatter emitter + bool atjoints { false }; // distance comes from TTrack::rail_phase }; struct powertrain_sounds { diff --git a/world/Segment.cpp b/world/Segment.cpp index c370cb1c..567bb23d 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -135,6 +135,10 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N fStoop = std::atan2(Point2.y - Point1.y, fLength); // pochylenie toru prostego, żeby nie liczyć wielokrotnie fStep = ( fNewStep > 0.0 && std::isfinite( fNewStep ) ) ? fNewStep : fLength; + if( m_uneven && pOwner->eType != tt_Switch ) { + // dense enough for the waves; a switch keeps its division, the blades depend on it + fStep = std::min( fStep, 2.0 ); + } // NOTE: optionally replace this part with the commented version, after solving geometry issues with double switches if( pOwner->eType == tt_Switch && fStep * (3.0 * Global.SplineFidelity) > fLength ) { @@ -426,7 +430,54 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const */ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { + design_position( fDistance, position, rotation ); + if( false == m_uneven ) { + return; + } + // direction and slope stay as designed: the physics takes the curve radius and the grade from them + auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; + glm::dvec3 const across { -std::cos( rotation.z ), 0.0, -std::sin( rotation.z ) }; + auto const amount { pOwner->uneven_at( fDistance ) }; + rotation.x += static_cast( track_uneven::stray_per_mm * ( + amount.up * track_uneven::roll( position, across, gauge ) + + amount.cross * track_uneven::level( position, across ) / gauge ) ); + position += stray( position, fDistance ); +} + +glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { + auto const amount { pOwner->uneven_at( Distance ) }; + auto const field { track_uneven::field( Point ) }; + glm::dvec3 result { field.x * amount.side, field.y * amount.up, field.z * amount.side }; + result *= track_uneven::stray_per_mm; + result.y -= pOwner->joint_dip( Distance ); + return result; +} + +glm::dvec3 TSegment::uneven_bend( double const Distance ) const { + if( false == m_uneven ) { + return glm::dvec3 { 0.0 }; + } + // over about a bogie; FastGetTFromS extrapolates past the ends of the path + auto const reach { 1.5 }; + auto const strayed = [&]( double const At ) { + return stray( FastGetPoint( FastGetTFromS( At ) ), At ); }; + return ( strayed( Distance + reach ) - 2.0 * strayed( Distance ) + strayed( Distance - reach ) ) / ( reach * reach ); +} + +void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const { + if( false == m_uneven ) { + return; + } + // the rails settling apart turn about the middle of the track, what's outside them follows the rail + auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; + auto const across { std::clamp( glm::dot( Vertex.position - Position, Parallel ), -0.5f * gauge, 0.5f * gauge ) }; + Vertex.position.y -= static_cast( track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position += glm::vec3 { stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; +} + +void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { if( false == m_pieces.empty() ) { + // the pieces keep to their design, the unevenness is the one of the whole path auto const piece { piece_at( fDistance ) }; m_pieces[ piece ]->RaPositionGet( fDistance - m_starts[ piece ], position, rotation ); } @@ -579,6 +630,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, glm::normalize( norm ), glm::vec2 { ( jmm1 * ShapePoints[ j ].texture.x + m1 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv1 } ); roll_vertex( Output.back(), pos1, parallel1, s - step ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); } // jeśli jest wskaźnik do tablicy if (p && *p && !j) // to dla pierwszego punktu @@ -598,6 +650,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, glm::normalize( norm ), glm::vec2 { ( jmm2 * ShapePoints[ j ].texture.x + m2 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv2 } ); roll_vertex( Output.back(), pos2, parallel2, s ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } // jeśli jest wskaźnik do tablicy // to dla pierwszego punktu @@ -621,6 +674,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv1 } ); roll_vertex( Output.back(), pos1, parallel1, s - step ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); pt = parallel2 * ( jmm2 * ( ShapePoints[ j ].position.x - fOffsetX.first ) + m2 * ( ShapePoints[ j ].position.x - fOffsetX.second ) ) + pos2; pt.y += ShapePoints[ j ].position.y; @@ -632,6 +686,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv2 } ); roll_vertex( Output.back(), pos2, parallel2, s ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } } } @@ -699,7 +754,8 @@ bool TSegment::RenderLoftSteps( gfx::vertex_array &Output, glm::dvec3 const &Ori auto normal { ( rest * from.normal.x + Share * to.normal.x ) * Parallel }; normal.y += rest * from.normal.y + Share * to.normal.y; Output.emplace_back( point, glm::normalize( normal ), glm::vec2 { rest * from.texture.x + Share * to.texture.x, V } ); - roll_vertex( Output.back(), Position, Parallel, Share * fLength ); }; + roll_vertex( Output.back(), Position, Parallel, Share * fLength ); + settle_vertex( Output.back(), Origin, Position, Parallel, Share * fLength ); }; glm::vec3 pos1, pos2, parallel1, parallel2; station( 0, pos1, parallel1 ); @@ -1044,6 +1100,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } for( auto &vertex : Ring ) { roll_vertex( vertex, position, parallel, Distance ); + settle_vertex( vertex, Origin, position, parallel, Distance ); } }; diff --git a/world/Segment.h b/world/Segment.h index f2d4aab1..ec87f98a 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -101,6 +101,7 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach + bool m_uneven { false }; // see TrackUneven.h double fLength { -1.0 }; // długość policzona std::vector fTsBuffer; // wartości parametru krzywej t dla równych odcinków s std::vector fTsSlope; // nachylenie dt/du (= dt/ds * fStep) w tych samych punktach @@ -131,6 +132,12 @@ class TSegment // the vertex of a profile lofted at the point and across the path there, turned from the baked roll to the one of the path at the distance void roll_vertex( gfx::basic_vertex &Vertex, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const; + // moves a lofted vertex with the unevenness of the track + void settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const; + // displacement of a point by the unevenness, at a distance along the path + glm::dvec3 stray( glm::dvec3 const &Point, double const Distance ) const; + void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; + public: bool bCurve = false; @@ -179,6 +186,13 @@ public: /// Calculated position /// Calculated rotation void RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const; + // call before Init, it sets the density of the geometry + void + set_uneven( bool const Uneven ) { + m_uneven = Uneven; } + // second derivative of the displacement along the path; times speed squared: acceleration of an axle + glm::dvec3 + uneven_bend( double const Distance ) const; glm::dvec3 FastGetPoint(double const t) const; inline glm::dvec3 diff --git a/world/Track.cpp b/world/Track.cpp index cc54067d..180063c5 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -85,6 +85,7 @@ void init_switch_piece( TSegment &Segment, segment_data const &Piece, bool const TTrack::profiles_array TTrack::m_profiles; TTrack::profiles_map TTrack::m_profilesmap; +int TTrack::s_layoutstamp { 0 }; TSwitchExtension::TSwitchExtension(TTrack *owner, int const what) { // na początku wszystko puste @@ -367,6 +368,7 @@ void TTrack::ConnectPrevPrev(TTrack *pTrack, int typ) { //łączenie torów - Point1 własny do Point1 cudzego if (pTrack) { //(pTrack) może być zwrotnicą, a (this) tylko zwykłym odcinkiem + ++s_layoutstamp; trPrev = pTrack; iPrevDirection = pTrack->eType == tt_Switch ? 0 : typ & 2; pTrack->trPrev = this; @@ -377,6 +379,7 @@ void TTrack::ConnectPrevNext(TTrack *pTrack, int typ) { //łaczenie torów - Point1 własny do Point2 cudzego if (pTrack) { + ++s_layoutstamp; trPrev = pTrack; iPrevDirection = typ | 1; // 1:zwykły lub pierwszy zwrotnicy, 3:drugi zwrotnicy pTrack->trNext = this; @@ -396,6 +399,7 @@ void TTrack::ConnectNextPrev(TTrack *pTrack, int typ) { //łaczenie torów - Point2 własny do Point1 cudzego if (pTrack) { + ++s_layoutstamp; trNext = pTrack; iNextDirection = pTrack->eType == tt_Switch ? 0 : typ & 2; pTrack->trPrev = this; @@ -415,6 +419,7 @@ void TTrack::ConnectNextNext(TTrack *pTrack, int typ) { //łaczenie torów - Point2 własny do Point2 cudzego if (pTrack) { + ++s_layoutstamp; trNext = pTrack; iNextDirection = typ | 1; // 1:zwykły lub pierwszy zwrotnicy, 3:drugi zwrotnicy pTrack->trNext = this; @@ -731,6 +736,27 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) ErrorLog( "Bad track: " + str + " at end " + std::to_string( end ) + " of track \"" + m_name + "\" which has no such end" ); } } + else if( str == "uneven" ) { + // uneven : see TrackUneven.h + auto const uneven { parser->getToken() }; + if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { + m_uneven = std::clamp( uneven, 0.f, track_uneven::most ); + } + } + else if( str == "unevenup" || str == "unevenside" || str == "unevencross" ) { + // the components of uneven set apart, mm + auto const value { parser->getToken() }; + if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { + ( str == "unevenup" ? m_unevenup : str == "unevenside" ? m_unevenside : m_unevencross ) = std::clamp( value, 0.f, track_uneven::most ); + } + } + else if( str == "jointdip" ) { + // jointdip : depth of the dip at rail joints, with or without uneven + auto const jointdip { parser->getToken() }; + if( eType == tt_Normal && iCategoryFlag == 1 ) { + m_jointdip = std::clamp( jointdip, 0.f, track_uneven::most ); + } + } else if( str == "friction" ) { // memory cell holding friction value modifiers m_friction.first = parser->getToken(); @@ -807,6 +833,159 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } +track_uneven::amounts TTrack::uneven_declared() const { + + return { + m_unevenup >= 0.f ? m_unevenup : m_uneven, + m_unevenside >= 0.f ? m_unevenside : m_uneven, + eType != tt_Normal ? 0.f : m_unevencross >= 0.f ? m_unevencross : m_uneven }; +} + +bool TTrack::is_uneven() const { + + auto const declared { uneven_declared() }; + return declared.up > 0.0 || declared.side > 0.0 || declared.cross > 0.0 || joint_depth() > 0.0; +} + +track_uneven::amounts TTrack::uneven_at( double const Distance ) const { + + if( eType == tt_Switch ) { + return switch_uneven(); + } + auto const own { uneven_declared() }; + auto const atend = [&]( TTrack const *Other ) -> track_uneven::amounts { + if( Other == nullptr ) { + return {}; + } + if( Other->eType == tt_Switch ) { + return Other->switch_uneven(); + } + auto const theirs { Other->uneven_declared() }; + return { std::min( own.up, theirs.up ), std::min( own.side, theirs.side ), std::min( own.cross, theirs.cross ) }; }; + auto const start { atend( trPrev ) }; + auto const end { atend( trNext ) }; + return { + track_uneven::along( own.up, start.up, end.up, Distance, Length() ), + track_uneven::along( own.side, start.side, end.side, Distance, Length() ), + track_uneven::along( own.cross, start.cross, end.cross, Distance, Length() ) }; +} + +track_uneven::amounts TTrack::switch_uneven() const { + + if( m_switchstray.stamp == s_layoutstamp ) { + return m_switchstray.value; + } + std::vector group { this }; + auto least { uneven_declared() }; + for( std::size_t index = 0; index < group.size(); ++index ) { + auto const &extension { *group[ index ]->SwitchExtension }; + for( auto const *other : { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] } ) { + if( other == nullptr ) { + continue; + } + auto const theirs { other->uneven_declared() }; + if( other->eType == tt_Switch && other->SwitchExtension ) { + least.up = std::min( least.up, theirs.up ); + least.side = std::min( least.side, theirs.side ); + if( std::find( group.begin(), group.end(), other ) == group.end() ) { + group.push_back( other ); + } + } + else { + // a track with none of a component keeps to its design in it, any other comes to the value of the switch + if( theirs.up <= 0.0 ) { least.up = 0.0; } + if( theirs.side <= 0.0 ) { least.side = 0.0; } + } + } + } + for( auto const *track : group ) { + track->m_switchstray = { least, s_layoutstamp }; + } + return least; +} + +double TTrack::joint_depth() const { + + if( eType != tt_Normal ) { + return 0.0; + } + return ( m_jointdip >= 0.f ? m_jointdip : track_uneven::dip_share * uneven_declared().up ); +} + +double TTrack::joint_dip( double const Distance ) const { + + auto const phase { rail_phase( Distance ) }; + if( phase < 0.0 ) { + return 0.0; + } + auto const fromjoint { std::min( phase, fSoundDistance - phase ) }; + if( fromjoint >= track_uneven::dip_reach ) { + return 0.0; + } + auto const depth { joint_depth() }; + auto const atend = [&]( TTrack const *Other ) { + return ( Other == nullptr || Other->fSoundDistance != fSoundDistance ? 0.0 : std::min( depth, Other->joint_depth() ) ); }; + return 0.001 * track_uneven::dip( fromjoint ) * track_uneven::along( depth, atend( trPrev ), atend( trNext ), Distance, Length() ); +} + +double TTrack::rail_phase( double const Distance ) const { + + if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { + return -1.0; + } + if( m_railrun.stamp != s_layoutstamp ) { + settle_rail_run(); + } + return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); +} + +void TTrack::settle_rail_run() const { + + // the next track of the run past an end of a track, and which of its ends is joined there + auto const onward = []( TTrack const &Track, int const End, int &Joined ) -> TTrack const * { + auto const *other { End == 0 ? Track.trPrev : Track.trNext }; + if( other == nullptr || other == &Track || other->joint_depth() <= 0.0 || other->fSoundDistance != Track.fSoundDistance ) { + return nullptr; + } + Joined = ( End == 0 ? Track.iPrevDirection : Track.iNextDirection ) & 1; + return other; }; + // walks from this track to an end of the run + auto const walk = [&]( int &End ) { + auto const *track { this }; + int joined { 0 }; + for( int step = 0; step < 1000000; ++step ) { + auto const *next { onward( *track, End, joined ) }; + if( next == nullptr || next == this ) { + break; + } + track = next; + End = 1 - joined; + } + return track; }; + auto const place = []( TTrack const &Track, int const End ) { + auto const point { End == 0 ? Track.Segment->FastGetPoint_0() : Track.Segment->FastGetPoint_1() }; + return std::make_pair( point.x, point.z ); }; + + // start at the end with the lesser coordinates, to get the same joints from any track of the run + int end { 0 }; + int otherend { 1 }; + auto const *track { walk( end ) }; + auto const *other { walk( otherend ) }; + if( place( *other, otherend ) < place( *track, end ) ) { + track = other; + end = otherend; + } + auto along { 0.0 }; + while( track != nullptr && track->m_railrun.stamp != s_layoutstamp ) { + auto const length { track->Length() }; + track->m_railrun = { end == 0 ? along : along + length, end == 0 ? 1.0 : -1.0, s_layoutstamp }; + along += length; + int joined { 0 }; + track = onward( *track, 1 - end, joined ); + end = joined; + } +} + void TTrack::update_location() { location( ( @@ -825,6 +1004,14 @@ void TTrack::init_segments( bool const Initial ) { fTexHeight1 -= fTexHeightOffset; fTexHeightOffset = 0.f; iTrapezoid = 0; + ++s_layoutstamp; + if( eType == tt_Switch && SwitchExtension ) { + SwitchExtension->Segments[ 0 ]->set_uneven( is_uneven() ); + SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven() ); + } + else if( Segment ) { + Segment->set_uneven( is_uneven() ); + } switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym @@ -1510,6 +1697,8 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const // wypełnianie tablic VBO void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { + ++s_layoutstamp; + track_rails::forget(); auto const erase = [ this ]( gfx::geometry_handle const &Chunk ) { if( SwitchExtension @@ -2841,6 +3030,21 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } + if( m_uneven > 0.f ) { + Output << "uneven " << m_uneven << ' '; + } + if( m_unevenup >= 0.f ) { + Output << "unevenup " << m_unevenup << ' '; + } + if( m_unevenside >= 0.f ) { + Output << "unevenside " << m_unevenside << ' '; + } + if( m_unevencross >= 0.f ) { + Output << "unevencross " << m_unevencross << ' '; + } + if( m_jointdip >= 0.f ) { + Output << "jointdip " << m_jointdip << ' '; + } if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } diff --git a/world/Track.h b/world/Track.h index e137c469..e14970d3 100644 --- a/world/Track.h +++ b/world/Track.h @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "world/RailLayout.h" #include "world/TrackSleepers.h" #include "world/TrackBlend.h" +#include "world/TrackUneven.h" #include "model/material.h" #include "scene/scenenode.h" #include "utilities/Names.h" @@ -286,6 +287,12 @@ public: TEnvironmentType eEnvironment = e_flat; // dźwięk i oświetlenie int iAction = 0; // czy modyfikowany eventami (specjalna obsługa przy skanowaniu) float fOverhead = -1.0; // można normalnie pobierać prąd (0 dla jazdy bezprądowej po danym odcinku, >0-z opuszczonym i ograniczeniem prędkości) + float m_uneven { 0.f }; // mm, see TrackUneven.h; plain tracks and switches + // the components set apart from m_uneven, mm; negative: m_uneven. Cross level on plain tracks only + float m_unevenup { -1.f }; + float m_unevenside { -1.f }; + float m_unevencross { -1.f }; + float m_jointdip { -1.f }; // depth of the dip at rail joints, mm; negative: from m_uneven; plain tracks only private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) public: @@ -406,6 +413,15 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; + // the unevenness the track declares + track_uneven::amounts uneven_declared() const; + bool is_uneven() const; + // the unevenness at a distance along the track, meeting the tracks joined at its ends + track_uneven::amounts uneven_at( double const Distance ) const; + double joint_depth() const; // mm + double joint_dip( double const Distance ) const; // m + // distance from the rail joint behind; negative: the track has no joints that dip + double rail_phase( double const Distance ) const; void ConnectionsLog(); bool DoubleSlip() const; static void fetch_default_profiles(); @@ -439,6 +455,22 @@ private: void profile_rolls( float &Roll1, float &Roll2 ) const; void share_joints(); void create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previous, TTrack const *Next ); + // rail joints run on across joined tracks, however the line is cut into them, so the clatter keeps its rhythm + struct rail_run { + double origin { 0.0 }; // position along the run of Point1 + double way { 1.0 }; // direction of the path along the run + int stamp { -1 }; + }; + void settle_rail_run() const; + mutable rail_run m_railrun; + // one value along all paths of a switch: the lowest of the switches joined, 0 next to a track with none + track_uneven::amounts switch_uneven() const; + struct switch_stray { + track_uneven::amounts value; + int stamp { -1 }; + }; + mutable switch_stray m_switchstray; + static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above void create_road_profile( gfx::vertex_array &Output, bool const Forcetransition = false ); void create_road_side_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const &Road, bool const Forcetransition = false ); /// diff --git a/world/TrackUneven.h b/world/TrackUneven.h new file mode 100644 index 00000000..9031c6ca --- /dev/null +++ b/world/TrackUneven.h @@ -0,0 +1,129 @@ +/* +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 + +// Track unevenness: a displacement field over the plan of the scenery, so everything at one place moves together: joined tracks, the +// paths of a switch, sleepers, axles. Waves 5-42 m long, shares after the German track spectrum with the long waves cut down; up and down to 83 m. +// The "uneven" value is the peak deviation of a rail on a 10 m chord, as in PLK Id-14 appendix 1; see test_track_uneven.cpp. +namespace track_uneven { + +inline constexpr float most { 100.f }; // largest value of the attributes, mm +inline constexpr double stray_per_mm { 0.00145 }; // field amplitude per mm of "uneven", m +inline constexpr double cross_share { 0.53 }; // share of the field amplitude the rails settle apart by +inline constexpr double fade { 20.0 }; // length over which a track goes from the value at its end to its own, m +inline constexpr double dip_share { 0.4 }; // joint dip depth per mm of "uneven", mm +inline constexpr double dip_reach { 3.0 }; // reach of the dip on each side of a joint, m + +// the value of each component of the unevenness, mm +struct amounts { + double up { 0.0 }; + double side { 0.0 }; + double cross { 0.0 }; +}; + +struct wave { + double x; + double z; + double phase; + double share; +}; + +template +using wave_set = std::array; + +// the waves run in all directions of the plan, so no direction of a track is favoured +template +wave_set make_waves( double const Turn, std::array const &Lengths, std::array const &Shares ) { + + auto const golden { glm::pi() * ( 3.0 - std::sqrt( 5.0 ) ) }; + wave_set result {}; + for( std::size_t i = 0; i < Count; ++i ) { + auto const number { glm::two_pi() / Lengths[ i ] }; + auto const heading { Turn + golden * i }; + result[ i ] = { number * std::cos( heading ), number * std::sin( heading ), golden * ( i + 1 ) * ( 1.0 + 3.0 * Turn ), Shares[ i ] }; + } + return result; +} + +inline constexpr std::array wave_lengths { 5.3, 7.1, 9.7, 13.1, 17.9, 23.9, 31.7, 41.9 }; +inline constexpr std::array wave_shares { 0.0721, 0.0997, 0.1344, 0.1703, 0.1549, 0.1386, 0.1226, 0.1074 }; + +// fewer short ones than sideways: the rails come near the limit in the vertical more often, with the peak still at it. +// The long ones hardly count on the 10 m chord: the body rises and falls on them, slowly enough for a pantograph to keep up +inline wave_set<8> const up_waves { make_waves<8>( 0.0, { 8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3 }, { 0.14, 0.19, 0.22, 0.22, 0.23, 0.23, 0.23, 0.23 } ) }; +inline wave_set<8> const x_waves { make_waves( 1.1, wave_lengths, wave_shares ) }; +inline wave_set<8> const z_waves { make_waves( 2.3, wave_lengths, wave_shares ) }; +// the tilt along each axis of the plan. Long waves only: with the short ones the cross level reaching its limit would break the limit of twist +inline wave_set<3> const tilt_x_waves { make_waves<3>( 0.7, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; +inline wave_set<3> const tilt_z_waves { make_waves<3>( 2.9, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; + +template +double sum( wave_set const &Waves, glm::dvec3 const &Point ) { + + auto total { 0.0 }; + for( auto const &wave : Waves ) { + total += wave.share * std::sin( wave.x * Point.x + wave.z * Point.z + wave.phase ); + } + return total; +} + +// displacement at the point for an amplitude of 1, sideways within +-1, up and down within +-1.69 +inline glm::dvec3 field( glm::dvec3 const &Point ) { + + return { sum( x_waves, Point ), sum( up_waves, Point ), sum( z_waves, Point ) }; +} + +// how much lower the rail towards Across lies by settling apart, for an amplitude of 1. A tilt of the plan, so tracks laid towards each +// other meet at one cant. Follows the middle of a path, so plain tracks only +inline double level( glm::dvec3 const &Point, glm::dvec3 const &Across ) { + + return cross_share * ( Across.x * sum( tilt_x_waves, Point ) + Across.z * sum( tilt_z_waves, Point ) ); +} + +// roll made by the field for an amplitude of 1; positive lowers the rail towards Across, as the roll of a path +inline double roll( glm::dvec3 const &Point, glm::dvec3 const &Across, double const Gauge ) { + + return ( sum( up_waves, Point - Across * ( 0.5 * Gauge ) ) - sum( up_waves, Point + Across * ( 0.5 * Gauge ) ) ) / Gauge; +} + +// value at a distance along a track: Full inside, exactly Atstart and Atend at its ends where it meets the neighbours +inline double along( double const Full, double const Atstart, double const Atend, double const Distance, double const Length ) { + + auto const smooth = []( double const Share ) { + auto const share { std::clamp( Share, 0.0, 1.0 ) }; + return share * share * ( 3.0 - 2.0 * share ); }; + auto const ends { Length > 0.0 ? Atstart + ( Atend - Atstart ) * smooth( Distance / Length ) : Atstart }; + return ends + ( Full - ends ) * std::min( smooth( Distance / fade ), smooth( ( Length - Distance ) / fade ) ); +} + +// distance from the joint behind the point +inline double rail_phase( double const Along, double const Rail ) { + + auto const phase { std::fmod( Along, Rail ) }; + return phase < 0.0 ? phase + Rail : phase; +} + +// share of the dip depth at a distance from the joint +inline double dip( double const Fromjoint ) { + + if( Fromjoint >= dip_reach ) { + return 0.0; + } + auto const height { std::cos( glm::half_pi() * Fromjoint / dip_reach ) }; + return height * height; +} + +} // namespace track_uneven diff --git a/world/TrkFoll.cpp b/world/TrkFoll.cpp index 0e7ab29a..e2f302f2 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -386,6 +386,11 @@ bool TTrackFollower::Move(double fDistance, bool bPrimary) } }; +double TTrackFollower::RailPhase( double const Offset ) const { + + return pCurrentTrack != nullptr ? pCurrentTrack->rail_phase( fCurrentDistance + fDirection * Offset ) : -1.0; +} + bool TTrackFollower::ComputatePosition() { // ustalenie współrzędnych XYZ if (pCurrentSegment) // o ile jest tor diff --git a/world/TrkFoll.h b/world/TrkFoll.h index 079aaae4..8abdab2a 100644 --- a/world/TrkFoll.h +++ b/world/TrkFoll.h @@ -39,6 +39,10 @@ public: // inline double GetLength(vector3 p1, vector3 cp1, vector3 cp2, vector3 p2) //{ return pCurrentSegment->ComputeLength(p1,cp1,cp2,p2); }; // inline double GetRadius(double L, double d); //McZapkie-150503 + inline glm::dvec3 UnevenBend() const { + return pCurrentSegment ? pCurrentSegment->uneven_bend( fCurrentDistance ) : glm::dvec3 { 0.0 }; }; + // TTrack::rail_phase of the point Offset ahead of the axle along the vehicle + double RailPhase( double const Offset ) const; bool Init(TTrack *pTrack, TDynamicObject *NewOwner, double fDir); void Reset(); void Render(float fNr);