diff --git a/CMakeLists.txt b/CMakeLists.txt index a99a2b3b..703138bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,7 @@ set(SOURCES "world/SleeperPaths.cpp" "world/SleeperFeatures.cpp" "world/SleeperLines.cpp" +"world/SleeperFastenings.cpp" "world/SleeperLayout.cpp" "world/SleeperModels.cpp" "world/SleeperWorker.cpp" @@ -437,6 +438,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46skyocclusion.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46hiz.cpp" + "rendering/opengl46/opengl46timbers.cpp" "rendering/opengl46/opengl46shadowfit.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index fc96c829..5e1d41bd 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -75,6 +75,7 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took // throw <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose +// fastentour [cell] [frames] the camera over each cell of the grid with switches laying timbers in turn, for a fastencheck after of the whole scenery // switchtimbers the model of the timbers set on all the switches and crossings, as the buttons of the switch tool do it // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings // geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 15cf6d92..d8e1e7b1 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,128 @@ 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, detail; + words >> name >> detail; + auto *track{simulation::Paths.find(name)}; + auto const beams{track != nullptr ? track_sleepers::place(*track) : std::vector{}}; + auto const *model{track != nullptr ? track_sleepers::timber_model(*track, beams) : 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; + } + track_sleepers::fastened_rails const rails{*track}; + std::array counts{}; + for (auto const &beam : beams) + { + 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)}; + if (detail == "rails") + { + auto const span{beam.to - beam.from}; + auto const length{glm::length(glm::dvec2{span.x, span.z})}; + text += " rails"; + for (auto const &rail : rails.lying(beam)) + { + auto const at{beam.from + span * (rail.at / length)}; + text += format(" %.3f %.3f k%d r%.3f;", at.x, at.z, static_cast(rail.kind), rail.rise); + } + text += " parts"; + } + for (auto const &fastening : rails.on(beam, *model)) + { + text += format(" %s%s %.3f %.3f %+.0f", fastening.pad ? "pad:" : "", track_sleepers::part_names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); + if (fastening.lift != 0.f) + text += format(" ^%.3f", fastening.lift); + if (detail == "rails" && (fastening.scale != 1.f || fastening.shift != 0.f)) + text += format(" s%.3f/%.3f/%.3f", fastening.scale, fastening.pivot, fastening.shift); + if (false == fastening.pad) + ++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 == "fastentour") + { + // the camera over each cell of the grid with switches or crossings laying timbers, in turn, as the tracks get their beams only + // near it: the steps of the tour go in after this one, a fastencheck after them takes the whole scenery + double cell{400.0}; + int frames{40}; + words >> cell >> frames; + std::map, glm::dvec3> stops; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->eType != tt_Switch && track->m_crossings.empty()) || false == track_sleepers::lays(*track)) + continue; + auto const at{track->location()}; + stops.emplace(std::make_pair(std::lround(std::floor(at.x / cell)), std::lround(std::floor(at.z / cell))), at); + } + std::vector steps; + for (auto const &stop : stops) + { + auto const &at{stop.second}; + steps.push_back(format("camera %.2f %.2f %.2f %.2f %.2f %.2f", at.x, at.y + 60.0, at.z + 1.0, at.x, at.y, at.z)); + steps.push_back("wait " + std::to_string(frames)); + } + m_selftest.lines.insert(m_selftest.lines.begin() + static_cast(m_selftest.next), steps.begin(), steps.end()); + WriteLog(format("SELFTEST fastentour: %zu stops of %.0f m", stops.size(), cell)); + return selftest_result::handled; + } + if (command == "fastencheck") + { + // the fastenings of all the tracks laying timbers with them: of each kind, and those over one another on a timber + std::string mode; + words >> mode; + auto const verbose{mode == "all"}; + auto const start{std::chrono::steady_clock::now()}; + auto const rows{track_sleepers::check_fastenings()}; + std::size_t beams{0}, overlaps{0}, tracks{0}, bare{0}; + std::array parts{}; + for (auto const &row : rows) + { + beams += row.beams; + overlaps += row.overlaps; + bare += row.bares.size(); + tracks += (row.overlaps > 0 ? 1 : 0); + for (int part = 0; part < track_sleepers::part_count; ++part) + parts[part] += row.parts[part]; + WriteLog(format("SELFTEST fastencheck %s: %zu timbers, %zu plates, %zu chairs, %zu checks, %zu bare rails, %zu overlaps%s", + row.track.c_str(), row.beams, row.parts[track_sleepers::part_plate], row.parts[track_sleepers::part_chair], row.parts[track_sleepers::part_check], row.bares.size(), row.overlaps, + row.overlaps > 0 ? format(", worst %.3f m %s at %.2f %.2f", row.worst, row.worstparts.c_str(), row.worstat.x, row.worstat.z).c_str() : "")); + if (verbose) + for (auto const &at : row.bares) + WriteLog(format("SELFTEST fastencheck bare %s at %.3f %.3f", row.track.c_str(), at.x, at.y)); + if (verbose) + for (auto const &overlap : row.cases) + WriteLog(format("SELFTEST fastencheck overlap %s %d %d apart %.3f over %.3f at %.2f %.2f", row.track.c_str(), overlap.parts[0], overlap.parts[1], overlap.apart, overlap.over, overlap.at.x, overlap.at.z)); + } + WriteLog(format("SELFTEST fastencheck: %zu tracks, %zu timbers, %zu plates, %zu chairs, %zu checks, %zu bare rails, %zu overlaps on %zu tracks; in %.1f ms", + rows.size(), beams, parts[track_sleepers::part_plate], parts[track_sleepers::part_chair], parts[track_sleepers::part_check], bare, overlaps, tracks, + std::chrono::duration(std::chrono::steady_clock::now() - start).count())); + return selftest_result::handled; + } + if (command == "checkseats") + { + // the check rails with timbers under them and no fastening seating them, along how much of them + auto const gaps{track_sleepers::check_rail_seats()}; + double bare{0.0}; + std::size_t unsupported{0}; + for (auto const &gap : gaps) + { + bare += gap.bare; + unsupported += gap.unsupported; + WriteLog(format("SELFTEST checkseats %s path %d side %+d %.2f-%.2f: %.0f of %zu timbers bare, %zu with nothing under, first at %.2f %.2f", gap.track.c_str(), gap.path, gap.side, gap.from, gap.to, gap.bare, gap.timbers, gap.unsupported, gap.at.x, gap.at.y)); + } + WriteLog(format("SELFTEST checkseats: %zu check rails with %.0f timbers bare, %zu with nothing under", gaps.size(), bare, unsupported)); + return selftest_result::handled; + } if (command == "sleepercheck") { auto const start{std::chrono::steady_clock::now()}; diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index cb6e4e1f..f6f3df06 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -34,6 +34,11 @@ void gl::glsl_common_setup() "#define HIZ_GROUP_SIZE " + std::to_string(HIZ_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_GROUP_SIZE " + std::to_string(SHADOW_FIT_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_SSBO_BINDING " + std::to_string(SHADOW_FIT_SSBO_BINDING) + "\n" + + "#define TIMBER_GROUP_SIZE " + std::to_string(TIMBER_GROUP_SIZE) + "\n" + + "#define TIMBER_LOCAL_SSBO_BINDING " + std::to_string(TIMBER_LOCAL_SSBO_BINDING) + "\n" + + "#define TIMBER_RANGE_SSBO_BINDING " + std::to_string(TIMBER_RANGE_SSBO_BINDING) + "\n" + + "#define TIMBER_OUTPUT_SSBO_BINDING " + std::to_string(TIMBER_OUTPUT_SSBO_BINDING) + "\n" + + "#define TIMBER_ORIGIN_SSBO_BINDING " + std::to_string(TIMBER_ORIGIN_SSBO_BINDING) + "\n" + "#define PARTICLE_GROUP_SIZE " + std::to_string(PARTICLE_GROUP_SIZE) + "\n" + "#define PARTICLE_SSBO_BINDING " + std::to_string(PARTICLE_SSBO_BINDING) + "\n" + "#define PARTICLE_SOURCE_SSBO_BINDING " + std::to_string(PARTICLE_SOURCE_SSBO_BINDING) + "\n" + diff --git a/gl/streambuffer.cpp b/gl/streambuffer.cpp index 38c6dbe2..070723aa 100644 --- a/gl/streambuffer.cpp +++ b/gl/streambuffer.cpp @@ -141,6 +141,17 @@ void gl::frame_stream::next_frame() } GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) +{ + auto const offset{reserve(Size)}; + auto const ¤t{m_slots[m_slot]}; + if (current.data != nullptr) + std::memcpy(current.data + offset, Data, static_cast(Size)); + else + glNamedBufferSubData(current.buffer, offset, Size, Data); + return offset; +} + +GLintptr gl::frame_stream::reserve(GLsizeiptr Size) { auto *current{&m_slots[m_slot]}; auto offset{align_up(m_fill, m_alignment)}; @@ -167,13 +178,8 @@ GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) glFinish(); ++m_waits; } - current = &m_slots[m_slot]; offset = m_start; } - if (current->data != nullptr) - std::memcpy(current->data + offset, Data, static_cast(Size)); - else - glNamedBufferSubData(current->buffer, offset, Size, Data); m_fill = offset + Size; return offset; } diff --git a/gl/streambuffer.h b/gl/streambuffer.h index e8363750..51efefca 100644 --- a/gl/streambuffer.h +++ b/gl/streambuffer.h @@ -34,6 +34,8 @@ namespace gl // bigger one (generation() changes, buffer() tells which to bind); the old one stays until the frame is done with, so // what was written to it can still be bound and drawn from later in the frame GLintptr push(void const *Data, GLsizeiptr Size); + // the room for Size bytes in the buffer of the frame, as push() makes it, left for the gpu to write (a compute pass) + GLintptr reserve(GLsizeiptr Size); GLuint buffer() const { diff --git a/gl/ubo.h b/gl/ubo.h index a0ec5847..c933cf52 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -619,6 +619,24 @@ namespace gl const GLuint PARTICLE_OUTPUT_SSBO_BINDING = 12; // what the draw reads, a particle_draw per particle drawn const GLuint PARTICLE_COMMAND_SSBO_BINDING = 13; // the indirect draw command, whose instances the passes count + // the instances of the timbers of the tracks (opengl46_timbers, timber_instances.comp): work group of the pass and the binding + // points of its buffers, ones of the instance culling, which binds its buffers before every use; the pass binds its own + const GLuint TIMBER_GROUP_SIZE = 64; + const GLuint TIMBER_LOCAL_SSBO_BINDING = 9; // the transforms of the instances of the tracks, relative to the origins of the tracks + const GLuint TIMBER_RANGE_SSBO_BINDING = 11; // the runs of them drawn, a timber_range each + const GLuint TIMBER_OUTPUT_SSBO_BINDING = 12; // the instance buffer of the frame, their modelviews written there + const GLuint TIMBER_ORIGIN_SSBO_BINDING = 13; // the modelviews of the origins of the cells of the runs + + // layout std430: GLSL timber_range. a run of instances: count of them from source in the transforms, to target in the output, + // turned by the modelview of origin + struct timber_range + { + uint32_t source; + uint32_t count; + uint32_t target; + uint32_t origin; + }; + // layout std430: GLSL smoke_particle_s struct particle_smoke { diff --git a/rendering/common/scenewalk.cpp b/rendering/common/scenewalk.cpp index e93d736f..8718ae45 100644 --- a/rendering/common/scenewalk.cpp +++ b/rendering/common/scenewalk.cpp @@ -25,9 +25,11 @@ http://mozilla.org/MPL/2.0/. #include "world/catenary_span.h" #include "world/MemCell.h" #include "world/TrackSleepers.h" +#include "world/SleeperRules.h" #include "model/AnimModel.h" #include "model/Model3d.h" #include "rendering/common/skyocclusion.h" +#include namespace gfx { @@ -587,6 +589,8 @@ void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator L ++First; } + // the sleepers and timbers of the tracks of all the cells at once + Flush_Timbers(); // second pass draws elements with their own vbos while (first != Last) { @@ -1488,10 +1492,11 @@ void scene_walk::Render_Sleepers( TTrack *Track ) walk_sleepers_drawn( *Track, lodbounds, survivors ); } -// Renders the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or -// of a whole model, kept by its track relative to the origin of the track. The runs of beams in range and in sight are gathered -// for the whole run of tracks by the model and its part or its level of detail, and each is drawn in one instanced batch. -// In the shadow pass only those near the camera are drawn, their shadows falling where they can be seen. +// Gathers the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or +// of a whole model, kept by its track relative to the origin of the track (and with walk_timbers_gpu() on the gpu as well). The runs +// of beams in range and in sight are gathered by the model and its part or its level of detail, for all the cells of the pass, which +// Flush_Timbers() draws each in one instanced batch. In the shadow pass only those near the camera are drawn, their shadows falling +// where they can be seen. void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ) { if( Global.SleeperDistance <= 0.f ) { return; } @@ -1511,19 +1516,21 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato track_sleepers::release_far( camera, range + 600.0 ); } } - glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); - for( auto &bucket : m_timberbuckets ) { - bucket.instances.clear(); + if( m_timberlayframe != m_framestamp ) { + m_timberlayframe = m_framestamp; + m_timberlaying = 0.0; } - auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> std::vector & { + // the fastenings laid out to a little past where they're drawn + auto const fasteningrange = std::min( static_cast( Global.SleeperDistance ), track_sleepers::fastening_range ) + track_sleepers::fastening_lead; + auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> timber_bucket & { for( auto &bucket : m_timberbuckets ) { if( bucket.model == Model && bucket.part == Part && bucket.band == Band ) { - return bucket.instances; + return bucket; } } - m_timberbuckets.push_back( { Model, Part, Band, {} } ); - return m_timberbuckets.back().instances; }; - // the level of detail of the whole model a beam is seen at, from its distance + m_timberbuckets.push_back( { Model, Part, Band, {}, {}, 0, 0 } ); + return m_timberbuckets.back(); }; + // the level of detail of the whole model a run of beams is seen at, from its distance auto const band_of = []( track_sleepers::beam_model const &Model, double const Distance ) { auto const squaredistance { static_cast( Distance * Distance ) }; auto band { Model.bands.empty() ? 0.f : Model.bands.front() }; @@ -1533,27 +1540,14 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato } } return band; }; - // the beams of a run made of a whole model, each at the level of detail of its own distance; the run is culled as one - auto const put_by_band = [&]( track_sleepers::beam_model const &Model, std::vector const &Transforms, std::uint32_t const From, std::uint32_t const To, glm::dvec3 const &Origin ) { - std::vector *instances { nullptr }; - auto current { 0.f }; - for( auto index = From; index < To; ++index ) { - auto const &local = Transforms[ index ]; - auto const band = band_of( Model, glm::length( Origin + glm::dvec3( glm::vec3( local[ 3 ] ) ) - camera ) ); - if( instances == nullptr || band != current ) { - // looked up again only as the band changes; a bucket added on the way moves the others, so the pointer goes with it - instances = &bucket_for( &Model, -1, band ); - current = band; - } - instances->emplace_back( originmodelview * local ); - } - }; + m_timbertracks.clear(); for( auto first { First }; first != Last; ++first ) { auto *track = *first; if( false == track->m_visible || false == track->m_sleepers3d.active ) { continue; } - if( glm::length( track->location() - camera ) > range + track->radius() ) { + auto const trackdistance = glm::length( track->location() - camera ); + if( trackdistance > range + track->radius() ) { // worked out ahead, in the background, before it comes in range - if( false == shadows && track->m_sleepers3d.dirty && glm::length( track->location() - camera ) < range + track->radius() + 300.0 ) { + if( false == shadows && track->m_sleepers3d.dirty && trackdistance < range + track->radius() + 300.0 ) { if( gfx::walk_sync::parallel() ) { m_deferredprefetch.push_back( track ); } @@ -1563,17 +1557,61 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato } continue; } - if( track->m_sleepers3d.dirty ) { - // laid by one walk at a time (walk_sync) - gfx::walk_sync::change change; - if( track->m_sleepers3d.dirty ) { - track_sleepers::lay( *track ); + auto const fastened = ( trackdistance < fasteningrange + track->radius() ); + if( track->m_sleepers3d.dirty || ( fastened && false == track->m_sleepers3d.fastened ) ) { + // the nearest at once, the others while the frame has the time, the cells nearest coming first; till then a track draws + // what it had + if( trackdistance < track_sleepers::lay_near + track->radius() || m_timberlaying < track_sleepers::lay_budget ) { + // laid by one walk at a time (walk_sync) + gfx::walk_sync::change change; + auto const start = std::chrono::steady_clock::now(); + if( track->m_sleepers3d.dirty ) { + track_sleepers::lay( *track, fastened ); + } + else if( fastened && false == track->m_sleepers3d.fastened ) { + track_sleepers::fasten( *track ); + } + m_timberlaying += std::chrono::duration( std::chrono::steady_clock::now() - start ).count(); } } + if( track->m_sleepers3d.timbers == nullptr || track->m_sleepers3d.chunks.empty() ) { continue; } + m_timbertracks.push_back( track ); + } + if( m_timbertracks.empty() ) { return; } + auto const gpu = walk_timbers_gpu(); + // the instances of the tracks on the gpu; those of the cells before expanded first when the buffer is to be started over + if( gpu && false == walk_timbers_place( m_timbertracks, false ) ) { + Flush_Timbers(); + walk_timbers_place( m_timbertracks, true ); + } + // the runs of them in range and in sight, turned by the modelview of the cell: on the gpu, or here + glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); + auto const origin { static_cast( m_timberorigins.size() ) }; + if( gpu ) { + m_timberorigins.push_back( originmodelview ); + } + // a run of instances of a track for the bucket; the reference to it goes with the next bucket added + auto const add_run = [&]( timber_bucket &Bucket, std::vector const &Transforms, std::uint32_t const From, std::uint32_t const Source, std::uint32_t const Count ) { + if( gpu ) { + Bucket.runs.push_back( { Source, Count, 0, origin } ); + } + else { + for( auto index = From; index < From + Count; ++index ) { + Bucket.instances.emplace_back( originmodelview * Transforms[ index ] ); + } + } + Bucket.count += Count; }; + for( auto *track : m_timbertracks ) { auto const &laid = track->m_sleepers3d; - if( laid.timbers == nullptr ) { continue; } - // the transforms of the beams are relative to the origin of the track, which the modelview is translated to already - auto const &trackmodelview = originmodelview; + // where the instances of the parts, of the whole timbers and of the plain sleepers of the track start on the gpu + std::array partfirst; + auto next { gpu ? walk_timbers_first( *track ) : 0 }; + for( int part = 0; part < track_sleepers::part_count; ++part ) { + partfirst[ part ] = next; + next += static_cast( laid.instances[ part ].size() ); + } + auto const wholefirst { next }; + auto const plainfirst { wholefirst + static_cast( laid.whole.size() ) }; for( auto const &chunk : laid.chunks ) { auto const centre = track->m_origin + glm::dvec3( chunk.centre ); auto const distance = glm::length( centre - camera ); @@ -1581,64 +1619,100 @@ void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterato 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 ) { - 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 ] ); + 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; } + add_run( bucket_for( laid.timbers, part, 0.f ), laid.instances[ part ], chunk.first[ part ], partfirst[ part ] + chunk.first[ part ], chunk.last[ part ] - chunk.first[ part ] ); + } + } + // the beams made of a whole model at the level of detail of the distance of the run + if( chunk.wholelast > chunk.wholefirst ) { + add_run( bucket_for( laid.timbers, -1, band_of( *laid.timbers, distance ) ), laid.whole, chunk.wholefirst, wholefirst + chunk.wholefirst, chunk.wholelast - chunk.wholefirst ); + } + if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { + add_run( bucket_for( laid.plains, -1, band_of( *laid.plains, distance ) ), laid.plain, chunk.plainfirst, plainfirst + chunk.plainfirst, chunk.plainlast - chunk.plainfirst ); + } + } + } +} + +// the sleepers and timbers gathered by Render_Timbers() over the cells of the pass placed in the instance buffer of the frame at once +// (with walk_timbers_gpu() made into modelviews on the gpu there), and each bucket drawn in one instanced batch +void scene_walk::Flush_Timbers() +{ + std::size_t total { 0 }; + for( auto const &bucket : m_timberbuckets ) { + total += bucket.count; + } + if( total > 0 ) { + // in the instance buffer of the frame bucket after bucket, as in Render_Sleepers(), which the recorded walk of the depth + // pass keeps for its replay as well + if( walk_timbers_gpu() ) { + auto target { walk_instances_reserve( total ) }; + m_timberranges.clear(); + for( auto &bucket : m_timberbuckets ) { + bucket.first = target; + for( auto const &run : bucket.runs ) { + m_timberranges.push_back( { run.source, run.count, target, run.origin } ); + target += run.count; + } + } + walk_timbers_expand( m_timberorigins, m_timberranges ); + } + else { + for( auto &bucket : m_timberbuckets ) { + if( bucket.count == 0 ) { continue; } + bucket.first = walk_instances( bucket.instances.data(), bucket.instances.size() ); + } + } + auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); + for( auto &bucket : m_timberbuckets ) { + if( bucket.count == 0 ) { continue; } + auto const &beammodel = *bucket.model; + auto *model = beammodel.model; + // the parts of the model shown and hidden for the bucket, held till it's drawn (walk_sync) + gfx::walk_sync::model_hold hold { model }; + 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 ) { + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); } } } - if( chunk.wholelast > chunk.wholefirst ) { - put_by_band( *laid.timbers, laid.whole, chunk.wholefirst, chunk.wholelast, track->m_origin ); + material_data skins {}; + if( beammodel.skin != null_handle ) { + skins.replacable_skins[ 1 ] = beammodel.skin; } - if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { - put_by_band( *laid.plains, laid.plain, chunk.plainfirst, chunk.plainlast, track->m_origin ); + ::glPushMatrix(); + ::glLoadIdentity(); + m_current_instance_count = bucket.count; + m_current_instance_base = bucket.first; + // inside the range of the level of detail the beams are at + model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); + model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); + model->Root->pRoot = model; + Render( model->Root ); + m_current_instance_count = 0; + m_current_instance_base = 0; + ::glPopMatrix(); + ++m_renderpass.draw_stats.instanced_drawcalls; + if( bucket.part >= 0 ) { + for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = 1; + } + } } } + m_renderpass.draw_stats.instances += static_cast( total ); } - - auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); - std::size_t total { 0 }; for( auto &bucket : m_timberbuckets ) { - auto const &instances = bucket.instances; - if( instances.empty() ) { continue; } - total += instances.size(); - auto const &beammodel = *bucket.model; - auto *model = beammodel.model; - // the parts of the model shown and hidden for the bucket, held till it's drawn (walk_sync) - gfx::walk_sync::model_hold hold { model }; - 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 ); - } - } - material_data skins {}; - if( beammodel.skin != null_handle ) { - skins.replacable_skins[ 1 ] = beammodel.skin; - } - // the instances go to the instance stream of the frame, as in Render_Sleepers(), which the recorded walk of the depth - // pass keeps for its replay as well - auto const baseinstance { walk_instances( instances.data(), instances.size() ) }; - ::glPushMatrix(); - ::glLoadIdentity(); - m_current_instance_count = instances.size(); - m_current_instance_base = baseinstance; - // inside the range of the level of detail the beams are at - model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); - model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); - model->Root->pRoot = model; - Render( model->Root ); - m_current_instance_count = 0; - m_current_instance_base = 0; - ::glPopMatrix(); - ++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; - } - } + bucket.instances.clear(); + bucket.runs.clear(); + bucket.count = 0; } - m_renderpass.draw_stats.instances += static_cast( total ); + m_timberorigins.clear(); } bool scene_walk::Render(TDynamicObject *Dynamic) diff --git a/rendering/common/scenewalk.h b/rendering/common/scenewalk.h index d4c6a1c5..b6f3c3de 100644 --- a/rendering/common/scenewalk.h +++ b/rendering/common/scenewalk.h @@ -168,9 +168,11 @@ class scene_walk // no-op if the track has no sleepermodel, Global.SleeperDistance is 0, or the camera is beyond // Global.SleeperDistance from the track origin. void Render_Sleepers(TTrack *Track); - // renders the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of date. - // the parts of the model are drawn in instanced batches for the whole run of tracks, the runs of beams out of sight left out + // gathers the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of + // date. the parts of the model are drawn in instanced batches for all the cells of the pass, the runs of beams out of sight left out void Render_Timbers(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); + // draws what Render_Timbers() gathered in the pass since the last time + void Flush_Timbers(); bool Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha = false); bool Render_interior(bool const Alpha = false); bool Render_lowpoly(TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha = false); @@ -234,6 +236,32 @@ class scene_walk virtual void walk_draw(std::vector::iterator First, std::vector::iterator Last) = 0; // places the modelview matrices of instances in the instance buffer of the frame. returns: base instance of the first of them virtual std::uint32_t walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) = 0; + // the instances of the sleepers and timbers of the tracks kept by the backend on the gpu, and the runs of them drawn made into the + // modelviews of the instance buffer of the frame there (the opengl renderer, opengl46_timbers); by default the walk turns them on + // the cpu and places them with walk_instances() + virtual bool walk_timbers_gpu() const + { + return false; + } + // the instances of the tracks placed on the gpu; false when they don't fit and Restart is false: the runs gathered before are to + // be drawn first + virtual bool walk_timbers_place(std::vector const &Tracks, bool const Restart) + { + return true; + } + // where the instances of a placed track lie on the gpu: the first of the parts in turn, then of the whole timbers, then of the + // plain sleepers + virtual std::uint32_t walk_timbers_first(TTrack const &Track) const + { + return 0; + } + // the room for Count modelviews in the instance buffer of the frame, for the gpu to write. returns: base instance of the first + virtual std::uint32_t walk_instances_reserve(std::size_t const Count) + { + return 0; + } + // the runs written to the instance buffer of the frame, each as the modelview of its origin times its transforms + virtual void walk_timbers_expand(std::vector const &Origins, std::vector const &Ranges) {} virtual void walk_cull_face(bool const Enabled) = 0; // the glare of a lamp: drawn without the depth test, added to what is there; drawn only where the point of the light was // seen (walk_light_query() of the last frame, when the backend has it) @@ -343,21 +371,29 @@ class scene_walk gl::model_ubs model_ubs; unsigned int m_framestamp{0}; // id of currently rendered gfx frame unsigned int m_timbersweepframe{0}; // the frame the beams of the tracks far away were last let go in + unsigned int m_timberlayframe{0}; // the frame the time spent laying the beams is counted for + double m_timberlaying{0.0}; // in ms float m_fogrange{2000.0f}; // when > 0, the walk is inside a Render_Instanced() call: the draws are made for this many instances from // m_current_instance_base onwards. The vertex shader reads per-instance modelview from the instance buffer std::size_t m_current_instance_count{0}; std::uint32_t m_current_instance_base{0}; - // camera-space transforms of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of - // detail; kept from frame to frame to reuse their allocations + // the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of detail: their camera-space + // transforms, or with walk_timbers_gpu() the runs of their instances on the gpu; kept from frame to frame to reuse their allocations struct timber_bucket { track_sleepers::beam_model const *model{nullptr}; int part{-1}; // of a model put together from parts; -1: the whole model float band{0.f}; // the squared distance the level of detail of the whole model is seen from std::vector instances; + std::vector runs; + std::uint32_t count{0}; // instances of the runs + std::uint32_t first{0}; // base instance of their modelviews in the instance buffer of the frame }; std::vector m_timberbuckets; + std::vector m_timbertracks; // the tracks of a cell with beams to draw + std::vector m_timberranges; // the runs of all the buckets, as they're expanded + std::vector m_timberorigins; // the modelviews of the cells of the runs // persistent scratch buffer for Render_Instanced(): holds the per-instance // camera-space root modelview matrices for the batch currently being built. // Kept as a member rather than a function-local so its heap allocation is diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 55c3eda9..55959a4b 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -213,6 +213,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) for (auto &shadowcull : m_shadowcull) shadowcull.init(&m_instancecull); m_hiz.init(); + m_timbers.init(); m_shadowfit.init(); m_exposure.init(); m_ao.init(); @@ -718,6 +719,7 @@ bool opengl46_renderer::Render() // the same for the model blocks of the draws m_instancestream->next_frame(); Bind_Instance_Stream(); + m_timbers.next_frame(); model_ubo->next_frame(); if (m_drawblockstream) m_drawblockstream->next_frame(); @@ -3604,6 +3606,16 @@ std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Mo return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); } +std::uint32_t opengl46_renderer::Reserve_Instance_Modelviews( std::size_t const Count ) +{ + auto const generation { m_instancestream->generation() }; + auto const offset { m_instancestream->reserve( static_cast( Count * sizeof( glm::mat4 ) ) ) }; + if( m_instancestream->generation() != generation ) { + Bind_Instance_Stream(); + } + return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); +} + void opengl46_renderer::Bind_Instance_Stream() const { GLuint const buffer { m_instancestream->buffer() }; @@ -4995,6 +5007,31 @@ std::uint32_t opengl46_renderer::walk_instances(glm::mat4 const *Modelviews, std return Upload_Instance_Modelviews(Modelviews, Count); } +bool opengl46_renderer::walk_timbers_gpu() const +{ + return true; +} + +bool opengl46_renderer::walk_timbers_place(std::vector const &Tracks, bool const Restart) +{ + return m_timbers.place(Tracks, Restart); +} + +std::uint32_t opengl46_renderer::walk_timbers_first(TTrack const &Track) const +{ + return m_timbers.first(Track); +} + +std::uint32_t opengl46_renderer::walk_instances_reserve(std::size_t const Count) +{ + return Reserve_Instance_Modelviews(Count); +} + +void opengl46_renderer::walk_timbers_expand(std::vector const &Origins, std::vector const &Ranges) +{ + m_timbers.expand(Origins, Ranges, m_instancestream->buffer()); +} + void opengl46_renderer::walk_cull_face(bool const Enabled) { if (Enabled) diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index c4e08b1b..788e6b80 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -47,6 +47,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46skyocclusion.h" #include "rendering/opengl46/opengl46instancecull.h" #include "rendering/opengl46/opengl46hiz.h" +#include "rendering/opengl46/opengl46timbers.h" #include "rendering/opengl46/opengl46shadowfit.h" #include "rendering/common/scenewalk.h" #include "rendering/common/temporalaa.h" @@ -217,6 +218,12 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { void walk_draw(gfx::geometry_handle const &Geometry) override; void walk_draw(std::vector::iterator First, std::vector::iterator Last) override; std::uint32_t walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) override; + // the instances of the sleepers and timbers of the tracks on the gpu (m_timbers) + bool walk_timbers_gpu() const override; + bool walk_timbers_place(std::vector const &Tracks, bool const Restart) override; + std::uint32_t walk_timbers_first(TTrack const &Track) const override; + std::uint32_t walk_instances_reserve(std::size_t const Count) override; + void walk_timbers_expand(std::vector const &Origins, std::vector const &Ranges) override; void walk_cull_face(bool const Enabled) override; void walk_glare(bool const Enabled) override; void walk_glare_draw(TSubModel *Submodel, gfx::geometry_handle const &Geometry) override; @@ -348,6 +355,8 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { bool Render(world_environment *Environment); // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them std::uint32_t Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ); + // the room for Count modelviews in the instance buffer of the frame, for the gpu to write. returns: base instance of the first + std::uint32_t Reserve_Instance_Modelviews( std::size_t const Count ); // binds the instance buffer of the frame for the vertex shaders (INSTANCE_SSBO_BINDING) void Bind_Instance_Stream() const; // draws the instances queued in the pass from the result of the gpu culling (Global.gfx_gpucull 1): makes the depth @@ -600,6 +609,7 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { int m_shadowcascade { -1 }; // the region is drawn for this cascade of the sun shadows // depth pyramid for the occlusion test of the gpu culling opengl46_hiz m_hiz; + opengl46_timbers m_timbers; // the instances of the sleepers and timbers of the tracks on the gpu // the cascades of the sun shadows fitted to the depth of the main view (Global.gfx_shadow_sdsm) opengl46_shadow_fit m_shadowfit; // with a cascade fitted: the corners of the area its map is to cover, for setup_pass() diff --git a/rendering/opengl46/opengl46timbers.cpp b/rendering/opengl46/opengl46timbers.cpp new file mode 100644 index 00000000..e6d122ff --- /dev/null +++ b/rendering/opengl46/opengl46timbers.cpp @@ -0,0 +1,152 @@ +/* +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 "rendering/opengl46/opengl46timbers.h" + +#include "utilities/Globals.h" +#include "world/Track.h" + +#include + +namespace { + +// the buffer to start with, and how soon after being started over it's too small for the tracks in sight +std::uint32_t constexpr initial_capacity{65536}; +unsigned int constexpr restart_frames{600}; + +// the instances of a track, in the order they lie in the buffer +std::uint32_t count_of(track_sleepers::laid const &Laid) +{ + std::size_t count{Laid.whole.size() + Laid.plain.size()}; + for (auto const &instances : Laid.instances) + count += instances.size(); + return static_cast(count); +} + +} // namespace + +opengl46_timbers::~opengl46_timbers() +{ + if (m_buffer != 0) + glDeleteBuffers(1, &m_buffer); +} + +void opengl46_timbers::init() +{ + gl::shader shader("timber_instances.comp"); + m_program = std::make_unique(std::vector>{shader}); + // the modelviews of the origins and the runs, both placed at whole matrices + m_ranges = std::make_unique( + static_cast(4096 * sizeof(gl::timber_range)), static_cast(sizeof(glm::mat4)), Global.gfx_bufferstreaming); + restart(0); +} + +void opengl46_timbers::restart(std::uint32_t const Needed) +{ + auto capacity{std::max(m_capacity, initial_capacity)}; + if ((m_capacity > 0) && (m_frame - m_restartframe < restart_frames)) + capacity *= 2; + while (capacity < 2 * Needed) + capacity *= 2; + // the draws and passes issued with the old buffer keep it until the gpu is done with them + if (m_buffer != 0) + glDeleteBuffers(1, &m_buffer); + glCreateBuffers(1, &m_buffer); + glNamedBufferStorage(m_buffer, static_cast(capacity) * static_cast(sizeof(glm::mat4)), nullptr, GL_DYNAMIC_STORAGE_BIT); + m_capacity = capacity; + m_fill = 0; + m_restartframe = m_frame; + m_tracks.clear(); +} + +bool opengl46_timbers::place(std::vector const &Tracks, bool const Restart) +{ + auto const placed_now = [&](TTrack const &Track) { + auto const found{m_tracks.find(&Track)}; + return (found != m_tracks.end()) && (found->second.made == Track.m_sleepers3d.made); + }; + std::uint32_t needed{0}; + std::uint32_t all{0}; + for (auto const *track : Tracks) + { + auto const count{count_of(track->m_sleepers3d)}; + all += count; + if (false == placed_now(*track)) + needed += count; + } + if (needed == 0) + return true; + if (m_fill + needed > m_capacity) + { + if (false == Restart) + return false; + restart(all); + } + for (auto const *track : Tracks) + { + if (placed_now(*track)) + continue; + auto const &laid{track->m_sleepers3d}; + auto const first{m_fill}; + auto const send = [&](std::vector const &Instances) { + if (false == Instances.empty()) + glNamedBufferSubData(m_buffer, static_cast(m_fill) * static_cast(sizeof(glm::mat4)), + static_cast(Instances.size() * sizeof(glm::mat4)), Instances.data()); + m_fill += static_cast(Instances.size()); + }; + for (auto const &instances : laid.instances) + send(instances); + send(laid.whole); + send(laid.plain); + m_tracks[track] = {laid.made, first}; + } + return true; +} + +std::uint32_t opengl46_timbers::first(TTrack const &Track) const +{ + auto const found{m_tracks.find(&Track)}; + return (found != m_tracks.end() ? found->second.first : 0); +} + +void opengl46_timbers::expand(std::vector const &Origins, std::vector const &Ranges, GLuint const Output) +{ + if (Ranges.empty()) + return; + // sent together, in one buffer of the stream + auto const originbytes{Origins.size() * sizeof(glm::mat4)}; + m_upload.resize(originbytes + Ranges.size() * sizeof(gl::timber_range)); + std::memcpy(m_upload.data(), Origins.data(), originbytes); + std::memcpy(m_upload.data() + originbytes, Ranges.data(), Ranges.size() * sizeof(gl::timber_range)); + auto const origins{m_ranges->push(m_upload.data(), static_cast(m_upload.size()))}; + auto const ranges{origins + static_cast(originbytes)}; + GLuint const buffers[]{m_buffer, m_ranges->buffer()}; + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_LOCAL_SSBO_BINDING, 1, &buffers[0]); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_RANGE_SSBO_BINDING, 1, &buffers[1]); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_OUTPUT_SSBO_BINDING, 1, &Output); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_ORIGIN_SSBO_BINDING, 1, &buffers[1]); + // a dispatch takes at most 65535 work groups + auto const first{static_cast(ranges / static_cast(sizeof(gl::timber_range)))}; + auto const origin{static_cast(origins / static_cast(sizeof(glm::mat4)))}; + for (std::size_t done = 0; done < Ranges.size();) + { + auto const count{std::min(Ranges.size() - done, 65535)}; + ::glProgramUniform4ui(*m_program, 0, first + static_cast(done), origin, 0, 0); + m_program->dispatch(static_cast(count)); + done += count; + } + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); +} + +void opengl46_timbers::next_frame() +{ + ++m_frame; + m_ranges->next_frame(); +} diff --git a/rendering/opengl46/opengl46timbers.h b/rendering/opengl46/opengl46timbers.h new file mode 100644 index 00000000..5790e8f2 --- /dev/null +++ b/rendering/opengl46/opengl46timbers.h @@ -0,0 +1,66 @@ +/* +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 "gl/shader.h" +#include "gl/streambuffer.h" +#include "gl/ubo.h" + +class TTrack; + +// the instances of the timbers and sleepers of the tracks (track_sleepers::laid) kept on the gpu, those of a track sent once as +// it's laid, and the runs of them drawn made into the modelviews of the instance buffer of the frame by a compute pass +// (timber_instances.comp): for a run the cpu gives only where it lies and where it goes, not its matrices +class opengl46_timbers +{ + public: + opengl46_timbers() = default; + opengl46_timbers(opengl46_timbers const &) = delete; + opengl46_timbers &operator=(opengl46_timbers const &) = delete; + ~opengl46_timbers(); + + // throws gl::shader_exception + void init(); + // the instances of the tracks on the gpu, sent again for a track laid anew since; when those to send don't fit in what's left + // of the buffer, it's started over for all the tracks, a bigger one when it was started over not long before. without + // Restart nothing is sent then, and false returned: the runs of the instances placed before are to be expanded first + bool place(std::vector const &Tracks, bool const Restart); + // where the instances of a track placed lie in the buffer: the first of the parts in turn, then of the whole timbers, then of + // the plain sleepers + std::uint32_t first(TTrack const &Track) const; + // the runs written to the instance buffer Output, each as the modelview of its origin times its transforms, for the draws after + void expand(std::vector const &Origins, std::vector const &Ranges, GLuint const Output); + void next_frame(); + + private: + struct placed + { + std::uint64_t made; // the making of the instances of the track sent + std::uint32_t first; + }; + + // a new buffer, room for Needed instances at least, the tracks to be sent again + void restart(std::uint32_t const Needed); + + std::unique_ptr m_program; + std::unique_ptr m_ranges; + GLuint m_buffer{0}; + std::uint32_t m_capacity{0}; // in instances + std::uint32_t m_fill{0}; + unsigned int m_frame{0}; + unsigned int m_restartframe{0}; + std::unordered_map m_tracks; + std::vector m_upload; // the origins and the runs of an expand(), as they're sent +}; diff --git a/shaders/ogl46/common.glsl b/shaders/ogl46/common.glsl index 5d626761..9c0a2ada 100644 --- a/shaders/ogl46/common.glsl +++ b/shaders/ogl46/common.glsl @@ -43,6 +43,11 @@ #define HIZ_GROUP_SIZE 8 #define SHADOW_FIT_GROUP_SIZE 16 #define SHADOW_FIT_SSBO_BINDING 9 +#define TIMBER_GROUP_SIZE 64 +#define TIMBER_LOCAL_SSBO_BINDING 9 +#define TIMBER_RANGE_SSBO_BINDING 11 +#define TIMBER_OUTPUT_SSBO_BINDING 12 +#define TIMBER_ORIGIN_SSBO_BINDING 13 #define PARTICLE_GROUP_SIZE 64 #define PARTICLE_SSBO_BINDING 9 #define PARTICLE_SOURCE_SSBO_BINDING 10 diff --git a/shaders/ogl46/timber_instances.comp b/shaders/ogl46/timber_instances.comp new file mode 100644 index 00000000..cf33d016 --- /dev/null +++ b/shaders/ogl46/timber_instances.comp @@ -0,0 +1,48 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the modelviews of the instances of the timbers and sleepers of the tracks drawn in a pass (opengl46_timbers): their transforms +// are kept relative to the origins of the tracks, each run of them is turned by the modelview of the origin of its cell and written +// to the instance buffer of the frame, where the instanced draws read it. a work group a run + +#include "common.glsl" + +layout(local_size_x = TIMBER_GROUP_SIZE) in; + +struct timber_range +{ + uint source; + uint count; + uint target; + uint origin; +}; + +layout(std430, binding = TIMBER_LOCAL_SSBO_BINDING) readonly buffer timber_local_ssbo +{ + mat4 timber_local[]; +}; + +layout(std430, binding = TIMBER_RANGE_SSBO_BINDING) readonly buffer timber_range_ssbo +{ + timber_range timber_ranges[]; +}; + +layout(std430, binding = TIMBER_OUTPUT_SSBO_BINDING) writeonly buffer timber_output_ssbo +{ + mat4 timber_output[]; +}; + +layout(std430, binding = TIMBER_ORIGIN_SSBO_BINDING) readonly buffer timber_origin_ssbo +{ + mat4 timber_origins[]; +}; + +// x: the first run of the dispatch, y: the first modelview of the origins +layout(location = 0) uniform uvec4 params; + +void main() +{ + timber_range range = timber_ranges[params.x + gl_WorkGroupID.x]; + mat4 origin = timber_origins[params.y + range.origin]; + for (uint index = gl_LocalInvocationID.x; index < range.count; index += TIMBER_GROUP_SIZE) + timber_output[range.target + index] = origin * timber_local[range.source + index]; +} diff --git a/world/RailCarve.cpp b/world/RailCarve.cpp index 59c5a392..3653e1b1 100644 --- a/world/RailCarve.cpp +++ b/world/RailCarve.cpp @@ -26,6 +26,7 @@ double constexpr window { 0.25 }; // how far around a station with a rail near t double constexpr nearby { 0.4 }; // how far along past the ends of a rail around it's near enough to be looked at closer double constexpr across_margin { 0.05 }; // and how far across from its reach, more where it runs at an angle double constexpr outside { 10.0 }; // how far past the ends of the track a part is looked at for where it ends +double constexpr joint_gap { 0.01 }; // how far past its ends the flangeway of a rail around keeps ours out, over the gap of a joint double constexpr slack { 0.0002 }; // how far a head reaches into a flangeway or another head unnoticed, and how far two heads planed against each other are kept apart double constexpr point_width { 0.001 }; // a wing rail ends where less than this is left of its head double constexpr blunt_width { 0.5 * nose_tip }; // the legs of a nose and the points of an obtuse crossing begin where this much is left of their heads @@ -251,7 +252,7 @@ bool continues( carve_rail const &A, carve_rail const &B ) { } for( int a = 0; a < 2; ++a ) { for( int b = 0; b < 2; ++b ) { - if( glm::length( plan( A.ends[ a ] - B.ends[ b ] ) ) < 0.05 && std::abs( glm::dot( A.directions[ a ], B.directions[ b ] ) ) > 0.99 ) { + if( glm::length( plan( A.ends[ a ] - B.ends[ b ] ) ) < 0.05 && std::abs( glm::dot( A.directions[ a ], B.directions[ b ] ) ) > 0.999 ) { return true; } } @@ -529,12 +530,13 @@ double run_of( Value const &Bound, double const At ) { } // how a run of stations along a part, where it runs into a flangeway or a head, planes it: from the side its head leaves that at the end of the -// run nearer the middle of the part; running into it all the way, from the side taking less of it where it takes least +// run nearer the middle of the part; running into it all the way, from the side taking less of it where it takes least; Outer, always from +// the outer side template -void settle_runs( std::vector const &Hits, long const First, double const Middle, Apply const &Applied ) { +void settle_runs( std::vector const &Hits, long const First, double const Middle, Apply const &Applied, bool const Outer = false ) { auto const upper_at = [&]( std::size_t const I ) { - return Hits[ I ].removed[ 0 ] <= Hits[ I ].removed[ 1 ]; }; + return Outer || Hits[ I ].removed[ 0 ] <= Hits[ I ].removed[ 1 ]; }; std::size_t i { 0 }; while( i < Hits.size() ) { if( false == Hits[ i ].in ) { @@ -586,7 +588,7 @@ band_hit band_at( station const &Station, carve_rail const &Ours, carve_rail con auto const centre { Station.offset + 0.5 * Ours.size.head }; glm::dvec2 outwards; auto const lateral { project( Theirs, across( Station, centre ), Distance, outwards ) }; - if( Distance < 0.0 || Distance > Theirs.length ) { + if( Distance < -joint_gap || Distance > Theirs.length + joint_gap ) { return hit; } // where the blades of a switch are, the rails of both paths on a side lie at each other as they're made to @@ -614,6 +616,25 @@ band_hit band_at( station const &Station, carve_rail const &Ours, carve_rail con return hit; } +// the part is the point of an obtuse crossing and theirs the knuckle it lies at, the rest of its head under the knuckle: the point keeps the +// gauge side of its head, which tracks the crossing is split among +bool beside_knuckle( carve_part const &Part, carve_rail const &Theirs ) { + + for( int end = 0; end < 2; ++end ) { + if( false == Part.obtuse[ end ] || Part.kinds[ end ] != rail_end::blunt ) { + continue; + } + for( auto const &other : Theirs.parts ) { + for( int otherend = 0; otherend < 2; ++otherend ) { + if( other.obtuse[ otherend ] && glm::length( Part.marks[ end ] - other.marks[ otherend ] ) < 0.1 ) { + return true; + } + } + } + } + return false; +} + // the part kept out of the flangeways of the rails around void trim( carve_rail &Rail, carve_part &Part ) { @@ -633,10 +654,12 @@ void trim( carve_rail &Rail, carve_part &Part ) { std::vector lower( scan.count(), -unbounded ); auto const around { around_of( *Rail.track ) }; for( auto *other : around ) { + design( *other, nullptr ); for( auto &theirs : track_of( *other ).rails ) { if( false == flangeway_counts( Rail, Part, theirs ) ) { continue; } + auto const knuckle { beside_knuckle( Part, theirs ) }; auto const windows { scan.stations_near( theirs, [&]( double ) { return glm::dvec2 { -flangeway - 0.5 * size.head, 0.5 * size.head }; }, { -nearby, theirs.length + nearby }, flangeway + size.head + nearby ) }; for( auto const &[ a, b ] : windows ) { if( debugging( Rail ) ) { @@ -657,7 +680,7 @@ void trim( carve_rail &Rail, carve_part &Part ) { } else { lower[ index ] = std::max( lower[ index ], hits[ I ].lower ); - } } ); + } }, knuckle ); } } } @@ -671,6 +694,7 @@ void trim( carve_rail &Rail, carve_part &Part ) { struct body_hit { bool in { false }; bool heads { false }; // or only the feet of two rails back to back + bool faces { false }; // the feet of two rails face to face across a flangeway, ourfoot and theirfoot how far in they reach double removed[ 2 ] {}; glm::dvec2 ours { 0.0 }; glm::dvec2 theirs { 0.0 }; @@ -693,7 +717,8 @@ struct body_hit { // the bounds of ours where its head lies under theirs, Above, or over it: the two meet in the middle of where they lie over each other, as far as // the wheels of each leave room; a head giving way runs on past the meeting over the rounded shoulder of the other one, facing it with its outer edge. -// Two rails back to back meet with their feet in the middle between them too, and the wheels of theirs roll on ours planed flush against it +// Two rails back to back meet with their feet in the middle between them too, and the wheels of theirs roll on ours planed flush against it; +// two face to face, as a wing rail and the leg of the nose, with their feet in the middle of the flangeway, the heads left whole void settle( body_hit const &Hit, bool const Above, double &Upper, double &Lower, double &Worn ) { auto const slant { std::max( std::abs( Hit.facing ), cos_wide ) }; @@ -713,6 +738,11 @@ void settle( body_hit const &Hit, bool const Above, double &Upper, double &Lower // as far from both bounds, whichever way they run: the same line from either rail auto const between = []( bound const &A, bound const &B ) { return bound { ( A.at * A.slant + B.at * B.slant ) / ( A.slant + B.slant ), 1.0 }; }; + if( Hit.faces ) { + auto const feet { between( { Hit.ourfoot, Hit.footslant.x }, { Hit.theirfoot, Hit.footslant.y } ).at }; + Lower = std::max( Lower, std::min( feet + slack, Hit.ours.x - slack ) ); + return; + } auto const a { higher( { Hit.ours.x, Hit.ourslant.x }, { Hit.theirs.x, Hit.theirslant.x } ) }; auto const b { lower( { Hit.ours.y, Hit.ourslant.y }, { Hit.theirs.y, Hit.theirslant.y } ) }; auto const overlap { Hit.heads && b.at > a.at }; @@ -860,6 +890,7 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { // how the bounds of ours run, once something of theirs is in their way glm::dvec2 ourslant { 1.0 }; auto footslant { 1.0 }; + auto innerslant { 1.0 }; auto slanted { false }; if( theirs.track == Rail.track && theirs.side == Rail.side && distance < theirs.heel + heel_margin ) { continue; @@ -870,18 +901,19 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { continue; } trim( theirs, part ); - // the head of theirs and how far out its profile reaches, out from its gauge line: the bound Which (from, to, out to) - // and how it runs on + // the head of theirs and how far its profile reaches, out from its gauge line: the bound Which (from, to, out to, the + // crown, in to) and how it runs on auto const trimmed { distance >= part.trimfrom && distance <= part.trimto }; auto const theirclearup { theirsize.high + 0.01 }; auto const theirclearlow { theirsize.low - 0.01 }; auto const bound = [&]( int const Which ) { return [&, Which]( double const At, bool &Bounding ) { auto const offset { knots::sample( part.carve, At, 0.0 ) }; - if( Which == 0 ) { + if( Which == 0 || Which == 4 ) { + auto const reach { Which == 0 ? 0.0 : theirsize.low }; auto const theirlow { trimmed ? knots::sample( part.lower, At, theirclearlow ) : theirclearlow }; - Bounding = theirlow > 0.0; - return offset + std::max( 0.0, theirlow ); + Bounding = theirlow > reach; + return offset + std::max( reach, theirlow ); } auto const reach { Which == 1 ? theirsize.head : Which == 2 ? theirsize.high : theirsize.head - theirsize.shoulder }; auto const theirup { trimmed ? knots::sample( part.upper, At, theirclearup ) : theirclearup }; @@ -905,16 +937,18 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { glm::dvec2 const heads { std::min( m0, m1 ), std::max( m0, m1 ) }; auto const overlap { std::min( ours.y, heads.y ) - std::max( ours.x, heads.x ) }; auto const above { heads.x + heads.y > ours.x + ours.y }; - auto const ourfoot { here.offset + std::min( size.high, up ) }; auto const backs { facing < 0.0 && above }; - auto const theirfoot { ( overlap > 2.0 * slack || backs ) ? meets( bound( 2 ) ) : 0.0 }; - auto const feet { backs && ourfoot > theirfoot + 2.0 * slack }; + auto const faces { facing < 0.0 && false == above && overlap <= 2.0 * slack }; + auto const ourfoot { faces ? here.offset + std::max( size.low, low ) : here.offset + std::min( size.high, up ) }; + auto const theirfoot { faces ? meets( bound( 4 ) ) : ( overlap > 2.0 * slack || backs ) ? meets( bound( 2 ) ) : 0.0 }; + auto const feet { faces ? theirfoot > ourfoot + 2.0 * slack : backs && ourfoot > theirfoot + 2.0 * slack }; if( overlap <= 2.0 * slack && false == feet ) { continue; } if( false == slanted ) { ourslant = { square( run_of( ourbound( Part.lower, clearlow, 0.0, false ), s ) ), square( run_of( ourbound( Part.upper, clearup, size.head, true ), s ) ) }; footslant = square( run_of( ourbound( Part.upper, clearup, size.high, true ), s ) ); + innerslant = square( run_of( ourbound( Part.lower, clearlow, size.low, false ), s ) ); slanted = true; } auto &hit { hits[ j ][ static_cast( k - a ) ] }; @@ -927,7 +961,8 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { hit.theirshoulder = theirsize.shoulder; hit.ourfoot = ourfoot; hit.theirfoot = theirfoot; - hit.footslant = { footslant, slant( bound( 2 ), theirfoot ) }; + hit.footslant = ( faces ? glm::dvec2 { innerslant, slant( bound( 4 ), theirfoot ) } : glm::dvec2 { footslant, slant( bound( 2 ), theirfoot ) } ); + hit.faces = faces; hit.ourcrown = here.offset + std::min( size.head - size.shoulder, up ); hit.theircrown = meets( bound( 3 ) ); hit.heads = ( overlap > 2.0 * slack ); @@ -939,7 +974,7 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { hit.removed[ 1 ] = b - ours.x; } else { - hit.removed[ 0 ] = hit.ourfoot - hit.theirfoot; + hit.removed[ 0 ] = std::abs( hit.ourfoot - hit.theirfoot ); hit.removed[ 1 ] = unbounded; } if( false == here.runon ) { diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 9ba6c119..3f7a855b 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/TrackRails.h" +#include "world/TrackSleepers.h" #include "world/TrackBlend.h" #include "rendering/renderer.h" @@ -381,6 +382,8 @@ struct rail_piece { bool walls { false }; float lift { 0.f }; float wallu { -1.f }; + knots::line rises; + knots::line lows; }; struct top_band { @@ -605,6 +608,9 @@ private: TSegment const &m_segment; double const m_length; double const m_guardoffset; + double const m_rise; // of the feet of the wing rails of the low section + glm::dvec2 const m_stock; // the stretch the rail is the stock rail of a blade along + double const m_heel; // of the blade the rail goes on from std::size_t m_topfirst; std::size_t m_toplast { 0 }; std::vector m_tints; @@ -626,6 +632,9 @@ TTrack::rail_layer::rail_layer( TTrack &Track, rail_layout const &Layout, rail_d m_segment { path_segment( Track, Path ) }, m_length { m_segment.GetLength() }, m_guardoffset { -( flangeway + Drawing.size.head ) }, + m_rise { track_sleepers::wing_rise( Track ) }, + m_stock { track_sleepers::stock_stretch( Track, Path, Side ) }, + m_heel { track_sleepers::blade_heel( Track, Path, Side ) }, m_topfirst { Profile.size() } { // the running surface on top of the head is split along the edge of the wheel treads, the part of it out of their reach goes matte; the rest of the profile and the end faces are laid whole for( std::size_t j = 0; j < Profile.size(); ++j ) { @@ -785,16 +794,14 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector tread_brightness( A, m_track.tread_band() ) ? B : A ); }; - knots::line offsets, extents, inner; + knots::line extents, inner; for( std::size_t i = 0; i <= Cuts.size(); ++i ) { auto const end { i < Cuts.size() ? Cuts[ i ].end : 1e9 }; // a piece carved between two cuts has the same lines at both auto const own { i < Cuts.size() && false == ( i > 0 && Cuts[ i - 1 ].carved && Cuts[ i ].carved ) }; - auto pieceoffsets { offsets }; - if( own ) { - pieceoffsets.insert( pieceoffsets.end(), Cuts[ i ].endoffsets.begin(), Cuts[ i ].endoffsets.end() ); - } - knots::sort( pieceoffsets ); + auto const pieceoffsets { part_offsets( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr ) }; + auto const pieceshares { Wheels && m_rise > 0.0 ? wing_shares( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr, m_stock ) : knots::line {} }; + auto const piecerises { part_rises( pieceshares, Wheels ? m_rise : 0.0, m_heel ) }; // the limits of the head from both ends can overlap along a short piece, as the planed wing rail of one frog and the cut of the next: the tighter of them holds auto const pieceextents { own ? knots::bound( extents, Cuts[ i ].endextents, true ) : extents }; auto const pieceinner { own ? knots::bound( inner, Cuts[ i ].endinner, false ) : inner }; @@ -842,7 +849,9 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vectoredge.empty() && m_topfirst < m_toplast ) { treaded( whole, *tread, ends ); } @@ -861,7 +870,6 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector #include #include #include @@ -49,6 +50,15 @@ double constexpr blade_gap { 0.0005 }; double constexpr blade_lowered { 0.050 }; double constexpr blade_spacing { 0.5 }; double constexpr blade_runon_margin { 0.2 }; // how far short of the end of the plain track a blade running on into it ends +double constexpr wing_low { 1.5 }; // how far before the throat the wing rails are of the low section of the blades, raised on their timbers as the blades +double constexpr wing_forged { 0.5 }; // how long the end of a wing rail forged to the plain rail is, going down to it +// the low section of the blades, Zu1-60 (60E1A1) against 60E1: its web 44 mm thick against 16.5, its foot 55 mm inwards and 85 mm outwards +// of the middle against 75 both ways, the edges of its foot 20 mm high against 11.5, its web from 36 mm up against 30.6 +double constexpr low_web { 44.0 / 16.5 }; +double constexpr low_inward { 55.0 / 75.0 }; +double constexpr low_outward { 85.0 / 75.0 }; +double constexpr low_edge { 20.0 / 11.5 }; +double constexpr low_root { 36.0 / 30.6 }; // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades struct switch_frog { @@ -164,6 +174,73 @@ struct rail_cut { struct rail_plan { std::vector cuts; }; +// how far out a part of a rail between two cuts is laid: as it goes on from the one and comes to the other, once where both are carved, which +// gives both its lines +inline knots::line part_offsets( rail_cut const *Before, rail_cut const *After ) { + + knots::line result { Before != nullptr ? Before->startoffsets : knots::line {} }; + if( After != nullptr && false == ( Before != nullptr && Before->carved && After->carved ) ) { + result.insert( result.end(), After->endoffsets.begin(), After->endoffsets.end() ); + } + knots::sort( result ); + return result; +} +// how much of the low section of the blades a part of a rail between two cuts is of along it, 0 to 1: a wing rail from wing_low before the throat +// on, down to the plain rail past it along its forged end; the rail whole along Stock, the stock rail of a blade there +inline knots::line wing_shares( rail_cut const *Before, rail_cut const *After, glm::dvec2 const &Stock ) { + + knots::line result; + auto const going { Before != nullptr && Before->startkind == rail_end::point }; + auto const coming { After != nullptr && After->endkind == rail_end::point }; + if( false == going && false == coming ) { + return result; + } + auto const lowto { going ? Before->startanchor + wing_low : 0.0 }; + auto const lowfrom { coming ? After->endanchor - wing_low : 0.0 }; + auto const through { going && coming && lowto + 2.0 * wing_forged >= lowfrom }; + auto const stock { Stock.y > Stock.x }; + // whole along the low section, none along the stock rail, between along the forged ends + auto const share = [&]( double const At ) { + auto const low { through ? 1.0 : std::max( + going ? std::clamp( ( lowto + wing_forged - At ) / wing_forged, 0.0, 1.0 ) : 0.0, + coming ? std::clamp( ( At - lowfrom + wing_forged ) / wing_forged, 0.0, 1.0 ) : 0.0 ) }; + auto const whole { stock ? std::clamp( std::max( Stock.x - At, At - Stock.y ) / wing_forged, 0.0, 1.0 ) : 1.0 }; + return std::min( low, whole ); }; + std::array places { lowto, lowto + wing_forged, lowfrom - wing_forged, lowfrom, Stock.x - wing_forged, Stock.x, Stock.y, Stock.y + wing_forged }; + std::sort( places.begin(), places.end() ); + for( auto const at : places ) { + if( result.empty() || at > result.back().x + 1e-6 ) { + result.emplace_back( at, share( at ) ); + } + } + return result; +} +// how far the foot of the part is raised: by Rise along the low section of a wing rail of Shares, and from the heel of a blade at Heel, the +// rail going on from it, down to the plain rail along the forged end of the blade; not along the blade itself, raised on its own +inline knots::line part_rises( knots::line const &Shares, double const Rise, double const Heel ) { + + knots::line result; + if( Rise <= 0.0 || ( Shares.empty() && Heel <= 0.0 ) ) { + return result; + } + std::vector places; + for( auto const &knot : Shares ) { + places.push_back( knot.x ); + } + if( Heel > 0.0 ) { + places.push_back( Heel - 1e-3 ); + places.push_back( Heel ); + places.push_back( Heel + wing_forged ); + } + std::sort( places.begin(), places.end() ); + for( auto const at : places ) { + if( result.empty() || at > result.back().x + 1e-6 ) { + auto const heel { Heel > 0.0 && at >= Heel ? std::clamp( ( Heel + wing_forged - at ) / wing_forged, 0.0, 1.0 ) : 0.0 }; + result.emplace_back( at, Rise * std::max( knots::sample( Shares, at, 0.0 ), heel ) ); + } + } + return result; +} struct rail_tread { double from { 0.0 }; double to { 0.0 }; diff --git a/world/Segment.cpp b/world/Segment.cpp index 798274dc..c370cb1c 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -746,7 +746,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig for( auto distance { std::ceil( Span.from / 0.5 ) * 0.5 }; distance < std::min( Span.to, m_dense ); distance += 0.5 ) { stations.push_back( distance ); } - for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner, &Span.drops } ) { + for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner, &Span.drops, &Span.rises, &Span.lows } ) { for( auto const &knot : *line ) { if( knot.x > Span.from && knot.x < Span.to ) { stations.push_back( knot.x ); @@ -784,6 +784,97 @@ 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, double const Rise ) { + if( Rise <= 0.0 || Height >= Span.head.x || Span.head.x <= bottom ) { + return Height; + } + return static_cast( Height + Rise * ( Span.head.x - Height ) / ( Span.head.x - bottom ) ); }; + // the profile below its head as drawn, about the middle of its foot: how far the foot reaches inwards and outwards, how thick the web + // is, how wide the head, where its underside is, how high the top of the edges of the foot and the root of the web are + struct { + double middle { 0.0 }, inward { 0.0 }, outward { 0.0 }, web { 1e6 }, head { 0.0 }, underside { 1e6 }, edge { 0.0 }, root { 1e6 }; + } drawn; + auto const across = [&]( gfx::basic_vertex const &Point ) { + return static_cast( Span.side * Point.position.x - Span.halfwidth ); }; + auto low { false == Span.lows.empty() && Span.head.x > bottom }; + if( low ) { + glm::dvec2 foot { 1e6, -1e6 }; + for( auto const &point : ShapePoints ) { + if( point.position.y < bottom + 0.0005f ) { + foot = { std::min( foot.x, across( point ) ), std::max( foot.y, across( point ) ) }; + } + } + drawn.middle = 0.5 * ( foot.x + foot.y ); + drawn.inward = drawn.middle - foot.x; + drawn.outward = foot.y - drawn.middle; + for( auto const &point : ShapePoints ) { + auto const off { std::abs( across( point ) - drawn.middle ) }; + if( point.position.y >= Span.head.x ) { + drawn.head = std::max( drawn.head, off ); + } + else if( point.position.y > bottom + 0.25 * ( Span.head.x - bottom ) ) { + drawn.web = std::min( drawn.web, off ); + } + } + for( auto const &point : ShapePoints ) { + auto const off { std::abs( across( point ) - drawn.middle ) }; + auto const height { static_cast( point.position.y - bottom ) }; + if( point.position.y >= Span.head.x && off >= drawn.head - 0.001 ) { + drawn.underside = std::min( drawn.underside, static_cast( point.position.y ) ); + } + if( point.position.y < Span.head.x && off <= drawn.web + 0.0005 ) { + drawn.root = std::min( drawn.root, height ); + } + if( off >= 0.9 * std::min( drawn.inward, drawn.outward ) ) { + drawn.edge = std::max( drawn.edge, height ); + } + } + low = ( drawn.web < std::min( drawn.inward, drawn.outward ) && drawn.web < drawn.head && drawn.edge > 0.0 && drawn.edge < drawn.root && drawn.root < Span.head.x - bottom && drawn.underside < 1e6 ); + } + // the profile with its foot raised by Rise made over into the low section by Share of it: below the head the web thickened, the foot + // reshaped, the web shortened; with none of it, only raised + std::vector lowpoints; + auto const lowered = [&]( double const Rise, double const Share ) -> gfx::vertex_array const & { + auto const share { std::clamp( Share, 0.0, 1.0 ) }; + auto const web { drawn.web * glm::mix( 1.0, Span.low.web, share ) }; + auto const inward { drawn.inward * glm::mix( 1.0, Span.low.inward, share ) }; + auto const outward { drawn.outward * glm::mix( 1.0, Span.low.outward, share ) }; + auto const edge { drawn.edge * glm::mix( 1.0, Span.low.edge, share ) }; + auto const root { drawn.root * glm::mix( 1.0, Span.low.root, share ) }; + auto const below { Span.head.x - bottom }; + auto const top { std::max( below - Rise, root + 0.001 ) }; + lowpoints = ShapePoints; + for( auto &point : lowpoints ) { + if( point.position.y >= drawn.underside ) { + continue; + } + auto const off { across( point ) - drawn.middle }; + auto const distance { std::abs( off ) }; + auto moved { distance }; + if( point.position.y >= Span.head.x ) { + moved = web + ( std::max( distance, drawn.web ) - drawn.web ) * ( drawn.head - web ) / std::max( drawn.head - drawn.web, 1e-6 ); + } + else { + auto const was { off < 0.0 ? drawn.inward : drawn.outward }; + auto const reach { off < 0.0 ? inward : outward }; + moved = ( distance <= drawn.web ? + distance * web / drawn.web : + web + ( distance - drawn.web ) * ( reach - web ) / std::max( was - drawn.web, 1e-6 ) ); + auto const height { static_cast( point.position.y - bottom ) }; + auto const made { + height <= drawn.edge ? height * edge / drawn.edge : + height <= drawn.root ? edge + ( height - drawn.edge ) * ( root - edge ) / ( drawn.root - drawn.edge ) : + root + ( height - drawn.root ) * ( top - root ) / ( below - drawn.root ) }; + point.position.y = static_cast( glm::mix( static_cast( raised( point.position.y, Rise ) ), bottom + below - top + made, share ) ); + } + point.position.x = static_cast( Span.side * ( Span.halfwidth + drawn.middle + std::copysign( moved, off ) ) ); + } + return lowpoints; }; + auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { glm::dvec3 point; glm::dvec3 across; @@ -816,6 +907,10 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } auto const worn { knots::sample( Span.ramp, Distance, -1e6 ) }; auto const drop { knots::sample( Span.drops, Distance, 0.0 ) }; + auto const sampled { Span.rises.empty() ? Span.rise : knots::sample( Span.rises, Distance, 0.0 ) }; + auto const share { low ? knots::sample( Span.lows, Distance, 0.0 ) : 0.0 }; + auto const &points { low && ( sampled > 0.0 || share > 0.0 ) ? lowered( sampled, share ) : ShapePoints }; + auto const rise { low ? 0.0 : sampled }; static loft_end const unshaped; auto const &shaped { Distance <= Span.from ? Span.fromend : Distance >= Span.to ? Span.toend : unshaped }; auto const bend { shaped.bend }; @@ -831,11 +926,11 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } Ring.clear(); Wear.clear(); - auto const count { ShapePoints.size() }; + auto const count { points.size() }; auto const reach = [&]( std::size_t const Index ) { - return static_cast( Span.side * ShapePoints[ Index ].position.x - Span.halfwidth ); }; + return static_cast( Span.side * points[ Index ].position.x - Span.halfwidth ); }; auto const twin = [&]( std::size_t const A, std::size_t const B ) { - return ShapePoints[ A ].position.x == ShapePoints[ B ].position.x && ShapePoints[ A ].position.y == ShapePoints[ B ].position.y; }; + return points[ A ].position.x == points[ B ].position.x && points[ A ].position.y == points[ B ].position.y; }; auto const clamped = [&]( std::size_t const Index ) { auto const outside { reach( Index ) }; return ( outside > middleextent ? 1 : outside < middleinner ? -1 : 0 ); }; @@ -849,15 +944,15 @@ 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( points.front().position.y, rise ) + 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 } ); + Ring.emplace_back( pt, glm::vec3{ 0.f, 1.f, 0.f }, glm::vec2{ points[ j ].texture.x, 0.f } ); Wear.push_back( 1.f ); } return; } for( std::size_t j = 0; j < count; ++j ) { - auto shape { ShapePoints[ j ] }; + auto shape { points[ j ] }; auto outside { reach( j ) }; auto wall { glm::vec3{ 0.f } }; auto const side { clamped( j ) }; @@ -884,7 +979,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig if( false == open( neighbour ) ) { continue; } - auto const &other { ShapePoints[ neighbour ] }; + auto const &other { points[ neighbour ] }; auto const t { std::clamp( static_cast( ( limit - reach( neighbour ) ) / ( side > 0 ? std::max( outside - reach( neighbour ), 1e-6 ) : std::min( outside - reach( neighbour ), -1e-6 ) ) ), 0.f, 1.f ) }; shape.position.y = glm::mix( other.position.y, shape.position.y, t ); shape.texture.x = glm::mix( other.texture.x, shape.texture.x, t ); @@ -907,7 +1002,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig continue; } auto const t { static_cast( ( limit - reach( static_cast( neighbour ) ) ) / ( reach( static_cast( index ) ) - reach( static_cast( neighbour ) ) ) ) }; - auto const height { glm::mix( ShapePoints[ neighbour ].position.y, ShapePoints[ index ].position.y, t ) }; + auto const height { glm::mix( points[ neighbour ].position.y, points[ index ].position.y, t ) }; top = std::max( top, height ); bottom = std::min( bottom, height ); ++ends; @@ -933,6 +1028,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, rise ); 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 ac30b0c4..f2d4aab1 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -28,6 +28,16 @@ struct loft_end { double bend { 0.0 }; }; +// a rail profile made over below its head into a lower section, as the wing rails of a frog of the section of the blades: its web, its +// foot inwards and outwards as many times as thick and wide, the top of the edges of its foot and the root of its web as many times as high +struct loft_low { + double web { 1.0 }; + double inward { 1.0 }; + double outward { 1.0 }; + double edge { 1.0 }; + double root { 1.0 }; +}; + struct loft_span { int side { 1 }; float halfwidth { 0.f }; @@ -47,6 +57,10 @@ 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 + knots::line rises; // along the span, in place of rise where given + knots::line lows; // along the span, how much of the low section the profile is made over into below its head, 0 to 1 + loft_low low; // what the profile is made over into along lows }; struct segment_data { diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp new file mode 100644 index 00000000..ea382ae4 --- /dev/null +++ b/world/SleeperFastenings.cpp @@ -0,0 +1,1178 @@ +/* +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" +#include "simulation/simulation.h" + +#include +#include +#include +#include + +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; +} + +bool fastened_rails::rail::guarded( double const Distance ) const { + + return std::any_of( guards.begin(), guards.end(), [&]( glm::dvec2 const &Stretch ) { return Distance >= Stretch.x && Distance <= Stretch.y; } ); +} + +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 ); + } } ); + auto &kept { fastened_rails::kept() }; + for( auto *track : tracks ) { + auto &rails { kept[ track ] }; + if( rails == nullptr ) { + rails = std::make_shared const>( rails_of( *track ) ); + } + m_kept.push_back( rails ); + m_rails.insert( m_rails.end(), rails->begin(), rails->end() ); + } +} + +fastened_rails::kept_rails &fastened_rails::kept() { + + static kept_rails tracks; + static unsigned int stamp { 0 }; + if( stamp != track_rails::memo_stamp() ) { + tracks.clear(); + stamp = track_rails::memo_stamp(); + } + return tracks; +} + +void forget_rails( TTrack const &Track ) { + + fastened_rails::kept().erase( &Track ); +} + +std::vector fastened_rails::rails_of( TTrack &Track ) { + + std::vector result; + auto const size { Track.rail_dimensions() }; + auto const middle { 0.5 * std::abs( Track.fTrackWidth ) + size.web }; + auto const cut { track_rails::cut_rails( Track ) }; + auto const switched { Track.eType == tt_Switch && Track.SwitchExtension != nullptr }; + auto const rise { wing_rise( Track ) }; + // what the rail is along the path: a blade, the stock rail of a blade, checked + auto const add = [&]( rail &&Line, int const Path, int const Side ) { + if( switched ) { + if( auto const heel { blade_heel( Track, Path, Side ) }; heel > 0.0 ) { + Line.blade = { 0.0, heel }; + } + Line.stock = stock_stretch( Track, Path, Side ); + } + if( cut ) { + for( auto const &check : track_rails::rail_checks( Track )[ Path ][ Side > 0 ? 1 : 0 ] ) { + Line.checks.emplace_back( check.from, check.to ); + } + Line.guards = Track.m_railguards[ Path ][ Side > 0 ? 1 : 0 ]; + } + result.emplace_back( std::move( Line ) ); }; + // the rail of the side of the path from and to along it, by points the given way out from the middle of the path + auto const sampled = [&]( int const Path, int const Side, double const From, double const To, double const Middle, knots::line const &Offsets ) { + auto const *segment { path_segment( Track, Path ) }; + auto const count { std::max( 1, static_cast( std::ceil( ( To - From ) / fastening_step ) ) ) }; + rail line; + line.track = &Track; + line.path = Path; + line.side = Side; + line.foot = size.web - size.low; + 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 { From + ( To - From ) * 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 + knots::sample( Offsets, distance, 0.0 ) ) }; + 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 ); + } + return line; }; + 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() }; + for( auto const side : { -1, 1 } ) { + // the rail in its parts between the cuts at the crossings, as they're laid: a wing rail bent out from the path; on over the + // gap at an obtuse crossing, where its check rail lies + auto cuts { cut ? Track.m_railplans[ path ][ side > 0 ].cuts : std::vector {} }; + std::stable_sort( cuts.begin(), cuts.end(), []( track_rails::rail_cut const &A, track_rails::rail_cut const &B ) { return A.at < B.at; } ); + for( std::size_t part = 0; part <= cuts.size(); ++part ) { + auto const *before { part > 0 ? &cuts[ part - 1 ] : nullptr }; + auto const *after { part < cuts.size() ? &cuts[ part ] : nullptr }; + auto const from { std::max( 0.0, before != nullptr ? before->start : 0.0 ) }; + auto const to { std::min( length, after != nullptr ? std::max( after->end, after->start ) : length ) }; + if( to - from < fastening_merge ) { + continue; + } + auto line { sampled( path, side, from, to, middle, track_rails::part_offsets( before, after ) ) }; + line.rises = track_rails::part_rises( rise > 0.0 ? track_rails::wing_shares( before, after, stock_stretch( Track, path, side ) ) : knots::line {}, rise, blade_heel( Track, path, side ) ); + add( std::move( line ), path, side ); + } + // the guard rails of the crossings at wide angles inside the rail, the flangeway from it + for( auto const &guard : cut ? Track.m_railguards[ path ][ side > 0 ] : std::vector {} ) { + auto const from { std::max( 0.0, guard.x ) }; + auto const to { std::min( length, guard.y ) }; + if( to - from < fastening_merge ) { + continue; + } + auto line { sampled( path, side, from, to, middle - ( track_rails::flangeway + size.head ), {} ) }; + line.guard = true; + result.emplace_back( std::move( line ) ); + } + } + } + return result; +} + +std::vector fastened_rails::rails_on( beam const &Beam ) const { + + auto const span { Beam.to - Beam.from }; + auto const length { glm::length( plan( span ) ) }; + if( length < 0.1 ) { + return {}; + } + 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( rail 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; + } + auto const distance { glm::mix( line.distances[ index - 1 ], line.distances[ index ], v ) }; + hits.push_back( { + u * length, + line.at( distance ), + glm::normalize( glm::mix( line.inward[ index - 1 ], line.inward[ index ], v ) ), + line.foot, + knots::sample( line.rises, distance, 0.0 ) } ); + } + } + 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 }; + auto rise { best.rise }; + 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 ]; + } + rise = std::max( rise, hits[ next ].rise ); + } + best.rise = rise; + if( best.kind != role::blade ) { + rails.push_back( best ); + } + index = next; + } + return rails; +} + +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 axis { plan( span ) / length }; + auto const rails { rails_on( Beam ) }; + auto const point = [&]( double const At ) { + return Beam.from + span * ( At / length ); }; + auto const has = [&]( int const Part ) { + return Model.parts[ Part ] != nullptr; }; + // a fastening of two rails with its seats at the origin the way given, holding more rails than its seats, as the rails of a frog + // or two rails with a check rail between: stretched under the rails from the first to the last and their check rails, its ends with + // the clips past the feet of the outer ones, short of the feet of the rails beside + auto const whole = [&]( std::size_t const First, std::size_t const Last, double const Origin, double const Turn ) { + auto const &reach { Model.spans[ part_check ] }; + auto low { std::numeric_limits::max() }; + auto high { std::numeric_limits::lowest() }; + for( auto index { First }; index <= Last; ++index ) { + auto const &rail { rails[ index ] }; + for( auto const at : { rail.at, rail.kind == role::checked ? rail.at + check_seats / glm::dot( rail.inward, axis ) : rail.at } ) { + low = std::min( low, at - rail.foot - fastening_clip ); + high = std::max( high, at + rail.foot + fastening_clip ); + } + } + if( First > 0 ) { + low = std::max( low, rails[ First - 1 ].at + rails[ First - 1 ].foot + fastening_gap ); + } + if( Last + 1 < rails.size() ) { + high = std::min( high, rails[ Last + 1 ].at - rails[ Last + 1 ].foot - fastening_gap ); + } + low = std::min( low, Origin + std::min( Turn * reach.x, Turn * reach.y ) ); + high = std::max( high, Origin + std::max( Turn * reach.x, Turn * reach.y ) ); + auto const scale { ( high - low ) / ( reach.y - reach.x ) }; + return fastening { part_check, point( Origin ), axis * Turn, static_cast( scale ), 0.f, + static_cast( Turn * ( 0.5 * ( low + high ) - Origin ) - scale * 0.5 * ( reach.x + reach.y ) ), true, true }; }; + // the rails from the first to the last under fastenings of two rails, each with its seats under two rails side by side and over as + // many of the rails around as it reaches: the one holding the most first, the rails before and after it in turn + std::function crowd = [&]( std::size_t const First, std::size_t const Last ) { + auto const &reach { Model.spans[ part_check ] }; + std::size_t covered { 0 }; + std::size_t from { First }; + auto origin { 0.0 }; + auto turn { 1.0 }; + for( auto seat { First }; seat < Last; ++seat ) { + for( auto const direction : { 1.0, -1.0 } ) { + auto const at { 0.5 * ( rails[ seat ].at + rails[ seat + 1 ].at - direction * check_seats ) }; + auto const low { at + std::min( direction * reach.x, direction * reach.y ) }; + auto const high { at + std::max( direction * reach.x, direction * reach.y ) }; + if( rails[ seat ].at < low || rails[ seat + 1 ].at > high ) { + continue; + } + auto start { seat }; + while( start > First && rails[ start - 1 ].at >= low ) { + --start; + } + auto end { seat + 1 }; + while( end < Last && rails[ end + 1 ].at <= high ) { + ++end; + } + if( end - start + 1 > covered ) { + covered = end - start + 1; + from = start; + origin = at; + turn = direction; + } + } + } + if( covered < 2 ) { + for( auto rail { First }; rail <= Last; ++rail ) { + result.push_back( { part_plate, point( rails[ rail ].at ), rails[ rail ].inward } ); + } + return; + } + result.push_back( covered > 2 ? + whole( from, from + covered - 1, origin, turn ) : + fastening { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true } ); + if( from > First ) { + crowd( First, from - 1 ); + } + if( from + covered <= Last ) { + crowd( from + covered, Last ); + } + }; + // the rails and the check rails along the timber, each with its foot over the foot of the one beside, with a check rail between two + // of them, as at the obtuse crossings of the slips, none a stock rail: from the first rail to the last, held by one fastening. a + // check rail where a rail lies is that rail + std::vector runs( rails.size(), rails.size() ); // of the first rail of each, the last + std::vector run( rails.size(), false ); + if( has( part_check ) ) { + struct line { + double at; + double foot; + std::size_t rail; + bool check; + }; + std::vector lines; + for( std::size_t index = 0; index < rails.size(); ++index ) { + auto const &rail { rails[ index ] }; + lines.push_back( { rail.at, rail.foot, index, false } ); + if( rail.kind != role::checked ) { + continue; + } + auto const at { rail.at + check_seats / glm::dot( rail.inward, axis ) }; + if( std::none_of( rails.begin(), rails.end(), [&]( hit const &Other ) { return std::abs( Other.at - at ) < fastening_nose; } ) ) { + lines.push_back( { at, rail.foot, index, true } ); + } + } + std::stable_sort( lines.begin(), lines.end(), []( line const &Left, line const &Right ) { return Left.at < Right.at; } ); + for( std::size_t first = 0; first < lines.size(); ) { + auto last { first }; + while( last + 1 < lines.size() && lines[ last + 1 ].at - lines[ last ].at < lines[ last ].foot + lines[ last + 1 ].foot ) { + ++last; + } + auto const between { last > first + 1 && std::any_of( lines.begin() + first + 1, lines.begin() + last, []( line const &Line ) { return Line.check; } ) }; + auto low { rails.size() }; + auto high { std::size_t { 0 } }; + auto stock { false }; + for( auto index { first }; index <= last; ++index ) { + low = std::min( low, lines[ index ].rail ); + high = std::max( high, lines[ index ].rail ); + stock = stock || rails[ lines[ index ].rail ].kind == role::stock; + } + if( between && false == stock && false == run[ low ] ) { + runs[ low ] = high; + std::fill( run.begin() + low, run.begin() + high + 1, true ); + } + first = last + 1; + } + } + for( std::size_t index = 0; index < rails.size(); ++index ) { + auto const &rail { rails[ index ] }; + if( runs[ index ] < rails.size() ) { + auto const &reach { Model.spans[ part_check ] }; + auto const last { runs[ index ] }; + result.push_back( whole( index, last, 0.5 * ( rail.at + rails[ last ].at - reach.x - reach.y ), 1.0 ) ); + index = last; + continue; + } + // three rails or more close beside one another, as at the crossings of the slips, none a stock rail or with a check rail + auto last { index }; + while( last + 1 < rails.size() && rails[ last + 1 ].at - rails[ last ].at < fastening_crowd && rails[ last ].kind == role::plain && rails[ last + 1 ].kind == role::plain && false == run[ last + 1 ] ) { + ++last; + } + if( last >= index + 2 && has( part_check ) ) { + crowd( index, last ); + index = last; + continue; + } + if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock && false == run[ index + 1 ] ) { + auto const &other { rails[ index + 1 ] }; + // a rail of the two with its check rail on the side away from the other one in the fastening with the check rail, + // the other one in a plate of its own + auto const away = [&]( hit const &Rail, double const Toward ) { + return Rail.kind == role::checked && glm::dot( Rail.inward, axis ) * Toward < 0.0; }; + auto const awayfirst { away( rail, 1.0 ) }; + auto const awaysecond { away( other, -1.0 ) }; + if( has( part_check ) && ( awayfirst || awaysecond ) ) { + auto const joined { other.at - rail.at < fastening_nose }; + for( auto const &[ one, checked ] : { std::make_pair( &rail, awayfirst ), std::make_pair( &other, awaysecond ) } ) { + if( checked ) { + result.push_back( { part_check, point( one->at ), one->inward, 1.f, static_cast( 0.5 * check_seats ) } ); + } + else if( false == joined ) { + result.push_back( { part_plate, point( one->at ), one->inward } ); + } + } + } + else 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( 0.5 * ( rail.at + other.at - check_seats ) ), axis, 1.f, static_cast( 0.5 * check_seats ), 0.f, true } ); + } + 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 } ); + if( has( part_chair ) ) { + // the rail on a plate as wide as its foot, between the brace and the slide plate + auto const &plate { Model.spans[ part_plate ] }; + auto const low { -rail.foot }; + auto const high { std::min( rail.foot, Model.spans[ part_chair ].x ) }; + auto const scale { ( high - low ) / ( plate.y - plate.x ) }; + result.push_back( { part_plate, point( rail.at ), -rail.inward, static_cast( scale ), 0.f, static_cast( low - scale * plate.x ) } ); + } + if( has( part_slide ) ) { + // the slide plate from the edge of the foot of the stock rail on, not under it + auto const &slide { Model.spans[ part_slide ] }; + auto const edge { std::min( slide.y, -rail.foot ) }; + result.push_back( { part_slide, point( rail.at ), -rail.inward, static_cast( ( edge - slide.x ) / ( slide.y - slide.x ) ), slide.y, static_cast( edge - slide.y ) } ); + } + break; + } + case role::checked: { + result.push_back( { has( part_check ) ? part_check : part_plate, point( rail.at ), rail.inward, 1.f, has( part_check ) ? static_cast( 0.5 * check_seats ) : 0.f } ); + break; + } + default: { + result.push_back( { part_plate, point( rail.at ), rail.inward } ); + break; + } + } + } + fit( result, Beam, Model ); + lift( result, rails, Beam, Model ); + along( result, Beam, Model ); + return result; +} + +void fastened_rails::along( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) const { + + auto const span { Beam.to - Beam.from }; + auto const length { glm::length( plan( span ) ) }; + auto const start { plan( Beam.from ) }; + auto const axis { plan( span ) / length }; + auto const side { glm::normalize( plan( Beam.along ) ) }; + auto const low { glm::min( start, plan( Beam.to ) ) - 2.0 * half_width }; + auto const high { glm::max( start, plan( Beam.to ) ) + 2.0 * half_width }; + // where the rail lies over the timber, along it: a stretch of a chord of the rail + struct stretch { + rail const *line; + std::size_t index; // of the end of the chord + double from; // along the timber + double to; + }; + std::vector stretches; + for( rail const &line : m_rails ) { + // a guard rail along the timber lies in the fastening of its running rail + if( line.guard || line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { + continue; + } + for( std::size_t index = 1; index < line.points.size(); ++index ) { + auto const chord { line.points[ index ] - line.points[ index - 1 ] }; + if( glm::length( chord ) < 1e-6 || std::abs( cross( axis, glm::normalize( chord ) ) ) >= crossing_sine_min ) { + continue; + } + if( std::abs( line.heights[ index ] - glm::mix( Beam.from.y, Beam.to.y, std::clamp( glm::dot( line.points[ index ] - start, axis ) / length, 0.0, 1.0 ) ) ) > level_tolerance ) { + continue; + } + // the share of the chord over the middle of the timber or its side toward the next one, and over its length + auto const u0 { glm::dot( line.points[ index - 1 ] - start, axis ) }; + auto const u1 { glm::dot( line.points[ index ] - start, axis ) }; + auto const w0 { glm::dot( line.points[ index - 1 ] - start, side ) }; + auto const w1 { glm::dot( line.points[ index ] - start, side ) }; + auto from { 0.0 }; + auto to { 1.0 }; + auto const clip = [&]( double const A, double const B, double const Low, double const High ) { + if( std::abs( B - A ) < 1e-9 ) { + if( A <= Low || A > High ) { + to = -1.0; + } + return; + } + auto const a { ( Low - A ) / ( B - A ) }; + auto const b { ( High - A ) / ( B - A ) }; + from = std::max( from, std::min( a, b ) ); + to = std::min( to, std::max( a, b ) ); }; + clip( w0, w1, -half_width + 2.0 * fastening_gap, half_width + 2.0 * fastening_gap ); + clip( u0, u1, 0.0, length ); + if( to <= from ) { + continue; + } + auto const a { glm::mix( u0, u1, from ) }; + auto const b { glm::mix( u0, u1, to ) }; + stretches.push_back( { &line, index, std::min( a, b ), std::max( a, b ) } ); + } + } + if( stretches.empty() ) { + return; + } + // kept clear of the fastenings of the rails across the timber, and of its ends + std::vector taken; + for( auto const &fastening : Fastenings ) { + auto const at { glm::dot( plan( fastening.point ) - start, axis ) }; + auto const turn { glm::dot( fastening.across, axis ) }; + auto const &reach { Model.spans[ fastening.part ] }; + auto const a { at + turn * fastening.placed( reach.x, fastening.scale ) }; + auto const b { at + turn * fastening.placed( reach.y, fastening.scale ) }; + taken.emplace_back( std::min( a, b ) - 0.5 * fastening_length - fastening_gap, std::max( a, b ) + 0.5 * fastening_length + fastening_gap ); + } + auto const end { 0.5 * fastening_length + fastening_gap }; + std::sort( stretches.begin(), stretches.end(), []( stretch const &Left, stretch const &Right ) { return Left.line != Right.line ? Left.line < Right.line : Left.from < Right.from; } ); + for( std::size_t first = 0; first < stretches.size(); ) { + // the stretches of one rail going on one from another + auto last { first }; + auto to { stretches[ first ].to }; + while( last + 1 < stretches.size() && stretches[ last + 1 ].line == stretches[ first ].line && stretches[ last + 1 ].from <= to + 0.01 ) { + to = std::max( to, stretches[ ++last ].to ); + } + std::vector free { glm::dvec2 { std::max( stretches[ first ].from, end ), std::min( to, length - end ) } }; + for( auto const &blocked : taken ) { + std::vector left; + for( auto const &room : free ) { + if( blocked.y <= room.x || blocked.x >= room.y ) { + left.push_back( room ); + continue; + } + if( blocked.x > room.x ) { + left.emplace_back( room.x, blocked.x ); + } + if( blocked.y < room.y ) { + left.emplace_back( blocked.y, room.y ); + } + } + free = std::move( left ); + } + auto const plate = [&]( double const At ) { + for( auto index { first }; index <= last; ++index ) { + auto const &piece { stretches[ index ] }; + if( At < piece.from - 1e-6 || At > piece.to + 1e-6 ) { + continue; + } + auto const &line { *piece.line }; + auto const u0 { glm::dot( line.points[ piece.index - 1 ] - start, axis ) }; + auto const u1 { glm::dot( line.points[ piece.index ] - start, axis ) }; + auto const share { std::abs( u1 - u0 ) > 1e-9 ? std::clamp( ( At - u0 ) / ( u1 - u0 ), 0.0, 1.0 ) : 0.0 }; + auto const offset { glm::dot( glm::mix( line.points[ piece.index - 1 ], line.points[ piece.index ], share ) - start, side ) }; + auto const inward { glm::normalize( glm::mix( line.inward[ piece.index - 1 ], line.inward[ piece.index ], share ) ) }; + auto const point { Beam.from + span * ( At / length ) + glm::dvec3 { side.x, 0.0, side.y } * offset }; + // with its check rail, or the guard rail of the crossing, in the fastening of both + auto const distance { glm::mix( line.distances[ piece.index - 1 ], line.distances[ piece.index ], share ) }; + if( Model.parts[ part_check ] != nullptr && ( line.at( distance ) == role::checked || line.guarded( distance ) ) ) { + Fastenings.push_back( { part_check, point, inward, 1.f, static_cast( 0.5 * check_seats ) } ); + } + else { + Fastenings.push_back( { part_plate, point, inward } ); + } + return; + } }; + for( auto const &room : free ) { + if( room.y < room.x ) { + continue; + } + if( room.y - room.x < 0.5 * pitch ) { + plate( 0.5 * ( room.x + room.y ) ); + continue; + } + auto const count { static_cast( std::ceil( ( room.y - room.x ) / pitch ) ) + 1 }; + for( int index = 0; index < count; ++index ) { + plate( glm::mix( room.x, room.y, static_cast( index ) / ( count - 1 ) ) ); + } + } + first = last + 1; + } +} + +std::vector fastened_rails::lying( beam const &Beam ) const { + + return rails_on( Beam ); +} + +std::vector fastened_rails::bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const { + + auto const span { plan( Beam.to - Beam.from ) }; + auto const length { glm::length( span ) }; + std::vector result; + for( auto const &rail : rails_on( Beam ) ) { + auto const at { plan( Beam.from ) + span * ( rail.at / length ) }; + auto const under { std::any_of( Fastenings.begin(), Fastenings.end(), [&]( fastening const &Fastening ) { + auto const along { glm::dot( at - plan( Fastening.point ), Fastening.across ) }; + auto const a { Fastening.placed( Model.spans[ Fastening.part ].x, Fastening.scale ) }; + auto const b { Fastening.placed( Model.spans[ Fastening.part ].y, Fastening.scale ) }; + return along >= std::min( a, b ) - 0.01 && along <= std::max( a, b ) + 0.01; } ) }; + if( false == under ) { + result.push_back( at ); + } + } + return result; +} + +void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) { + + auto const start { plan( Beam.from ) }; + auto const axis { glm::normalize( plan( Beam.to - Beam.from ) ) }; + auto const at = [&]( fastening const &Fastening ) { + return glm::dot( plan( Fastening.point ) - start, axis ); }; + // how far it reaches along the timber before and after where it is, at the scale + auto const reach = [&]( fastening const &Fastening, double const Scale ) { + auto const &span { Model.spans[ Fastening.part ] }; + auto const turn { glm::dot( Fastening.across, axis ) }; + auto const a { turn * Fastening.placed( span.x, Scale ) }; + auto const b { turn * Fastening.placed( span.y, Scale ) }; + return glm::dvec2 { -std::min( a, b ), std::max( a, b ) }; }; + auto const least = [&]( fastening const &Fastening ) { + switch( Fastening.part ) { + case part_plate: { return plate_least; } + case part_slide: { return slide_least; } + case part_check: { return Fastening.whole ? static_cast( Fastening.scale ) : check_least; } + default: { return 1.0; } + } }; + // those of one rail together, the rails in the order along the timber + std::vector order( Fastenings.size() ); + std::iota( order.begin(), order.end(), std::size_t { 0 } ); + std::stable_sort( order.begin(), order.end(), [&]( std::size_t const Left, std::size_t const Right ) { + return at( Fastenings[ Left ] ) < at( Fastenings[ Right ] ) - 1e-6; } ); + std::vector> rails; + for( std::size_t index = 0; index < order.size(); ) { + auto next { index + 1 }; + while( next < order.size() && at( Fastenings[ order[ next ] ] ) - at( Fastenings[ order[ index ] ] ) < 1e-6 ) { + ++next; + } + rails.emplace_back( index, next ); + index = next; + } + for( std::size_t index = 1; index < rails.size(); ++index ) { + auto const [ firstfrom, firstto ] { rails[ index - 1 ] }; + auto const [ secondfrom, secondto ] { rails[ index ] }; + auto const room { at( Fastenings[ order[ secondfrom ] ] ) - at( Fastenings[ order[ firstfrom ] ] ) - fastening_gap }; + // the ones of both reaching less toward the other as they're narrowed; with them narrowed by the share, how far the two + // lie over each other + std::vector narrowed; + for( auto member { firstfrom }; member < secondto; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + auto const side { member < firstto ? 1 : 0 }; + if( reach( fastening, std::min( fastening.scale, least( fastening ) ) )[ side ] < reach( fastening, fastening.scale )[ side ] - 1e-6 ) { + narrowed.push_back( order[ member ] ); + } + } + auto const over = [&]( double const Share ) { + auto after { 0.0 }; + auto before { 0.0 }; + for( auto member { firstfrom }; member < secondto; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + auto const shrinks { std::find( narrowed.begin(), narrowed.end(), order[ member ] ) != narrowed.end() }; + auto const scale { shrinks ? glm::mix( fastening.scale, least( fastening ), Share ) : fastening.scale }; + if( member < firstto ) { + after = std::max( after, reach( fastening, scale ).y ); + } + else { + before = std::max( before, reach( fastening, scale ).x ); + } + } + return after + before - room; }; + if( narrowed.empty() || over( 0.0 ) <= 0.0 ) { + continue; + } + auto share { 1.0 }; + if( over( 1.0 ) < 0.0 ) { + auto low { 0.0 }; + for( int step = 0; step < 20; ++step ) { + auto const middle { 0.5 * ( low + share ) }; + ( over( middle ) > 0.0 ? low : share ) = middle; + } + } + for( auto const index : narrowed ) { + auto &fastening { Fastenings[ index ] }; + fastening.scale = static_cast( glm::mix( fastening.scale, least( fastening ), share ) ); + } + } + // a rail still lying on the fastening of the rail beside it goes without one of its own, unless it's a chair + std::vector dropped( Fastenings.size(), false ); + auto const chair = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( Fastenings[ order[ member ] ].part == part_chair || Fastenings[ order[ member ] ].part == part_slide ) { + return true; + } + } + return false; }; + auto const checks = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + if( fastening.part == part_check && false == fastening.both ) { + return true; + } + } + return false; }; + auto const faces = [&]( std::pair const &Rail, double const Toward ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + if( fastening.part == part_check && false == fastening.both && glm::dot( fastening.across, axis ) * Toward > 0.0 ) { + return true; + } + } + return false; }; + auto const plates = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( Fastenings[ order[ member ] ].part != part_plate ) { + return false; + } + } + return true; }; + auto const extent = [&]( std::pair const &Rail ) { + glm::dvec2 result { 0.0 }; + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( false == dropped[ order[ member ] ] ) { + result = glm::max( result, reach( Fastenings[ order[ member ] ], Fastenings[ order[ member ] ].scale ) ); + } + } + return result; }; + auto kept { std::size_t { 0 } }; + for( std::size_t index = 1; index < rails.size(); ++index ) { + auto const &first { rails[ kept ] }; + auto const &second { rails[ index ] }; + auto const apart { at( Fastenings[ order[ second.first ] ] ) - at( Fastenings[ order[ first.first ] ] ) }; + auto const overlaps = [&]() { + return extent( first ).y + extent( second ).x - apart > fastening_gap; }; + if( false == overlaps() ) { + kept = index; + continue; + } + auto firstreach { extent( first ) }; + auto secondreach { extent( second ) }; + auto const drop = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + dropped[ order[ member ] ] = true; + } }; + // the plates of the rail with their end toward the rail beside Room from it, the other end kept where it is + auto const clip = [&]( std::pair const &Rail, double const Toward, double const Room ) { + if( Room < 0.0 ) { + return false; + } + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto &fastening { Fastenings[ order[ member ] ] }; + auto const &span { Model.spans[ fastening.part ] }; + auto const turn { glm::dot( fastening.across, axis ) }; + auto const toward { turn * Toward > 0.0 ? span.y : span.x }; + auto const away { turn * Toward > 0.0 ? span.x : span.y }; + // the other end where the narrowing left it, or as the plate was if that leaves too little of it + auto stays { fastening.placed( away, fastening.scale ) }; + auto scale { ( Toward * turn * Room - stays ) / ( toward - away ) }; + if( scale < clip_least ) { + stays = fastening.placed( away, 1.0 ); + scale = ( Toward * turn * Room - stays ) / ( toward - away ); + } + if( scale < clip_least ) { + return false; + } + fastening.pivot = static_cast( away ); + fastening.shift = static_cast( stays - away ); + fastening.scale = static_cast( scale ); + } + return true; }; + // the fastenings of the rail made plates of their own + auto const replate = [&]( std::pair const &Rail ) -> std::pair const & { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto &fastening { Fastenings[ order[ member ] ] }; + fastening.part = part_plate; + fastening.scale = 1.f; + fastening.pivot = 0.f; + fastening.shift = 0.f; + } + return Rail; }; + // the brace of a chair where the rail beside comes too close for it, its slide plate kept + auto const brace = [&]( std::pair const &Rail, int const Side, double const Reaching = fastening_gap ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + if( fastening.part == part_chair && false == dropped[ order[ member ] ] && reach( fastening, fastening.scale )[ Side ] > Reaching ) { + dropped[ order[ member ] ] = true; + return true; + } + } + return false; }; + // a rail lying on the brace of a chair beside takes the brace away + if( brace( first, 1, apart ) || brace( second, 0, apart ) ) { + if( false == overlaps() ) { + kept = index; + continue; + } + firstreach = extent( first ); + secondreach = extent( second ); + } + // of two rails with check rails, the one with its check rail toward the other one, lying on the fastening of the other + // one then, goes in a plate cut short at that fastening, or without its own, the other one keeping its check rail seated + if( checks( first ) && checks( second ) && faces( first, 1.0 ) != faces( second, -1.0 ) ) { + if( faces( first, 1.0 ) && apart - check_seats <= secondreach.x ) { + if( false == clip( replate( first ), 1.0, apart - secondreach.x - fastening_gap ) ) { + drop( first ); + } + kept = index; + continue; + } + if( faces( second, -1.0 ) && apart - check_seats <= firstreach.y ) { + if( false == clip( replate( second ), -1.0, apart - firstreach.y - fastening_gap ) ) { + drop( second ); + } + continue; + } + } + if( false == chair( second ) && apart <= firstreach.y && ( false == checks( second ) || checks( first ) ) ) { + drop( second ); + continue; + } + if( false == chair( first ) && apart <= secondreach.x && ( false == checks( first ) || checks( second ) ) ) { + drop( first ); + } + else { + brace( first, 1 ); + if( overlaps() ) { + brace( second, 0 ); + } + } + // a rail in a plate still lying over the fastening of a rail beside held with more than a plate gets its plate cut short + // at the edge of that fastening, as on one plate with it, or goes without its own if the plate would no longer be under it + if( overlaps() ) { + if( plates( second ) && false == plates( first ) ) { + if( false == clip( second, -1.0, apart - extent( first ).y - fastening_gap ) ) { + drop( second ); + continue; + } + } + else if( plates( first ) && false == plates( second ) ) { + if( false == clip( first, 1.0, apart - extent( second ).x - fastening_gap ) ) { + drop( first ); + } + } + } + // the plates of two rails still lying over each other, as of the stock rails back to back, one plate under both + auto const plate = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( Fastenings[ order[ member ] ].part == part_plate && false == dropped[ order[ member ] ] ) { + return order[ member ]; + } + } + return Fastenings.size(); }; + auto const firstplate { plate( first ) }; + auto const secondplate { plate( second ) }; + if( firstplate < Fastenings.size() && secondplate < Fastenings.size() ) { + auto &one { Fastenings[ firstplate ] }; + auto const &other { Fastenings[ secondplate ] }; + auto const from { -reach( one, one.scale ).x }; + auto const to { apart + reach( other, other.scale ).y }; + if( reach( one, one.scale ).y + reach( other, other.scale ).x > apart ) { + auto const &span { Model.spans[ part_plate ] }; + auto const turn { glm::dot( one.across, axis ) }; + auto const scale { ( to - from ) / ( span.y - span.x ) }; + one.pivot = 0.f; + one.scale = static_cast( scale ); + one.shift = static_cast( ( turn > 0.0 ? from : -to ) - scale * span.x ); + dropped[ secondplate ] = true; + } + } + kept = index; + } + std::vector fitted; + for( std::size_t index = 0; index < Fastenings.size(); ++index ) { + if( false == dropped[ index ] ) { + fitted.push_back( Fastenings[ index ] ); + } + } + Fastenings = std::move( fitted ); +} + +void fastened_rails::lift( std::vector &Fastenings, std::vector const &Rails, beam const &Beam, beam_model const &Model ) { + + if( std::none_of( Rails.begin(), Rails.end(), []( hit const &Rail ) { return Rail.rise > 0.0; } ) ) { + return; + } + auto const start { plan( Beam.from ) }; + auto const axis { glm::normalize( plan( Beam.to - Beam.from ) ) }; + auto const padpart { Model.parts[ part_slide ] != nullptr ? part_slide : part_plate }; + auto const &padspan { Model.spans[ padpart ] }; + auto const &padheights { Model.heights[ padpart ] }; + // a pad on the timber under the fastening, along its across from Low to High, up from Bottom to Top in the model + auto const pad = [&]( fastening const &Over, double const Low, double const High, float const Bottom, float const Top ) { + fastening result { padpart, Over.point, Over.across }; + result.scale = static_cast( ( High - Low ) / ( padspan.y - padspan.x ) ); + result.shift = static_cast( Low - result.scale * padspan.x ); + result.lift = Bottom - padheights.x; + result.height = ( Top - Bottom ) / std::max( 0.001f, padheights.y - padheights.x ); + result.pad = true; + return result; }; + // where along the timber the fastening reaches, from and to + auto const footprint = [&]( fastening const &Fastening ) { + auto const at { glm::dot( plan( Fastening.point ) - start, axis ) }; + auto const turn { glm::dot( Fastening.across, axis ) }; + auto const &span { Model.spans[ Fastening.part ] }; + auto const a { at + turn * Fastening.placed( span.x, Fastening.scale ) }; + auto const b { at + turn * Fastening.placed( span.y, Fastening.scale ) }; + return glm::dvec2 { std::min( a, b ), std::max( a, b ) }; }; + auto const holds = [&]( fastening const &Fastening, hit const &Rail ) { + auto const reach { footprint( Fastening ) }; + return Fastening.part != part_chair && Rail.at >= reach.x && Rail.at <= reach.y; }; + std::vector pads; + for( auto &fastening : Fastenings ) { + if( fastening.pad || fastening.part == part_chair ) { + continue; + } + // moved up on a pad as wide as it as far as the least raised of the raised rails it holds, a rail not raised sunk in it + auto lift { 0.0 }; + for( auto const &rail : Rails ) { + if( rail.rise >= pad_least && holds( fastening, rail ) ) { + lift = ( lift > 0.0 ? std::min( lift, rail.rise ) : rail.rise ); + } + } + if( lift <= 0.0 ) { + continue; + } + auto const &span { Model.spans[ fastening.part ] }; + auto const a { fastening.placed( span.x, fastening.scale ) }; + auto const b { fastening.placed( span.y, fastening.scale ) }; + pads.push_back( pad( fastening, std::min( a, b ), std::max( a, b ), Model.bed, Model.heights[ fastening.part ].x + static_cast( lift ) ) ); + fastening.lift = static_cast( lift ); + } + // a rail raised more than its fastening on a pad of its own on it, as wide as its foot, short of the rails beside + for( std::size_t index = 0; index < Rails.size(); ++index ) { + auto const &rail { Rails[ index ] }; + auto const under { std::find_if( Fastenings.begin(), Fastenings.end(), [&]( fastening const &Fastening ) { + return false == Fastening.pad && holds( Fastening, rail ); } ) }; + if( under == Fastenings.end() || rail.rise - under->lift < pad_least ) { + continue; + } + auto low { rail.foot }; + auto high { rail.foot }; + if( index > 0 ) { + low = std::min( low, 0.5 * ( rail.at - Rails[ index - 1 ].at ) - fastening_gap ); + } + if( index + 1 < Rails.size() ) { + high = std::min( high, 0.5 * ( Rails[ index + 1 ].at - rail.at ) - fastening_gap ); + } + if( low + high < pad_narrowest ) { + continue; + } + fastening const at { padpart, Beam.from + ( Beam.to - Beam.from ) * ( rail.at / glm::length( plan( Beam.to - Beam.from ) ) ), axis }; + pads.push_back( pad( at, -low, high, Model.seat + under->lift, Model.seat + static_cast( rail.rise ) ) ); + } + Fastenings.insert( Fastenings.end(), pads.begin(), pads.end() ); +} + +std::vector check_fastenings() { + + std::vector result; + for( auto *track : simulation::Paths.sequence() ) { + if( track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active ) { + continue; + } + auto const beams { place( *track ) }; + auto const *model { timber_model( *track, beams ) }; + if( model == nullptr || false == model->fastened() ) { + continue; + } + fastening_row row; + row.track = track->name(); + std::optional rails; + for( auto const &beam : beams ) { + if( beam.plain ) { + continue; + } + if( false == rails.has_value() ) { + rails.emplace( *track ); + } + ++row.beams; + auto const start { plan( beam.from ) }; + auto const axis { glm::normalize( plan( beam.to - beam.from ) ) }; + struct footprint { + double from; + double to; + int part; + glm::dvec3 point; + }; + std::vector footprints; + auto const fastenings { rails->on( beam, *model ) }; + for( auto const &at : rails->bare( beam, *model, fastenings ) ) { + row.bares.push_back( at ); + } + for( auto const &fastening : fastenings ) { + if( fastening.pad ) { + continue; + } + ++row.parts[ fastening.part ]; + auto const at { glm::dot( plan( fastening.point ) - start, axis ) }; + auto const turn { glm::dot( fastening.across, axis ) }; + auto const a { at + turn * fastening.placed( model->spans[ fastening.part ].x, fastening.scale ) }; + auto const b { at + turn * fastening.placed( model->spans[ fastening.part ].y, fastening.scale ) }; + footprints.push_back( { std::min( a, b ), std::max( a, b ), fastening.part, fastening.point } ); + } + std::sort( footprints.begin(), footprints.end(), []( footprint const &Left, footprint const &Right ) { return Left.from < Right.from; } ); + for( std::size_t index = 1; index < footprints.size(); ++index ) { + auto const over { footprints[ index - 1 ].to - footprints[ index ].from }; + if( over <= fastening_overlap ) { + continue; + } + ++row.overlaps; + row.cases.push_back( { { footprints[ index - 1 ].part, footprints[ index ].part }, glm::length( plan( footprints[ index ].point - footprints[ index - 1 ].point ) ), over, footprints[ index ].point } ); + if( over > row.worst ) { + row.worst = over; + row.worstat = footprints[ index ].point; + row.worstparts = std::string { part_names[ footprints[ index - 1 ].part ] } + "/" + part_names[ footprints[ index ].part ]; + } + } + } + if( row.beams > 0 ) { + result.push_back( std::move( row ) ); + } + } + return result; +} + +std::vector check_rail_seats() { + + // the check rails, by points along their middle + struct check_line { + check_rail_gap gap; + std::vector points; + glm::dvec2 low { std::numeric_limits::max() }; + glm::dvec2 high { std::numeric_limits::lowest() }; + }; + std::vector lines; + for( auto *track : simulation::Paths.sequence() ) { + if( track == nullptr || track->m_editorremoved || false == bedded( *track ) || false == track_rails::cut_rails( *track ) ) { + continue; + } + auto const size { track->rail_dimensions() }; + auto const middle { 0.5 * std::abs( track->fTrackWidth ) + size.web }; + auto const &checks { track_rails::rail_checks( *track ) }; + 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() }; + for( auto const side : { -1, 1 } ) { + // the check rails and the guard rails of the crossings at wide angles + auto spans { track->m_railguards[ path ][ side > 0 ? 1 : 0 ] }; + for( auto const &check : checks[ path ][ side > 0 ? 1 : 0 ] ) { + spans.emplace_back( check.from, check.to ); + } + for( auto const &check : spans ) { + check_line line { { track->name(), path, side, check.x, check.y } }; + auto const from { std::max( 0.0, check.x ) }; + auto const to { std::min( length, check.y ) }; + auto const count { static_cast( std::ceil( ( to - from ) / fastening_step ) ) }; + for( int step = 0; step <= count && to > from; ++step ) { + glm::dvec3 point, across; + segment->plan_frame( from + ( to - from ) * step / std::max( 1, count ), point, across ); + auto const flat { plan( across ) }; + if( glm::length( flat ) < 1e-9 ) { + continue; + } + auto const inward { glm::normalize( flat ) * static_cast( -side ) }; + auto const position { plan( point ) - inward * ( middle - check_seats ) }; + line.points.push_back( position ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); + } + if( line.points.size() > 1 ) { + lines.push_back( std::move( line ) ); + } + } + } + } + } + // where each timber with the fastenings crosses a check rail, a seat of a fastening of the timber there + std::vector timbers( lines.size(), 0 ); + for( auto *track : simulation::Paths.sequence() ) { + if( track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active ) { + continue; + } + auto const beams { place( *track ) }; + auto const *model { timber_model( *track, beams ) }; + if( model == nullptr || false == model->fastened() ) { + continue; + } + std::optional rails; + for( auto const &beam : beams ) { + if( beam.plain ) { + continue; + } + if( false == rails.has_value() ) { + rails.emplace( *track ); + } + auto const start { plan( beam.from ) }; + auto const span { plan( beam.to - beam.from ) }; + auto const low { glm::min( start, start + span ) }; + auto const high { glm::max( start, start + span ) }; + std::vector seats; + auto const fastenings { rails->on( beam, *model ) }; + auto const under = [&]( glm::dvec2 const &Point, bool const Whole ) { + return std::any_of( fastenings.begin(), fastenings.end(), [&]( fastening const &Fastening ) { + if( Whole && false == Fastening.whole ) { + return false; + } + auto const along { glm::dot( Point - plan( Fastening.point ), Fastening.across ) }; + auto const a { Fastening.placed( model->spans[ Fastening.part ].x, Fastening.scale ) }; + auto const b { Fastening.placed( model->spans[ Fastening.part ].y, Fastening.scale ) }; + return along >= std::min( a, b ) && along <= std::max( a, b ); } ); }; + for( auto const &fastening : fastenings ) { + if( fastening.pad ) { + continue; + } + seats.push_back( plan( fastening.point ) + fastening.across * fastening.placed( 0.0, fastening.scale ) ); + if( fastening.part == part_check ) { + seats.push_back( plan( fastening.point ) + fastening.across * fastening.placed( check_seats, fastening.scale ) ); + } + } + for( std::size_t index = 0; index < lines.size(); ++index ) { + auto &line { lines[ index ] }; + if( line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { + continue; + } + for( std::size_t point = 1; point < line.points.size(); ++point ) { + auto const chord { line.points[ point ] - line.points[ point - 1 ] }; + double u, v; + if( false == track_rails::meet( start, span, line.points[ point - 1 ], chord, 1e-12, u, v ) || u < 0.0 || u > 1.0 || v < 0.0 || v >= 1.0 ) { + continue; + } + auto const at { start + span * u }; + ++timbers[ index ]; + // in a seat, or on a fastening holding all the rails over it + auto const seated { under( at, true ) || std::any_of( seats.begin(), seats.end(), [&]( glm::dvec2 const &Seat ) { + return glm::length( Seat - at ) < 0.03; } ) }; + if( false == seated ) { + if( line.gap.bare == 0.0 ) { + line.gap.at = at; + } + line.gap.bare += 1.0; + if( false == under( at, false ) ) { + ++line.gap.unsupported; + } + } + break; + } + } + } + } + std::vector result; + for( std::size_t index = 0; index < lines.size(); ++index ) { + if( lines[ index ].gap.bare > 0.0 ) { + lines[ index ].gap.timbers = timbers[ index ]; + result.push_back( lines[ index ].gap ); + } + } + return result; +} + +} // namespace track_sleepers diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h new file mode 100644 index 00000000..41c1ee80 --- /dev/null +++ b/world/SleeperFastenings.h @@ -0,0 +1,159 @@ +/* +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 +#include + +#include + +#include "world/Knots.h" +#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 + float scale { 1.f }; // along its x, a plate or a slide plate narrowed where it'd lie over the fastenings of rails close beside, a plate stretched under two + float pivot { 0.f }; // the x of the part kept in place as it's narrowed + float shift { 0.f }; // along its x, how far the pivot is moved + bool both { false }; // the fastening of two rails close beside each other, neither with a check rail of its own seated + bool whole { false }; // holding more rails than its seats, out to its ends: not narrowed + float lift { 0.f }; // how far up it's moved, under a rail of the low section raised as a blade + float height { 1.f }; // along its y, from the bottom of the part: a pad stretched to fill under a raised rail + bool pad { false }; // under a raised rail or its fastening, not holding a rail itself + // where an x of the part lands along its across, at the scale + double placed( double const X, double const Scale ) const { + return pivot + shift + Scale * ( X - pivot ); } +}; + +// the rails of the tracks around a track, which lie on its timbers +class fastened_rails { + +public: + // 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 + }; + // 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 }; + double foot { 0.0 }; + double rise { 0.0 }; + }; + + 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; + // the rails lying on the timber, in the order along it + std::vector lying( beam const &Beam ) const; + // where the rails on the timber lie over none of the fastenings + std::vector bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const; + +private: + // a rail by points along its middle + struct rail { + TTrack *track { nullptr }; + int path { 0 }; + int side { 1 }; + knots::line rises; // of its foot, along the path: a wing rail of the low section raised as the blades, the rail going on from the heel of a blade + 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 }; + double foot { 0.0 }; // from the middle of the rail to the edge of its foot toward the middle of the path + // 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; + std::vector guards; // with the guard rail of a crossing at a wide angle inside it + bool guard { false }; // the guard rail itself + role at( double const Distance ) const; + bool guarded( double const Distance ) const; + }; + // the rails of each track, worked out as the first track around it is laid and kept until it's laid anew or goes far, or + // the crossings change + using kept_rails = std::unordered_map const>>; + static kept_rails &kept(); + friend void forget_rails( TTrack const &Track ); + // the rails of the track, in their parts between the cuts, and its guard rails + static std::vector rails_of( TTrack &Track ); + + // the rails lying on the timber, in the order along it, one where several lie on one another, but the blades + std::vector rails_on( beam const &Beam ) const; + // the fastenings of rails close by fitted to the room between them: narrowed across, a plate cut short at the fastening of the + // rail beside it or left out, the plates of two rails lying over each other made one + static void fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ); + // the fastenings under the raised rails: one holding them moved up on a pad as far as the least raised of them, a rail not raised + // sunk in it, a rail raised more on a pad of its own on it + static void lift( std::vector &Fastenings, std::vector const &Rails, beam const &Beam, beam_model const &Model ); + // the plates of the rails lying along the timber rather than across it, as of a track crossing another square to it, on two + // timbers side by side under each of its rails: as far apart as the beams at most, in the room the fastenings of the rails + // across leave, on the timber with the rail over its middle or over its side toward the next one + void along( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) const; + + std::vector const>> m_kept; + std::vector> m_rails; +}; + +// the fastenings of a track with them in the model of its timbers, as laid on the timbers laid with it +struct fastening_row { + std::string track; + std::size_t beams { 0 }; + std::array parts {}; + std::size_t overlaps { 0 }; // fastenings lying over one another on a timber, along it + std::vector bares; // where rails lie on a timber over no fastening + double worst { 0.0 }; + glm::dvec3 worstat { 0.0 }; + std::string worstparts; + struct overlap { + int parts[ 2 ]; + double apart; // of the rails the two lie under + double over; + glm::dvec3 at; + }; + std::vector cases; +}; +// those of all the tracks laying timbers with fastenings +std::vector check_fastenings(); + +// a check rail of a track with timbers under it and no fastening seating it there, along some of its length +struct check_rail_gap { + std::string track; + int path { 0 }; + int side { 1 }; + double from { 0.0 }; // the check rail, along the path + double to { 0.0 }; + std::size_t timbers { 0 }; // with the fastenings, under it + double bare { 0.0 }; // of those, with no seat under it + std::size_t unsupported { 0 }; // of those, with no fastening under it at all + glm::dvec2 at { 0.0 }; // where the first of those crosses it +}; +// those of all the tracks with fastenings in the model of their timbers +std::vector check_rail_seats(); + +} // namespace track_sleepers diff --git a/world/SleeperLayout.cpp b/world/SleeperLayout.cpp index 23bca2fa..5fc40183 100644 --- a/world/SleeperLayout.cpp +++ b/world/SleeperLayout.cpp @@ -152,6 +152,7 @@ public: keep_apart(); fill_gaps(); keep_apart(); + share_panels(); for( auto const &laid : beams ) { result->beams[ laid.owner ].push_back( laid.value ); } @@ -203,6 +204,12 @@ private: return result; } + // the sine of the angle the paths of a grid cross at, no less than of 30 degrees + static double grid_sine( feature const &Feature ) { + + return std::max( 0.5, std::sin( 2.0 * std::asin( Feature.sine ) ) ); + } + // the pins of the lines led along the path: the rails of a path crossing this one so wide it's laid on a grid of beams; elsewhere // the lines are spread evenly std::vector pins( sampled_path const &Path ) const { @@ -216,17 +223,46 @@ private: if( feature.paths[ side ] != &Path ) { continue; } - // under the rails of the other path, which runs across the beams + // under the middles of the rails of the other path, which runs across the beams auto const *other { feature.paths[ 1 - side ] }; - auto const sine { std::max( 0.5, std::sin( 2.0 * std::asin( feature.sine ) ) ) }; - result.push_back( feature.at[ side ] - 0.5 * other->gauge / sine ); - result.push_back( feature.at[ side ] + 0.5 * other->gauge / sine ); + auto const rail { ( 0.5 * other->gauge + rail_web ) / grid_sine( feature ) }; + result.push_back( feature.at[ side ] - rail ); + result.push_back( feature.at[ side ] + rail ); } } std::sort( result.begin(), result.end() ); return result; } + // whether the beam of the path at the distance is a timber of a crossing at a wide angle: where it crosses another path so wide + // they're laid as a grid, the other path laying none there, or under the guard rails of the crossing, as far as they reach + bool crossing_timber( sampled_path const &Path, double const Distance ) const { + + return std::any_of( features.begin(), features.end(), [&]( feature const &Feature ) { + if( Feature.frog ) { + return false; + } + auto const angle { 2.0 * std::asin( std::min( 1.0, Feature.sine ) ) }; + auto const acute { std::min( angle, glm::pi() - angle ) }; + for( int side = 0; side < 2; ++side ) { + if( Feature.paths[ side ] != &Path ) { + continue; + } + auto const *other { Feature.paths[ 1 - side ] }; + auto reach { Feature.grid ? ( other->half + half_width + end_gap ) / grid_sine( Feature ) : 0.0 }; + if( acute > track_rails::wide_angle ) { + // past where the rails cross, as far as the guard rails go on along them, a beam reaching under their ends too + reach = std::max( + reach, + ( 0.5 * other->gauge + 0.5 * Path.gauge * std::cos( acute ) + track_rails::flangeway + rail_head + track_rails::guard_reach ) / std::sin( acute ) + half_width ); + } + if( std::abs( Distance - Feature.at[ side ] ) < reach ) { + return true; + } + } + return false; } ); + } + // the line of the timbers carrying the path nearest to the distance on the side, between the distance and the reach from it std::optional record_near( std::size_t const Index, double const Distance, double const Side, double const Reach ) const { @@ -343,8 +379,10 @@ private: } std::vector anchors { start }; for( auto const pin : pins( path ) ) { - if( pin > start.at + 0.5 * pitch_min && pin < end.at - 0.5 * pitch_min && pin - anchors.back().at >= pitch_min ) { - anchors.push_back( { pin, 0.0 } ); + // two timbers side by side under the rail running along them, its fastenings on both + if( pin - half_width > start.at + 0.5 * pitch_min && pin + half_width < end.at - 0.5 * pitch_min && pin - half_width - anchors.back().at >= pitch_min ) { + anchors.push_back( { pin - half_width, 0.0 } ); + anchors.push_back( { pin + half_width, 0.0 } ); } } anchors.push_back( end ); @@ -515,18 +553,48 @@ private: auto const top { point - rolledup * static_cast( path.track->rail_height() ) }; // a sleeper of a plain track running alone and square to it, the rest are the timbers of the switches and crossings auto const carried { std::count_if( bands.begin(), bands.end(), []( band const &Band ) { return Band.carried; } ) }; - auto const plain { path.track->eType == tt_Normal && carried == 1 && pieces.size() == 1 && false == pieces.front().clipped && std::abs( glm::dot( forward, along ) ) > 0.9998 }; + auto const plain { path.track->eType == tt_Normal && carried == 1 && pieces.size() == 1 && false == pieces.front().clipped && std::abs( glm::dot( forward, along ) ) > 0.9998 && false == crossing_timber( path, Distance ) }; + auto const owner { std::find_if( bands.begin(), bands.end(), []( band const &Band ) { + return Band.carried && false == Band.path->track->m_timber_model_name.empty(); } ) }; + TTrack const *panel { owner != bands.end() ? owner->path->track : nullptr }; for( std::size_t index = 0; index < pieces.size(); ++index ) { auto const &piece { pieces[ index ] }; auto const &way { turned[ index ] }; if( way == along ) { - beams.push_back( { path.track, { top + rolledacross * piece.from, top + rolledacross * piece.to, glm::dvec3 { along.x, 0.0, along.y }, plain, piece.corefrom - piece.from, piece.coreto - piece.from } } ); + beams.push_back( { path.track, { top + rolledacross * piece.from, top + rolledacross * piece.to, glm::dvec3 { along.x, 0.0, along.y }, plain, piece.corefrom - piece.from, piece.coreto - piece.from, panel } } ); continue; } // turned about where the middle of its track crosses the line auto const centre { top + rolledacross * piece.single->centre }; glm::dvec3 const turnedacross { glm::dvec3 { -way.y, 0.0, way.x } * std::cos( roll ) - up * std::sin( roll ) }; - beams.push_back( { path.track, { centre + turnedacross * ( piece.from - piece.single->centre ), centre + turnedacross * ( piece.to - piece.single->centre ), glm::dvec3 { way.x, 0.0, way.y }, false, piece.corefrom - piece.from, piece.coreto - piece.from } } ); + beams.push_back( { path.track, { centre + turnedacross * ( piece.from - piece.single->centre ), centre + turnedacross * ( piece.to - piece.single->centre ), glm::dvec3 { way.x, 0.0, way.y }, false, piece.corefrom - piece.from, piece.coreto - piece.from, panel } } ); + } + } + + // the timbers whose lines carry no track with a model of timbers of its own of the model of the nearest timber with one + void share_panels() { + + std::vector> panels; + for( auto const &laid : beams ) { + if( false == laid.value.plain && laid.value.panel != nullptr ) { + panels.emplace_back( plan( 0.5 * ( laid.value.from + laid.value.to ) ), laid.value.panel ); + } + } + if( panels.empty() ) { + return; + } + for( auto &laid : beams ) { + if( laid.value.plain || laid.value.panel != nullptr ) { + continue; + } + auto const middle { plan( 0.5 * ( laid.value.from + laid.value.to ) ) }; + auto nearest { panel_reach }; + for( auto const &[ at, panel ] : panels ) { + if( auto const distance { glm::length( at - middle ) }; distance < nearest ) { + nearest = distance; + laid.value.panel = panel; + } + } } } diff --git a/world/SleeperModels.cpp b/world/SleeperModels.cpp index 648fe434..9138450d 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,22 +109,32 @@ 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" }; 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.parts[ part ] = model.model->GetFromName( part_names[ part ] ); + 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 ) { + if( model.parts[ part ] == nullptr ) { + continue; + } glm::vec2 span { std::numeric_limits::max(), std::numeric_limits::lowest() }; + glm::vec2 heights { std::numeric_limits::max(), std::numeric_limits::lowest() }; + auto const timber { part < timber_parts }; vertices_of( model.parts[ part ], [&]( glm::vec3 const &Position ) { span = { std::min( span.x, Position.x ), std::max( span.y, Position.x ) }; - top = std::max( top, Position.y ); } ); + heights = { std::min( heights.x, Position.y ), std::max( heights.y, Position.y ) }; + if( timber ) { + top = std::max( top, Position.y ); + } } ); model.spans[ part ] = span; - model.span = { std::min( model.span.x, span.x ), std::max( model.span.y, span.y ) }; + model.heights[ part ] = heights; + if( timber ) { + model.span = { std::min( model.span.x, span.x ), std::max( model.span.y, span.y ) }; + } } model.stretchable = model.spans[ part_main ].y > model.spans[ part_main ].x; } @@ -112,8 +157,19 @@ 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.bed = top; 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() ) { + for( auto const part : { part_chair, part_slide } ) { + if( model.parts[ part ] != nullptr ) { + model.slide = std::max( model.slide, surface_at( model.parts[ part ], 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 +181,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" : "" ) + ( model.parts[ part_slide ] != nullptr ? " slide" : "" ) : + 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 ); @@ -156,6 +216,27 @@ beam_model const *timber_model( TTrack const &Track ) { return timber_model(); } +beam_model const *timber_model( TTrack const &Track, std::vector const &Beams ) { + + if( false == Track.m_timber_model_name.empty() ) { + return timber_model( Track ); + } + std::vector> panels; + for( auto const &beam : Beams ) { + if( beam.plain || beam.panel == nullptr ) { + continue; + } + auto found { std::find_if( panels.begin(), panels.end(), [&]( auto const &Panel ) { return Panel.first == beam.panel; } ) }; + if( found == panels.end() ) { + panels.emplace_back( beam.panel, 0 ); + found = std::prev( panels.end() ); + } + ++found->second; + } + auto const most { std::max_element( panels.begin(), panels.end(), []( auto const &Left, auto const &Right ) { return Left.second < Right.second; } ) }; + return ( most != panels.end() ? timber_model( *most->first ) : timber_model( Track ) ); +} + beam_model const *plain_model( TTrack const &Track ) { if( Track.m_sleeper_enabled && false == Track.m_sleeper_model_name.empty() ) { diff --git a/world/SleeperRules.h b/world/SleeperRules.h index c41dc67c..b6fa0b8d 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -20,6 +20,9 @@ double constexpr pitch_min { 0.83 * pitch }; double constexpr pitch_max { 1.08 * pitch }; // how far out from the middle of a track its rail reaches past half the gauge: the foot of the rail outside the gauge line double constexpr rail_reach { 0.1115 }; +// how far out from the gauge line the middle of the rail is, and how wide its head is +double constexpr rail_web { 0.0365 }; +double constexpr rail_head { 0.0735 }; // how far a beam reaches out past the foot of the rail, 2.6 m sleepers under the standard gauge, and how far at least double constexpr overhang { 0.471 }; double constexpr overhang_min { 0.25 }; @@ -47,8 +50,52 @@ double constexpr level_tolerance { 1.0 }; // how far around a track the tracks are taken in to lay its beams: those whose beams it carries, or which carry its own, and // those which set where the beams of these go double constexpr around_margin { 20.0 }; +// a timber whose line carries no track with a model of timbers of its own, as under the short tracks between the halves of a slip, +// is of the model of the nearest timber with one so far from it +double constexpr panel_reach { 1.5 }; // 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.2 }; +double constexpr fastening_nose { 0.05 }; +// three rails or more with less than this between each and the next, as the legs of a nose with the wing rails beside, are held together +double constexpr fastening_crowd { 0.3 }; +// how far apart the rails lie in the fastening of both, the check part of the model +double constexpr check_seats { 0.113 }; +// how long a fastening is along its rail, at most +double constexpr fastening_length { 0.19 }; +// how far past the edge of the foot of a rail its fastening reaches, with the clip holding it +double constexpr fastening_clip { 0.09 }; +// the room left between the fastenings of rails close by, and how narrow their plates get to keep it +double constexpr fastening_gap { 0.01 }; +double constexpr plate_least { 0.6 }; +// a plate cut short at the fastening of the rail beside, as on one plate with it, no narrower than this +double constexpr clip_least { 0.4 }; +double constexpr slide_least { 0.25 }; +// a fastening of two rails narrowed no more than this, the rails lying a little off its seats then +double constexpr check_least { 0.8 }; +// 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 }; +// a rail raised less than this goes without a pad under it, and a pad narrower than the second one is left out +double constexpr pad_least { 0.002 }; +double constexpr pad_narrowest { 0.02 }; +// how far two fastenings on a timber may reach over one another before the check counts them +double constexpr fastening_overlap { 0.01 }; +// how far from the camera the fastenings are drawn at most +double constexpr fastening_range { 120.0 }; +// how much further out the fastenings are laid, there as the camera comes near +double constexpr fastening_lead { 30.0 }; +// how near the camera the beams of a track are laid at once as they're needed; further out while the laying in a frame took less +// than the budget, in ms, the rest left for the frames after +double constexpr lay_near { 50.0 }; +double constexpr lay_budget { 2.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 5c6f25b3..cc54067d 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1633,6 +1633,16 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { auto const layout { plan_rails( Bank != 0 ) }; if( Bank != 0 ) { m_crossings = layout.crossed; + for( int path = 0; path < 2; ++path ) { + for( int side = 0; side < 2; ++side ) { + m_railplans[ path ][ side ] = layout.plans[ path ][ side ]; + m_railguards[ path ][ side ].clear(); + for( auto const &guard : layout.guards[ path ][ side ] ) { + m_railguards[ path ][ side ].emplace_back( guard.from, guard.to ); + } + } + } + track_sleepers::forget_rails( *this ); } auto const lay_rail = [&]( int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, double const From, bool const Openstart, bool const Openend, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { return this->lay_rail( layout, drawing, Path, Profile, Side, Texturelength, From, Openstart, Openend, Geometry, Cuts ); }; @@ -2356,8 +2366,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 ) { @@ -2366,7 +2377,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/Track.h b/world/Track.h index b7b5b4d5..e137c469 100644 --- a/world/Track.h +++ b/world/Track.h @@ -217,6 +217,8 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; std::vector m_crossings; // tracks whose rails cross the rails of this one, found when its geometry was made + track_rails::rail_plan m_railplans[ 2 ][ 2 ]; // the cuts of its rails, by path and right side, as its geometry was made + std::vector m_railguards[ 2 ][ 2 ]; // the guard rails inside its rails at the crossings at wide angles, from and to along the path, as its geometry was made scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index bcb5cf09..34709d4b 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -10,12 +10,14 @@ 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" #include "world/SleeperRules.h" #include "world/SleeperWorker.h" #include "world/Track.h" +#include "world/TrackRails.h" #include "simulation/simulation.h" #include "scene/scene.h" #include "utilities/utilities.h" @@ -24,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include namespace track_sleepers { @@ -63,6 +66,33 @@ bool lays( TTrack const &Track ) { return bedded( Track ) && timber_model( Track ) != nullptr; } +double blade_rise( TTrack const &Track ) { + + return ( Track.eType == tt_Switch ? wing_rise( Track ) : 0.0 ); +} + +double wing_rise( TTrack const &Track ) { + + return ( lays( Track ) ? timber_model( Track )->slide : 0.0 ); +} + +glm::dvec2 stock_stretch( TTrack const &Track, int const Path, int const Side ) { + + if( Track.eType != tt_Switch || Track.SwitchExtension == nullptr || Side != ( Track.SwitchExtension->RightSwitch == ( Path == 1 ) ? 1 : -1 ) ) { + return glm::dvec2 { -1.0 }; + } + auto const &shape { Track.blade( 1 - Path ) }; + return { std::max( 0.0, ( shape.valid ? shape.tip : 0.0 ) - chair_lead ), track_rails::blade_length( Track, 1 - Path ) }; +} + +double blade_heel( TTrack const &Track, int const Path, int const Side ) { + + if( Track.eType != tt_Switch || Track.SwitchExtension == nullptr || Side != ( Track.SwitchExtension->RightSwitch == ( Path == 0 ) ? 1 : -1 ) ) { + return 0.0; + } + return track_rails::blade_length( Track, Path ); +} + std::vector> shared_zones( TTrack const &Track, int const Path ) { std::vector> result; @@ -93,29 +123,26 @@ std::pair shared_zone( TTrack const &Track, int const Path ) { return { zones.front().first, zones.back().second }; } -void lay( TTrack &Track ) { +namespace { + +// the instances made so far, of all the tracks +std::uint64_t makings { 0 }; + +// the beams as instances of the parts of the model of the track: those of the fastenings with them, or left for later and the beams +// kept for them meanwhile +void instance( TTrack &Track, std::vector &&Beams, bool const Fastened ) { auto &laid { Track.m_sleepers3d }; - std::vector placed; - if( laid.active && in_background() && timber_model( Track ) != nullptr ) { - // worked out in the background; till then the track keeps what it has, and tries again - auto beams { laid_out( Track ) }; - if( false == beams.has_value() ) { - ask_layout( Track ); - return; - } - placed = std::move( *beams ); - } - laid.dirty = false; + laid.made = ++makings; for( auto &instances : laid.instances ) { instances.clear(); } laid.whole.clear(); laid.plain.clear(); laid.chunks.clear(); - laid.timbers = timber_model( Track ); - laid.plains = plain_model( Track ); - if( false == laid.active || laid.timbers == nullptr ) { + laid.fastened = true; + std::vector {}.swap( laid.beams ); + if( laid.timbers == nullptr ) { return; } // the sleepers of the model of the track itself (sleepermodel) at the height they're drawn at without the 3D sleepers: the @@ -123,16 +150,9 @@ void lay( TTrack &Track ) { auto const ownplains { laid.plains != nullptr && Track.m_sleeper_enabled && laid.plains != plain_model() }; auto const plainlift { ownplains ? Track.rail_height() + Track.m_sleeper_offset.z : 0.f }; auto const &parts { *laid.timbers }; - if( auto const panel { panel_off_plane( Track ) }; panel.first > coplanar_tolerance ) { - WriteLog( - "sleepers3d: switch \"" + Track.name() + "\" has its paths " + to_string( panel.first * 1000.0, 1 ) + " mm off one plane " - + to_string( panel.second, 2 ) + " m along, where they share the timbers of its panel" ); - } - laid.laying = true; - auto const beams { in_background() ? std::move( placed ) : place( Track ) }; - laid.laying = false; - // 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; found at the first timber, none for plain sleepers + std::optional rails; auto const partlength = [&]( int const Part ) { return static_cast( parts.spans[ Part ].y - parts.spans[ Part ].x ); }; @@ -166,7 +186,7 @@ void lay( TTrack &Track ) { runbeams.clear(); }; - for( auto const &beam : beams ) { + for( auto const &beam : Beams ) { if( runbeams.empty() ) { for( int part = 0; part < part_count; ++part ) { run.first[ part ] = static_cast( laid.instances[ part ].size() ); @@ -232,6 +252,24 @@ 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( Fastened && parts.fastened() && false == ( beam.plain && laid.plains != nullptr ) ) { + if( false == rails.has_value() ) { + rails.emplace( Track ); + } + // 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 }; + auto const pivot { glm::vec3 { fastening.pivot, parts.heights[ fastening.part ].x, 0.f } }; + laid.instances[ fastening.part ].emplace_back( + glm::translate( glm::scale( glm::translate( place, glm::vec3 { fastening.shift, fastening.lift - parts.seat, 0.f } + pivot ), glm::vec3 { fastening.scale, fastening.height, 1.f } ), -pivot ) ); + } + } runbeams.emplace_back( middle, half ); if( runbeams.size() >= chunk_beams ) { close_run(); @@ -245,6 +283,56 @@ void lay( TTrack &Track ) { laid.whole.shrink_to_fit(); laid.plain.shrink_to_fit(); laid.chunks.shrink_to_fit(); + if( false == Fastened && parts.fastened() ) { + laid.fastened = false; + laid.beams = std::move( Beams ); + } +} + +} // namespace + +void lay( TTrack &Track, bool const Fastened ) { + + auto &laid { Track.m_sleepers3d }; + std::vector placed; + if( laid.active && in_background() && timber_model( Track ) != nullptr ) { + // worked out in the background; till then the track keeps what it has, and tries again + auto beams { laid_out( Track ) }; + if( false == beams.has_value() ) { + ask_layout( Track ); + return; + } + placed = std::move( *beams ); + } + laid.dirty = false; + laid.timbers = timber_model( Track ); + laid.plains = plain_model( Track ); + if( false == laid.active || laid.timbers == nullptr ) { + instance( Track, {}, true ); + return; + } + if( auto const panel { panel_off_plane( Track ) }; panel.first > coplanar_tolerance ) { + WriteLog( + "sleepers3d: switch \"" + Track.name() + "\" has its paths " + to_string( panel.first * 1000.0, 1 ) + " mm off one plane " + + to_string( panel.second, 2 ) + " m along, where they share the timbers of its panel" ); + } + laid.laying = true; + auto beams { in_background() ? std::move( placed ) : place( Track ) }; + laid.laying = false; + laid.timbers = timber_model( Track, beams ); + // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn + forget_layout( Track ); + instance( Track, std::move( beams ), Fastened ); +} + +void fasten( TTrack &Track ) { + + auto &laid { Track.m_sleepers3d }; + if( laid.fastened || laid.dirty ) { + return; + } + auto beams { std::move( laid.beams ) }; + instance( Track, std::move( beams ), true ); } void release_far( glm::dvec3 const &Camera, double const Range ) { @@ -258,15 +346,19 @@ void release_far( glm::dvec3 const &Camera, double const Range ) { next = 0; } auto *track { tracks[ next++ ] }; - if( track == nullptr || false == track->m_sleepers3d.active || track->m_sleepers3d.laying ) { + if( track == nullptr || track->m_sleepers3d.laying ) { continue; } if( glm::length( track->location() - Camera ) - track->radius() <= Range ) { continue; } + forget_rails( *track ); + if( false == track->m_sleepers3d.active ) { + continue; + } auto &laid { track->m_sleepers3d }; forget_layout( *track ); - auto const holds { laid.chunks.capacity() > 0 || laid.whole.capacity() > 0 || laid.plain.capacity() > 0 + auto const holds { laid.chunks.capacity() > 0 || laid.whole.capacity() > 0 || laid.plain.capacity() > 0 || laid.beams.capacity() > 0 || std::any_of( laid.instances.begin(), laid.instances.end(), []( std::vector const &Instances ) { return Instances.capacity() > 0; } ) }; if( false == holds ) { continue; @@ -277,6 +369,7 @@ void release_far( glm::dvec3 const &Camera, double const Range ) { std::vector {}.swap( laid.whole ); std::vector {}.swap( laid.plain ); std::vector {}.swap( laid.chunks ); + std::vector {}.swap( laid.beams ); laid.dirty = true; } } diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index 06fb1c62..596544d6 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,18 @@ 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 brace of the slide chair of a stock rail with the blade beside it, its origin under the stock rail, 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_slide, // the slide plate of the chair, apart from it on its -x side, from the edge of the foot of the stock rail on, shortened + // toward it where another rail lies close part_count }; +// the names of the submodels of the parts +inline constexpr std::array part_names { "begin", "main", "end", "plate", "chair", "check", "slide" }; +// 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 { @@ -44,6 +56,8 @@ struct beam { bool plain { false }; // a sleeper of a track running alone, square to it, not a timber of a switch or a crossing double corefrom { 0.0 }; // the stretch the feet of the rails it carries take, along it from its from end double coreto { 0.0 }; + TTrack const *panel { nullptr }; // of the tracks its line carries, the one with a model of timbers of its own: its own track first; else + // that of the nearest timber with one }; // a model sleepers or timbers are made of, as found in it @@ -59,8 +73,13 @@ 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 + float bed { 0.f }; // the height in the model of the top of the timber + std::array heights {}; // from and to along y, of the parts 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 @@ -86,6 +105,9 @@ struct laid { std::vector whole; // transforms of the timbers of a model without parts, drawn whole std::vector plain; // transforms of the plain sleepers, whole models, relative to the origin of the track std::vector chunks; + std::uint64_t made { 0 }; // which making of the instances they are, none other of any track the same: a copy of them tells by it + bool fastened { false }; // the fastenings are laid, else they're laid as the track comes near where they're drawn + std::vector beams; // kept for the fastenings till they're laid }; // how far the ballast of a track with beams is lowered, so they stick out of it @@ -99,12 +121,25 @@ beam_model const *plain_model(); // the models the track lays its beams of: those set for it with timbermodel and sleepermodel, else the ones of eu07.ini beam_model const *timber_model( TTrack const &Track ); beam_model const *plain_model( TTrack const &Track ); +// the model the track lays the timbers of the beams of: its own, else that of the tracks most of its timbers carry with one of their +// own, as a switch it shares its timbers with, else the one of eu07.ini +beam_model const *timber_model( TTrack const &Track, std::vector const &Beams ); // how long the plain sleepers of the track are, of the model it lays them of; 2.6 m at the standard gauge where there's none double plain_length( TTrack const &Track ); // whether the track has the ballast the simulator makes, which the beams are laid on when there's a model of them 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 ); +// how far the feet of its wing rails of the low section of the blades are raised as the blades; 0 where it lays no timbers with chairs +double wing_rise( TTrack const &Track ); +// the stretch along the path its rail of Side is the stock rail of the blade of the other path along, with the slide chairs from +// a little before the tip of the blade to its heel; from past to where there's none +glm::dvec2 stock_stretch( TTrack const &Track, int const Path, int const Side ); +// how far along the path its rail of Side is the blade of the path, to its heel, the rail going on from there; 0 where it's none +double blade_heel( TTrack const &Track, int const Path, int const Side ); // 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 @@ -120,10 +155,14 @@ struct pack_model { // the models of sleepers and timbers in the scenery data, found once in models/ by the names of their files telling sleepers // or timbers: podklad, podrozjazd, sleeper, timber. the timber ones first, then by the name std::vector const &pack_models(); -// the beams of the track placed anew, as instances of the parts of the model -void lay( TTrack &Track ); +// the beams of the track placed anew, as instances of the parts of the model; with the fastenings, or those left for later +void lay( TTrack &Track, bool const Fastened = true ); +// the fastenings left out laid on the beams of the track +void fasten( TTrack &Track ); // the beams of the tracks around the track to be placed again, as they depend on where it runs void unsettle_around( TTrack &Track ); +// the rails of the track kept for the fastenings of the tracks around let go, as they're laid anew or the track goes far +void forget_rails( TTrack const &Track ); // the beams worked out in a thread of their own as the tracks come near, while the simulation runs; off in the editor, where the // tracks change and the beams are worked out as they're needed. turning it off waits for the beams being worked out void lay_in_background( bool const Background );