diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 354dc963..d207b204 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -577,6 +577,10 @@ bool opengl33_renderer::Render() } // generate new frame opengl_texture::reset_unit_cache(); + if( EditorModeFlag ) { + // the editor moves and resizes model instances in place, which leaves the copies of their bounds out of date + ++m_instanceboundsversion; + } m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_stats = debug_stats(); @@ -2765,10 +2769,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite / ( 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() ); - } + Queue_Instances( cell ); } } // remaining (non-instanceable) opaque instance nodes go through the per-node path @@ -2798,10 +2799,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite && ( cellnearestdistance <= Global.reflectiontune.range_instances ) ) { // 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 ) { - auto &dest = m_frame_instance_buckets[ bucket.first ]; - dest.insert( dest.end(), bucket.second.begin(), bucket.second.end() ); - } + Queue_Instances( cell ); for( auto *instance : cell->m_instancesopaque ) { if( instance->m_instanceable ) { continue; } Render( instance ); @@ -3054,6 +3052,88 @@ void opengl33_renderer::Render(TAnimModel *Instance) } } +// adds the bucketed instances of a scene cell to the frame-level buckets, for Render_Instanced() to draw. +// Render_Instanced() culls each instance it's given. the tests it makes are repeated here on the copies of instance +// bounds kept by the buckets, with some slack for the single precision of the copies, and the instances which fail are +// left out: the instances themselves are fetched only when there's a chance for them to be drawn +void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) +{ + // the editor moves and resizes the instances in place, the copies can't be relied on while it's active + auto const usebounds { false == EditorModeFlag }; + auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; + // same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections + // ignore zoom and distance factor, test the plain distance against their own range and add the fidelity offset + auto const &distancecamera { m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera : m_renderpass.pass_camera }; + glm::dvec3 const cameraposition { distancecamera.position() }; + auto const distancescale { reflections ? 1.0 : 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) }; + auto const distanceoffset { reflections ? sq( static_cast( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 }; + auto const slack { 1.001 }; + auto const distancelimit { slack * ( reflections ? sq( static_cast( Global.reflectiontune.range_instances ) ) : sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) }; + // up close the rounding of the copies is too large a part of the distance for the slack to cover it + auto const distancetested { sq( 64.0 ) }; + + for( auto const &bucket : Cell->m_instancebuckets_opaque ) { + auto const &instances { bucket.second.instances }; + if( false == usebounds ) { + auto &dest { m_frame_instance_buckets[ bucket.first ] }; + dest.insert( dest.end(), instances.begin(), instances.end() ); + continue; + } + if( ( bucket.second.boundsversion != m_instanceboundsversion ) + || ( bucket.second.bounds.size() != instances.size() ) ) { + Update_Instance_Bounds( bucket.first.pModel, bucket.second ); + } + auto const &bounds { bucket.second.bounds }; + std::vector *dest { nullptr }; // looked up when there's something to put in it + for( std::size_t idx = 0; idx < bounds.size(); ++idx ) { + auto const &instancebounds { bounds[ idx ] }; + auto const center { glm::dvec3{ instancebounds.center } }; + auto const distancesquared { glm::length2( center - cameraposition ) }; + if( distancesquared > distancetested ) { + auto const scaleddistancesquared { distancesquared * distancescale }; + if( scaleddistancesquared > distancelimit ) { continue; } + if( scaleddistancesquared + distanceoffset >= slack * instancebounds.rangesquaredmax ) { continue; } + } + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, instancebounds.radius } ) ) { continue; } + if( dest == nullptr ) { + dest = &m_frame_instance_buckets[ bucket.first ]; + } + dest->emplace_back( instances[ idx ] ); + } + } +} + +// makes the copies of the bounds of the instances held by a bucket of a scene cell +void opengl33_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ) +{ + // a submodel is drawn only closer than its fSquareMaxDist, see Render( TSubModel * ). past the largest of these + // there's nothing of the model left to draw, which makes it another limit of the range of its instances + auto lodrangesquared { ( Model != nullptr && Model->Root != nullptr ) ? 0.f : std::numeric_limits::max() }; + m_instance_lodpending.clear(); + m_instance_lodpending.push_back( Model != nullptr ? Model->Root : nullptr ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + lodrangesquared = std::max( lodrangesquared, submodel->fSquareMaxDist ); + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + + Bucket.bounds.clear(); + Bucket.bounds.reserve( Bucket.instances.size() ); + for( auto const *instance : Bucket.instances ) { + scene::basic_cell::instance_bounds instancebounds; + if( instance != nullptr ) { + instancebounds.center = glm::vec3{ instance->m_area.center }; + instancebounds.radius = instance->m_area.radius; + instancebounds.rangesquaredmax = std::min( lodrangesquared, static_cast( std::min( instance->m_rangesquaredmax, static_cast( std::numeric_limits::max() ) ) ) ); + } + Bucket.bounds.emplace_back( instancebounds ); + } + Bucket.boundsversion = m_instanceboundsversion; +} + // True GPU-instanced render path for a group of TAnimModel instances sharing the same // TModel3d. The submodel tree is walked ONCE per batch; at every submodel that draws // geometry we issue a single glDrawElementsInstancedBaseVertex(N) covering all visible @@ -3239,17 +3319,17 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorupdate( reinterpret_cast( &identity ), 0, sizeof( identity ) ); - } - offset_idx += this_batch; ++m_renderpass.draw_stats.instanced_drawcalls; } } + // 3. Restore instance_modelview[0] to identity so subsequent + // non-instanced draws continue to compute identity * modelview. + // done once the last batch is out, as everything drawn up to that point is instanced + { + glm::mat4 const identity( 1.0f ); + instance_ubo->update( reinterpret_cast( &identity ), 0, sizeof( identity ) ); + } m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); m_renderpass.draw_stats.models += static_cast( m_instance_survivors.size() ); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 3cc46aa8..d3b177f5 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -284,6 +284,11 @@ class opengl33_renderer : public gfx_renderer { // instanced_drawcall in draw_stats and contributes Instances.size() to the // instances counter. Instances are still individually frustum/distance culled. void Render_Instanced( TModel3d *Model, std::vector const &Instances ); + // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), + // less the ones the copies of their bounds show to be out of sight in the current render pass + void Queue_Instances( scene::basic_cell const *Cell ); + // makes the copies of the bounds of the instances held by a bucket of a scene cell + void Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ); bool Render(TDynamicObject *Dynamic); bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance); @@ -383,6 +388,9 @@ class opengl33_renderer : public gfx_renderer { // of once per cell -- collapsing many tiny instanced draws into a few large // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). scene::basic_cell::instance_bucket_map m_frame_instance_buckets; + // copies of instance bounds kept by the buckets of scene cells are good as long as they carry this number. + // it changes with each frame drawn with the scenery editor active, as the editor moves and resizes instances in place + unsigned int m_instanceboundsversion { 1 }; renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages std::vector m_pickcontrolsitems; diff --git a/scene/scene.cpp b/scene/scene.cpp index 2667c730..7b0e2994 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -379,7 +379,9 @@ basic_cell::insert( TAnimModel *Instance ) { if( mat != nullptr ) { for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } } - m_instancebuckets_opaque[ key ].emplace_back( Instance ); + auto &bucket { m_instancebuckets_opaque[ key ] }; + bucket.instances.emplace_back( Instance ); + bucket.bounds.clear(); } } // re-calculate cell bounding area, in case model extends outside the cell's boundaries @@ -478,10 +480,12 @@ basic_cell::erase( TAnimModel *Instance ) { // NOTE: searched by pointer rather than by key: the instance may have lost its instanceable // status or changed its skins since it was inserted, which would leave a dangling pointer behind for( auto bucket = m_instancebuckets_opaque.begin(); bucket != m_instancebuckets_opaque.end(); ) { - bucket->second.erase( - std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), - std::end( bucket->second ) ); - if( bucket->second.empty() ) { + auto &instances { bucket->second.instances }; + instances.erase( + std::remove( std::begin( instances ), std::end( instances ), Instance ), + std::end( instances ) ); + bucket->second.bounds.clear(); + if( instances.empty() ) { bucket = m_instancebuckets_opaque.erase( bucket ); } else { diff --git a/scene/scene.h b/scene/scene.h index 6b1ba175..086e2e83 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -239,6 +239,21 @@ public: } }; using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector, instance_bucket_key_hash >; + // what it takes to tell that an instance is out of sight, copied from the instance. + // a cell can hold thousands of instances, each of them large and placed wherever the memory was free, and a render + // pass rejects most of them. the renderer makes the copies and goes by them, to fetch only the instances it may draw + struct instance_bounds { + glm::vec3 center { 0.f }; // location of the instance, in the precision the frustum test takes it + float radius { 0.f }; + float rangesquaredmax { 0.f }; // past it the instance isn't drawn: by its own range, or the lod stages of its model + }; + struct instance_bucket { + std::vector instances; + // caches owned by the renderer; emptied whenever the content of the bucket changes + mutable std::vector bounds; // copies for the instances, in the same order + mutable unsigned int boundsversion { 0 }; + }; + using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using sound_sequence = std::vector; using eventlauncher_sequence = std::vector; using memorycell_sequence = std::vector; @@ -259,7 +274,7 @@ public: // batched instance buckets keyed by shared TModel3d*; populated alongside // m_instancesopaque for nodes whose TAnimModel::m_instanceable == true. // The renderer uses these to amortise per-model state setup across many instances. - instance_bucket_map m_instancebuckets_opaque; + instance_bucketdata_map m_instancebuckets_opaque; traction_sequence m_traction; sound_sequence m_sounds; eventlauncher_sequence m_eventlaunchers;