16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 00:49:19 +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

@@ -2374,6 +2374,8 @@ void opengl33_renderer::Render(scene::basic_region *Region)
m_sectionqueue.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
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));
@@ -2422,6 +2424,12 @@ void opengl33_renderer::Render(scene::basic_region *Region)
// at this stage the z-buffer is filled with only ground geometry
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));
break;
}
@@ -2679,9 +2687,42 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::shadows:
{
// TBD, TODO: refactor in to a method to reuse in branch below?
// opaque parts of instanced models -- batched path first
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
Render_Instanced( bucket.first.pModel, bucket.second );
// opaque parts of instanced models -- accumulate this cell's buckets
// into the frame-level map; the actual Render_Instanced() calls are
// 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
for (auto *instance : cell->m_instancesopaque)
@@ -2702,9 +2743,11 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
case rendermode::reflections:
{
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 ) {
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 ) {
if( instance->m_instanceable ) { continue; }
@@ -2744,6 +2787,17 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
++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)
@@ -2962,8 +3016,13 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
// 1. Visibility / distance cull. Build parallel arrays of surviving
// instances and their precomputed camera-space root modelview matrices.
std::vector<glm::mat4> instance_modelviews;
instance_modelviews.reserve( Instances.size() );
// m_instance_modelviews is a persistent member reused across every
// 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
// 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 ) {
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
// stack starts at identity; the per-instance camera transform comes from
// 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;
while( offset_idx < total ) {
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].
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,
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 );
}
// 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)
{
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
switch (m_renderpass.draw_mode)
{

View File

@@ -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 );
@@ -361,6 +366,12 @@ class opengl33_renderer : public gfx_renderer {
renderpass_config m_renderpass; // parameters for current render pass
section_sequence m_sectionqueue; // list of sections in current render pass
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
std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages
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
// from instance_ubo[gl_InstanceID]. Reset to 0 by Render_Instanced() on exit.
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::model_ubs model_ubs;
gl::light_ubs light_ubs;

View File

@@ -20,67 +20,73 @@ class opengl_stack {
public:
// 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:
glm::mat4 const &
data() const {
return m_stack.top(); }
return m_stack.back(); }
void
push_matrix() {
m_stack.emplace( m_stack.top() ); }
glm::mat4 const top { m_stack.back() };
m_stack.emplace_back( top ); }
void
pop_matrix( bool const Upload = true ) {
if( m_stack.size() > 1 ) {
m_stack.pop();
m_stack.pop_back();
if( Upload ) { upload(); } } }
void
load_identity( bool const Upload = true ) {
m_stack.top() = glm::mat4( 1.f );
m_stack.back() = glm::mat4( 1.f );
if( Upload ) { upload(); } }
void
load_matrix( glm::mat4 const &Matrix, bool const Upload = true ) {
m_stack.top() = Matrix;
m_stack.back() = Matrix;
if( Upload ) { upload(); } }
void
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(); } }
void
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(); } }
void
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(); } }
void
multiply( glm::mat4 const &Matrix, bool const Upload = true ) {
m_stack.top() *= Matrix;
m_stack.back() *= Matrix;
if( Upload ) { upload(); } }
void
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(); } }
void
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(); } }
void
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(); } }
private:
// types:
typedef std::stack<glm::mat4> mat4_stack;
// methods:
void
upload() { ::glLoadMatrixf( glm::value_ptr( m_stack.top() ) ); }
upload() { ::glLoadMatrixf( glm::value_ptr( m_stack.back() ) ); }
// members:
mat4_stack m_stack;
std::vector<glm::mat4> m_stack;
};
enum stack_mode { gl_modelview = 0, gl_projection = 1, gl_texture = 2 };