From 587077629f84b2c8e072be281c35235efc57f040 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 9 May 2026 23:29:20 +0200 Subject: [PATCH] Enable instancing --- gl/glsl_common.cpp | 27 ++++++ gl/shader.cpp | 9 ++ gl/ubo.h | 11 +++ rendering/geometrybank.cpp | 20 +++++ rendering/geometrybank.h | 10 +++ rendering/opengl33geometrybank.cpp | 51 +++++++++++ rendering/opengl33geometrybank.h | 3 + rendering/opengl33renderer.cpp | 135 ++++++++++++++++++++++------- rendering/opengl33renderer.h | 12 +++ scene/scene.cpp | 28 ++++-- scene/scene.h | 25 +++++- shaders/simpleuv.vert | 2 +- shaders/vertex.vert | 19 ++-- shaders/vertexonly.vert | 2 +- 14 files changed, 304 insertions(+), 50 deletions(-) diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 2ca344f4..032f7e21 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -14,6 +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" + R"STRING( const uint LIGHT_SPOT = 0U; const uint LIGHT_POINT = 1U; @@ -78,5 +79,31 @@ 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 + { + mat4 instance_modelview[MAX_INSTANCES_PER_BATCH]; + }; + + #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; + } + mat3 effective_modelviewnormal() { + // instance_modelview is rotation+translation only (no scale), so its + // upper-3x3 is its own inverse-transpose (rotation matrix). + return mat3(instance_modelview[gl_InstanceID]) * modelviewnormal; + } + #endif + )STRING"; } diff --git a/gl/shader.cpp b/gl/shader.cpp index edc52a54..248b50f1 100644 --- a/gl/shader.cpp +++ b/gl/shader.cpp @@ -106,6 +106,12 @@ std::pair gl::shader::process_source(const std::string &fil str += "precision highp sampler2DShadow;\n"; str += "precision highp sampler2DArrayShadow;\n"; } + // expose the shader stage to glsl_common so vertex-only built-ins + // (gl_InstanceID, effective_modelview, etc.) can be guarded out of + // fragment/geometry compilations. + if (type == GL_VERTEX_SHADER) str += "#define STAGE_VERTEX 1\n"; + else if (type == GL_FRAGMENT_SHADER) str += "#define STAGE_FRAGMENT 1\n"; + else if (type == GL_GEOMETRY_SHADER) str += "#define STAGE_GEOMETRY 1\n"; str += "vec4 FBOUT(vec4 x) { return " + (Global.gfx_shadergamma ? std::string("vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a)") : std::string("x")) + "; }\n"; str += read_file(basedir + filename); @@ -293,6 +299,9 @@ void gl::program::init() if ((index = glGetUniformBlockIndex(*this, "light_ubo")) != GL_INVALID_INDEX) glUniformBlockBinding(*this, index, 2); + + 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 9932bfbe..b22b17e1 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -87,6 +87,17 @@ 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"); + struct light_element_ubs { enum type_e diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index 137dfca2..fcd731dd 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -325,6 +325,13 @@ geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units con return draw_( Geometry, Units, Streams ); } +// 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 ) { + if( InstanceCount == 0 ) { return 0; } + return draw_instanced_( Geometry, Units, InstanceCount, Streams ); +} + // frees subclass-specific resources associated with the bank, typically called when the bank wasn't in use for a period of time void geometry_bank::release() { @@ -416,6 +423,19 @@ geometrybank_manager::draw( gfx::geometry_handle const &Geometry, unsigned int c m_primitivecount += bankrecord.first->draw( Geometry, m_units, Streams ); } +// 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 ) { + + if( Geometry == null_handle ) { return; } + if( InstanceCount == 0 ) { return; } + + auto &bankrecord = bank( Geometry ); + + bankrecord.second = m_garbagecollector.timestamp(); + m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, Streams ); +} + // provides direct access to index data of specfied chunk gfx::index_array const & geometrybank_manager::indices( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 9828d578..39735e00 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -119,6 +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 supplied list of chunks template auto draw( Iterator_ First, Iterator_ Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) ->std::size_t { @@ -180,6 +182,11 @@ private: virtual void replace_( gfx::geometry_handle const &Geometry ) = 0; // 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 { + std::size_t count { 0 }; + for( std::size_t i = 0; i < InstanceCount; ++i ) { count += draw_( Geometry, Units, Streams ); } + return count; } // resource release subclass details virtual void release_() = 0; }; @@ -208,6 +215,9 @@ public: auto append( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; // 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 ); template void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) { while( First != Last ) { diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 28ad6b1b..91c9a8dd 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -187,6 +187,57 @@ opengl33_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]. +std::size_t +opengl33_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) +{ + setup_buffer(); + + auto &chunkrecord = m_chunkrecords.at(Geometry.chunk - 1); + if( chunkrecord.vertex_count == 0 ) { return 0; } + + auto const &chunk = gfx::geometry_bank::chunk( Geometry ); + if( !chunkrecord.is_good ) { + m_vao->bind(); + if( chunkrecord.index_count > 0 ) { + m_indexbuffer->upload( gl::buffer::ELEMENT_ARRAY_BUFFER, chunk.indices.data(), chunkrecord.index_offset * sizeof( gfx::basic_index ), chunkrecord.index_count * sizeof( gfx::basic_index ) ); + } + m_vertexbuffer->upload( gl::buffer::ARRAY_BUFFER, chunk.vertices.data(), chunkrecord.vertex_offset * sizeof( gfx::basic_vertex ), chunkrecord.vertex_count * sizeof( gfx::basic_vertex ) ); + if( chunkrecord.has_userdata ) { + m_vertexbuffer->upload( gl::buffer::ARRAY_BUFFER, chunk.userdata.data(), + static_cast(m_vertex_count * sizeof(gfx::basic_vertex) + chunkrecord.vertex_offset * sizeof(gfx::vertex_userdata)), + static_cast(chunkrecord.vertex_count * sizeof(gfx::vertex_userdata)) ); + } + chunkrecord.is_good = true; + } + + GLsizei const inst_count = static_cast(InstanceCount); + + if( chunkrecord.index_count > 0 ) { + m_vao->bind(); + ::glDrawElementsInstancedBaseVertex( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count, + chunkrecord.vertex_offset ); + } + else { + m_vao->bind(); + ::glDrawArraysInstanced( chunk.type, chunkrecord.vertex_offset, chunkrecord.vertex_count, inst_count ); + } + + auto const vertexcount { ( chunkrecord.index_count > 0 ? chunkrecord.index_count : chunkrecord.vertex_count ) }; + std::size_t prims = 0; + switch( chunk.type ) { + case GL_TRIANGLES: prims = vertexcount / 3; break; + case GL_TRIANGLE_STRIP: prims = ( vertexcount > 2 ? vertexcount - 2 : 0 ); break; + default: prims = 0; break; + } + return prims * InstanceCount; +} + // release () subclass details void opengl33_vaogeometrybank::release_() { diff --git a/rendering/opengl33geometrybank.h b/rendering/opengl33geometrybank.h index 7dd54ba8..f5cd7e37 100644 --- a/rendering/opengl33geometrybank.h +++ b/rendering/opengl33geometrybank.h @@ -53,6 +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 + 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; // release() subclass details void release_() override; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 0832b3af..19ae7343 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -142,6 +142,7 @@ bool opengl33_renderer::Init(GLFWwindow *Window) scene_ubo = std::make_unique(sizeof(gl::scene_ubs), 0); model_ubo = std::make_unique(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW); light_ubo = std::make_unique(sizeof(gl::light_ubs), 2); + instance_ubo = std::make_unique(sizeof(gl::instance_ubs), 3, GL_STREAM_DRAW); // better initialize with 0 to not crash driver/whole system // when we forget @@ -153,6 +154,14 @@ bool opengl33_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 + // continue to multiply effective_modelview = identity * modelview = modelview. + { + glm::mat4 identity( 1.0f ); + instance_ubo->update( reinterpret_cast(&identity), 0, sizeof(identity) ); + } + int samples = 1 << Global.iMultisampling; if (!Global.gfx_usegles && samples > 1) glEnable(GL_MULTISAMPLE); @@ -2671,7 +2680,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // 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, bucket.second ); + Render_Instanced( bucket.first.pModel, bucket.second ); } // remaining (non-instanceable) opaque instance nodes go through the per-node path for (auto *instance : cell->m_instancesopaque) @@ -2694,7 +2703,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite if( Global.reflectiontune.fidelity >= 1 ) { // opaque parts of instanced models -- batched path first for( auto const &bucket : cell->m_instancebuckets_opaque ) { - Render_Instanced( bucket.first, bucket.second ); + Render_Instanced( bucket.first.pModel, bucket.second ); } for( auto *instance : cell->m_instancesopaque ) { if( instance->m_instanceable ) { continue; } @@ -2751,7 +2760,14 @@ void opengl33_renderer::draw(const gfx::geometry_handle &handle) model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); model_ubo->update(model_ubs); - m_geometry.draw(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 ); + } + else { + m_geometry.draw( handle ); + } } void opengl33_renderer::draw(std::vector::iterator it, std::vector::iterator end) @@ -2905,19 +2921,34 @@ void opengl33_renderer::Render(TAnimModel *Instance) } } -// batched render path for instanceable TAnimModel groups sharing the same TModel3d. -// Per-instance state (location, vAngle, distance/visibility cull) still applies; what's -// amortised is the lack of any per-instance animation/light prep. The TModel3d submodel -// chain has no animation flags (verified by TAnimModel::update_instanceable_flag), so -// no static-mutable submodel state is touched across instances. +// 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 opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector const &Instances ) { if( Model == nullptr ) { return; } if( Instances.empty() ) { return; } - int rendered_count = 0; - bool prepared_shared_state = false; // RaPrepare()'s side-effects on shared seasonal-variant - // submodels only need to happen once per pModel per frame + // 1. Visibility / distance cull. Build parallel arrays of surviving + // instances and their precomputed camera-space root modelview matrices. + std::vector instance_modelviews; + 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 + // is called, OpenGLMatrices is set to the camera view (scene root) — no + // per-instance transform has been pushed yet. + glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW ); + + bool prepared_shared_state = false; // RaPrepare() runs once per bucket + float closest_distancesquared = std::numeric_limits::max(); + material_data const *batch_material { nullptr }; for( auto *Instance : Instances ) { if( Instance == nullptr ) { continue; } @@ -2931,9 +2962,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorlocation() - m_renderpass.pass_camera.position() ) ); - if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { - continue; - } + if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; } distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET ); break; default: @@ -2949,34 +2978,76 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorradius() * Instance->radius() }; if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; } - // for instanceable nodes we can skip RaAnimate entirely (no anim list, no light state). - // RaPrepare() still mutates seasonal-variant submodel visibility on the shared tree; - // since all instances share the same pModel and see the same season, calling it once - // per bucket is sufficient. if( !prepared_shared_state ) { Instance->RaPrepare(); prepared_shared_state = true; + batch_material = Instance->Material(); } Instance->m_framestamp = m_framestamp; + closest_distancesquared = std::min( closest_distancesquared, static_cast(distancesquared) ); - // per-instance modelview is pushed via the standard matrix stack so the - // existing shader path picks it up through model_ubs.modelview without - // any shader changes. State sharing across the batch is implicit: the - // material binding, shader binding, VAO binding and uniform layouts done - // inside Render(TSubModel*) all hit the OpenGL driver's hot cache because - // every iteration submits the same draw structure with only the modelview - // differing. That alone halves the CPU cost in driver validation paths. - Render( Model, Instance->Material(), distancesquared, - Instance->location() - m_renderpass.pass_camera.position(), - Instance->vAngle ); - ++rendered_count; - ++m_renderpass.draw_stats.models; + // Build the camera-relative root modelview for this instance: + // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) + glm::dvec3 const offset = Instance->location() - m_renderpass.pass_camera.position(); + glm::mat4 mv = view_matrix; + mv = glm::translate( mv, glm::vec3( offset ) ); + auto const &angle = Instance->vAngle; + if( angle.y != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); } + if( angle.x != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); } + if( angle.z != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); } + instance_modelviews.emplace_back( mv ); } - if( rendered_count > 0 ) { + if( 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 offset_idx = 0; + while( offset_idx < total ) { + std::size_t const this_batch = std::min( 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 ), + 0, + static_cast( this_batch * sizeof( glm::mat4 ) ) ); + + // 2b. Push identity onto the matrix stack so submodel transforms + // accumulate from identity, not from camera/instance space. + ::glPushMatrix(); + ::glLoadIdentity(); + + // 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 = closest_distancesquared; // shared global, used by submodel LOD + auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); + alpha ^= 0x0F0F000F; + Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); + Model->Root->pRoot = Model; + + Render( Model->Root ); + + m_current_instance_count = 0; + + ::glPopMatrix(); + + // 2d. Restore instance_modelview[0] to identity so subsequent + // non-instanced draws continue to compute identity * modelview. + { + 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; - m_renderpass.draw_stats.instances += rendered_count; } + + m_renderpass.draw_stats.instances += static_cast( total ); + m_renderpass.draw_stats.models += static_cast( total ); } bool opengl33_renderer::Render(TDynamicObject *Dynamic) diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 52ae1e6b..7cfa187e 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -391,6 +391,18 @@ class opengl33_renderer : public gfx_renderer { std::unique_ptr scene_ubo; std::unique_ptr model_ubo; std::unique_ptr light_ubo; + // 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; + // 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. + std::size_t m_current_instance_count { 0 }; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; diff --git a/scene/scene.cpp b/scene/scene.cpp index e2c5a5b1..c2100449 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -374,12 +374,20 @@ basic_cell::insert( TAnimModel *Instance ) { if( alpha & flags & 0x1F1F001F ) { // opaque pieces m_instancesopaque.emplace_back( Instance ); - // additionally route instanceable nodes into a per-pModel bucket so the - // renderer can amortise material/state setup across all instances of the - // same TModel3d. The flat list is kept too for fallback paths - // (picking, reflections at low fidelity, etc.). + // additionally route instanceable nodes into a per-(pModel, skins) bucket so + // the renderer can amortise material/state setup across many instances of the + // same TModel3d AND issue a single GPU-instanced draw call per submodel. + // Sub-bucketing on skin set ensures every batch has consistent textures — + // instances of the same pModel with different replacable skins land in + // different buckets and render via separate (correct) instanced draws. if( Instance->m_instanceable && Instance->Model() != nullptr ) { - m_instancebuckets_opaque[ Instance->Model() ].emplace_back( Instance ); + instance_bucket_key key; + key.pModel = Instance->Model(); + auto const *mat = Instance->Material(); + if( mat != nullptr ) { + for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } + } + m_instancebuckets_opaque[ key ].emplace_back( Instance ); } } // re-calculate cell bounding area, in case model extends outside the cell's boundaries @@ -445,9 +453,15 @@ basic_cell::erase( TAnimModel *Instance ) { [=]( TAnimModel *instance ) { return instance == Instance; } ), std::end( m_instancesopaque ) ); - // also remove from the per-pModel instance bucket if present + // also remove from the per-(pModel, skins) instance bucket if present if( Instance->m_instanceable && Instance->Model() != nullptr ) { - auto bucket = m_instancebuckets_opaque.find( Instance->Model() ); + instance_bucket_key key; + key.pModel = Instance->Model(); + auto const *mat = Instance->Material(); + if( mat != nullptr ) { + for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } + } + auto bucket = m_instancebuckets_opaque.find( key ); if( bucket != m_instancebuckets_opaque.end() ) { bucket->second.erase( std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), diff --git a/scene/scene.h b/scene/scene.h index 4e6b0b5f..9b2b955f 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -180,7 +180,30 @@ public: using linesnode_sequence = std::vector; using traction_sequence = std::vector; using instance_sequence = std::vector; - using instance_bucket_map = std::unordered_map< TModel3d *, std::vector >; + // Composite key: instances of the same TModel3d sharing the same replacable + // skin set can be GPU-batched together, but instances with different skins + // must go to different buckets so each batched draw call uses one consistent + // material binding. Sub-bucketing here keeps Render_Instanced correct without + // forcing per-instance material switching inside a single batch. + struct instance_bucket_key { + TModel3d *pModel { nullptr }; + std::array skins {}; + + bool operator==( instance_bucket_key const &other ) const { + return pModel == other.pModel && skins == other.skins; + } + }; + struct instance_bucket_key_hash { + std::size_t operator()( instance_bucket_key const &k ) const { + std::size_t h = std::hash()( k.pModel ); + for( auto s : k.skins ) { + // boost-style hash combine + h ^= std::hash()( s ) + 0x9e3779b9 + ( h << 6 ) + ( h >> 2 ); + } + return h; + } + }; + using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector, instance_bucket_key_hash >; using sound_sequence = std::vector; using eventlauncher_sequence = std::vector; using memorycell_sequence = std::vector; diff --git a/shaders/simpleuv.vert b/shaders/simpleuv.vert index f3cd0ea2..673130ae 100644 --- a/shaders/simpleuv.vert +++ b/shaders/simpleuv.vert @@ -7,6 +7,6 @@ out vec2 f_coord; void main() { - gl_Position = (projection * modelview) * vec4(v_vert, 1.0f); + gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0f); f_coord = v_coord; } diff --git a/shaders/vertex.vert b/shaders/vertex.vert index feadbe1e..5326545e 100644 --- a/shaders/vertex.vert +++ b/shaders/vertex.vert @@ -23,24 +23,27 @@ out vec4 UserData; void main() { - f_normal = normalize(modelviewnormal * v_normal); + mat4 mv = effective_modelview(); + mat3 mvn = effective_modelviewnormal(); + + f_normal = normalize(mvn * v_normal); f_normal_raw = v_normal; f_coord = v_coord; - f_pos = modelview * vec4(v_vert, 1.0); + f_pos = mv * vec4(v_vert, 1.0); for (uint idx = 0U ; idx < MAX_CASCADES ; ++idx) { f_light_pos[idx] = lightview[idx] * f_pos; - } - f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0); - f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0); - + } + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + gl_Position = f_clip_pos; gl_PointSize = param[1].x; - vec3 T = normalize(modelviewnormal * v_tangent.xyz); + vec3 T = normalize(mvn * v_tangent.xyz); vec3 N = f_normal; vec3 B = normalize(cross(N, T)); f_tbn = mat3(T, B, N); - + mat3 TBN = transpose(f_tbn); // TangentLightPos = TBN * f_light_pos.xyz; // TangentViewPos = TBN * vec3(0.0, 0.0, 0.0); diff --git a/shaders/vertexonly.vert b/shaders/vertexonly.vert index c121b09b..370b4aa3 100644 --- a/shaders/vertexonly.vert +++ b/shaders/vertexonly.vert @@ -4,5 +4,5 @@ layout(location = 0) in vec3 v_vert; void main() { - gl_Position = (projection * modelview) * vec4(v_vert, 1.0); + gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0); }