From 9277c28805ab5ccdd97cbe7b1e9e2b65c37f9868 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 16 Oct 2026 00:37:54 +0200 Subject: [PATCH] Cull instances by compact records and draw them from a shader storage buffer --- gl/glsl_common.cpp | 29 +- gl/shader.cpp | 3 - gl/ubo.h | 15 +- model/AnimModel.cpp | 2 + rendering/geometrybank.cpp | 8 +- rendering/geometrybank.h | 10 +- rendering/opengl46/opengl46geometrybank.cpp | 11 +- rendering/opengl46/opengl46geometrybank.h | 4 +- rendering/opengl46/opengl46renderer.cpp | 322 +++++++++++--------- rendering/opengl46/opengl46renderer.h | 56 ++-- scene/scene.cpp | 4 +- scene/scene.h | 26 +- scene/scenenode.h | 7 + 13 files changed, 269 insertions(+), 228 deletions(-) diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 77008286..27d1f540 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -14,7 +14,7 @@ void gl::glsl_common_setup() "#define MAX_LIGHTS " + std::to_string(MAX_LIGHTS) + "U\n" + "#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" + "#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" + - "#define MAX_INSTANCES_PER_BATCH " + std::to_string(MAX_INSTANCES_PER_BATCH) + "U\n" + + "#define INSTANCE_SSBO_BINDING " + std::to_string(INSTANCE_SSBO_BINDING) + "\n" + "#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" + "#define CLUSTER_GRID_X " + std::to_string(CLUSTER_GRID_X) + "\n" + "#define CLUSTER_GRID_Y " + std::to_string(CLUSTER_GRID_Y) + "\n" + @@ -184,24 +184,21 @@ void gl::glsl_common_setup() vec4 wiper_timer_return; }; - // Per-instance modelview matrices for GPU-instanced draws. - // The UBO itself is declared in every stage so the binding stays valid, - // but the helper functions reference gl_InstanceID which is vertex-only, - // so they are guarded behind STAGE_VERTEX. - layout (std140) uniform instance_ubo + #ifdef STAGE_VERTEX + // Per-instance modelview matrices for GPU-instanced draws, element 0 is identity. + // Draws which aren't instanced have gl_BaseInstance == 0 and gl_InstanceID == 0, + // so effective_modelview() reduces to model_ubo.modelview. For instanced draws + // the C++ side uploads camera-space root transforms of the instances and draws + // them from base instance onwards, with model_ubo.modelview set to the submodel-local + // chain (computed from identity, NOT from the camera/instance), so the product + // gives the correct final transform per instance per submodel. + layout(std430, binding = INSTANCE_SSBO_BINDING) readonly buffer instance_ssbo { - mat4 instance_modelview[MAX_INSTANCES_PER_BATCH]; + mat4 instance_modelview[]; }; - #ifdef STAGE_VERTEX - // For non-instanced draws gl_InstanceID == 0 and slot 0 is permanently set - // to identity, so effective_modelview() reduces to model_ubo.modelview. - // For instanced draws the C++ side uploads camera-space root transforms for - // instances 0..N-1 and sets model_ubo.modelview to the submodel-local chain - // (computed from identity, NOT from the camera/instance), so the product - // gives the correct final transform per instance per submodel. mat4 effective_modelview() { - return instance_modelview[gl_InstanceID] * modelview; + return instance_modelview[gl_BaseInstance + gl_InstanceID] * modelview; } mat3 effective_modelviewnormal() { // instance_modelview can include per-instance scale (uniform or per-axis), @@ -209,7 +206,7 @@ void gl::glsl_common_setup() // case. Compute the correct normal matrix via transpose(inverse(...)). // For pure rotation+translation this still produces the rotation matrix // (one extra mat3 inverse per vertex — modern GPUs handle it cheaply). - mat3 instance_mv3 = mat3(instance_modelview[gl_InstanceID]); + mat3 instance_mv3 = mat3(instance_modelview[gl_BaseInstance + gl_InstanceID]); mat3 instance_normal = transpose(inverse(instance_mv3)); return instance_normal * modelviewnormal; } diff --git a/gl/shader.cpp b/gl/shader.cpp index 70cccbff..c025a233 100644 --- a/gl/shader.cpp +++ b/gl/shader.cpp @@ -281,9 +281,6 @@ void gl::program::init() if ((index = glGetUniformBlockIndex(*this, "model_ubo")) != GL_INVALID_INDEX) glUniformBlockBinding(*this, index, 1); - - if ((index = glGetUniformBlockIndex(*this, "instance_ubo")) != GL_INVALID_INDEX) - glUniformBlockBinding(*this, index, 3); } gl::program::program() diff --git a/gl/ubo.h b/gl/ubo.h index 6b0802f5..18d7105a 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -104,17 +104,6 @@ namespace gl static_assert(sizeof(model_ubs) == 208 + 16 * MAX_PARAMS, "bad size of ubs"); - // maximum number of instances per single GPU-instanced draw call. - // 256 mat4 = 16 KiB, the guaranteed minimum UBO size. Bigger batches must split. - const size_t MAX_INSTANCES_PER_BATCH = 256; - - struct instance_ubs - { - glm::mat4 instance_modelview[MAX_INSTANCES_PER_BATCH]; - }; - - static_assert(sizeof(instance_ubs) == 64 * MAX_INSTANCES_PER_BATCH, "bad size of instance_ubs"); - // layout std430 (shader storage buffers) // structs must match with GLSL, offsets checked below @@ -295,6 +284,10 @@ namespace gl const GLuint ATMOSPHERE_RESULT_SSBO_BINDING = 7; const GLuint ATMOSPHERE_UBO_BINDING = 4; + // binding point of the per-instance modelview matrices of instanced draws (glsl: instance_ssbo, mat4 per instance). + // element 0 is identity, the matrix of every draw which isn't instanced + const GLuint INSTANCE_SSBO_BINDING = 8; + // number of coefficients of the spherical harmonics of order 2 (bands 0..2) const size_t SKY_SH_COEFFICIENTS = 9; diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 770790c1..d686e637 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -552,6 +552,7 @@ std::shared_ptr TAnimModel::AddContainer(std::string const &Name // containers are added by events after the instance was loaded and bucketed; // the instanced path skips RaAnimate(), so this instance has to be drawn on its own m_instanceable = false; + ++scene::node_state_version; return tmp; } return nullptr; @@ -781,6 +782,7 @@ void TAnimModel::SkinSet( int const Index, material_handle const Material ) { m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = Material; // the instance stays in the bucket of its old skin set, where it would get the textures of the batch m_instanceable = false; + ++scene::node_state_version; } void TAnimModel::AnimUpdate(double dt) diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index f2db8b8b..ef2d8f4e 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -327,9 +327,9 @@ geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units con // draws geometry stored in specified chunk InstanceCount times via instanced draw call std::size_t -geometry_bank::draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) { +geometry_bank::draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) { if( InstanceCount == 0 ) { return 0; } - return draw_instanced_( Geometry, Units, InstanceCount, Streams ); + return draw_instanced_( Geometry, Units, InstanceCount, BaseInstance, Streams ); } // draws geometry stored in supplied sequence of chunks of the bank @@ -432,7 +432,7 @@ geometrybank_manager::draw( gfx::geometry_handle const &Geometry, unsigned int c // draws geometry stored in specified chunk InstanceCount times via instanced draw call void -geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams ) { +geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) { if( Geometry == null_handle ) { return; } if( InstanceCount == 0 ) { return; } @@ -440,7 +440,7 @@ geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std: auto &bankrecord = bank( Geometry ); bankrecord.second = m_garbagecollector.timestamp(); - m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, Streams ); + m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, BaseInstance, Streams ); } // draws geometry stored in supplied sequence of chunks diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 20baf4ed..2bc51dfa 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -119,8 +119,8 @@ public: auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool; // draws geometry stored in specified chunk auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; - // draws geometry stored in specified chunk N times via glDrawElementsInstanced* - auto draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ) -> std::size_t; + // draws geometry stored in specified chunk N times via glDrawElementsInstanced*, as instances BaseInstance and onwards + auto draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in supplied sequence of chunks of the bank, in their order. the bank is free to combine them into fewer draw calls auto draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in supplied list of chunks @@ -185,7 +185,7 @@ private: // draw() subclass details virtual auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t = 0; // draw_instanced() subclass details. Default implementation falls back to N regular draws. - virtual auto draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t { + virtual auto draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) -> std::size_t { std::size_t count { 0 }; for( std::size_t i = 0; i < InstanceCount; ++i ) { count += draw_( Geometry, Units, Streams ); } return count; } @@ -223,8 +223,8 @@ public: // draws geometry stored in specified chunk void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams ); // draws geometry stored in specified chunk InstanceCount times via GPU instancing. - // The shader reads per-instance modelview matrices from instance_ubo[gl_InstanceID]. - void draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ); + // The shader reads per-instance modelview matrices from instance_ssbo[gl_BaseInstance + gl_InstanceID]. + void draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams = basic_streams ); // draws geometry stored in supplied sequence of chunks, in their order. neighbouring chunks of the same bank are passed to it together void draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, unsigned int const Streams = basic_streams ); template diff --git a/rendering/opengl46/opengl46geometrybank.cpp b/rendering/opengl46/opengl46geometrybank.cpp index 6b017379..00950b71 100644 --- a/rendering/opengl46/opengl46geometrybank.cpp +++ b/rendering/opengl46/opengl46geometrybank.cpp @@ -171,9 +171,9 @@ opengl46_vaogeometrybank::draw_( gfx::geometry_handle const &Geometry, gfx::stre } // draw_instanced() subclass details — single GL instanced draw for InstanceCount instances. -// The vertex shader reads per-instance modelview from instance_ubo[gl_InstanceID]. +// The vertex shader reads per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID]. std::size_t -opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) +opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) { setup_buffer(); @@ -199,16 +199,17 @@ opengl46_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, if( chunkrecord.index_count > 0 ) { m_vao->bind(); - ::glDrawElementsInstancedBaseVertex( + ::glDrawElementsInstancedBaseVertexBaseInstance( chunk.type, chunkrecord.index_count, GL_UNSIGNED_INT, reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), inst_count, - chunkrecord.vertex_offset ); + chunkrecord.vertex_offset, + BaseInstance ); } else { m_vao->bind(); - ::glDrawArraysInstanced( chunk.type, chunkrecord.vertex_offset, chunkrecord.vertex_count, inst_count ); + ::glDrawArraysInstancedBaseInstance( chunk.type, chunkrecord.vertex_offset, chunkrecord.vertex_count, inst_count, BaseInstance ); } auto const vertexcount { ( chunkrecord.index_count > 0 ? chunkrecord.index_count : chunkrecord.vertex_count ) }; diff --git a/rendering/opengl46/opengl46geometrybank.h b/rendering/opengl46/opengl46geometrybank.h index 553dfb2c..957c9d12 100644 --- a/rendering/opengl46/opengl46geometrybank.h +++ b/rendering/opengl46/opengl46geometrybank.h @@ -53,9 +53,9 @@ private: // draw() subclass details auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override; - // draw_instanced() subclass details — issues glDrawElementsInstancedBaseVertex + // draw_instanced() subclass details — issues glDrawElementsInstancedBaseVertexBaseInstance auto - draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t override; + draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, std::uint32_t const BaseInstance, unsigned int const Streams ) -> std::size_t override; // draw_batch() subclass details — issues glMultiDrawArrays for runs of non-indexed chunks of the same type auto draw_batch_( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index e7d8213d..d007d22d 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -179,7 +179,11 @@ bool opengl46_renderer::Init(GLFWwindow *Window) model_ubo = std::make_unique(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW); light_ssbo = std::make_unique(sizeof(gl::light_data), gl::LIGHT_SSBO_BINDING); light_ssbo->bind_base(); - instance_ubo = std::make_unique(sizeof(gl::instance_ubs), 3, GL_STREAM_DRAW); + for (auto &buffer : m_instancebuffers) + { + // grows when a frame needs more + buffer = std::make_unique(static_cast(16384 * sizeof(glm::mat4)), gl::INSTANCE_SSBO_BINDING); + } try { @@ -203,13 +207,15 @@ bool opengl46_renderer::Init(GLFWwindow *Window) model_ubo->update(model_ubs); scene_ubo->update(scene_ubs); - // initialize instance_ubo slot 0 to identity. This is the matrix sampled by - // gl_InstanceID==0 in non-instanced draws, so existing rendering paths + // element 0 of the instance buffers is identity. This is the matrix sampled by + // non-instanced draws, so existing rendering paths // continue to multiply effective_modelview = identity * modelview = modelview. + for (auto &buffer : m_instancebuffers) { - glm::mat4 identity( 1.0f ); - instance_ubo->update( reinterpret_cast(&identity), 0, sizeof(identity) ); + glm::mat4 const identity(1.0f); + buffer->update(identity); } + m_instancebuffers[m_instancebufferindex]->bind_base(); int samples = 1 << Global.iMultisampling; if (samples > 1) @@ -630,10 +636,16 @@ bool opengl46_renderer::Render() opengl_texture::reset_unit_cache(); // textures of new heightmap terrain layers go to the arrays the terrain shader reads them from m_terrainmaterials.update(); - if( EditorModeFlag ) { - // the editor moves and resizes model instances in place, which leaves the copies of their bounds out of date - ++m_instanceboundsversion; + if( ( EditorModeFlag ) || ( m_nodestateversion != scene::node_state_version ) ) { + // the editor moves and resizes model instances in place, events show and hide them and make them drawn on + // their own: the instance records are out of date + ++m_instancerecordsversion; + m_nodestateversion = scene::node_state_version; } + // instanced draws of the frame place their matrices in the next of the instance buffers, the one used the longest ago + m_instancebufferindex = ( m_instancebufferindex + 1 ) % m_instancebuffers.size(); + m_instancebufferfill = 1; + m_instancebuffers[ m_instancebufferindex ]->bind_base(); m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_stats = debug_stats(); @@ -729,6 +741,8 @@ void opengl46_renderer::SwapBuffers() + " instanced:" + to_string( m_colorpass.draw_stats.instances, 7 ) + " +" + to_string( shadowstats.instances, 7 ) + " =" + to_string( m_colorpass.draw_stats.instances + shadowstats.instances, 7 ) + " (" + std::to_string( m_colorpass.draw_stats.instanced_drawcalls + shadowstats.instanced_drawcalls ) + " batches)\n" + + " instance buffer: " + std::to_string( m_instancebufferfill - 1 ) + " matrices, " + + std::to_string( m_instancebuffers[ m_instancebufferindex ]->size() / 1024 ) + " KiB\n" + " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total ) + "/" + std::to_string( TAnimModel::s_classified_total ) + " rej(noModel/lights/anim/animSubM): " @@ -2999,7 +3013,7 @@ void opengl46_renderer::draw(const gfx::geometry_handle &handle) if( m_current_instance_count > 0 ) { // inside Render_Instanced(): one GL instanced draw replaces what would // otherwise be N regular draws (one per instance) at this submodel - m_geometry.draw_instanced( handle, m_current_instance_count ); + m_geometry.draw_instanced( handle, m_current_instance_count, m_current_instance_base ); } else { m_geometry.draw( handle ); @@ -3182,13 +3196,13 @@ void opengl46_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 +// Render_Instanced() culls each instance it's given by its record. the tests it makes are repeated here, with some slack +// for the single precision of the records, and the instances which fail are left out of the queue void opengl46_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 }; + // the editor moves and resizes the instances in place. the records are made anew for each frame then, + // but aren't relied on to leave instances out of the queue + auto const userecords { 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 @@ -3198,42 +3212,39 @@ void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell ) 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 + // up close the rounding of the records 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; + auto const &instancebucket { bucket.second }; + if( ( instancebucket.recordsversion != m_instancerecordsversion ) + || ( instancebucket.records.size() != instancebucket.instances.size() ) ) { + Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center ); } - 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; } + auto const &records { instancebucket.records }; + std::vector *dest { nullptr }; // looked up when there's something to put in it + for( std::size_t idx = 0; idx < records.size(); ++idx ) { + if( userecords ) { + auto const &record { records[ idx ] }; + auto const center { instancebucket.recordorigin + glm::dvec3{ record.position } }; + auto const distancesquared { glm::length2( center - cameraposition ) }; + if( distancesquared > distancetested ) { + auto const scaleddistancesquared { distancesquared * distancescale }; + if( scaleddistancesquared > distancelimit ) { continue; } + if( scaleddistancesquared + distanceoffset >= slack * std::min( instancebucket.lodrangesquared, record.rangesquaredmax ) ) { continue; } + } + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, record.radius } ) ) { 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 ] ); + dest->push_back( { &instancebucket, idx } ); } } } -// makes the copies of the bounds of the instances held by a bucket of a scene cell -void opengl46_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ) +// makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin +void opengl46_renderer::Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin ) { // 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 @@ -3248,31 +3259,63 @@ void opengl46_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::ba m_instance_lodpending.push_back( submodel->Child ); m_instance_lodpending.push_back( submodel->Next ); } + Bucket.lodrangesquared = lodrangesquared; + Bucket.recordorigin = Origin; - Bucket.bounds.clear(); - Bucket.bounds.reserve( Bucket.instances.size() ); - for( auto const *instance : Bucket.instances ) { - scene::basic_cell::instance_bounds instancebounds; + auto const tofloat = []( double const Value ) { + return static_cast( std::min( Value, static_cast( std::numeric_limits::max() ) ) ); }; + + Bucket.records.clear(); + Bucket.records.reserve( Bucket.instances.size() ); + for( auto *instance : Bucket.instances ) { + scene::basic_cell::instance_record record; 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() ) ) ) ); + record.position = glm::vec3{ instance->location() - Origin }; + record.radius = instance->radius(); + record.rangesquaredmin = tofloat( instance->m_rangesquaredmin ); + record.rangesquaredmax = tofloat( instance->m_rangesquaredmax ); + record.flags = + ( instance->visible() ? scene::basic_cell::instance_visible : 0u ) + | ( instance->m_instanceable ? scene::basic_cell::instance_instanceable : 0u ); + auto const &rotationscale { instance->rotation_scale() }; + for( int column = 0; column < 3; ++column ) { + record.rotationscale[ column ] = rotationscale[ column ]; + } } - Bucket.bounds.emplace_back( instancebounds ); + // an empty slot keeps its record without flags, and isn't drawn + Bucket.records.emplace_back( record ); } - Bucket.boundsversion = m_instanceboundsversion; + Bucket.recordsversion = m_instancerecordsversion; +} + +// places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them +std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ) +{ + auto &buffer { m_instancebuffers[ m_instancebufferindex ] }; + auto const needed { static_cast( ( m_instancebufferfill + Count ) * sizeof( glm::mat4 ) ) }; + if( needed > buffer->size() ) { + // the draws issued with the replaced buffer keep it until the gpu is done with them. the new one starts with identity + buffer->resize( std::max( needed, 2 * buffer->size() ) ); + glm::mat4 const identity( 1.0f ); + buffer->update( identity ); + buffer->bind_base(); + m_instancebufferfill = 1; + } + auto const base { static_cast( m_instancebufferfill ) }; + buffer->update( Modelviews, static_cast( base * sizeof( glm::mat4 ) ), static_cast( Count * sizeof( glm::mat4 ) ) ); + m_instancebufferfill += Count; + return base; } // 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 -// instances. The vertex shader multiplies effective_modelview = instance_modelview[gl_InstanceID] -// * model_ubs.modelview, where instance_modelview[i] is the camera-space root transform -// of instance i (precomputed below) and model_ubs.modelview is the submodel-local chain -// (accumulated by the matrix stack starting from identity, NOT the camera/instance frame). -// -// Batch size is capped at MAX_INSTANCES_PER_BATCH (256). Larger groups are split. -void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector const &Instances ) +// TModel3d. The submodel tree is walked ONCE per lod band; at every submodel that draws +// geometry we issue a single glDrawElementsInstancedBaseVertexBaseInstance(N) covering all +// visible instances of the band. The vertex shader multiplies effective_modelview = +// instance_modelview[gl_BaseInstance + gl_InstanceID] * model_ubs.modelview, where +// instance_modelview[i] is the camera-space root transform of an instance (precomputed below) +// and model_ubs.modelview is the submodel-local chain (accumulated by the matrix stack starting +// from identity, NOT the camera/instance frame). +void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector const &Instances ) { if( Model == nullptr ) { return; } if( Instances.empty() ) { return; } @@ -3287,7 +3330,8 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vectorMaterial() != nullptr ? Instances.front()->Material()->replacable_skins : nullptr }; + auto const *frontinstance { Instances.front().bucket->instances[ Instances.front().index ] }; + auto const *skins { frontinstance != nullptr && frontinstance->Material() != nullptr ? frontinstance->Material()->replacable_skins : nullptr }; bool castsshadow { m_renderpass.draw_mode != rendermode::shadows }; m_instance_lodbounds.clear(); m_instance_lodpending.clear(); @@ -3310,9 +3354,9 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vectorrecords[ queued.index ] }; // instances which lost their instanceable status after being bucketed (animation or skin // assigned by an event) are drawn by the per-node path - if( false == Instance->m_instanceable ) { continue; } - if( false == Instance->m_visible ) { continue; } - if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; } + if( ( record.flags & scene::basic_cell::instance_instanceable ) == 0 ) { continue; } + if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } + glm::dvec3 const location { queued.bucket->recordorigin + glm::dvec3{ record.position } }; + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ location, record.radius } ) ) { continue; } double distancesquared; switch( m_renderpass.draw_mode ) { case rendermode::shadows: - distancesquared = glm::length2( ( Instance->location() - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; + distancesquared = glm::length2( ( location - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; break; case rendermode::reflections: - distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) ); + distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) ); if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; } distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET ); break; default: - distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; + distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; break; } - if( ( distancesquared < Instance->m_rangesquaredmin ) - || ( distancesquared >= Instance->m_rangesquaredmax ) ) { continue; } + if( ( distancesquared < record.rangesquaredmin ) + || ( distancesquared >= record.rangesquaredmax ) ) { continue; } auto const drawdistancethreshold { m_renderpass.draw_range + 250 }; if( distancesquared > sq( drawdistancethreshold ) ) { continue; } - auto const radiussquared { Instance->radius() * Instance->radius() }; + auto const radiussquared { record.radius * record.radius }; if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; } - if( !prepared_shared_state ) { - Instance->RaPrepare(); - prepared_shared_state = true; - batch_material = Instance->Material(); + if( batch_instance == nullptr ) { + batch_instance = queued.bucket->instances[ queued.index ]; + batch_instance->RaPrepare(); + batch_material = batch_instance->Material(); } - Instance->m_framestamp = m_framestamp; // Build the camera-relative root modelview for this instance: // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) - // The rotation and scale part is cached by the instance, the translation goes into its last + // The rotation and scale part comes from the record, the translation goes into its last // column. The scale is folded in so the GPU-instanced path produces visually identical output // to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local. - glm::mat4 root { Instance->rotation_scale() }; - root[ 3 ] = glm::vec4( glm::vec3( Instance->location() - m_renderpass.pass_camera.position() ), 1.f ); + glm::mat4 const root { + record.rotationscale[ 0 ], + record.rotationscale[ 1 ], + record.rotationscale[ 2 ], + glm::vec4( glm::vec3( location - m_renderpass.pass_camera.position() ), 1.f ) }; auto const distance { static_cast( distancesquared ) }; farthest_distancesquared = std::max( farthest_distancesquared, distance ); @@ -3384,7 +3431,7 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vectortextures_alpha : 0x30300030 ); alpha ^= 0x0F0F000F; - // 2. Walk the submodel tree once per sub-batch. The submodel-local matrix + // 2. Walk the submodel tree once per lod band. The submodel-local matrix // stack starts at identity; the per-instance camera transform comes from - // instance_modelview[gl_InstanceID] in the shader. + // instance_modelview[gl_BaseInstance + gl_InstanceID] in the shader. for( std::size_t band = 0; band < bandcount; ++band ) { - std::size_t offset_idx = m_instance_bandstarts[ band * distanceslots ]; + std::size_t const band_start = m_instance_bandstarts[ band * distanceslots ]; std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ]; - while( offset_idx < band_end ) { - std::size_t const this_batch = std::min( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); + if( band_start == band_end ) { continue; } - // 2a. Upload N modelviews to instance_ubo[0..N-1]. - instance_ubo->update( - reinterpret_cast( m_instance_modelviews.data() + offset_idx ), - 0, - static_cast( this_batch * sizeof( glm::mat4 ) ) ); + // 2a. Push identity onto the matrix stack so submodel transforms + // accumulate from identity, not from camera/instance space. + ::glPushMatrix(); + ::glLoadIdentity(); - // 2b. Push identity onto the matrix stack so submodel transforms - // accumulate from identity, not from camera/instance space. - ::glPushMatrix(); - ::glLoadIdentity(); + // 2b. Configure the existing TModel3d/TSubModel render path. Setting + // m_current_instance_count routes draw(handle) calls to draw_instanced. + m_current_instance_count = band_end - band_start; + m_current_instance_base = baseinstance + static_cast( band_start ); - // 2c. Configure the existing TModel3d/TSubModel render path. Setting - // m_current_instance_count routes draw(handle) calls to draw_instanced. - m_current_instance_count = this_batch; + Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD + Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); + Model->Root->pRoot = Model; - Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD - Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); - Model->Root->pRoot = Model; + Render( Model->Root ); - Render( Model->Root ); + m_current_instance_count = 0; + m_current_instance_base = 0; - m_current_instance_count = 0; + ::glPopMatrix(); - ::glPopMatrix(); - - 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.instanced_drawcalls; } m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); @@ -3467,8 +3502,8 @@ void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector const &Left, std::pair const &Right ) { return Left.first < Right.first; } ); - // contiguous copy of the sorted modelview matrices, for the UBO upload. + // contiguous copy of the sorted modelview matrices, for the upload. // Per-sleeper distances stay available as survivors[i].first, in lockstep. std::vector instance_modelviews; instance_modelviews.reserve( survivors.size() ); @@ -3705,12 +3740,12 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track ) } // 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. + // band is submitted as one instanced draw from its own base instance; every + // draw 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(); + auto const baseinstance { Upload_Instance_Modelviews( instance_modelviews.data(), total ) }; std::size_t band_start = 0; while( band_start < total ) { // the band ends at the first sleeper distance that reaches the next LOD @@ -3728,42 +3763,27 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track ) // 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( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); + ::glPushMatrix(); + ::glLoadIdentity(); - instance_ubo->update( - reinterpret_cast( instance_modelviews.data() + offset_idx ), - 0, - static_cast( this_batch * sizeof( glm::mat4 ) ) ); + m_current_instance_count = band_end - band_start; + m_current_instance_base = baseinstance + static_cast( band_start ); - ::glPushMatrix(); - ::glLoadIdentity(); + 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; - m_current_instance_count = this_batch; + Render( Model->Root ); - 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; + m_current_instance_count = 0; + m_current_instance_base = 0; - Render( Model->Root ); + ::glPopMatrix(); - m_current_instance_count = 0; + ++m_renderpass.draw_stats.instanced_drawcalls; - ::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( &identity ), 0, sizeof( identity ) ); - } - - offset_idx += this_batch; - ++m_renderpass.draw_stats.instanced_drawcalls; - } band_start = band_end; } diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 17ac82d6..9ebce345 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -305,17 +305,25 @@ class opengl46_renderer : public gfx_renderer { void Render(cell_sequence::iterator First, cell_sequence::iterator Last); void Render(scene::shape_node const &Shape, bool const Ignorerange); void Render(TAnimModel *Instance); + // an instance of a bucket of a scene cell, queued to be drawn by Render_Instanced() + struct queued_instance { + scene::basic_cell::instance_bucket const *bucket; + std::size_t index; // of the instance and its record in the bucket + }; // batched render path for many TAnimModel instances that share the same TModel3d. - // Caller guarantees every instance has m_instanceable == true (no per-instance lights, - // no submodel animation, no replacable skins). Each call counts as one - // 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 ); + // Caller guarantees every instance was instanceable when bucketed (no per-instance lights, + // no submodel animation, same replacable skins); the records of the instances tell which + // ones still are. Instances are individually frustum/distance culled by their records, the + // TAnimModel itself is read only for the first instance drawn. Each lod band drawn counts as + // one instanced_drawcall in draw_stats; the instances drawn are added to the instances counter. + 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 + // less the ones their records 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 ); + // makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin + void Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin ); + // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them + std::uint32_t Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ); 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); @@ -455,14 +463,16 @@ class opengl46_renderer : public gfx_renderer { // 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; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). - scene::basic_cell::instance_bucket_map m_frame_instance_buckets; + std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_frame_instance_buckets; // textures used by vehicle models, for the release of far textures std::unordered_map> m_modeltextures; // geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence) std::vector m_pathbatch; - // 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 }; + // instance records 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, + // and whenever scene::node_state_version does, as instances are shown, hidden or drawn on their own + unsigned int m_instancerecordsversion { 1 }; + unsigned int m_nodestateversion { 0 }; // scene::node_state_version which m_instancerecordsversion was last checked against 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; @@ -506,18 +516,20 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr scene_ubo; std::unique_ptr model_ubo; std::unique_ptr light_ssbo; - // per-instance modelview UBO for GPU-instanced draws. - // Slot 0 is initialized once to identity and never overwritten by non-instanced - // rendering, so existing draws (where gl_InstanceID==0) compute identity*modelview - // in the vertex shader and behave exactly as before. Render_Instanced uploads - // per-instance camera-space root matrices to slots 0..N-1, calls glDrawElementsInstanced*, - // then restores slot 0 to identity for subsequent non-instanced draws. - std::unique_ptr instance_ubo; + // per-instance modelview matrices of GPU-instanced draws, one buffer for each of the last frames, used in turn so + // a frame doesn't write to the buffer the gpu may still read for an earlier one. element 0 of each is identity + // and never overwritten: draws which aren't instanced (gl_BaseInstance == gl_InstanceID == 0) compute + // identity*modelview in the vertex shader. Render_Instanced and Render_Sleepers append the camera-space root + // matrices of their instances past the ones placed earlier in the frame, and draw them from that base instance + std::array, 3> m_instancebuffers; + std::size_t m_instancebufferindex { 0 }; // the buffer of the current frame + std::size_t m_instancebufferfill { 1 }; // matrices placed in it so far, identity included // when > 0, the renderer is inside a Render_Instanced() call. The draw(handle) - // method routes to glDrawElementsInstanced* with this many instances rather - // 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. + // method routes to glDrawElementsInstanced* with this many instances from + // m_current_instance_base onwards rather than a single regular draw. The vertex shader reads + // per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID]. Reset to 0 on exit. std::size_t m_current_instance_count { 0 }; + std::uint32_t m_current_instance_base { 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 diff --git a/scene/scene.cpp b/scene/scene.cpp index 7b0e2994..96fe4c7b 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -381,7 +381,7 @@ basic_cell::insert( TAnimModel *Instance ) { } auto &bucket { m_instancebuckets_opaque[ key ] }; bucket.instances.emplace_back( Instance ); - bucket.bounds.clear(); + bucket.records.clear(); } } // re-calculate cell bounding area, in case model extends outside the cell's boundaries @@ -484,7 +484,7 @@ basic_cell::erase( TAnimModel *Instance ) { instances.erase( std::remove( std::begin( instances ), std::end( instances ), Instance ), std::end( instances ) ); - bucket->second.bounds.clear(); + bucket->second.records.clear(); if( instances.empty() ) { bucket = m_instancebuckets_opaque.erase( bucket ); } diff --git a/scene/scene.h b/scene/scene.h index 400fdac3..5b599d8a 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -237,19 +237,31 @@ 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. + // what it takes to cull and place an instance, 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 + // pass rejects most of them. the renderer makes the copies and goes by them, and reads the instance itself only + // for the first one of a batch drawn. the layout follows std430, for the copies to be handed to the gpu as they are + struct instance_record { + glm::vec3 position { 0.f }; // location of the instance, relative to the origin of the records of its bucket 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 + float rangesquaredmin { 0.f }; // visibility range of the instance + float rangesquaredmax { 0.f }; + std::uint32_t flags { 0 }; // instance_record_flags + std::uint32_t pad { 0 }; + glm::vec4 rotationscale[ 3 ] {}; // columns of rotation and scale of the instance, w unused + }; + static_assert( sizeof( instance_record ) == 80, "instance_record has to keep its std430 layout" ); + enum instance_record_flags : std::uint32_t { + instance_visible = 1 << 0, + instance_instanceable = 1 << 1, // false once an event gave the instance animations or a skin of its own }; 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 }; + mutable std::vector records; // copies for the instances, in the same order + mutable glm::dvec3 recordorigin { 0.0 }; // the records hold locations relative to it + mutable float lodrangesquared { 0.f }; // past it no submodel of the model of the bucket is drawn + mutable unsigned int recordsversion { 0 }; }; using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using sound_sequence = std::vector; diff --git a/scene/scenenode.h b/scene/scenenode.h index 48dad5b6..b087bb09 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -345,6 +345,10 @@ private: +// changes whenever a node is shown or hidden, or a model instance stops being drawn along with other instances of its +// model. copies of these states (instance records of the renderer) are made anew once it changes +inline unsigned int node_state_version { 1 }; + // base interface for nodes which can be actvated in scenario editor class basic_node { @@ -460,6 +464,9 @@ basic_node::location() { inline void basic_node::visible( bool const Visible ) { + if( Visible != m_visible ) { + ++node_state_version; + } m_visible = Visible; }