mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-17 23:39:18 +02:00
Enable instancing
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -106,6 +106,12 @@ std::pair<GLuint, std::string> 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()
|
||||
|
||||
11
gl/ubo.h
11
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <typename Iterator_>
|
||||
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 <typename Iterator_>
|
||||
void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) {
|
||||
while( First != Last ) {
|
||||
|
||||
@@ -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<int>(m_vertex_count * sizeof(gfx::basic_vertex) + chunkrecord.vertex_offset * sizeof(gfx::vertex_userdata)),
|
||||
static_cast<int>(chunkrecord.vertex_count * sizeof(gfx::vertex_userdata)) );
|
||||
}
|
||||
chunkrecord.is_good = true;
|
||||
}
|
||||
|
||||
GLsizei const inst_count = static_cast<GLsizei>(InstanceCount);
|
||||
|
||||
if( chunkrecord.index_count > 0 ) {
|
||||
m_vao->bind();
|
||||
::glDrawElementsInstancedBaseVertex(
|
||||
chunk.type,
|
||||
chunkrecord.index_count, GL_UNSIGNED_INT,
|
||||
reinterpret_cast<void const *>( 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_() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -142,6 +142,7 @@ bool opengl33_renderer::Init(GLFWwindow *Window)
|
||||
scene_ubo = std::make_unique<gl::ubo>(sizeof(gl::scene_ubs), 0);
|
||||
model_ubo = std::make_unique<gl::ubo>(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW);
|
||||
light_ubo = std::make_unique<gl::ubo>(sizeof(gl::light_ubs), 2);
|
||||
instance_ubo = std::make_unique<gl::ubo>(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<uint8_t const*>(&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<gfx::geometrybank_handle>::iterator it, std::vector<gfx::geometrybank_handle>::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<TAnimModel *> 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<glm::mat4> 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<float>::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::vector<TAnimMode
|
||||
break;
|
||||
case rendermode::reflections:
|
||||
distancesquared = glm::length2( ( Instance->location() - 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::vector<TAnimMode
|
||||
auto const radiussquared { Instance->radius() * 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<float>( closest_distancesquared, static_cast<float>(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<std::size_t>( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
|
||||
|
||||
// 2a. Upload N modelviews to instance_ubo[0..N-1].
|
||||
instance_ubo->update(
|
||||
reinterpret_cast<uint8_t const *>( instance_modelviews.data() + offset_idx ),
|
||||
0,
|
||||
static_cast<int>( 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<uint8_t const *>( &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<int>( total );
|
||||
m_renderpass.draw_stats.models += static_cast<int>( total );
|
||||
}
|
||||
|
||||
bool opengl33_renderer::Render(TDynamicObject *Dynamic)
|
||||
|
||||
@@ -391,6 +391,18 @@ class opengl33_renderer : public gfx_renderer {
|
||||
std::unique_ptr<gl::ubo> scene_ubo;
|
||||
std::unique_ptr<gl::ubo> model_ubo;
|
||||
std::unique_ptr<gl::ubo> 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<gl::ubo> 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;
|
||||
|
||||
@@ -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 ),
|
||||
|
||||
@@ -180,7 +180,30 @@ public:
|
||||
using linesnode_sequence = std::vector<lines_node>;
|
||||
using traction_sequence = std::vector<TTraction *>;
|
||||
using instance_sequence = std::vector<TAnimModel *>;
|
||||
using instance_bucket_map = std::unordered_map< TModel3d *, std::vector<TAnimModel *> >;
|
||||
// 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<material_handle, 5> 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<TModel3d *>()( k.pModel );
|
||||
for( auto s : k.skins ) {
|
||||
// boost-style hash combine
|
||||
h ^= std::hash<int>()( s ) + 0x9e3779b9 + ( h << 6 ) + ( h >> 2 );
|
||||
}
|
||||
return h;
|
||||
}
|
||||
};
|
||||
using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector<TAnimModel *>, instance_bucket_key_hash >;
|
||||
using sound_sequence = std::vector<sound_source *>;
|
||||
using eventlauncher_sequence = std::vector<TEventLauncher *>;
|
||||
using memorycell_sequence = std::vector<TMemCell *>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,20 +23,23 @@ 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user