From 371de52342a501ceca7df43ddf2abfde06301cd3 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 19:02:55 +0100 Subject: [PATCH] fix(track): what the review of unevenness found in the engine - A path of no length gave a joint with a vector across of NaN to the path joined to it, on any scenery, with unevenness or not. It now has no vector across, and so makes no joint. - Axles no longer stray along the track. It moved vehicles against each other, changing the distance their couplers go by. - The bend under an axle goes straight on past the ends of a path: the polynomial ran off on a short curved one, with a kick in the cab shake. - A track left out of its rail run, where tracks are joined one way only, no longer walks the whole run again on every call. - The clatter of an axle goes by the outer axle on its side of the vehicle, and not always by the same one, as the two can be on different tracks. - A vertex works out the amounts once, and the foot of the ballast not at all. --- tests/unit/test_track_uneven.cpp | 33 +++++++++++++++++++++++ vehicle/DynObj.cpp | 6 +++-- world/Segment.cpp | 45 +++++++++++++++++++++----------- world/Segment.h | 5 +++- world/Track.cpp | 16 +++++++----- world/TrackUneven.h | 25 ++++++++++++++++++ 6 files changed, 105 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 247f9cbf..fd95cec9 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -194,6 +194,39 @@ TEST_CASE("uneven track: the attribute goes by the entry it's written into") CHECK(track_uneven::write_attribute(track_uneven::read_attribute("0;;;;6", false), false) == "uneven 0;;;;6 "); } +TEST_CASE("uneven track: axles don't stray along the track") +{ + glm::dvec3 const across{0.6, 0.0, -0.8}; + glm::dvec3 const along{0.8, 0.0, 0.6}; + auto const moved{track_uneven::across_only(across * 0.03 + along * 0.05 + glm::dvec3{0.0, 0.02, 0.0}, across)}; + CHECK(glm::dot(moved, along) == doctest::Approx(0.0)); + CHECK(glm::dot(moved, across) == doctest::Approx(0.03)); + CHECK(moved.y == doctest::Approx(0.02)); +} + +TEST_CASE("uneven track: a path of no length gives no vector across") +{ + glm::dvec3 const none{0.0}; + CHECK(track_uneven::across_of(none, none) == none); + CHECK(glm::length(track_uneven::across_of({3.0, 0.0, 4.0}, none)) == doctest::Approx(1.0)); + // it can't pass for a joint: the check of the two vectors being alike fails + CHECK(glm::dot(track_uneven::across_of(none, none), glm::dvec3{1.0, 0.0, 0.0}) < 0.9); + // falls back on the chord + auto const chord{track_uneven::across_of(none, {0.0, 0.0, 10.0})}; + CHECK(chord.x == doctest::Approx(-1.0)); + CHECK(chord.z == doctest::Approx(0.0)); + // straight up has no direction in the plan + CHECK(glm::length(track_uneven::across_of({0.0, 5.0, 0.0}, {0.0, 0.0, 10.0})) == doctest::Approx(1.0)); +} + +TEST_CASE("uneven track: the cant at a joint") +{ + CHECK(track_uneven::joint_roll(0.1f, -0.1f, true, true) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.2f, 0.4f, false, false) == doctest::Approx(0.3f)); + CHECK(track_uneven::joint_roll(0.1f, 0.0f, true, false) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.0f, 0.1f, false, true) == doctest::Approx(0.f)); +} + TEST_CASE("uneven track: amounts take the lowest of each component") { auto const least{track_uneven::least({14.0, 9.0, 0.0}, {7.0, 35.0, 3.0})}; diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index af882828..3f633ccc 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3662,11 +3662,13 @@ bool TDynamicObject::Update(double dt, double dt1) ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { // 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 ) }; + // by the outer axle on its side of the vehicle: the two can be on different tracks + auto const &outer { axle.offset >= 0.0 ? Axle0 : Axle1 }; + auto const railphase { outer.RailPhase( axle.offset - std::copysign( 0.5 * fAxleDist, axle.offset ) ) }; 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; + crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * outer.GetTrack()->fSoundDistance; axle.distance = railphase; } else { diff --git a/world/Segment.cpp b/world/Segment.cpp index dd236c61..9c0e1077 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -385,13 +385,12 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: 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 ); + position += track_uneven::across_only( stray( position, amount, fDistance ), across ); } -glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { - auto const amount { pOwner->uneven_at( Distance, m_path ) }; +glm::dvec3 TSegment::stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const { auto const field { track_uneven::field( Point ) }; - glm::dvec3 result { field.x * amount.side, field.y * amount.up, field.z * amount.side }; + 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; @@ -401,13 +400,25 @@ 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 + // over about a bogie auto const reach { 1.5 }; auto const strayed = [&]( double const At ) { - return stray( FastGetPoint( FastGetTFromS( At ) ), At ); }; + return stray( point_extended( At ), pOwner->uneven_at( At, m_path ), At ); }; return ( strayed( Distance + reach ) - 2.0 * strayed( Distance ) + strayed( Distance - reach ) ) / ( reach * reach ); } +glm::dvec3 TSegment::point_extended( double const Distance ) const { + auto const on { std::clamp( Distance, 0.0, fLength ) }; + auto const t { FastGetTFromS( on ) }; + auto const point { FastGetPoint( t ) }; + if( on == Distance ) { + return point; + } + auto const direction { FastGetDirection( t, 0.1 / fLength ) }; + auto const size { glm::length( direction ) }; + return size > 0.0 ? point + direction * ( ( Distance - on ) / size ) : point; +} + 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; @@ -416,8 +427,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi 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 ) }; auto const share { pOwner->uneven_bed_share( Position.y - Vertex.position.y ) }; - Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * pOwner->uneven_at( Distance, m_path ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); - Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; + if( share <= 0.0 ) { + return; + } + auto const amount { pOwner->uneven_at( Distance, m_path ) }; + Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * amount.cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, amount, Distance ) }; } void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { @@ -446,15 +461,15 @@ void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm }; void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const { + if( fTsBuffer.empty() ) { + // never made into a path: tt_Cross and the like + Point = Point1; + Across = glm::dvec3 { 0.0 }; + return; + } auto const t { FastGetTFromS( std::clamp( Distance, 0.0, fLength ) ) }; Point = FastGetPoint( t ); - auto const dir { FastGetDirection( t, 0.1 / fLength ) }; - Across = glm::dvec3{ -dir.z, 0.0, dir.x }; - if( glm::length2( Across ) == 0.0 ) { - auto const chord { FastGetPoint_1() - FastGetPoint_0() }; - Across = glm::dvec3{ -chord.z, 0.0, chord.x }; - } - Across = glm::normalize( Across ); + Across = track_uneven::across_of( FastGetDirection( t, 0.1 / fLength ), FastGetPoint_1() - FastGetPoint_0() ); } glm::dvec3 TSegment::FastGetPoint(double const t) const diff --git a/world/Segment.h b/world/Segment.h index 77e61f90..67e4ed05 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "rendering/geometrybank.h" +#include "world/TrackUneven.h" #include struct map_colored_paths { @@ -77,7 +78,9 @@ class TSegment // 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; + glm::dvec3 stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const; + // a point of the path at a distance, past its ends straight on: the polynomial would run off on a short curved path + glm::dvec3 point_extended( double const Distance ) const; void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; public: diff --git a/world/Track.cpp b/world/Track.cpp index 31b5f5c5..bdb1cfe9 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -908,6 +908,10 @@ void TTrack::settle_rail_run() const { track = onward( *track, 1 - end, joined ); end = joined; } + // tracks joined one way only can leave this one out of the run: it stands on its own then, not to walk all of it again on every call + if( m_railrun.stamp != s_layoutstamp ) { + m_railrun = { 0.0, 1.0, s_layoutstamp }; + } } void TTrack::update_location() { @@ -1636,13 +1640,11 @@ void TTrack::share_joints() { closest = gap; joint.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); if( iCategoryFlag & 1 ) { - // the cant of a plain track is what was designed, a switch takes it; alike ones meet halfway - auto const own { segment->design_roll( end ) }; - auto const their { theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ) }; - roll = ( - ( eType == tt_Switch ) == ( neighbour->eType == tt_Switch ) ? 0.5f * ( own + their ) : - eType == tt_Switch ? their : - own ); + roll = track_uneven::joint_roll( + segment->design_roll( end ), + theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ), + eType == tt_Switch, + neighbour->eType == tt_Switch ); } } } diff --git a/world/TrackUneven.h b/world/TrackUneven.h index b46ebf7a..dbec7a24 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -224,6 +224,31 @@ inline double bed_share( double const Depth, double const Rail, double const Hei return Height > 0.0 ? std::clamp( 1.0 - ( Depth - Rail ) / Height, 0.0, 1.0 ) : 1.0; } +// a displacement with the part along the track taken out, Across being a unit vector in the plan. Along the track +// it would only change the distance between coupled vehicles, and with it the forces between them +inline glm::dvec3 across_only( glm::dvec3 const &Offset, glm::dvec3 const &Across ) { + + return Across * glm::dot( Offset, Across ) + glm::dvec3 { 0.0, Offset.y, 0.0 }; +} + +// the unit vector across a direction in the plan, or across the chord if it has none; zero for a path of no length, which has neither +inline glm::dvec3 across_of( glm::dvec3 const &Direction, glm::dvec3 const &Chord ) { + + for( auto const &along : { Direction, Chord } ) { + glm::dvec3 const across { -along.z, 0.0, along.x }; + if( glm::dot( across, across ) > 0.0 ) { + return glm::normalize( across ); + } + } + return glm::dvec3 { 0.0 }; +} + +// the cant where two paths meet, theirs turned to the way of this one: a switch takes that of a plain track, alike ones meet halfway +inline float joint_roll( float const Own, float const Their, bool const Switch, bool const Theirswitch ) { + + return Switch == Theirswitch ? 0.5f * ( Own + Their ) : Switch ? Their : Own; +} + // distance from the joint behind the point inline double rail_phase( double const Along, double const Rail ) {