From e5266e1e6b025e5b7c53c8e02083b0e747acaebf Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 15:47:10 +0100 Subject: [PATCH 1/4] feat(track): track unevenness declared in the track entry Usage, optional attributes of a track entry, before endtrack: uneven peak deviation of a rail on a 10 m chord, 0-100; normal and switch unevenup up and down alone, instead of uneven; normal and switch unevenside sideways alone, instead of uneven; normal and switch unevencross cross level alone, instead of uneven; normal only jointdip depth of the dip at rail joints, every sound distance; normal only, default 0.4 of the up and down component Values of uneven follow PLK Id-14 appendix 1: 6 mm is the limit at 160 km/h, 10 at 120, 18 at 80, 35 at 40; half of it for a track in a good state. Example: uneven 18 unevencross 25 jointdip 6 endtrack The same fields are in the editor, track panel, section Ride and sound. See track_uneven_doc.md. The unevenness is a displacement field over the plan of the scenery, with no random numbers: one place strays the same every time, and joined tracks, the paths of a switch, sleepermodel and axles at one place move together. Tracks meet at a joint at the lower of their values. Joined switches take the lowest of theirs, the tracks joined to them come to it, and a track with none of a component keeps the switch to its design in it. The clatter comes from the dipping joints. Curve radius and grade for the physics stay as designed. Tracks with it get geometry every 2 m on straights: 4x the vertices on Baltyk with all tracks flagged, 0.2 ms more per frame on td.scn. --- application/editormodetrack.cpp | 30 ++++- editor/editorTrack.cpp | 20 +++ editor/editorTrack.hpp | 5 + lang/editor_pl.po | 24 ++++ tests/unit/test_track_uneven.cpp | 152 ++++++++++++++++++++++ track_uneven_doc.md | 81 ++++++++++++ vehicle/DynObj.cpp | 23 +++- vehicle/DynObj.h | 1 + world/Segment.cpp | 54 ++++++++ world/Segment.h | 14 +++ world/Track.cpp | 208 +++++++++++++++++++++++++++++++ world/Track.h | 32 +++++ world/TrackUneven.h | 125 +++++++++++++++++++ world/TrkFoll.cpp | 5 + world/TrkFoll.h | 4 + 15 files changed, 774 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_track_uneven.cpp create mode 100644 track_uneven_doc.md create mode 100644 world/TrackUneven.h diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..5b048396 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1159,7 +1159,35 @@ void editor_mode::render_path_parameters(TTrack &Track) finish_edit(rebuild::none); ImGui::InputFloat(STR_C("Sound distance"), &track->fSoundDistance, 0.0f, 0.0f, "%.1f"); item_tooltip("Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default"); - finish_edit(rebuild::none); + finish_edit(rebuild::parameters); + if ((track->eType == tt_Normal || track->eType == tt_Switch) && (track->iCategoryFlag & 15) == 1) + { + if (ImGui::InputFloat(STR_C("Unevenness (mm)"), &track->m_uneven, 0.0f, 0.0f, "%.1f")) + track->m_uneven = std::clamp(track->m_uneven, 0.f, track_uneven::most); + item_tooltip("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"); + finish_edit(rebuild::parameters); + struct component + { + char const *label; + float *value; + }; + for (auto const &part : {component{"Up and down (mm)", &track->m_unevenup}, component{"Sideways (mm)", &track->m_unevenside}, component{"Cross level (mm)", &track->m_unevencross}}) + { + if (part.value == &track->m_unevencross && track->eType != tt_Normal) + continue; + if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) + *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); + item_tooltip("Set apart from the unevenness, mm. Negative: the unevenness"); + finish_edit(rebuild::parameters); + } + } + if (track->eType == tt_Normal && (track->iCategoryFlag & 15) == 1) + { + if (ImGui::InputFloat(STR_C("Dip at rail joints (mm)"), &track->m_jointdip, 0.0f, 0.0f, "%.1f")) + track->m_jointdip = track->m_jointdip < 0.f ? -1.f : std::min(track->m_jointdip, track_uneven::most); + item_tooltip("Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness"); + finish_edit(rebuild::parameters); + } ImGui::InputInt(STR_C("Quality"), &track->iQualityFlag, 0); item_tooltip("Quality of the track, 20 is the nominal one; lower values give a rougher ride"); finish_edit(rebuild::none); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 418d908f..cbe7c26b 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -152,6 +152,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; @@ -250,6 +255,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; @@ -2350,6 +2360,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 43d42762..b6137bd8 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -55,6 +55,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 58b30109..a9c61b83 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -861,6 +861,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..6333b6ab --- /dev/null +++ b/tests/unit/test_track_uneven.cpp @@ -0,0 +1,152 @@ +#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)) * 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 < 25.0); // appendix 1a: 15 mm off the survey marks +} + +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.295557487607).epsilon(1e-9)); + CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); + CHECK(track_uneven::level(place) == doctest::Approx(0.6 * -0.178598957983).epsilon(1e-9)); +} + +TEST_CASE("uneven track: the field stays within its bound") +{ + auto most{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.y), std::abs(stray.z)}); + } + CHECK(most <= 1.0); + CHECK(most > 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..8cb443ac --- /dev/null +++ b/track_uneven_doc.md @@ -0,0 +1,81 @@ +# 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 | 17 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. 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. 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. + +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 32ba307f..83354288 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3627,6 +3627,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; @@ -3660,9 +3661,19 @@ 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 ) { + // 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 ) { + crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * dRailLength; + axle.distance = railphase; + } + else { + axle.distance += dDOMoveLen * directioninconsist; + crossed = axle.distance < 0 || axle.distance > dRailLength; + } + axle.atjoints = ( railphase >= 0.0 ); + if( crossed ) { axle.distance = clamp_circular( axle.distance, dRailLength ); 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 @@ -8035,6 +8046,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 36335a32..1bc50b1d 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -402,6 +402,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 69886f42..ad817b5e 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -118,6 +118,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; + 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 ) { @@ -366,6 +370,52 @@ 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 ) / 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 } ) * 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 (bCurve) { // można by wprowadzić uproszczony wzór dla okręgów płaskich auto const t = FastGetTFromS(fDistance); // aproksymacja dystansu na krzywej Beziera na parametr (t) @@ -500,6 +550,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ( jmm1 * ShapePoints[ j ].texture.x + m1 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv1 } ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); } // jeśli jest wskaźnik do tablicy if (p && *p && !j) // to dla pierwszego punktu @@ -518,6 +569,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ( jmm2 * ShapePoints[ j ].texture.x + m2 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv2 } ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } // jeśli jest wskaźnik do tablicy // to dla pierwszego punktu @@ -540,6 +592,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv1 } ); + 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; @@ -550,6 +603,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv2 } ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } } } diff --git a/world/Segment.h b/world/Segment.h index a3c83ade..492cab94 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -47,6 +47,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 @@ -67,6 +68,12 @@ class TSegment glm::dvec3 RaInterpolate(double const t) const; glm::dvec3 RaInterpolate0(double const t) 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; @@ -106,6 +113,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 f1a29798..ff5a378a 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -47,6 +47,7 @@ TIsolated *TIsolated::pRoot = nullptr; 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 @@ -325,6 +326,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; @@ -335,6 +337,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; @@ -354,6 +357,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; @@ -373,6 +377,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; @@ -633,6 +638,27 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_profile1 = fetch_track_rail_profile( railprofile ); } } + 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(); @@ -674,6 +700,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) *parser >> token; str = token; } + // the segments were made before the attributes were read, and unevenness changes them + if( is_uneven() ) { + init_segments( false ); + } // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) { @@ -687,6 +717,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( ( @@ -705,6 +888,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 @@ -1351,6 +1542,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; + Geometry1.clear(); Geometry2.clear(); if( SwitchExtension ) { @@ -2445,6 +2638,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 72a6b7ff..ac6d1425 100644 --- a/world/Track.h +++ b/world/Track.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "world/Segment.h" +#include "world/TrackUneven.h" #include "model/material.h" #include "scene/scenenode.h" #include "utilities/Names.h" @@ -238,6 +239,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: @@ -339,6 +346,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(); @@ -370,6 +386,22 @@ private: void create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); void create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); 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..7486cea2 --- /dev/null +++ b/world/TrackUneven.h @@ -0,0 +1,125 @@ +/* +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. +// 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.6 }; // 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 }; + +inline wave_set<8> const up_waves { make_waves( 0.0, wave_lengths, wave_shares ) }; +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 ) }; +// long waves only: with the short ones the cross level reaching its limit would break the limit of twist +inline wave_set<3> const cross_waves { make_waves<3>( 0.7, { 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, each axis within +-1 +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 right rail lies by settling apart, for an amplitude of 1. Follows the middle of a path, so plain tracks only +inline double level( glm::dvec3 const &Point ) { + + return cross_share * sum( cross_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 c437553d..035e0bd9 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -267,6 +267,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 bba97681..62f386a9 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); From b2dcb8a42e419e60ce689462c4a10a9004bd48b0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:21:28 +0100 Subject: [PATCH 2/4] fix(vehicle): wheel clatter on an uneven track doesn't loop into a buzz The phase of the rails was wrapped to the rail length of the track the vehicle is on, while an axle can be on a track with longer rails. From then on every frame looked like a joint crossed. --- vehicle/DynObj.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 83354288..2c56cb7c 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3665,16 +3665,19 @@ bool TDynamicObject::Update(double dt, double dt1) auto const railphase { Axle0.RailPhase( axle.offset - 0.5 * fAxleDist ) }; auto crossed { false }; if( railphase >= 0.0 ) { - crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * dRailLength; + // 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 ) { - axle.distance = clamp_circular( axle.distance, dRailLength ); 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; From e74bbf951f87e399b95a824dc70bdefa587c97e0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:23:19 +0100 Subject: [PATCH 3/4] fix(track): the cross level of an uneven track goes by the place, not by how it was laid Tracks laid towards each other settled to opposite sides: a step of cant of twice the value at their joint, in the geometry and under the axles. It's now a tilt of the plan, which reads the same from either track. --- tests/unit/test_track_uneven.cpp | 23 +++++++++++++++++++++-- track_uneven_doc.md | 2 +- world/Segment.cpp | 4 ++-- world/TrackUneven.h | 14 ++++++++------ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 6333b6ab..195a2d72 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -59,7 +59,7 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") { 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)) * amplitude); + 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); @@ -102,7 +102,26 @@ TEST_CASE("uneven track: a place strays the same every time") CHECK(stray.x == doctest::Approx(0.309777548158).epsilon(1e-9)); CHECK(stray.y == doctest::Approx(-0.295557487607).epsilon(1e-9)); CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); - CHECK(track_uneven::level(place) == doctest::Approx(0.6 * -0.178598957983).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") diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 8cb443ac..ebced90e 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -58,7 +58,7 @@ 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. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. +- **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. diff --git a/world/Segment.cpp b/world/Segment.cpp index ad817b5e..84a0c22b 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -380,7 +380,7 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: 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 ) / gauge ) ); + + amount.cross * track_uneven::level( position, across ) / gauge ) ); position += stray( position, fDistance ); } @@ -411,7 +411,7 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi // 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 } ) * across / 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 ) }; } diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 7486cea2..5974f42d 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -22,7 +22,7 @@ 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.6 }; // share of the field amplitude the rails settle apart by +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 @@ -64,8 +64,9 @@ inline constexpr std::array wave_shares { 0.0721, 0.0997, 0.1344, 0.1 inline wave_set<8> const up_waves { make_waves( 0.0, wave_lengths, wave_shares ) }; 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 ) }; -// long waves only: with the short ones the cross level reaching its limit would break the limit of twist -inline wave_set<3> const cross_waves { make_waves<3>( 0.7, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; +// 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 ) { @@ -83,10 +84,11 @@ 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 right rail lies by settling apart, for an amplitude of 1. Follows the middle of a path, so plain tracks only -inline double level( glm::dvec3 const &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 * sum( cross_waves, Point ); + 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 From 6aed9ec5bd38d942a400a7085f044a28602b3c2a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:25:16 +0100 Subject: [PATCH 4/4] feat(track): vertical unevenness comes near its limit more often, with longer buckles too Fewer short waves: at uneven 18 the humps over 12.6 mm on the 10 m chord go from 28 to 38 per km, with the peak still at 18 mm. Waves of 47-83 m join them. They hardly count on that chord, the body rises and falls on them. The documentation tells how far unevenness goes before a pantograph can't keep up. A wire rising fast enough trips the main breaker on a straight track too: it does at tor0290 on Baltyk, over about 46 km/h. --- tests/unit/test_track_uneven.cpp | 10 +++++++--- track_uneven_doc.md | 6 ++++-- world/TrackUneven.h | 8 +++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 195a2d72..afea0b67 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -91,7 +91,7 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") 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 < 25.0); // appendix 1a: 15 mm off the survey marks + 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") @@ -100,7 +100,7 @@ TEST_CASE("uneven track: a place strays the same every time") 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.295557487607).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)); @@ -127,13 +127,17 @@ TEST_CASE("uneven track: tracks laid towards each other meet at one cant") 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.y), std::abs(stray.z)}); + 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") diff --git a/track_uneven_doc.md b/track_uneven_doc.md index ebced90e..aca9ca32 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -44,14 +44,14 @@ Tor z `uneven 18` mierzy się tak (`tests/unit/test_track_uneven.cpp`), w nawias |---------------------------|---------|---------| | nierówności pionowe | 18 mm | 18 mm | | nierówności poziome | 19 mm | 17 mm | -| wichrowatość na bazie 5 m | 17 mm | 16 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. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. +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: @@ -64,6 +64,8 @@ Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła 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ść diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 5974f42d..9031c6ca 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -16,7 +16,7 @@ http://mozilla.org/MPL/2.0/. #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. +// 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 { @@ -61,7 +61,9 @@ wave_set make_waves( double const Turn, std::array const & 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 }; -inline wave_set<8> const up_waves { make_waves( 0.0, wave_lengths, wave_shares ) }; +// 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 @@ -78,7 +80,7 @@ double sum( wave_set const &Waves, glm::dvec3 const &Point ) { return total; } -// displacement at the point for an amplitude of 1, each axis within +-1 +// 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 ) };