diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 28ac2534..c1f9b647 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2684,9 +2684,39 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // 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). - 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() ); + // + // 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( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) ) + / static_cast( Global.fDistanceFactor ) }; + if( cellnearestdistancesquared <= sq( static_cast( 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) @@ -2980,8 +3010,13 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector 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 @@ -3046,22 +3081,22 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); // 2a. Upload N modelviews to instance_ubo[0..N-1]. instance_ubo->update( - reinterpret_cast( instance_modelviews.data() + offset_idx ), + reinterpret_cast( m_instance_modelviews.data() + offset_idx ), 0, static_cast( this_batch * sizeof( glm::mat4 ) ) ); @@ -3145,12 +3180,40 @@ void opengl33_renderer::Render_Sleepers( TTrack *Track ) // per-sleeper local transform to get the final modelview. glm::mat4 const origin_mv = OpenGLMatrices.data( GL_MODELVIEW ); + // per-sleeper frustum cull. The whole-track SleeperDistance gate above + // keeps or drops the track as a unit; here each sleeper is additionally + // tested against the camera frustum on its own, so an in-range track that + // is only partially on screen (or off to the side / behind the camera) + // uploads and draws just the sleepers that can actually be seen. Survivors + // are still gathered into one matrix array and submitted through the same + // instanced draw path below -- culling only thins the batch, it does not + // break batching. + // + // 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) is used as the test sphere, so a sleeper whose origin + // sits just off screen while its geometry still reaches into view is + // kept rather than wrongly culled. + auto const sleeperradius = std::max( Track->m_sleeper_model->bounding_radius(), 2.0f ); + std::vector instance_modelviews; instance_modelviews.reserve( Track->m_sleeper_local_transforms.size() ); for( auto const &local : Track->m_sleeper_local_transforms ) { + // world-space position of this sleeper = track origin + local translation + 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; + } instance_modelviews.emplace_back( origin_mv * local ); } + // every sleeper of this track was frustum-culled -- nothing left to draw + if( instance_modelviews.empty() ) { return; } + // 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 {}; diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 56069e0f..499c8f87 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -414,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 m_instance_modelviews; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs;