mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-18 07:49:19 +02:00
Add sleepermodel optional parameter for tracks
This commit is contained in:
@@ -3083,6 +3083,109 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
|
||||
m_renderpass.draw_stats.models += static_cast<int>( total );
|
||||
}
|
||||
|
||||
// Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the
|
||||
// existing GPU-instanced submodel pipeline. The track owns a vector of pre-baked
|
||||
// local-space matrices; we compose each with `view * translate(track_origin - camera)`
|
||||
// to get a camera-space modelview, then issue batched glDrawElementsInstancedBaseVertex
|
||||
// calls -- one batch per MAX_INSTANCES_PER_BATCH sleepers.
|
||||
//
|
||||
// Skipped entirely when:
|
||||
// - Global.SleeperDistance == 0 (sleeper rendering globally disabled)
|
||||
// - the track has no sleepermodel
|
||||
// - the track is farther than Global.SleeperDistance meters from the camera
|
||||
void opengl33_renderer::Render_Sleepers( TTrack *Track )
|
||||
{
|
||||
if( Track == nullptr ) { return; }
|
||||
if( false == Track->m_sleeper_enabled ) { return; }
|
||||
if( Track->m_sleeper_model == nullptr ) { return; }
|
||||
if( Track->m_sleeper_local_transforms.empty() ) { return; }
|
||||
if( Global.SleeperDistance <= 0.f ) { return; }
|
||||
|
||||
// only the color and reflection passes draw sleepers; shadow/pick skip them on purpose
|
||||
// (sleeper shadows would mostly fall back under the trackbed and pick already operates on
|
||||
// the track itself).
|
||||
switch( m_renderpass.draw_mode ) {
|
||||
case rendermode::color:
|
||||
case rendermode::reflections:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// distance gate -- compare against Globals.SleeperDistance squared to avoid the sqrt
|
||||
auto const camerapos = m_renderpass.pass_camera.position();
|
||||
auto const trackpos = Track->location();
|
||||
auto const distsq = glm::length2( trackpos - camerapos );
|
||||
auto const cutoffsq = static_cast<double>( Global.SleeperDistance ) * static_cast<double>( Global.SleeperDistance );
|
||||
if( distsq > cutoffsq ) { return; }
|
||||
|
||||
// build camera-space modelview matrices.
|
||||
// each sleeper's stored matrix is in track-local space (relative to Track->m_origin).
|
||||
// Render_Sleepers is called from inside the per-cell origin push -- the cell's center
|
||||
// already equals Track->m_origin (see basic_cell::insert), so the current GL_MODELVIEW
|
||||
// is already view * translate(m_origin - camera). We just need to compose with each
|
||||
// per-sleeper local transform to get the final modelview.
|
||||
glm::mat4 const origin_mv = OpenGLMatrices.data( GL_MODELVIEW );
|
||||
|
||||
std::vector<glm::mat4> instance_modelviews;
|
||||
instance_modelviews.reserve( Track->m_sleeper_local_transforms.size() );
|
||||
for( auto const &local : Track->m_sleeper_local_transforms ) {
|
||||
instance_modelviews.emplace_back( origin_mv * local );
|
||||
}
|
||||
|
||||
// optional replacable skin: build a transient material_data so we can drive ReplacableSet
|
||||
// the same way Render_Instanced does. when no skin is set we fall back to the model defaults.
|
||||
material_data sleeper_material {};
|
||||
bool const has_skin = ( Track->m_sleeper_skin != null_handle );
|
||||
if( has_skin ) {
|
||||
sleeper_material.replacable_skins[ 1 ] = Track->m_sleeper_skin;
|
||||
}
|
||||
|
||||
float const closest_distancesquared = static_cast<float>( std::max( 0.0, distsq ) );
|
||||
|
||||
auto *Model = Track->m_sleeper_model;
|
||||
std::size_t const total = instance_modelviews.size();
|
||||
std::size_t offset_idx = 0;
|
||||
while( offset_idx < total ) {
|
||||
std::size_t const this_batch = std::min<std::size_t>( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
|
||||
|
||||
instance_ubo->update(
|
||||
reinterpret_cast<uint8_t const *>( instance_modelviews.data() + offset_idx ),
|
||||
0,
|
||||
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
|
||||
|
||||
::glPushMatrix();
|
||||
::glLoadIdentity();
|
||||
|
||||
m_current_instance_count = this_batch;
|
||||
|
||||
Model->Root->fSquareDist = closest_distancesquared;
|
||||
auto alpha = ( has_skin ? sleeper_material.textures_alpha : 0x30300030 );
|
||||
alpha ^= 0x0F0F000F;
|
||||
Model->Root->ReplacableSet( ( has_skin ? sleeper_material.replacable_skins : nullptr ), alpha );
|
||||
Model->Root->pRoot = Model;
|
||||
|
||||
Render( Model->Root );
|
||||
|
||||
m_current_instance_count = 0;
|
||||
|
||||
::glPopMatrix();
|
||||
|
||||
// restore instance_modelview[0] to identity so subsequent non-instanced draws
|
||||
// continue to compute identity * modelview (mirroring Render_Instanced).
|
||||
{
|
||||
glm::mat4 const identity( 1.0f );
|
||||
instance_ubo->update( reinterpret_cast<uint8_t const *>( &identity ), 0, sizeof( identity ) );
|
||||
}
|
||||
|
||||
offset_idx += this_batch;
|
||||
++m_renderpass.draw_stats.instanced_drawcalls;
|
||||
}
|
||||
|
||||
m_renderpass.draw_stats.instances += static_cast<int>( total );
|
||||
m_renderpass.draw_stats.models += static_cast<int>( total );
|
||||
}
|
||||
|
||||
bool opengl33_renderer::Render(TDynamicObject *Dynamic)
|
||||
{
|
||||
glDebug("Render TDynamicObject");
|
||||
@@ -3792,6 +3895,24 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator
|
||||
}
|
||||
}
|
||||
|
||||
// fourth pass: per-track sleeper models (sleepermodel optional directive).
|
||||
// drawn after rails/trackbeds so depth pre-pass culling is favourable, and only in passes
|
||||
// where Render_Sleepers actually does work (it gates itself on draw mode / distance).
|
||||
switch( m_renderpass.draw_mode ) {
|
||||
case rendermode::color:
|
||||
case rendermode::reflections: {
|
||||
for( auto first { First }; first != Last; ++first ) {
|
||||
auto *track = *first;
|
||||
if( false == track->m_visible ) { continue; }
|
||||
if( false == track->m_sleeper_enabled ) { continue; }
|
||||
Render_Sleepers( track );
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// post-render reset
|
||||
switch (m_renderpass.draw_mode)
|
||||
{
|
||||
|
||||
@@ -282,6 +282,11 @@ class opengl33_renderer : public gfx_renderer {
|
||||
void Render(TSubModel *Submodel);
|
||||
void Render(TTrack *Track);
|
||||
void Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last);
|
||||
// renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) via GPU instancing.
|
||||
// caller must already have the camera-relative world-space transform set on the matrix stack.
|
||||
// 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 );
|
||||
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 );
|
||||
|
||||
@@ -582,6 +582,15 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string&
|
||||
return true;
|
||||
}
|
||||
|
||||
if (token == "sleeperdistance")
|
||||
{
|
||||
float sleeperdistance = 0.f;
|
||||
ParseOne(Parser, sleeperdistance);
|
||||
// negative values disable the cap; we clamp at 0 so 0 means "do not render sleepers"
|
||||
SleeperDistance = std::max(0.f, sleeperdistance);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (token == "createswitchtrackbeds")
|
||||
{
|
||||
ParseOne(Parser, CreateSwitchTrackbeds);
|
||||
@@ -1567,6 +1576,7 @@ global_settings::export_as_text( std::ostream &Output ) const {
|
||||
export_as_text( Output, "gfx.smoke.fidelity", SmokeFidelity );
|
||||
export_as_text( Output, "smoothtraction", bSmoothTraction );
|
||||
export_as_text( Output, "splinefidelity", SplineFidelity );
|
||||
export_as_text( Output, "sleeperdistance", SleeperDistance );
|
||||
export_as_text( Output, "rendercab", render_cab );
|
||||
export_as_text( Output, "createswitchtrackbeds", CreateSwitchTrackbeds );
|
||||
export_as_text( Output, "gfx.resource.sweep", ResourceSweep );
|
||||
|
||||
@@ -165,6 +165,7 @@ struct global_settings {
|
||||
GLint iMaxCabTextureSize{ 4096 }; // largest allowed texture in vehicle cab
|
||||
int iMultisampling{ 2 }; // tryb antyaliasingu: 0=brak,1=2px,2=4px,3=8px,4=16px
|
||||
float SplineFidelity{ 1.f }; // determines segment size during conversion of splines to geometry
|
||||
float SleeperDistance{ 250.f }; // max distance (in meters) at which per-track sleeper models are still drawn; 0 disables sleeper rendering entirely
|
||||
bool Smoke{ true }; // toggles smoke simulation and visualization
|
||||
float SmokeFidelity{ 1.f }; // determines amount of generated smoke particles
|
||||
bool ResourceSweep{ true }; // gfx resource garbage collection
|
||||
|
||||
@@ -337,29 +337,29 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const
|
||||
}
|
||||
};
|
||||
*/
|
||||
// ustalenie pozycji osi na torze, przechyłki, pochylenia i kierunku jazdy
|
||||
void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &p, glm::vec3 &a) const {
|
||||
|
||||
void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const {
|
||||
if (bCurve) {
|
||||
// można by wprowadzić uproszczony wzór dla okręgów płaskich
|
||||
auto const t = GetTFromS(fDistance); // aproksymacja dystansu na krzywej Beziera na parametr (t)
|
||||
p = FastGetPoint( t );
|
||||
position = FastGetPoint( t );
|
||||
// przechyłka w danym miejscu (zmienia się liniowo)
|
||||
a.x = std::lerp( fRoll1, fRoll2, t );
|
||||
rotation.x = std::lerp( fRoll1, fRoll2, t );
|
||||
// pochodna jest 3*A*t^2+2*B*t+C
|
||||
auto const tangent = t * ( t * 3.0 * vA + vB + vB ) + vC;
|
||||
// pochylenie krzywej (w pionie)
|
||||
a.y = std::atan( tangent.y );
|
||||
rotation.y = std::atan( tangent.y );
|
||||
// kierunek krzywej w planie
|
||||
a.z = -std::atan2( tangent.x, tangent.z );
|
||||
rotation.z = -std::atan2( tangent.x, tangent.z );
|
||||
}
|
||||
else {
|
||||
// wyliczenie dla odcinka prostego jest prostsze
|
||||
auto const t = fDistance / fLength; // zerowych torów nie ma
|
||||
p = FastGetPoint( t );
|
||||
position = FastGetPoint( t );
|
||||
// przechyłka w danym miejscu (zmienia się liniowo)
|
||||
a.x = std::lerp( fRoll1, fRoll2, t );
|
||||
a.y = fStoop; // pochylenie toru prostego
|
||||
a.z = fDirection; // kierunek toru w planie
|
||||
rotation.x = std::lerp( fRoll1, fRoll2, t );
|
||||
rotation.y = fStoop; // pochylenie toru prostego
|
||||
rotation.z = fDirection; // kierunek toru w planie
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,14 @@ public:
|
||||
Math3D::vector3
|
||||
GetPoint(double const fDistance) const;
|
||||
*/
|
||||
void RaPositionGet(double const fDistance, glm::dvec3 &p, glm::vec3 &a) const;
|
||||
|
||||
/// <summary>
|
||||
/// ustalenie pozycji osi na torze, przechyłki, pochylenia i kierunku jazdy
|
||||
/// </summary>
|
||||
/// <param name="fDistance">Distance from p1</param>
|
||||
/// <param name="position">Calculated position</param>
|
||||
/// <param name="rotation">Calculated rotation</param>
|
||||
void RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const;
|
||||
glm::dvec3 FastGetPoint(double const t) const;
|
||||
inline
|
||||
glm::dvec3
|
||||
|
||||
213
world/Track.cpp
213
world/Track.cpp
@@ -23,6 +23,8 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "vehicle/DynObj.h"
|
||||
#include "vehicle/Driver.h"
|
||||
#include "model/AnimModel.h"
|
||||
#include "model/MdlMngr.h"
|
||||
#include "model/Model3d.h"
|
||||
#include "utilities/Timer.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "rendering/renderer.h"
|
||||
@@ -923,6 +925,37 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
|
||||
// memory cell holding friction value modifiers
|
||||
m_friction.first = parser->getToken<std::string>();
|
||||
}
|
||||
else if( str == "sleepermodel" ) {
|
||||
// sleepermodel <frequency> <model> <skin> <offsetX> <offsetY> <offsetZ> <ballastZ>
|
||||
// - frequency: meters between consecutive sleeper instances (must be > 0)
|
||||
// - model: path to the .e3d sleeper model
|
||||
// - skin: replacable skin path, or "none" for the model's defaults
|
||||
// - offset: local-space offset applied per-instance (x=left/right, y=forward/back, z=up/down)
|
||||
// - ballastZ: vertical shift applied to the auto-generated trackbed (ballast). negative pushes ballast down.
|
||||
float frequency { 0.f };
|
||||
float offsetx { 0.f }, offsety { 0.f }, offsetz { 0.f };
|
||||
float ballastz { 0.f };
|
||||
parser->getTokens( 1, false ); *parser >> frequency;
|
||||
auto modelpath { parser->getToken<std::string>( false ) };
|
||||
auto skinpath { parser->getToken<std::string>( false ) };
|
||||
parser->getTokens( 3, false ); *parser >> offsetx >> offsety >> offsetz;
|
||||
parser->getTokens( 1, false ); *parser >> ballastz;
|
||||
|
||||
if( frequency <= 0.01f ) {
|
||||
ErrorLog( "Bad track: invalid sleepermodel frequency (" + std::to_string( frequency ) + ") for track \"" + m_name + "\"" );
|
||||
}
|
||||
else {
|
||||
replace_slashes( modelpath );
|
||||
m_sleeper_enabled = true;
|
||||
m_sleeper_frequency = frequency;
|
||||
m_sleeper_model_name = modelpath;
|
||||
m_sleeper_skin_name = skinpath;
|
||||
m_sleeper_offset = glm::vec3( offsetx, offsety, offsetz );
|
||||
m_sleeper_ballast_z = ballastz;
|
||||
// model and skin are resolved (and instance transforms baked) in build_sleeper_transforms,
|
||||
// called after segment initialisation so the path geometry is final.
|
||||
}
|
||||
}
|
||||
else
|
||||
ErrorLog("Bad track: unknown property: \"" + str + "\" defined for track \"" + m_name + "\"");
|
||||
parser->getTokens();
|
||||
@@ -942,6 +975,9 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
|
||||
+ CurrentSegment()->FastGetPoint( 0.5 )
|
||||
+ CurrentSegment()->FastGetPoint_1() )
|
||||
/ 3.0 );
|
||||
// sleeper transforms are baked later in create_geometry(), once the owning cell has
|
||||
// assigned this track its m_origin (otherwise the local-space matrices would be relative
|
||||
// to a stale origin and the renderer would draw sleepers in the wrong place).
|
||||
}
|
||||
|
||||
bool TTrack::AssignEvents() {
|
||||
@@ -1313,6 +1349,11 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const
|
||||
// wypełnianie tablic VBO
|
||||
void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
|
||||
gfx::userdata_array empty_userdata;
|
||||
// bake per-instance sleeper transforms now that the owning cell has assigned m_origin.
|
||||
// safe to call here even if the track has no sleepermodel (early-outs internally).
|
||||
if( m_sleeper_enabled && m_sleeper_local_transforms.empty() ) {
|
||||
build_sleeper_transforms();
|
||||
}
|
||||
switch (iCategoryFlag & 15)
|
||||
{
|
||||
case 1: // tor
|
||||
@@ -1328,6 +1369,14 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
|
||||
{ // podsypka z podkładami jest tylko dla zwykłego toru
|
||||
gfx::vertex_array bpts1;
|
||||
create_track_bed_profile( bpts1, trPrev, trNext );
|
||||
// optional vertical shift of the auto-generated ballast (sleepermodel ballastZ).
|
||||
// positive value raises the trackbed, negative pushes it down so a custom
|
||||
// sleeper model placed on top can sit flush with the ballast surface.
|
||||
if( m_sleeper_enabled && ( m_sleeper_ballast_z != 0.f ) ) {
|
||||
for( auto &v : bpts1 ) {
|
||||
v.position.y += m_sleeper_ballast_z;
|
||||
}
|
||||
}
|
||||
auto const texturelength { texture_length( m_material2 ) };
|
||||
gfx::vertex_array vertices;
|
||||
Segment->RenderLoft(vertices, m_origin, bpts1, iTrapezoid > 0, texturelength);
|
||||
@@ -2360,6 +2409,17 @@ TTrack::export_as_text_( std::ostream &Output ) const {
|
||||
if( false == m_friction.first.empty() ) {
|
||||
Output << "friction " << m_friction.first << ' ';
|
||||
}
|
||||
if( m_sleeper_enabled && ( false == m_sleeper_model_name.empty() ) ) {
|
||||
Output
|
||||
<< "sleepermodel "
|
||||
<< m_sleeper_frequency << ' '
|
||||
<< m_sleeper_model_name << ' '
|
||||
<< ( m_sleeper_skin_name.empty() ? std::string{ "none" } : m_sleeper_skin_name ) << ' '
|
||||
<< m_sleeper_offset.x << ' '
|
||||
<< m_sleeper_offset.y << ' '
|
||||
<< m_sleeper_offset.z << ' '
|
||||
<< m_sleeper_ballast_z << ' ';
|
||||
}
|
||||
// footer
|
||||
Output
|
||||
<< "endtrack"
|
||||
@@ -3620,3 +3680,156 @@ path_table::IsolatedBusy( std::string const &Name ) const {
|
||||
}
|
||||
multiplayer::WyslijString( Name, 10 ); // wolny (technically not found but, eh)
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Returns the list of segments to walk when laying out sleepers for a given track.
|
||||
// For plain tracks/turntables we just use the active Segment. For switches, crossings
|
||||
// and tributaries we use every initialised sub-path so the user gets sleepers covering
|
||||
// the full footprint of the junction (not just the currently-selected route).
|
||||
std::vector<TSegment *> sleeper_segments_for( TTrack const &Track )
|
||||
{
|
||||
std::vector<TSegment *> out;
|
||||
switch( Track.eType ) {
|
||||
case tt_Switch:
|
||||
case tt_Tributary: {
|
||||
// both main + diverging branches
|
||||
if( Track.SwitchExtension ) {
|
||||
for( int i = 0; i < 2; ++i ) {
|
||||
auto *seg = Track.SwitchExtension->Segments[ i ].get();
|
||||
if( seg != nullptr ) { out.push_back( seg ); }
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case tt_Cross: {
|
||||
// a road crossing potentially holds up to 6 connection segments; iterate them all,
|
||||
// skipping zero-length / null entries.
|
||||
if( Track.SwitchExtension ) {
|
||||
for( int i = 0; i < 6; ++i ) {
|
||||
auto *seg = Track.SwitchExtension->Segments[ i ].get();
|
||||
if( seg == nullptr ) { continue; }
|
||||
if( seg->GetLength() <= 0.0 ) { continue; }
|
||||
out.push_back( seg );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case tt_Normal:
|
||||
case tt_Table:
|
||||
default: {
|
||||
if( Track.Segment ) { out.push_back( Track.Segment.get() ); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
// Resolves the sleeper model + (optional) replacable skin via the global model/material
|
||||
// managers, then walks every active sub-segment at the configured spacing and bakes a
|
||||
// local-space transform matrix (relative to m_origin) for every instance. The renderer
|
||||
// turns these into final camera-space modelview matrices at draw time.
|
||||
//
|
||||
// Per-instance orientation is built from an explicit tangent-based basis (right, up, forward)
|
||||
// rather than RPY-decomposed angles, so curves and switches stay aligned with the path
|
||||
// regardless of where they live in the parameter space.
|
||||
void TTrack::build_sleeper_transforms()
|
||||
{
|
||||
m_sleeper_local_transforms.clear();
|
||||
m_sleeper_model = nullptr;
|
||||
m_sleeper_skin = 0;
|
||||
|
||||
if( false == m_sleeper_enabled ) { return; }
|
||||
if( m_sleeper_model_name.empty() ) { return; }
|
||||
if( m_sleeper_frequency <= 0.01f ) { return; }
|
||||
|
||||
auto const segments = sleeper_segments_for( *this );
|
||||
if( segments.empty() ) { return; }
|
||||
|
||||
// resolve model
|
||||
m_sleeper_model = TModelsManager::GetModel( m_sleeper_model_name, false );
|
||||
if( m_sleeper_model == nullptr ) {
|
||||
ErrorLog( "Bad track: sleepermodel model \"" + m_sleeper_model_name + "\" failed to load for track \"" + m_name + "\"" );
|
||||
m_sleeper_enabled = false;
|
||||
return;
|
||||
}
|
||||
// resolve replacable skin (optional)
|
||||
if( ( false == m_sleeper_skin_name.empty() ) && ( m_sleeper_skin_name != "none" ) ) {
|
||||
auto skinpath { m_sleeper_skin_name };
|
||||
replace_slashes( skinpath );
|
||||
m_sleeper_skin = GfxRenderer->Fetch_Material( skinpath );
|
||||
}
|
||||
|
||||
auto const spacing = static_cast<double>( m_sleeper_frequency );
|
||||
// small finite-difference epsilon used to extract the tangent from RaPositionGet.
|
||||
// RaPositionGet's reported angles are correct in principle but for curves they're
|
||||
// derived from the polynomial first derivative, which is sensitive to numerical noise
|
||||
// at the segment endpoints. Sampling positions directly is robust for both straight
|
||||
// segments and bezier curves, and it costs us two extra evaluations per sleeper.
|
||||
double const eps = std::min( 0.1, spacing * 0.25 );
|
||||
glm::vec3 const world_up { 0.f, 1.f, 0.f };
|
||||
|
||||
// user offset is (left/right, forward/back, up/down) in the local frame established by
|
||||
// the basis below (x=right, y=up, z=forward). swap y<->z to match the documented axes.
|
||||
glm::vec3 const local_offset { m_sleeper_offset.x, m_sleeper_offset.z, m_sleeper_offset.y };
|
||||
|
||||
for( auto *segment : segments ) {
|
||||
auto const length = segment->GetLength();
|
||||
if( length <= 0.0 ) { continue; }
|
||||
// start half a frequency in so the first sleeper doesn't sit on the joint.
|
||||
auto const start = std::min( spacing * 0.5, length * 0.5 );
|
||||
auto const expected = static_cast<std::size_t>( std::max( 0.0, ( length - start ) / spacing ) ) + 1u;
|
||||
m_sleeper_local_transforms.reserve( m_sleeper_local_transforms.size() + expected );
|
||||
|
||||
for( double s = start; s < length; s += spacing ) {
|
||||
glm::dvec3 pos;
|
||||
glm::vec3 angles;
|
||||
segment->RaPositionGet( s, pos, angles );
|
||||
|
||||
// tangent direction via central difference (clamped to the segment endpoints)
|
||||
auto const s_back = std::max( 0.0, s - eps );
|
||||
auto const s_fwd = std::min( length, s + eps );
|
||||
glm::dvec3 p_back, p_fwd;
|
||||
glm::vec3 dummy;
|
||||
segment->RaPositionGet( s_back, p_back, dummy );
|
||||
segment->RaPositionGet( s_fwd, p_fwd, dummy );
|
||||
auto tangent = glm::vec3( p_fwd - p_back );
|
||||
if( glm::length2( tangent ) < 1e-8f ) {
|
||||
// degenerate sample (e.g. zero-length sub-segment); skip this position rather
|
||||
// than emit a junk transform with NaN normals.
|
||||
continue;
|
||||
}
|
||||
tangent = glm::normalize( tangent );
|
||||
|
||||
// build an orthonormal basis around the tangent. world up is the reference; if the
|
||||
// track is almost vertical we fall back to world X so cross() doesn't collapse.
|
||||
glm::vec3 up_ref = world_up;
|
||||
if( std::abs( glm::dot( tangent, up_ref ) ) > 0.999f ) { up_ref = glm::vec3( 1.f, 0.f, 0.f ); }
|
||||
glm::vec3 right = glm::normalize( glm::cross( up_ref, tangent ) );
|
||||
glm::vec3 up = glm::cross( tangent, right );
|
||||
|
||||
// apply track roll (banking) around the tangent / forward axis.
|
||||
float const roll = angles.x;
|
||||
if( roll != 0.f ) {
|
||||
auto const roll_mat = glm::rotate( glm::mat4( 1.f ), roll, tangent );
|
||||
right = glm::vec3( roll_mat * glm::vec4( right, 0.f ) );
|
||||
up = glm::vec3( roll_mat * glm::vec4( up, 0.f ) );
|
||||
}
|
||||
|
||||
// assemble local transform: columns are (right, up, forward, translation).
|
||||
// a sleeper modelled with X = sideways, Y = up, Z = along-track now ends up
|
||||
// correctly oriented along the path tangent regardless of curve direction.
|
||||
auto const localpos = glm::vec3( pos - m_origin );
|
||||
glm::mat4 m { 1.f };
|
||||
m[ 0 ] = glm::vec4( right, 0.f );
|
||||
m[ 1 ] = glm::vec4( up, 0.f );
|
||||
m[ 2 ] = glm::vec4( tangent, 0.f );
|
||||
m[ 3 ] = glm::vec4( localpos, 1.f );
|
||||
|
||||
if( local_offset != glm::vec3( 0.f ) ) {
|
||||
m = glm::translate( m, local_offset );
|
||||
}
|
||||
m_sleeper_local_transforms.emplace_back( m );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/mat4x4.hpp>
|
||||
|
||||
#include "utilities/Classes.h"
|
||||
#include "world/Segment.h"
|
||||
#include "model/material.h"
|
||||
@@ -183,6 +186,25 @@ public:
|
||||
std::vector<segment_data> m_paths; // source data for owned paths
|
||||
int iterate_stamp = 0;
|
||||
|
||||
// sleepermodel optional parameter -------------------------------------------------
|
||||
// Repeats a model along the path at fixed intervals (typically rail sleepers).
|
||||
// Defined in scenery file as:
|
||||
// sleepermodel <frequency> <model> <skin> <offsetX> <offsetY> <offsetZ> <ballastZ>
|
||||
// The renderer draws the model instances via GPU instancing and skips them entirely
|
||||
// once the camera-to-track distance exceeds Global.SleeperDistance.
|
||||
bool m_sleeper_enabled { false };
|
||||
float m_sleeper_frequency { 0.6f }; // spacing along the path, in meters
|
||||
std::string m_sleeper_model_name; // path to the e3d sleeper model (as written in the .scn)
|
||||
std::string m_sleeper_skin_name; // replacable skin path, or "none" for default
|
||||
glm::vec3 m_sleeper_offset { 0.f, 0.f, 0.f }; // local offset from track centerline (x: left/right, y: forward/back, z: up/down)
|
||||
float m_sleeper_ballast_z { 0.f }; // vertical offset applied to the trackbed (ballast) profile
|
||||
TModel3d *m_sleeper_model { nullptr }; // resolved on init; nullptr means no model / failed to load
|
||||
material_handle m_sleeper_skin { 0 }; // resolved replacable skin handle, 0 = use model defaults
|
||||
// precomputed local-space transforms (relative to m_origin) for every sleeper instance along the path.
|
||||
// Each matrix is translate(world_pos - m_origin) * rotate(direction, roll) * translate(local_offset).
|
||||
// The renderer composes this with (view * translate(m_origin - camera_pos)) per draw.
|
||||
std::vector<glm::mat4> m_sleeper_local_transforms;
|
||||
|
||||
public:
|
||||
using dynamics_sequence = std::deque<TDynamicObject *>;
|
||||
using event_sequence = std::vector<std::pair<std::string, basic_event *> >;
|
||||
@@ -346,6 +368,12 @@ private:
|
||||
void create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previous, TTrack const *Next );
|
||||
void create_road_profile( gfx::vertex_array &Output, bool const Forcetransition = false );
|
||||
void create_road_side_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const &Road, bool const Forcetransition = false );
|
||||
/// <summary>
|
||||
/// resolves the sleeper model/skin via the model and material managers, and fills
|
||||
/// m_sleeper_local_transforms by walking the active segment(s) at m_sleeper_frequency.
|
||||
/// Safe to call multiple times; clears any previously cached transforms first.
|
||||
/// </summary>
|
||||
void build_sleeper_transforms();
|
||||
// members
|
||||
static profiles_array m_profiles; // shared database of path element profiles
|
||||
static profiles_map m_profilesmap;
|
||||
|
||||
Reference in New Issue
Block a user