16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 00:49:19 +02:00

Enable instancing

This commit is contained in:
2026-05-09 23:29:20 +02:00
parent b7283d8fb9
commit 587077629f
14 changed files with 304 additions and 50 deletions

View File

@@ -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)