diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bd71ff7..e53d4aa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ set(SOURCES "world/SleeperPaths.cpp" "world/SleeperFeatures.cpp" "world/SleeperLines.cpp" +"world/SleeperFastenings.cpp" "world/SleeperLayout.cpp" "world/SleeperModels.cpp" "world/SleeperWorker.cpp" diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 15cf6d92..97969ae9 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -49,6 +49,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodeselftest.h" #include "world/TrackSleepers.h" +#include "world/SleeperFastenings.h" editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &command, std::istringstream &words) { @@ -149,6 +150,37 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c } return selftest_result::handled; } + if (command == "fastenings") + { + // the fastenings on the timbers of the track: along the track, the part and where across it, which way its +x goes + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + auto const *model{track != nullptr ? track_sleepers::timber_model(*track) : nullptr}; + if (model == nullptr || false == model->fastened()) + { + WriteLog("SELFTEST fastenings: no track " + name + " with the fastenings in the model of its timbers"); + return selftest_result::handled; + } + std::array const names{"begin", "main", "end", "plate", "chair", "check"}; + track_sleepers::fastened_rails const rails{*track}; + std::array counts{}; + for (auto const &beam : track_sleepers::place(*track)) + { + if (beam.plain) + continue; + auto const middle{0.5 * (beam.from + beam.to)}; + std::string text{format("SELFTEST fastenings %s at %.2f %.2f:", track->name().c_str(), middle.x, middle.z)}; + for (auto const &fastening : rails.on(beam, *model)) + { + text += format(" %s %.3f %.3f %+.0f", names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); + ++counts[fastening.part]; + } + WriteLog(text); + } + WriteLog(format("SELFTEST fastenings %s: %d plates, %d chairs, %d checks", track->name().c_str(), counts[track_sleepers::part_plate], counts[track_sleepers::part_chair], counts[track_sleepers::part_check])); + return selftest_result::handled; + } if (command == "sleepercheck") { auto const start{std::chrono::steady_clock::now()}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 1cd93fcd..992b43c7 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46geometrybank.h" #include "rendering/screenshot.h" #include "rendering/editoroverlay.h" +#include "world/SleeperRules.h" #include //#define EU07_DEBUG_OPENGL @@ -6263,6 +6264,9 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ centre, chunk.radius } ) ) { continue; } if( laid.timbers->stretchable ) { for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( chunk.last[ part ] <= chunk.first[ part ] ) { continue; } + // the fastenings only near, they're small + if( part >= track_sleepers::timber_parts && distance > std::min( range, track_sleepers::fastening_range ) + chunk.radius ) { continue; } auto &instances = bucket_for( laid.timbers, part, 0.f ); for( auto index = chunk.first[ part ]; index < chunk.last[ part ]; ++index ) { instances.emplace_back( trackmodelview * laid.instances[ part ][ index ] ); @@ -6289,7 +6293,9 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ if( bucket.part >= 0 ) { // only the part of the model is visible, its siblings are skipped as the model is walked for( int part = 0; part < track_sleepers::part_count; ++part ) { - beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); + } } } material_data skins {}; @@ -6314,7 +6320,9 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ ++m_renderpass.draw_stats.instanced_drawcalls; if( bucket.part >= 0 ) { for( int part = 0; part < track_sleepers::part_count; ++part ) { - beammodel.parts[ part ]->iVisible = 1; + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = 1; + } } } } diff --git a/world/Segment.cpp b/world/Segment.cpp index 448d421f..afb2f2bc 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -755,6 +755,16 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig && ( Distance < Span.from + frommargin || Distance > Span.to - tomargin ); } ), stations.end() ); } + auto bottom { ShapePoints.front().position.y }; + for( auto const &point : ShapePoints ) { + bottom = std::min( bottom, point.position.y ); + } + auto const raised = [&]( float const Height ) { + if( Span.rise <= 0.0 || Height >= Span.head.x || Span.head.x <= bottom ) { + return Height; + } + return static_cast( Height + Span.rise * ( Span.head.x - Height ) / ( Span.head.x - bottom ) ); }; + auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { glm::dvec3 point; glm::dvec3 across; @@ -820,7 +830,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig // nothing of the profile between the limits there: the ring shrinks to one point, the step draws nothing auto const outside { std::clamp( 0.5 * ( std::min( extent, farthest ) + std::max( innerextent, nearest ) ), nearest, farthest ) }; auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position }; - pt.y += ShapePoints.front().position.y + Span.lift; + pt.y += raised( ShapePoints.front().position.y ) + Span.lift; for( std::size_t j = 0; j < count; ++j ) { Ring.emplace_back( pt, glm::vec3{ 0.f, 1.f, 0.f }, glm::vec2{ ShapePoints[ j ].texture.x, 0.f } ); Wear.push_back( 1.f ); @@ -899,6 +909,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig if( drop > 0.0 && shape.position.y > Span.head.x && Span.head.y > Span.head.x ) { shape.position.y -= static_cast( drop * ( shape.position.y - Span.head.x ) / ( Span.head.y - Span.head.x ) ); } + shape.position.y = raised( shape.position.y ); auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) }; pt.y += shape.position.y + Span.lift; auto norm { shape.normal.x * parallel }; diff --git a/world/Segment.h b/world/Segment.h index a2e5d9c3..69e47e7f 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -47,6 +47,7 @@ struct loft_span { loft_end toend; knots::line drops; glm::dvec2 head { 0.0 }; + double rise { 0.0 }; // how far the foot of the profile is raised, the web below the head (head.x) shortened by as much }; struct segment_data { diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp new file mode 100644 index 00000000..641a187f --- /dev/null +++ b/world/SleeperFastenings.cpp @@ -0,0 +1,207 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" + +#include "world/SleeperFastenings.h" +#include "world/SleeperGeometry.h" +#include "world/SleeperPaths.h" +#include "world/SleeperRules.h" +#include "world/TrackRails.h" +#include "world/Track.h" + +namespace track_sleepers { + +fastened_rails::role fastened_rails::rail::at( double const Distance ) const { + + auto const within = [&]( glm::dvec2 const &Stretch ) { + return Stretch.y > Stretch.x && Distance >= Stretch.x && Distance <= Stretch.y; }; + if( within( blade ) ) { + return role::blade; + } + if( within( stock ) ) { + return role::stock; + } + if( std::any_of( checks.begin(), checks.end(), within ) ) { + return role::checked; + } + return role::plain; +} + +fastened_rails::fastened_rails( TTrack &Track ) { + + std::vector tracks; + tracks_near( Track, [&]( TTrack *Other ) { + if( bedded( *Other ) && std::find( tracks.begin(), tracks.end(), Other ) == tracks.end() ) { + tracks.push_back( Other ); + } } ); + for( auto *track : tracks ) { + auto const middle { 0.5 * std::abs( track->fTrackWidth ) + track->rail_dimensions().web }; + auto const cut { track_rails::cut_rails( *track ) }; + auto const switched { track->eType == tt_Switch && track->SwitchExtension != nullptr }; + // the side of the blade of the path, and the stretch along it from the tip to the heel + auto const bladeside = [&]( int const Path ) { + return ( track->SwitchExtension->RightSwitch == ( Path == 0 ) ? 1 : -1 ); }; + auto const bladestretch = [&]( int const Path ) { + auto const &shape { track->blade( Path ) }; + return glm::dvec2 { shape.valid ? shape.tip : 0.0, track_rails::blade_length( *track, Path ) }; }; + for( int path = 0; path < path_count( *track ); ++path ) { + auto const *segment { path_segment( *track, path ) }; + if( segment == nullptr ) { + continue; + } + auto const length { segment->GetLength() }; + auto const count { std::max( 1, static_cast( std::ceil( length / fastening_step ) ) ) }; + for( auto const side : { -1, 1 } ) { + rail line; + line.track = track; + line.path = path; + line.side = side; + line.low = glm::dvec2 { std::numeric_limits::max() }; + line.high = glm::dvec2 { std::numeric_limits::lowest() }; + for( int step = 0; step <= count; ++step ) { + auto const distance { length * step / count }; + glm::dvec3 point, across; + segment->plan_frame( distance, point, across ); + auto const flat { plan( across ) }; + auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -side ) : glm::dvec2 { 1.0, 0.0 } }; + auto const position { plan( point ) - inward * middle }; + line.points.push_back( position ); + line.heights.push_back( point.y ); + line.inward.push_back( inward ); + line.distances.push_back( distance ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); + } + if( switched ) { + if( side == bladeside( path ) ) { + line.blade = { 0.0, bladestretch( path ).y }; + } + if( side == bladeside( 1 - path ) ) { + auto const stretch { bladestretch( 1 - path ) }; + line.stock = { std::max( 0.0, stretch.x - chair_lead ), stretch.y }; + } + } + if( cut ) { + for( auto const &check : track_rails::rail_checks( *track )[ path ][ side > 0 ? 1 : 0 ] ) { + line.checks.emplace_back( check.from, check.to ); + } + } + m_rails.emplace_back( std::move( line ) ); + } + } + } +} + +std::vector fastened_rails::on( beam const &Beam, beam_model const &Model ) const { + + std::vector result; + auto const span { Beam.to - Beam.from }; + auto const length { glm::length( plan( span ) ) }; + if( length < 0.1 ) { + return result; + } + auto const start { plan( Beam.from ) }; + auto const axis { plan( span ) / length }; + auto const low { glm::min( start, plan( Beam.to ) ) }; + auto const high { glm::max( start, plan( Beam.to ) ) }; + std::vector hits; + for( auto const &line : m_rails ) { + if( line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { + continue; + } + auto const last { line.points.size() - 1 }; + for( std::size_t index = 1; index <= last; ++index ) { + auto const from { line.points[ index - 1 ] }; + auto const chord { line.points[ index ] - from }; + double u, v; + if( false == track_rails::meet( start, plan( span ), from, chord, 1e-12, u, v ) ) { + continue; + } + if( u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 || ( v == 1.0 && index < last ) ) { + continue; + } + auto const direction { glm::normalize( chord ) }; + if( std::abs( cross( axis, direction ) ) < crossing_sine_min ) { + continue; + } + auto const height { glm::mix( line.heights[ index - 1 ], line.heights[ index ], v ) }; + if( std::abs( height - glm::mix( Beam.from.y, Beam.to.y, u ) ) > level_tolerance ) { + continue; + } + hits.push_back( { + u * length, + line.at( glm::mix( line.distances[ index - 1 ], line.distances[ index ], v ) ), + glm::normalize( glm::mix( line.inward[ index - 1 ], line.inward[ index ], v ) ) } ); + } + } + std::sort( hits.begin(), hits.end(), []( hit const &Left, hit const &Right ) { return Left.at < Right.at; } ); + // one rail where several lie on one another, the stock rail before the rail checked, before a plain one; a blade with no + // other rail there slides on the chair of its stock rail + auto const rank = []( role const Kind ) { + switch( Kind ) { + case role::stock: { return 3; } + case role::checked: { return 2; } + case role::plain: { return 1; } + default: { return 0; } + } }; + std::vector rails; + for( std::size_t index = 0; index < hits.size(); ) { + auto best { hits[ index ] }; + auto next { index + 1 }; + for( ; next < hits.size() && hits[ next ].at - hits[ next - 1 ].at < fastening_merge; ++next ) { + if( rank( hits[ next ].kind ) > rank( best.kind ) ) { + best = hits[ next ]; + } + } + if( best.kind != role::blade ) { + rails.push_back( best ); + } + index = next; + } + auto const point = [&]( double const At ) { + return Beam.from + span * ( At / length ); }; + auto const has = [&]( int const Part ) { + return Model.parts[ Part ] != nullptr; }; + for( std::size_t index = 0; index < rails.size(); ++index ) { + auto const &rail { rails[ index ] }; + if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock ) { + auto const &other { rails[ index + 1 ] }; + if( other.at - rail.at < fastening_nose ) { + result.push_back( { part_plate, point( 0.5 * ( rail.at + other.at ) ), rail.inward } ); + } + else if( has( part_check ) ) { + result.push_back( { part_check, point( rail.at ), axis } ); + } + else { + result.push_back( { part_plate, point( rail.at ), rail.inward } ); + result.push_back( { part_plate, point( other.at ), other.inward } ); + } + ++index; + continue; + } + switch( rail.kind ) { + case role::stock: { + result.push_back( { has( part_chair ) ? part_chair : part_plate, point( rail.at ), -rail.inward } ); + break; + } + case role::checked: { + result.push_back( { has( part_check ) ? part_check : part_plate, point( rail.at ), rail.inward } ); + break; + } + default: { + result.push_back( { part_plate, point( rail.at ), rail.inward } ); + break; + } + } + } + return result; +} + +} // namespace track_sleepers diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h new file mode 100644 index 00000000..1d2f876a --- /dev/null +++ b/world/SleeperFastenings.h @@ -0,0 +1,73 @@ +/* +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 "world/TrackSleepers.h" + +// the fastenings of the rails on the timbers: where a rail lies on a timber, the part of the model of the timbers which holds it +// there. a stock rail with the blade beside it lies in a slide chair, a rail with a check rail or another rail of the frog close +// beside it in a fastening of both, the blade on the chair of its stock rail, and the other rails each in a plate of its own +namespace track_sleepers { + +// a fastening on a timber +struct fastening { + int part { part_plate }; + glm::dvec3 point { 0.0 }; // under the middle of the rail, on the top of the timber + glm::dvec2 across { 1.0, 0.0 }; // unit vector in plan square to the rail, the way the +x of the part goes +}; + +// the rails of the tracks around a track, which lie on its timbers +class fastened_rails { + +public: + explicit fastened_rails( TTrack &Track ); + // the fastenings of the rails on the timber, of the parts the model has + std::vector on( beam const &Beam, beam_model const &Model ) const; + +private: + // what a rail is where it lies on a timber + enum class role { + plain, + blade, // a blade of a switch, which slides on the chair of its stock rail + stock, // the stock rail of a switch with the blade beside it + checked // a rail with a check rail beside it + }; + // a rail by points along its middle + struct rail { + TTrack *track { nullptr }; + int path { 0 }; + int side { 1 }; + std::vector points; + std::vector heights; + std::vector inward; // unit vectors in plan from the rail toward the middle of its path + std::vector distances; + glm::dvec2 low { 0.0 }; + glm::dvec2 high { 0.0 }; + // the stretches along the path the rail is a blade, a stock rail with a chair, checked along + glm::dvec2 blade { -1.0 }; + glm::dvec2 stock { -1.0 }; + std::vector checks; + role at( double const Distance ) const; + }; + // where a rail crosses a timber + struct hit { + double at { 0.0 }; // along the timber, from its from end + role kind { role::plain }; + glm::dvec2 inward { 1.0, 0.0 }; + }; + + std::vector m_rails; +}; + +} // namespace track_sleepers diff --git a/world/SleeperModels.cpp b/world/SleeperModels.cpp index 648fe434..90e0a5d8 100644 --- a/world/SleeperModels.cpp +++ b/world/SleeperModels.cpp @@ -25,6 +25,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace track_sleepers { @@ -50,6 +51,40 @@ void vertices_of( TSubModel *Submodel, Visit Visitor ) { } } +// the highest the faces of the submodel reach over the point of the plan of the model, in the space of the model; none where +// no face lies over it +std::optional surface_at( TSubModel *Submodel, float const X, float const Z ) { + + glm::mat4 transform { 1.f }; + if( ( Submodel->iFlags & 0xC000 ) && ( Submodel->GetMatrix() != nullptr ) ) { + transform = glm::make_mat4( Submodel->GetMatrix()->readArray() ); + } + auto const indexed { Submodel->m_geometry.handle != null_handle }; + auto const &vertices { indexed ? GfxRenderer->Vertices( Submodel->m_geometry.handle ) : Submodel->Vertices }; + auto const &indices { indexed ? GfxRenderer->Indices( Submodel->m_geometry.handle ) : Submodel->Indices }; + auto const count { indices.empty() ? vertices.size() : indices.size() }; + auto const corner = [&]( std::size_t const Index ) { + return glm::vec3 { transform * glm::vec4 { vertices[ indices.empty() ? Index : indices[ Index ] ].position, 1.f } }; }; + std::optional result; + for( std::size_t index = 0; index + 2 < count; index += 3 ) { + auto const a { corner( index ) }; + auto const b { corner( index + 1 ) }; + auto const c { corner( index + 2 ) }; + auto const area { ( b.x - a.x ) * ( c.z - a.z ) - ( c.x - a.x ) * ( b.z - a.z ) }; + if( std::abs( area ) < 1e-9f ) { + continue; + } + auto const u { ( ( X - a.x ) * ( c.z - a.z ) - ( c.x - a.x ) * ( Z - a.z ) ) / area }; + auto const v { ( ( b.x - a.x ) * ( Z - a.z ) - ( X - a.x ) * ( b.z - a.z ) ) / area }; + if( u < -1e-5f || v < -1e-5f || u + v > 1.f + 1e-5f ) { + continue; + } + auto const height { a.y + u * ( b.y - a.y ) + v * ( c.y - a.y ) }; + result = ( result ? std::max( *result, height ) : height ); + } + return result; +} + // the model of the beams of the name and skin, loaded and measured on the first use; null when it can't be loaded. the feet of // the rails go to the given seat in it, or to its top where the seat is below zero beam_model const *find_model( std::string const &Name, std::string const &Skinname, float const Seat ) { @@ -74,16 +109,16 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna model.skin = GfxRenderer->Fetch_Material( Skin ); } // a timber put together from its parts, with a part between the ends stretched and repeated to the length - std::array const names { "begin", "main", "end" }; + std::array const names { "begin", "main", "end", "plate", "chair", "check" }; model.stretchable = true; for( int part = 0; part < part_count; ++part ) { model.parts[ part ] = model.model->GetFromName( names[ part ] ); - model.stretchable = model.stretchable && model.parts[ part ] != nullptr; + model.stretchable = model.stretchable && ( part >= timber_parts || model.parts[ part ] != nullptr ); } auto top { std::numeric_limits::lowest() }; model.span = { std::numeric_limits::max(), std::numeric_limits::lowest() }; if( model.stretchable ) { - for( int part = 0; part < part_count; ++part ) { + for( int part = 0; part < timber_parts; ++part ) { glm::vec2 span { std::numeric_limits::max(), std::numeric_limits::lowest() }; vertices_of( model.parts[ part ], [&]( glm::vec3 const &Position ) { span = { std::min( span.x, Position.x ), std::max( span.y, Position.x ) }; @@ -112,8 +147,14 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna } } std::sort( model.bands.begin(), model.bands.end() ); - // the rails sit on the seat of the sleeper, below the fastenings sticking up over it + // the rails sit on the seat of the sleeper, below the fastenings sticking up over it, or on the plate of the fastening model.seat = ( Seat >= 0.f ? Seat : top ); + if( Seat < 0.f && model.fastened() ) { + model.seat = std::max( top, surface_at( model.parts[ part_plate ], 0.f, 0.f ).value_or( top ) ); + } + if( model.fastened() && model.parts[ part_chair ] != nullptr ) { + model.slide = std::max( 0.f, surface_at( model.parts[ part_chair ], static_cast( -slide_probe ), 0.f ).value_or( model.seat ) - model.seat ); + } if( model.length() < 0.5f ) { ErrorLog( "Bad sleepers3d: model \"" + Name + "\" has no beam along x" ); model.model = nullptr; @@ -125,7 +166,11 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna + ( model.stretchable ? ", main " + to_string( model.spans[ part_main ].x, 3 ) + ".." + to_string( model.spans[ part_main ].y, 3 ) : std::string {} ) - + ", seat " + to_string( model.seat, 3 ) ); + + ( model.fastened() ? + std::string { ", fastenings plate" } + ( model.parts[ part_chair ] != nullptr ? " chair" : "" ) + ( model.parts[ part_check ] != nullptr ? " check" : "" ) : + std::string {} ) + + ", seat " + to_string( model.seat, 3 ) + + ( model.slide > 0.f ? ", slide " + to_string( model.slide, 3 ) : std::string {} ) ); return &model; } return ( found->second.model != nullptr ? &found->second : nullptr ); diff --git a/world/SleeperRules.h b/world/SleeperRules.h index c41dc67c..3fb63044 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -49,6 +49,20 @@ double constexpr level_tolerance { 1.0 }; double constexpr around_margin { 20.0 }; // how many beams make a run drawn or left out together std::size_t constexpr chunk_beams { 16 }; +// how far apart the points of the rails are, as the places they lie on the timbers are found +double constexpr fastening_step { 0.25 }; +// rails closer than this where they cross a timber are one rail there, held by one fastening +double constexpr fastening_merge { 0.03 }; +// two rails closer than this lie in one fastening of both, as a rail of the frog with the one beside it; closer than the second +// one, as the legs of the nose where they're joined, in one plate between them +double constexpr fastening_pair { 0.24 }; +double constexpr fastening_nose { 0.05 }; +// how far before the tip of the blade the slide chairs of its stock rail begin +double constexpr chair_lead { 0.3 }; +// how far from the stock rail toward its blade the slide plate of a chair is measured +double constexpr slide_probe { 0.12 }; +// how far from the camera the fastenings are drawn at most +double constexpr fastening_range { 120.0 }; // how many tracks a cluster takes at most std::size_t constexpr cluster_cap { 256 }; diff --git a/world/Track.cpp b/world/Track.cpp index b9b6b01d..e0d0ac12 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -2259,8 +2259,9 @@ void TTrack::loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::ver offsets.emplace_back( bend.x, -thrown * bend.y ); } auto const size { rail_dimensions() }; + auto const rise { track_sleepers::blade_rise( *this ) }; auto const length { SwitchExtension->Segments[ Path ]->GetLength() }; - SwitchExtension->Segments[ Path ]->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = shape.tip, .to = std::min( shape.heel, length ), .offsets = offsets, .extents = shape.extents, .walls = true, .drops = shape.drops, .head = { size.neck, size.crest } } ); + SwitchExtension->Segments[ Path ]->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = shape.tip, .to = std::min( shape.heel, length ), .offsets = offsets, .extents = shape.extents, .walls = true, .drops = shape.drops, .head = { size.neck, size.crest }, .rise = rise } ); // the blade running on past the end of the path goes on along the plain track joined there, whose rail starts behind the heel if( auto const *next { track_rails::runon_track( *this, Path ) }; next != nullptr && shape.heel > length + 1e-3 ) { auto const along = [&]( std::vector const &Knots ) { @@ -2269,7 +2270,7 @@ void TTrack::loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::ver result.emplace_back( knot.x - length, knot.y ); } return result; }; - next->Segment->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = 0.0, .to = shape.heel - length, .offsets = along( offsets ), .extents = along( shape.extents ), .walls = true, .drops = along( shape.drops ), .head = { size.neck, size.crest } } ); + next->Segment->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = 0.0, .to = shape.heel - length, .offsets = along( offsets ), .extents = along( shape.extents ), .walls = true, .drops = along( shape.drops ), .head = { size.neck, size.crest }, .rise = rise } ); } return; } diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index bcb5cf09..e23fa7db 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/TrackSleepers.h" +#include "world/SleeperFastenings.h" #include "world/SleeperGeometry.h" #include "world/SleeperLayout.h" #include "world/SleeperPaths.h" @@ -24,6 +25,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include namespace track_sleepers { @@ -63,6 +65,11 @@ bool lays( TTrack const &Track ) { return bedded( Track ) && timber_model( Track ) != nullptr; } +double blade_rise( TTrack const &Track ) { + + return ( Track.eType == tt_Switch && lays( Track ) ? timber_model( Track )->slide : 0.0 ); +} + std::vector> shared_zones( TTrack const &Track, int const Path ) { std::vector> result; @@ -134,6 +141,9 @@ void lay( TTrack &Track ) { // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn forget_layout( Track ); + // the rails around, where they lie on the timbers, for the fastenings of the model + std::optional const rails { parts.fastened() ? std::make_optional( Track ) : std::nullopt }; + auto const partlength = [&]( int const Part ) { return static_cast( parts.spans[ Part ].y - parts.spans[ Part ].x ); }; auto const beginlength { partlength( part_begin ) }; @@ -232,6 +242,19 @@ void lay( TTrack &Track ) { put( part_end, scale, parts.spans[ part_end ].x, -half ); put( part_begin, scale, parts.spans[ part_begin ].y, half ); } + if( rails && false == ( beam.plain && laid.plains != nullptr ) ) { + // each square to its rail on the top of the timber, the plate under the foot of the rail + for( auto const &fastening : rails->on( beam, parts ) ) { + auto across { glm::dvec3 { fastening.across.x, 0.0, fastening.across.y } }; + across = glm::normalize( across - y * glm::dot( across, y ) ); + glm::mat4 place { 1.f }; + place[ 0 ] = glm::vec4 { glm::vec3 { across }, 0.f }; + place[ 1 ] = glm::vec4 { glm::vec3 { y }, 0.f }; + place[ 2 ] = glm::vec4 { glm::vec3 { glm::cross( across, y ) }, 0.f }; + place[ 3 ] = glm::vec4 { glm::vec3 { fastening.point - Track.m_origin }, 1.f }; + laid.instances[ fastening.part ].emplace_back( glm::translate( place, glm::vec3 { 0.f, -parts.seat, 0.f } ) ); + } + } runbeams.emplace_back( middle, half ); if( runbeams.size() >= chunk_beams ) { close_run(); diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index 06fb1c62..fd994446 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -26,6 +26,8 @@ class TSubModel; // them, repeated and stretched to the length of the beam, so one model makes the timbers of any length; a model without such // parts is drawn whole, stretched. The sleepers of plain tracks are a whole model of their own, set with sleepers3dplain, drawn // at their own length. A track can have models of its own: of its sleepers with sleepermodel, of its timbers with timbermodel. +// A model of timbers put together from parts can have the fastenings of the rails among its submodels too: plate, chair and +// check, put on the timbers where the rails lie on them, and the rails sit on the plate. namespace track_sleepers { // the parts of the model a beam is put together from, by the names of the submodels @@ -33,8 +35,14 @@ enum part : int { part_begin, // the end of the beam on its +x side part_main, // the piece between the ends part_end, // the end on the -x side + part_plate, // the fastening of a rail, its origin under the middle of the foot of the rail + part_chair, // the slide chair of a stock rail with the blade beside it, its origin under the stock rail, the slide plate on its -x side + part_check, // the fastening of a rail with another one close beside it, as a check rail or a rail of the frog, its origin under the + // rail, the other one on its +x side part_count }; +// the parts a timber is put together from, the fastenings after them +int constexpr timber_parts { part_end + 1 }; // a sleeper or a timber, by the middle of its top face at both ends struct beam { @@ -59,8 +67,11 @@ struct beam_model { std::array spans {}; // from and to along x, of the parts glm::vec2 span { -1.3f, 1.3f }; // from and to along x, of the most detailed submodels of the whole model float seat { 0.f }; // the height in the model the feet of the rails go to + float slide { 0.f }; // how far over the seat the blades of a switch slide, on the slide plates of the chairs; 0 without chairs std::vector bands; // the squared distances the levels of detail of the model are seen from, the nearest first float length() const { return span.y - span.x; } + // the model has the fastenings of the rails, put where the rails lie on its timbers + bool fastened() const { return stretchable && parts[ part_plate ] != nullptr; } }; // a run of beams of a track drawn or left out together, by the sphere around them @@ -105,6 +116,9 @@ double plain_length( TTrack const &Track ); bool bedded( TTrack const &Track ); // whether the track gets beams: a rail track with the ballast made by the simulator, and the model is there, its own or of eu07.ini bool lays( TTrack const &Track ); +// how far the feet of the blades of the switch ride over those of its other rails, on the slide plates of the chairs of its +// timbers; 0 where it lays none with chairs +double blade_rise( TTrack const &Track ); // where along the path of the track its rails lie on timbers shared with other tracks: where another track runs so near that // the plain sleepers of the two would touch, of the lengths of the models they'd be made of. this goes by the shape of the // tracks alone, so it holds whether the beams are drawn or not. the stretches in the order along the path, none where there