16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-17 23:39:18 +02:00

Merge pull request #111 from MaSzyna-EU07/opengl-instancing

Some rendering improvements
This commit is contained in:
2026-06-16 23:09:12 +02:00
committed by GitHub
14 changed files with 776 additions and 60 deletions

View File

@@ -411,6 +411,8 @@ if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO " /MD /Zi /O2 /DNDEBUG") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO " /MD /Zi /O2 /DNDEBUG")
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO " /DEBUG /INCREMENTAL:NO /OPT:REF /OPT:ICF") set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO " /DEBUG /INCREMENTAL:NO /OPT:REF /OPT:ICF")
target_link_options(${PROJECT_NAME} PRIVATE "/MAP")
# /wd4996: disable "deprecation" warnings # /wd4996: disable "deprecation" warnings
# /wd4244: disable warnings for conversion with possible loss of data # /wd4244: disable warnings for conversion with possible loss of data
# /wd5033: disable because it is all over Python headers # /wd5033: disable because it is all over Python headers

View File

@@ -2,6 +2,7 @@
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cstring>
#include "shader.h" #include "shader.h"
#include "glsl_common.h" #include "glsl_common.h"
#include "utilities/Logs.h" #include "utilities/Logs.h"
@@ -13,6 +14,44 @@ inline bool strcend(std::string const &value, std::string const &ending)
return std::equal(ending.rbegin(), ending.rend(), value.rbegin()); return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
} }
namespace {
// The project's glad config only generates GLAD_GL_ARB_texture_filter_anisotropic
// among the ARB/EXT extension flags -- GL_ARB_texture_gather (desktop) and
// GL_EXT_gpu_shader5 (GLES 3.1) aren't compiled in, so we can't gate the
// shadow textureGather() optimisation on a GLAD constant. Query the live
// extension string instead. The first call walks the extension list once
// (no extension count in the dozens is large enough to matter here) and
// the result is cached in the static bool inside has_gl_extension(), so
// every subsequent shader compile is a plain bool read.
//
// SHADERVALIDATOR_STANDALONE gates out the GL queries because the offline
// shader validator tool links glad.c but never calls gladLoadGL(); the
// function pointers stay null and a real call would crash. Returning
// false there means the standalone validator simply compiles the original
// 16-tap PCF fallback path in light_common.glsl, which is what we want.
bool has_gl_extension(char const *name) {
#ifdef SHADERVALIDATOR_STANDALONE
(void)name;
return false;
#else
if (!glGetIntegerv || !glGetStringi) {
return false;
}
GLint count = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
for (GLint i = 0; i < count; ++i) {
char const *ext = reinterpret_cast<char const *>(glGetStringi(GL_EXTENSIONS, i));
if (ext != nullptr && std::strcmp(ext, name) == 0) {
return true;
}
}
return false;
#endif
}
} // anonymous namespace
std::string gl::shader::read_file(const std::string &filename) std::string gl::shader::read_file(const std::string &filename)
{ {
std::stringstream stream; std::stringstream stream;
@@ -91,11 +130,33 @@ std::pair<GLuint, std::string> gl::shader::process_source(const std::string &fil
if (!Global.gfx_usegles) if (!Global.gfx_usegles)
{ {
str += "#version 330 core\n"; str += "#version 330 core\n";
// textureGather() on sampler2DArrayShadow is core in GLSL 4.0. On 3.30
// desktop it requires GL_ARB_gpu_shader5 -- the older
// GL_ARB_texture_gather (2010) only adds the non-shadow and the plain
// sampler2DShadow overloads, NOT the sampler2DArrayShadow one we need
// for cascaded shadow PCF. Some drivers advertise texture_gather but
// reject the shadow-array overload because the spec for it lives in
// gpu_shader5; so emit only when gpu_shader5 is advertised. When the
// extension is missing, calc_shadow() in light_common.glsl falls back
// to the original 16-tap hardware-PCF loop via #ifndef.
// (Project's glad config doesn't generate GLAD_GL_ARB_gpu_shader5,
// so we query the live extension list -- see has_gl_extension above.)
static bool const have_gpu_shader5 = has_gl_extension("GL_ARB_gpu_shader5");
if (have_gpu_shader5)
str += "#extension GL_ARB_gpu_shader5 : enable\n";
} }
else else
{ {
if (GLAD_GL_ES_VERSION_3_1) { if (GLAD_GL_ES_VERSION_3_1) {
str += "#version 310 es\n"; str += "#version 310 es\n";
// GLES 3.1 lacks textureGather on shadow samplers in core; the
// EXT_gpu_shader5 extension adds it. Only emit the directive when
// the driver advertises support (see desktop comment above).
// (Glad config doesn't generate GLAD_GL_EXT_gpu_shader5 -- query
// the live extension string the same way.)
static bool const have_gpu_shader5 = has_gl_extension("GL_EXT_gpu_shader5");
if (have_gpu_shader5)
str += "#extension GL_EXT_gpu_shader5 : enable\n";
if (type == GL_GEOMETRY_SHADER) if (type == GL_GEOMETRY_SHADER)
str += "#extension GL_EXT_geometry_shader : require\n"; str += "#extension GL_EXT_geometry_shader : require\n";
} else { } else {

View File

@@ -81,7 +81,12 @@ namespace gl
void set_modelview(const glm::mat4 &mv) void set_modelview(const glm::mat4 &mv)
{ {
modelview = mv; modelview = mv;
modelviewnormal = glm::mat3x4(glm::mat3(glm::transpose(glm::inverse(mv)))); // normal matrix = transpose(inverse(modelview)). The modelview is
// always affine, so its 3x3 normal matrix depends only on the
// upper-left 3x3 block; inverting that mat3 directly is markedly
// cheaper than a full mat4 inverse and yields an identical result
// (for affine M, mat3(inverse(M)) == inverse(mat3(M))).
modelviewnormal = glm::mat3x4(glm::transpose(glm::inverse(glm::mat3(mv))));
} }
}; };

View File

@@ -540,14 +540,30 @@ material_manager::create( std::string const &Filename, bool const Loadnow ) {
} }
if( false == material.name.empty() ) { if( false == material.name.empty() ) {
// if we have material name and shader it means resource was processed succesfully // if we have material name and shader it means resource was processed succesfully.
materialhandle = m_materials.size(); //
m_materialmappings.emplace( material.name, materialhandle ); // IMPORTANT: capture the handle ONLY after emplace_back succeeds. The
// previous version pre-assigned `materialhandle = m_materials.size()`
// and then ran `finalize(Loadnow)` (which can throw shader_exception
// when a shader fails to compile) and `emplace_back` together inside
// the try. If finalize() threw, emplace_back never ran, but the
// handle was already set to size() -- pointing one past the actual
// last element. Subsequent `m_materials[handle]` lookups would then
// read garbage past end-of-vector, producing the classic 0x30-offset
// access violation seen in TSubModel::BinInit (vtable read on a
// bogus IMaterial pointer). Now we leave materialhandle as
// null_handle if anything throws, and the caller treats it as a
// failed-to-load material.
try { try {
material.finalize(Loadnow); material.finalize(Loadnow);
materialhandle = m_materials.size();
m_materials.emplace_back( std::move(material) ); m_materials.emplace_back( std::move(material) );
m_materialmappings.emplace( m_materials.back().name, materialhandle );
} catch (gl::shader_exception const &e) { } catch (gl::shader_exception const &e) {
ErrorLog("invalid shader: " + std::string(e.what())); ErrorLog("invalid shader: " + std::string(e.what()));
// record the failure so subsequent Fetch_Material(filename) calls
// short-circuit without re-running the failing compile.
m_materialmappings.emplace( material.name, null_handle );
} }
} }
else { else {

View File

@@ -2374,6 +2374,8 @@ void opengl33_renderer::Render(scene::basic_region *Region)
m_sectionqueue.clear(); m_sectionqueue.clear();
m_cellqueue.clear(); m_cellqueue.clear();
// discard last pass's accumulated instance buckets before this pass starts
m_frame_instance_buckets.clear();
// build a list of region sections to render // build a list of region sections to render
glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; glm::vec3 const cameraposition{m_renderpass.pass_camera.position()};
auto const camerax = static_cast<int>(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); auto const camerax = static_cast<int>(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2));
@@ -2422,6 +2424,12 @@ void opengl33_renderer::Render(scene::basic_region *Region)
// at this stage the z-buffer is filled with only ground geometry // at this stage the z-buffer is filled with only ground geometry
Update_Mouse_Position(); Update_Mouse_Position();
} }
// draw opaque cells front-to-back: with the depth test enabled this
// lets the GPU reject hidden fragments early, before the (expensive)
// lit fragment shader runs on them. Order is irrelevant to the final
// image for opaque geometry, so this is purely a fill-rate win.
std::sort( std::begin( m_cellqueue ), std::end( m_cellqueue ),
[]( distancecell_pair const &Left, distancecell_pair const &Right ) { return Left.first < Right.first; } );
Render(std::begin(m_cellqueue), std::end(m_cellqueue)); Render(std::begin(m_cellqueue), std::end(m_cellqueue));
break; break;
} }
@@ -2679,9 +2687,42 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::shadows: case rendermode::shadows:
{ {
// TBD, TODO: refactor in to a method to reuse in branch below? // TBD, TODO: refactor in to a method to reuse in branch below?
// opaque parts of instanced models -- batched path first // opaque parts of instanced models -- accumulate this cell's buckets
for( auto const &bucket : cell->m_instancebuckets_opaque ) { // into the frame-level map; the actual Render_Instanced() calls are
Render_Instanced( bucket.first.pModel, bucket.second ); // issued once per unique model after the cell loop (see flush below).
//
// Cell-level far-distance pre-cull: when the whole cell lies beyond
// the instance draw distance, every instance in it would fail the
// per-instance drawdistancethreshold test inside Render_Instanced(),
// so there is no point merging its buckets into the frame map. The
// test reproduces Render_Instanced()'s distance maths exactly -- same
// ZoomFactor / fDistanceFactor scaling, same +250 margin -- applied to
// the nearest point of the cell's bounding sphere. basic_cell::enclose_area
// guarantees m_area.radius >= |m_area.center - instance.location()| for
// every contained instance, so the nearest-point distance is a true
// lower bound on every instance's distance: the cull can never drop a
// cell that still holds a drawable instance. Shadows measure from the
// real (viewport) camera, matching Render_Instanced()'s shadow branch;
// every other gated mode measures from the pass camera. Non-instanced
// scenery and vehicles below keep their own per-node culling and are
// intentionally left untouched.
{
auto const &distancecamera = (
m_renderpass.draw_mode == rendermode::shadows
? m_renderpass.viewport_camera
: m_renderpass.pass_camera );
auto const cellcenterdistance { glm::length( cell->m_area.center - distancecamera.position() ) };
auto const cellnearestdistance { std::max( 0.0, cellcenterdistance - cell->m_area.radius ) };
auto const cellnearestdistancesquared {
( cellnearestdistance * cellnearestdistance )
/ ( static_cast<double>( Global.ZoomFactor ) * static_cast<double>( Global.ZoomFactor ) )
/ static_cast<double>( Global.fDistanceFactor ) };
if( cellnearestdistancesquared <= sq( static_cast<double>( m_renderpass.draw_range ) + 250.0 ) ) {
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
auto &dest = m_frame_instance_buckets[ bucket.first ];
dest.insert( dest.end(), bucket.second.begin(), bucket.second.end() );
}
}
} }
// remaining (non-instanceable) opaque instance nodes go through the per-node path // remaining (non-instanceable) opaque instance nodes go through the per-node path
for (auto *instance : cell->m_instancesopaque) for (auto *instance : cell->m_instancesopaque)
@@ -2702,9 +2743,11 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::reflections: case rendermode::reflections:
{ {
if( Global.reflectiontune.fidelity >= 1 ) { if( Global.reflectiontune.fidelity >= 1 ) {
// opaque parts of instanced models -- batched path first // opaque parts of instanced models -- accumulate into the
// frame-level map; flushed once per unique model after the loop.
for( auto const &bucket : cell->m_instancebuckets_opaque ) { for( auto const &bucket : cell->m_instancebuckets_opaque ) {
Render_Instanced( bucket.first.pModel, bucket.second ); auto &dest = m_frame_instance_buckets[ bucket.first ];
dest.insert( dest.end(), bucket.second.begin(), bucket.second.end() );
} }
for( auto *instance : cell->m_instancesopaque ) { for( auto *instance : cell->m_instancesopaque ) {
if( instance->m_instanceable ) { continue; } if( instance->m_instanceable ) { continue; }
@@ -2744,6 +2787,17 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
++first; ++first;
} }
// flush accumulated instance buckets: issue one Render_Instanced() per unique
// (TModel3d*, skins) key across every cell visited in this pass, instead of
// one call per cell. All per-instance frustum/distance/pixel-area culling
// still happens inside Render_Instanced(), so correctness is unchanged -- only
// the number of calls and glBufferSubData round-trips drops sharply.
// (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.)
for( auto const &bucket : m_frame_instance_buckets ) {
Render_Instanced( bucket.first.pModel, bucket.second );
}
m_frame_instance_buckets.clear();
} }
void opengl33_renderer::Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end) void opengl33_renderer::Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end)
@@ -2962,8 +3016,13 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
// 1. Visibility / distance cull. Build parallel arrays of surviving // 1. Visibility / distance cull. Build parallel arrays of surviving
// instances and their precomputed camera-space root modelview matrices. // instances and their precomputed camera-space root modelview matrices.
std::vector<glm::mat4> instance_modelviews; // m_instance_modelviews is a persistent member reused across every
instance_modelviews.reserve( Instances.size() ); // Render_Instanced() call: clear() drops the contents but keeps the
// allocated capacity, so after the first few frames this stops calling
// malloc/free entirely (the reserve() below becomes a no-op once the
// buffer has grown to the largest batch encountered).
m_instance_modelviews.clear();
m_instance_modelviews.reserve( Instances.size() );
// Pull the current pass camera/view transform once. We use the current GL // Pull the current pass camera/view transform once. We use the current GL
// modelview matrix as the view matrix because at the point Render_Instanced // modelview matrix as the view matrix because at the point Render_Instanced
@@ -3028,22 +3087,22 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) { if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) {
mv = glm::scale( mv, scale ); mv = glm::scale( mv, scale );
} }
instance_modelviews.emplace_back( mv ); m_instance_modelviews.emplace_back( mv );
} }
if( instance_modelviews.empty() ) { return; } if( m_instance_modelviews.empty() ) { return; }
// 2. Walk the submodel tree once per sub-batch. The submodel-local matrix // 2. Walk the submodel tree once per sub-batch. The submodel-local matrix
// stack starts at identity; the per-instance camera transform comes from // stack starts at identity; the per-instance camera transform comes from
// instance_modelview[gl_InstanceID] in the shader. // instance_modelview[gl_InstanceID] in the shader.
std::size_t const total = instance_modelviews.size(); std::size_t const total = m_instance_modelviews.size();
std::size_t offset_idx = 0; std::size_t offset_idx = 0;
while( offset_idx < total ) { while( offset_idx < total ) {
std::size_t const this_batch = std::min<std::size_t>( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); std::size_t const this_batch = std::min<std::size_t>( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
// 2a. Upload N modelviews to instance_ubo[0..N-1]. // 2a. Upload N modelviews to instance_ubo[0..N-1].
instance_ubo->update( instance_ubo->update(
reinterpret_cast<uint8_t const *>( instance_modelviews.data() + offset_idx ), reinterpret_cast<uint8_t const *>( m_instance_modelviews.data() + offset_idx ),
0, 0,
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) ); static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
@@ -3083,6 +3142,200 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
m_renderpass.draw_stats.models += static_cast<int>( total ); 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 );
// per-sleeper frustum cull + LOD selection. The whole-track SleeperDistance
// gate above keeps or drops the track as a unit; everything from here on
// operates on individual sleepers.
//
// Each m_sleeper_local_transforms entry is
// translate(world_pos - m_origin) * rotate(...) * translate(local_offset)
// so its translation column is the sleeper position relative to m_origin;
// adding m_origin back yields the sleeper's world-space position. The model's
// bounding radius (floored to a small minimum, in case it was never measured)
// serves both as the frustum test sphere and -- via the distance to that
// world position -- as the per-sleeper LOD distance.
auto const sleeperradius = std::max( Track->m_sleeper_model->bounding_radius(), 2.0f );
// Phase 1 -- frustum cull. For every sleeper that survives, store its
// camera-space modelview paired with the squared distance from the camera
// to ITS OWN world position (not the track / segment origin). That
// per-sleeper distance is what drives LOD selection below. A sleeper whose
// origin sits just off screen while its geometry still reaches into view is
// kept rather than wrongly culled.
std::vector<std::pair<float, glm::mat4>> survivors;
survivors.reserve( Track->m_sleeper_local_transforms.size() );
for( auto const &local : Track->m_sleeper_local_transforms ) {
glm::dvec3 const sleeperworldpos {
Track->m_origin + glm::dvec3( local[ 3 ].x, local[ 3 ].y, local[ 3 ].z ) };
if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ sleeperworldpos, sleeperradius } ) ) {
continue;
}
auto const sleeperdistancesquared = static_cast<float>( glm::length2( sleeperworldpos - camerapos ) );
survivors.emplace_back( sleeperdistancesquared, origin_mv * local );
}
// every sleeper of this track was frustum-culled -- nothing left to draw
if( survivors.empty() ) { return; }
// Phase 2 -- sort survivors near-to-far. A submodel is drawn only while
// fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) range, so
// every distance between two consecutive range bounds selects an identical
// set of submodels -- i.e. the same LOD. Sorting turns each such LOD band
// into a contiguous run, letting the draw loop emit one instanced batch per
// band instead of one track-wide batch at a single distance.
std::sort( survivors.begin(), survivors.end(),
[]( std::pair<float, glm::mat4> const &Left, std::pair<float, glm::mat4> const &Right ) {
return Left.first < Right.first; } );
// contiguous copy of the sorted modelview matrices, for the UBO upload.
// Per-sleeper distances stay available as survivors[i].first, in lockstep.
std::vector<glm::mat4> instance_modelviews;
instance_modelviews.reserve( survivors.size() );
for( auto const &survivor : survivors ) {
instance_modelviews.emplace_back( survivor.second );
}
// collect the model's distinct LOD distance bounds. The sorted, de-duplicated
// set of every submodel's fSquareMinDist / fSquareMaxDist partitions distance
// into bands within which the selected LOD is constant. A model with no LOD
// yields a single band, and the draw loop below then behaves exactly like a
// single plain batched draw.
std::vector<float> lodbounds;
{
std::vector<TSubModel const *> pending;
if( Track->m_sleeper_model->Root != nullptr ) {
pending.push_back( Track->m_sleeper_model->Root );
}
while( false == pending.empty() ) {
auto const *submodel = pending.back();
pending.pop_back();
lodbounds.emplace_back( submodel->fSquareMinDist );
lodbounds.emplace_back( submodel->fSquareMaxDist );
if( submodel->Child != nullptr ) { pending.push_back( submodel->Child ); }
if( submodel->Next != nullptr ) { pending.push_back( submodel->Next ); }
}
}
std::sort( lodbounds.begin(), lodbounds.end() );
lodbounds.erase( std::unique( lodbounds.begin(), lodbounds.end() ), lodbounds.end() );
// 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;
}
// Phase 3 -- draw. Walk the sorted survivors one LOD band at a time. Each
// band is submitted as one or more instanced draws (split only when it
// exceeds MAX_INSTANCES_PER_BATCH); every draw in the band uses an in-band
// fSquareDist, so each sleeper renders at the LOD its own distance selects
// while instancing stays fully in effect.
auto *Model = Track->m_sleeper_model;
std::size_t const total = instance_modelviews.size();
std::size_t band_start = 0;
while( band_start < total ) {
// the band ends at the first sleeper distance that reaches the next LOD
// bound above the band's starting distance.
auto const upperbound = std::upper_bound(
lodbounds.begin(), lodbounds.end(), survivors[ band_start ].first );
float const band_limit = ( upperbound == lodbounds.end()
? std::numeric_limits<float>::max()
: *upperbound );
std::size_t band_end = band_start;
while( ( band_end < total ) && ( survivors[ band_end ].first < band_limit ) ) {
++band_end;
}
// every distance in the band selects the same LOD; the band's nearest
// sleeper is used as the representative fSquareDist.
float const band_distancesquared = survivors[ band_start ].first;
std::size_t offset_idx = band_start;
while( offset_idx < band_end ) {
std::size_t const this_batch = std::min<std::size_t>( band_end - 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 = band_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;
}
band_start = band_end;
}
m_renderpass.draw_stats.instances += static_cast<int>( total );
m_renderpass.draw_stats.models += static_cast<int>( total );
}
bool opengl33_renderer::Render(TDynamicObject *Dynamic) bool opengl33_renderer::Render(TDynamicObject *Dynamic)
{ {
glDebug("Render TDynamicObject"); glDebug("Render TDynamicObject");
@@ -3792,6 +4045,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 // post-render reset
switch (m_renderpass.draw_mode) switch (m_renderpass.draw_mode)
{ {

View File

@@ -282,6 +282,11 @@ class opengl33_renderer : public gfx_renderer {
void Render(TSubModel *Submodel); void Render(TSubModel *Submodel);
void Render(TTrack *Track); void Render(TTrack *Track);
void Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); 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_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha = false);
bool Render_interior( 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 ); bool Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha = false );
@@ -361,6 +366,12 @@ class opengl33_renderer : public gfx_renderer {
renderpass_config m_renderpass; // parameters for current render pass renderpass_config m_renderpass; // parameters for current render pass
section_sequence m_sectionqueue; // list of sections in current render pass section_sequence m_sectionqueue; // list of sections in current render pass
cell_sequence m_cellqueue; cell_sequence m_cellqueue;
// frame-level accumulation of per-cell opaque instance buckets. Each visited
// cell's buckets are merged here keyed by (TModel3d*, skins), so that
// Render_Instanced() runs once per unique model across the whole pass instead
// of once per cell -- collapsing many tiny instanced draws into a few large
// batches. Reused every pass; cleared at the top of Render(scene::basic_region*).
scene::basic_cell::instance_bucket_map m_frame_instance_buckets;
renderpass_config m_colorpass; // parametrs of most recent color pass renderpass_config m_colorpass; // parametrs of most recent color pass
std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages
std::vector<TSubModel const *> m_pickcontrolsitems; std::vector<TSubModel const *> m_pickcontrolsitems;
@@ -403,6 +414,12 @@ class opengl33_renderer : public gfx_renderer {
// than a single regular draw. The vertex shader reads per-instance modelview // than a single regular draw. The vertex shader reads per-instance modelview
// from instance_ubo[gl_InstanceID]. Reset to 0 by Render_Instanced() on exit. // from instance_ubo[gl_InstanceID]. Reset to 0 by Render_Instanced() on exit.
std::size_t m_current_instance_count { 0 }; std::size_t m_current_instance_count { 0 };
// 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
// reused across calls -- clear() retains capacity, so once it has grown to
// the largest batch seen, steady-state frames perform no allocation here.
std::vector<glm::mat4> m_instance_modelviews;
gl::scene_ubs scene_ubs; gl::scene_ubs scene_ubs;
gl::model_ubs model_ubs; gl::model_ubs model_ubs;
gl::light_ubs light_ubs; gl::light_ubs light_ubs;

View File

@@ -20,67 +20,73 @@ class opengl_stack {
public: public:
// constructors: // constructors:
opengl_stack() { m_stack.emplace(1.f); } opengl_stack() {
// reserve generously up front: the matrix stack is pushed/popped once
// per submodel during scene traversal, and std::deque (the previous
// backing store) allocated a fresh heap block on every push for a
// 64-byte glm::mat4. A reserved vector never reallocates within this
// depth, so push/pop become allocation-free and references returned
// by data() stay valid across pushes, exactly as before.
m_stack.reserve( 256 );
m_stack.emplace_back( 1.f ); }
// methods: // methods:
glm::mat4 const & glm::mat4 const &
data() const { data() const {
return m_stack.top(); } return m_stack.back(); }
void void
push_matrix() { push_matrix() {
m_stack.emplace( m_stack.top() ); } glm::mat4 const top { m_stack.back() };
m_stack.emplace_back( top ); }
void void
pop_matrix( bool const Upload = true ) { pop_matrix( bool const Upload = true ) {
if( m_stack.size() > 1 ) { if( m_stack.size() > 1 ) {
m_stack.pop(); m_stack.pop_back();
if( Upload ) { upload(); } } } if( Upload ) { upload(); } } }
void void
load_identity( bool const Upload = true ) { load_identity( bool const Upload = true ) {
m_stack.top() = glm::mat4( 1.f ); m_stack.back() = glm::mat4( 1.f );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
load_matrix( glm::mat4 const &Matrix, bool const Upload = true ) { load_matrix( glm::mat4 const &Matrix, bool const Upload = true ) {
m_stack.top() = Matrix; m_stack.back() = Matrix;
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
rotate( float const Angle, glm::vec3 const &Axis, bool const Upload = true ) { rotate( float const Angle, glm::vec3 const &Axis, bool const Upload = true ) {
m_stack.top() = glm::rotate( m_stack.top(), Angle, Axis ); m_stack.back() = glm::rotate( m_stack.back(), Angle, Axis );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
translate( glm::vec3 const &Translation, bool const Upload = true ) { translate( glm::vec3 const &Translation, bool const Upload = true ) {
m_stack.top() = glm::translate( m_stack.top(), Translation ); m_stack.back() = glm::translate( m_stack.back(), Translation );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
scale( glm::vec3 const &Scale, bool const Upload = true ) { scale( glm::vec3 const &Scale, bool const Upload = true ) {
m_stack.top() = glm::scale( m_stack.top(), Scale ); m_stack.back() = glm::scale( m_stack.back(), Scale );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
multiply( glm::mat4 const &Matrix, bool const Upload = true ) { multiply( glm::mat4 const &Matrix, bool const Upload = true ) {
m_stack.top() *= Matrix; m_stack.back() *= Matrix;
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
ortho( float const Left, float const Right, float const Bottom, float const Top, float const Znear, float const Zfar, bool const Upload = true ) { ortho( float const Left, float const Right, float const Bottom, float const Top, float const Znear, float const Zfar, bool const Upload = true ) {
m_stack.top() *= glm::ortho( Left, Right, Bottom, Top, Znear, Zfar ); m_stack.back() *= glm::ortho( Left, Right, Bottom, Top, Znear, Zfar );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
perspective( float const Fovy, float const Aspect, float const Znear, float const Zfar, bool const Upload = true ) { perspective( float const Fovy, float const Aspect, float const Znear, float const Zfar, bool const Upload = true ) {
m_stack.top() *= glm::perspective( Fovy, Aspect, Znear, Zfar ); m_stack.back() *= glm::perspective( Fovy, Aspect, Znear, Zfar );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
void void
look_at( glm::vec3 const &Eye, glm::vec3 const &Center, glm::vec3 const &Up, bool const Upload = true ) { look_at( glm::vec3 const &Eye, glm::vec3 const &Center, glm::vec3 const &Up, bool const Upload = true ) {
m_stack.top() *= glm::lookAt( Eye, Center, Up ); m_stack.back() *= glm::lookAt( Eye, Center, Up );
if( Upload ) { upload(); } } if( Upload ) { upload(); } }
private: private:
// types:
typedef std::stack<glm::mat4> mat4_stack;
// methods: // methods:
void void
upload() { ::glLoadMatrixf( glm::value_ptr( m_stack.top() ) ); } upload() { ::glLoadMatrixf( glm::value_ptr( m_stack.back() ) ); }
// members: // members:
mat4_stack m_stack; std::vector<glm::mat4> m_stack;
}; };
enum stack_mode { gl_modelview = 0, gl_projection = 1, gl_texture = 2 }; enum stack_mode { gl_modelview = 0, gl_projection = 1, gl_texture = 2 };

View File

@@ -51,7 +51,6 @@ float calc_shadow()
float shadow = 0.0;
float bias = 0.00005f * float(cascade + 1U); float bias = 0.00005f * float(cascade + 1U);
vec2 texel = vec2(1.0) / vec2(textureSize(shadowmap, 0)); vec2 texel = vec2(1.0) / vec2(textureSize(shadowmap, 0));
//float radius = 1.0; f_light_pos[cascade].w; //0.5 + 2.0 * max(abs(2.0 * coords.x - 1.0), abs(2.0 * coords.y - 1.0)); //float radius = 1.0; f_light_pos[cascade].w; //0.5 + 2.0 * max(abs(2.0 * coords.x - 1.0), abs(2.0 * coords.y - 1.0));
@@ -63,37 +62,117 @@ float calc_shadow()
radius = mix(minradius, f_light_pos[cascade+1U].w/f_light_pos[cascade].w, dist_casc); radius = mix(minradius, f_light_pos[cascade+1U].w/f_light_pos[cascade].w, dist_casc);
else else
radius = 0.5; radius = 0.5;
#if defined(GL_ARB_gpu_shader5) || defined(GL_EXT_gpu_shader5) || __VERSION__ >= 400
// Fast path -- replace the original 4x4 grid of individual hardware-PCF
// lookups with 4 textureGather() calls. Each gather returns the 4 raw
// shadow comparisons of a 2x2 texel footprint, so 4 gathers laid out at
// (+-1, +-1) * radius * texel from the sample center cover the same 4x4
// sample area as the original kernel; summing all 16 comparisons and
// dividing by 16 reproduces the original loop's averaging. The cost on
// the TMUs drops from 16 hardware-PCF samples to 4 gathers (the gather
// path returns 4 values per fetch where the original needed 4 fetches),
// roughly a 4x reduction in shadow-sample work. The only thing dropped
// vs. the hardware-PCF path is the implicit bilinear blending inside
// each 2x2 footprint -- effectively turning a tent-weighted kernel into
// a box-weighted one of the same extent, which is imperceptible in
// motion. calc_shadow() is by far the heaviest piece of the lighting
// shader, so this is a measurable GPU saving on every shaded fragment.
float refz = coords.z + bias;
float layer = float(cascade);
vec2 off = radius * texel;
vec4 g0 = textureGather(shadowmap, vec3(coords.xy + vec2(-off.x, -off.y), layer), refz);
vec4 g1 = textureGather(shadowmap, vec3(coords.xy + vec2( off.x, -off.y), layer), refz);
vec4 g2 = textureGather(shadowmap, vec3(coords.xy + vec2(-off.x, off.y), layer), refz);
vec4 g3 = textureGather(shadowmap, vec3(coords.xy + vec2( off.x, off.y), layer), refz);
float shadow = dot(g0 + g1 + g2 + g3, vec4(1.0 / 16.0));
return shadow;
#else
// Fallback for drivers without textureGather on shadow samplers
// (notably GLES 3.0 and any 3.3 desktop driver that doesn't expose
// GL_ARB_texture_gather). Identical to the previous implementation.
float shadow = 0.0;
for (float y = -1.5; y <= 1.5; y += 1.0) for (float y = -1.5; y <= 1.5; y += 1.0)
for (float x = -1.5; x <= 1.5; x += 1.0) for (float x = -1.5; x <= 1.5; x += 1.0)
shadow += texture(shadowmap, vec4(coords.xy + vec2(x, y) * radius * texel, cascade, coords.z + bias) ); shadow += texture(shadowmap, vec4(coords.xy + vec2(x, y) * radius * texel, cascade, coords.z + bias) );
shadow /= 16.0; shadow /= 16.0;
return shadow; return shadow;
#endif
#else #else
return 0.0; return 0.0;
#endif #endif
} }
// -----------------------------------------------------------------------
// GGX Microfacet BRDF helpers (Cook-Torrance)
// -----------------------------------------------------------------------
// Trowbridge-Reitz (GGX) Normal Distribution Function
// D(N,H,α) = α⁴ / (π · ((NdotH)²·(α1)+1)²)
// α = roughness² (perceptual remapping so the slider feels linear)
float D_GGX(float NdotH, float roughness)
{
float a = roughness * roughness; // perceptual -> linear roughness
float a2 = a * a;
float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
return a2 / (3.14159265359 * d * d);
}
// Schlick-GGX single-term masking/shadowing (k remapped for direct lighting)
float G_SchlickGGX(float NdotX, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) * (1.0 / 8.0); // k_direct = (roughness+1)²/8
return NdotX / (NdotX * (1.0 - k) + k);
}
// Height-correlated Smith geometry term
// G(N,V,L) = G_SchlickGGX(NdotV) · G_SchlickGGX(NdotL)
float G_Smith(float NdotV, float NdotL, float roughness)
{
return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
}
// Returns vec2(diffuse, specular) for a single punctual light.
//
// diffuse Lambert N·L (Fresnel-weighted diffuse is handled per-material
// in apply_lights, so we return raw N·L here).
// specular Cook-Torrance GGX: D·G / (4·NdotL·NdotV).
// The Fresnel factor (F) is intentionally omitted here;
// apply_lights already carries a per-material Fresnel term
// that is applied to env reflections and can be routed to
// direct specular there.
//
// Roughness is derived identically to env_roughness in apply_lights so
// that direct and indirect specular highlights read consistently.
vec2 calc_light(vec3 light_dir, vec3 fragnormal) vec2 calc_light(vec3 light_dir, vec3 fragnormal)
{ {
vec3 view_dir = normalize(vec3(0.0f, 0.0f, 0.0f) - f_pos.xyz); vec3 N = fragnormal;
vec3 halfway_dir = normalize(light_dir + view_dir); vec3 L = light_dir;
vec3 V = normalize(-f_pos.xyz);
vec3 H = normalize(L + V);
float diffuse_v = max(dot(fragnormal, light_dir), 0.0); float NdotL = max(dot(N, L), 0.0);
float NdotV = max(dot(N, V), 1e-4);
float NdotH = max(dot(N, H), 0.0);
// Energy-conserving Blinn-Phong normalization: float diffuse_v = NdotL;
// (n+8)/(8*pi) ensures the specular lobe integrates to the same
// total energy regardless of glossiness — low glossiness stays dim
// and spreads wide (blurry), high glossiness is bright and tight (sharp).
// Capped at 4.0 so very high glossiness (n>~92) does not produce pinhole
// highlights that blow past the tonemap shoulder and read as burnt white.
float n = max(glossiness, 0.01);
float normalization = min((n + 8.0) / (8.0 * 3.14159265), 4.0);
float NdotH = max(dot(fragnormal, halfway_dir), 0.0);
float specular_v = normalization * pow(NdotH, n);
return vec2(diffuse_v, specular_v); // Mirror the env-map roughness derivation so direct and indirect lobes match.
// glossiness == param[1].w → roughness == 0.04 (near-mirror)
// glossiness == 0 → roughness == 1.0 (fully diffuse)
float roughness = clamp(1.0 - glossiness / max(abs(param[1].w), 1.0), 0.04, 1.0);
// Cook-Torrance specular (no Fresnel — see above):
// f_spec = D(N,H,α) · G(N,V,L,α) / (4 · NdotL · NdotV)
float D = D_GGX(NdotH, roughness);
float G = G_Smith(NdotV, NdotL, roughness);
float specular_v = (NdotL > 0.0)
? (D * G) / max(4.0 * NdotL * NdotV, 1e-4)
: 0.0;
return vec2(diffuse_v, specular_v);
} }
vec2 calc_point_light(light_s light, vec3 fragnormal) vec2 calc_point_light(light_s light, vec3 fragnormal)

View File

@@ -582,6 +582,15 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string&
return true; 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") if (token == "createswitchtrackbeds")
{ {
ParseOne(Parser, 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, "gfx.smoke.fidelity", SmokeFidelity );
export_as_text( Output, "smoothtraction", bSmoothTraction ); export_as_text( Output, "smoothtraction", bSmoothTraction );
export_as_text( Output, "splinefidelity", SplineFidelity ); export_as_text( Output, "splinefidelity", SplineFidelity );
export_as_text( Output, "sleeperdistance", SleeperDistance );
export_as_text( Output, "rendercab", render_cab ); export_as_text( Output, "rendercab", render_cab );
export_as_text( Output, "createswitchtrackbeds", CreateSwitchTrackbeds ); export_as_text( Output, "createswitchtrackbeds", CreateSwitchTrackbeds );
export_as_text( Output, "gfx.resource.sweep", ResourceSweep ); export_as_text( Output, "gfx.resource.sweep", ResourceSweep );

View File

@@ -165,6 +165,7 @@ struct global_settings {
GLint iMaxCabTextureSize{ 4096 }; // largest allowed texture in vehicle cab GLint iMaxCabTextureSize{ 4096 }; // largest allowed texture in vehicle cab
int iMultisampling{ 2 }; // tryb antyaliasingu: 0=brak,1=2px,2=4px,3=8px,4=16px 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 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 bool Smoke{ true }; // toggles smoke simulation and visualization
float SmokeFidelity{ 1.f }; // determines amount of generated smoke particles float SmokeFidelity{ 1.f }; // determines amount of generated smoke particles
bool ResourceSweep{ true }; // gfx resource garbage collection bool ResourceSweep{ true }; // gfx resource garbage collection

View File

@@ -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) { if (bCurve) {
// można by wprowadzić uproszczony wzór dla okręgów płaskich // 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) 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) // 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 // pochodna jest 3*A*t^2+2*B*t+C
auto const tangent = t * ( t * 3.0 * vA + vB + vB ) + vC; auto const tangent = t * ( t * 3.0 * vA + vB + vB ) + vC;
// pochylenie krzywej (w pionie) // pochylenie krzywej (w pionie)
a.y = std::atan( tangent.y ); rotation.y = std::atan( tangent.y );
// kierunek krzywej w planie // kierunek krzywej w planie
a.z = -std::atan2( tangent.x, tangent.z ); rotation.z = -std::atan2( tangent.x, tangent.z );
} }
else { else {
// wyliczenie dla odcinka prostego jest prostsze // wyliczenie dla odcinka prostego jest prostsze
auto const t = fDistance / fLength; // zerowych torów nie ma auto const t = fDistance / fLength; // zerowych torów nie ma
p = FastGetPoint( t ); position = FastGetPoint( t );
// przechyłka w danym miejscu (zmienia się liniowo) // przechyłka w danym miejscu (zmienia się liniowo)
a.x = std::lerp( fRoll1, fRoll2, t ); rotation.x = std::lerp( fRoll1, fRoll2, t );
a.y = fStoop; // pochylenie toru prostego rotation.y = fStoop; // pochylenie toru prostego
a.z = fDirection; // kierunek toru w planie rotation.z = fDirection; // kierunek toru w planie
} }
}; };

View File

@@ -91,7 +91,14 @@ public:
Math3D::vector3 Math3D::vector3
GetPoint(double const fDistance) const; 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; glm::dvec3 FastGetPoint(double const t) const;
inline inline
glm::dvec3 glm::dvec3

View File

@@ -23,6 +23,8 @@ http://mozilla.org/MPL/2.0/.
#include "vehicle/DynObj.h" #include "vehicle/DynObj.h"
#include "vehicle/Driver.h" #include "vehicle/Driver.h"
#include "model/AnimModel.h" #include "model/AnimModel.h"
#include "model/MdlMngr.h"
#include "model/Model3d.h"
#include "utilities/Timer.h" #include "utilities/Timer.h"
#include "utilities/Logs.h" #include "utilities/Logs.h"
#include "rendering/renderer.h" #include "rendering/renderer.h"
@@ -923,6 +925,37 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
// memory cell holding friction value modifiers // memory cell holding friction value modifiers
m_friction.first = parser->getToken<std::string>(); 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 else
ErrorLog("Bad track: unknown property: \"" + str + "\" defined for track \"" + m_name + "\""); ErrorLog("Bad track: unknown property: \"" + str + "\" defined for track \"" + m_name + "\"");
parser->getTokens(); parser->getTokens();
@@ -942,6 +975,9 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
+ CurrentSegment()->FastGetPoint( 0.5 ) + CurrentSegment()->FastGetPoint( 0.5 )
+ CurrentSegment()->FastGetPoint_1() ) + CurrentSegment()->FastGetPoint_1() )
/ 3.0 ); / 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() { bool TTrack::AssignEvents() {
@@ -1313,6 +1349,11 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const
// wypełnianie tablic VBO // wypełnianie tablic VBO
void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
gfx::userdata_array empty_userdata; 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) switch (iCategoryFlag & 15)
{ {
case 1: // tor 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 { // podsypka z podkładami jest tylko dla zwykłego toru
gfx::vertex_array bpts1; gfx::vertex_array bpts1;
create_track_bed_profile( bpts1, trPrev, trNext ); 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 ) }; auto const texturelength { texture_length( m_material2 ) };
gfx::vertex_array vertices; gfx::vertex_array vertices;
Segment->RenderLoft(vertices, m_origin, bpts1, iTrapezoid > 0, texturelength); 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() ) { if( false == m_friction.first.empty() ) {
Output << "friction " << m_friction.first << ' '; 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 // footer
Output Output
<< "endtrack" << "endtrack"
@@ -3620,3 +3680,156 @@ path_table::IsolatedBusy( std::string const &Name ) const {
} }
multiplayer::WyslijString( Name, 10 ); // wolny (technically not found but, eh) 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 );
}
}
}

View File

@@ -13,6 +13,9 @@ http://mozilla.org/MPL/2.0/.
#include <vector> #include <vector>
#include <deque> #include <deque>
#include <glm/glm.hpp>
#include <glm/mat4x4.hpp>
#include "utilities/Classes.h" #include "utilities/Classes.h"
#include "world/Segment.h" #include "world/Segment.h"
#include "model/material.h" #include "model/material.h"
@@ -183,6 +186,25 @@ public:
std::vector<segment_data> m_paths; // source data for owned paths std::vector<segment_data> m_paths; // source data for owned paths
int iterate_stamp = 0; 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: public:
using dynamics_sequence = std::deque<TDynamicObject *>; using dynamics_sequence = std::deque<TDynamicObject *>;
using event_sequence = std::vector<std::pair<std::string, basic_event *> >; 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_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_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 ); 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 // members
static profiles_array m_profiles; // shared database of path element profiles static profiles_array m_profiles; // shared database of path element profiles
static profiles_map m_profilesmap; static profiles_map m_profilesmap;