mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-18 00:49:19 +02:00
Instanced object grouping
This commit is contained in:
@@ -376,9 +376,70 @@ bool TAnimModel::Load(cParser *parser, bool ter)
|
||||
} while( ( false == token.empty() )
|
||||
&& ( token != "endmodel" ) );
|
||||
|
||||
update_instanceable_flag();
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// returns true if this animation type mutates per-instance state on the shared
|
||||
// TSubModel tree, which would make batched rendering unsafe. Most animations
|
||||
// loaded from .t3d files are global functions of time (clocks, wind, sky) and
|
||||
// only transform the local modelview matrix — those are safe to share. Camera-
|
||||
// relative billboards also operate purely on the local matrix using whatever
|
||||
// modelview the caller pushed, which is exactly per-instance behaviour.
|
||||
// The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ /
|
||||
// at_Translate) are tied to per-instance iAnimOwner and are unsafe to share —
|
||||
// in practice they're driven by m_animlist which we filter at the AnimModel
|
||||
// level, but we still blacklist them here as a safety net.
|
||||
bool anim_type_unsafe_for_instancing( TAnimType a ) {
|
||||
switch( a ) {
|
||||
case TAnimType::at_Rotate:
|
||||
case TAnimType::at_RotateXYZ:
|
||||
case TAnimType::at_Translate:
|
||||
case TAnimType::at_DigiClk: // mutates child submodels via SetRotate
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// recursively walks a submodel tree and returns true if any submodel declares
|
||||
// an animation type that's unsafe to batch.
|
||||
bool submodel_tree_blocks_instancing( TSubModel const *Sub ) {
|
||||
if( Sub == nullptr ) { return false; }
|
||||
if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; }
|
||||
if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; }
|
||||
if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; }
|
||||
return false;
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
int TAnimModel::s_instanceable_total = 0;
|
||||
int TAnimModel::s_classified_total = 0;
|
||||
int TAnimModel::s_rejected_no_pmodel = 0;
|
||||
int TAnimModel::s_rejected_lights = 0;
|
||||
int TAnimModel::s_rejected_animlist = 0;
|
||||
int TAnimModel::s_rejected_animated_submodel = 0;
|
||||
|
||||
void TAnimModel::update_instanceable_flag() {
|
||||
// The instanceable path skips RaAnimate() entirely and only calls RaPrepare()
|
||||
// once per bucket. The conditions below ensure that's safe:
|
||||
// - no lights: per-instance light state machines must not exist
|
||||
// - no anim list: per-instance submodel animations must not exist
|
||||
// - no animated submodels in the shared TModel3d
|
||||
// Replacable skins, seasonal variants, and submodel replacable-skin material
|
||||
// refs are intentionally allowed — they're either passed per-instance via
|
||||
// Material() or share global state (season) across all instances.
|
||||
m_instanceable = false;
|
||||
++s_classified_total;
|
||||
if( pModel == nullptr ) { ++s_rejected_no_pmodel; return; }
|
||||
if( iNumLights != 0 ) { ++s_rejected_lights; return; }
|
||||
if( !m_animlist.empty() ) { ++s_rejected_animlist; return; }
|
||||
if( submodel_tree_blocks_instancing( pModel->Root ) ) { ++s_rejected_animated_submodel; return; }
|
||||
m_instanceable = true;
|
||||
++s_instanceable_total;
|
||||
}
|
||||
|
||||
std::shared_ptr<TAnimContainer> TAnimModel::AddContainer(std::string const &Name)
|
||||
{ // dodanie sterowania submodelem dla egzemplarza
|
||||
if (!pModel)
|
||||
|
||||
@@ -189,6 +189,22 @@ public:
|
||||
// float fTransitionTime { fOnTime * 0.9f }; // time
|
||||
bool m_transition { true }; // smooth transition between light states
|
||||
unsigned int m_framestamp { 0 }; // id of last rendered gfx frame
|
||||
|
||||
// true if this instance is eligible for the batched/instanced render path:
|
||||
// no lights, no submodel animations, no replacable skins, no seasonal variants.
|
||||
// computed once at end of Load() so the renderer can simply test the flag.
|
||||
bool m_instanceable { false };
|
||||
// helper: evaluates current state and updates m_instanceable accordingly.
|
||||
void update_instanceable_flag();
|
||||
|
||||
// diagnostic counters (process-wide). Updated inside update_instanceable_flag()
|
||||
// so the renderer can surface load-time classification stats in the debug overlay.
|
||||
static int s_instanceable_total; // count of TAnimModel*'s classified instanceable
|
||||
static int s_classified_total; // count of TAnimModel*'s that ran through the classifier
|
||||
static int s_rejected_no_pmodel;
|
||||
static int s_rejected_lights;
|
||||
static int s_rejected_animlist;
|
||||
static int s_rejected_animated_submodel;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -653,6 +653,16 @@ void opengl33_renderer::SwapBuffers()
|
||||
+ " =" + to_string( m_colorpass.draw_stats.models + shadowstats.models, 7 ) + "\n"
|
||||
+ "drawcalls: " + to_string( m_colorpass.draw_stats.drawcalls, 7 ) + " +" + to_string( shadowstats.drawcalls, 7 )
|
||||
+ " =" + to_string( m_colorpass.draw_stats.drawcalls + shadowstats.drawcalls, 7 ) + "\n"
|
||||
+ " 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"
|
||||
+ " inst-pool:" + std::to_string( TAnimModel::s_instanceable_total )
|
||||
+ "/" + std::to_string( TAnimModel::s_classified_total )
|
||||
+ " rej(noModel/lights/anim/animSubM): "
|
||||
+ std::to_string( TAnimModel::s_rejected_no_pmodel ) + "/"
|
||||
+ std::to_string( TAnimModel::s_rejected_lights ) + "/"
|
||||
+ std::to_string( TAnimModel::s_rejected_animlist ) + "/"
|
||||
+ std::to_string( TAnimModel::s_rejected_animated_submodel ) + "\n"
|
||||
+ " submodels:" + to_string( m_colorpass.draw_stats.submodels, 7 ) + " +" + to_string( shadowstats.submodels, 7 )
|
||||
+ " =" + to_string( m_colorpass.draw_stats.submodels + shadowstats.submodels, 7 ) + "\n"
|
||||
+ " paths: " + to_string( m_colorpass.draw_stats.paths, 7 ) + " +" + to_string( shadowstats.paths, 7 )
|
||||
@@ -2659,9 +2669,14 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
|
||||
case rendermode::shadows:
|
||||
{
|
||||
// TBD, TODO: refactor in to a method to reuse in branch below?
|
||||
// opaque parts of instanced models
|
||||
// opaque parts of instanced models -- batched path first
|
||||
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
|
||||
Render_Instanced( bucket.first, bucket.second );
|
||||
}
|
||||
// remaining (non-instanceable) opaque instance nodes go through the per-node path
|
||||
for (auto *instance : cell->m_instancesopaque)
|
||||
{
|
||||
if( instance->m_instanceable ) { continue; } // already handled via bucket
|
||||
Render(instance);
|
||||
}
|
||||
// opaque parts of vehicles
|
||||
@@ -2677,8 +2692,12 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
|
||||
case rendermode::reflections:
|
||||
{
|
||||
if( Global.reflectiontune.fidelity >= 1 ) {
|
||||
// opaque parts of instanced models
|
||||
// opaque parts of instanced models -- batched path first
|
||||
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
|
||||
Render_Instanced( bucket.first, bucket.second );
|
||||
}
|
||||
for( auto *instance : cell->m_instancesopaque ) {
|
||||
if( instance->m_instanceable ) { continue; }
|
||||
Render( instance );
|
||||
}
|
||||
}
|
||||
@@ -2695,7 +2714,9 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
|
||||
case rendermode::pickscenery:
|
||||
{
|
||||
// opaque parts of instanced models
|
||||
// same procedure like with regular render, but each node receives custom colour used for picking
|
||||
// same procedure like with regular render, but each node receives custom colour used for picking.
|
||||
// picking always uses the per-instance path because each instance needs a unique pick colour;
|
||||
// batching would assign the same colour to every instance in the bucket.
|
||||
for (auto *instance : cell->m_instancesopaque)
|
||||
{
|
||||
model_ubs.param[0] = glm::vec4(pick_color(m_picksceneryitems.size() + 1), 1.0f);
|
||||
@@ -2884,6 +2905,80 @@ 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.
|
||||
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
|
||||
|
||||
for( auto *Instance : Instances ) {
|
||||
if( Instance == nullptr ) { continue; }
|
||||
if( false == Instance->m_visible ) { continue; }
|
||||
if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { 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;
|
||||
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;
|
||||
}
|
||||
distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET );
|
||||
break;
|
||||
default:
|
||||
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor;
|
||||
break;
|
||||
}
|
||||
if( ( distancesquared < Instance->m_rangesquaredmin )
|
||||
|| ( distancesquared >= Instance->m_rangesquaredmax ) ) { continue; }
|
||||
|
||||
auto const drawdistancethreshold { m_renderpass.draw_range + 250 };
|
||||
if( distancesquared > sq( drawdistancethreshold ) ) { continue; }
|
||||
|
||||
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;
|
||||
}
|
||||
Instance->m_framestamp = m_framestamp;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if( rendered_count > 0 ) {
|
||||
++m_renderpass.draw_stats.instanced_drawcalls;
|
||||
m_renderpass.draw_stats.instances += rendered_count;
|
||||
}
|
||||
}
|
||||
|
||||
bool opengl33_renderer::Render(TDynamicObject *Dynamic)
|
||||
{
|
||||
glDebug("Render TDynamicObject");
|
||||
|
||||
@@ -162,6 +162,8 @@ class opengl33_renderer : public gfx_renderer {
|
||||
int lines{0};
|
||||
int particles{0};
|
||||
int drawcalls{0};
|
||||
int instanced_drawcalls{0}; // drawcalls issued via instanced render path (one per TModel3d batch)
|
||||
int instances{0}; // total instances drawn via instanced render path
|
||||
int triangles{0};
|
||||
|
||||
debug_stats& operator+=( const debug_stats& Right ) {
|
||||
@@ -174,6 +176,8 @@ class opengl33_renderer : public gfx_renderer {
|
||||
lines += Right.lines;
|
||||
particles += Right.particles;
|
||||
drawcalls += Right.drawcalls;
|
||||
instanced_drawcalls += Right.instanced_drawcalls;
|
||||
instances += Right.instances;
|
||||
return *this; }
|
||||
};
|
||||
|
||||
@@ -266,6 +270,12 @@ class opengl33_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);
|
||||
// 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<TAnimModel *> const &Instances );
|
||||
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);
|
||||
|
||||
@@ -374,6 +374,13 @@ 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.).
|
||||
if( Instance->m_instanceable && Instance->Model() != nullptr ) {
|
||||
m_instancebuckets_opaque[ Instance->Model() ].emplace_back( Instance );
|
||||
}
|
||||
}
|
||||
// re-calculate cell bounding area, in case model extends outside the cell's boundaries
|
||||
enclose_area( Instance );
|
||||
@@ -438,6 +445,18 @@ basic_cell::erase( TAnimModel *Instance ) {
|
||||
[=]( TAnimModel *instance ) {
|
||||
return instance == Instance; } ),
|
||||
std::end( m_instancesopaque ) );
|
||||
// also remove from the per-pModel instance bucket if present
|
||||
if( Instance->m_instanceable && Instance->Model() != nullptr ) {
|
||||
auto bucket = m_instancebuckets_opaque.find( Instance->Model() );
|
||||
if( bucket != m_instancebuckets_opaque.end() ) {
|
||||
bucket->second.erase(
|
||||
std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ),
|
||||
std::end( bucket->second ) );
|
||||
if( bucket->second.empty() ) {
|
||||
m_instancebuckets_opaque.erase( bucket );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: update cell bounding area
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <array>
|
||||
#include <stack>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <map>
|
||||
|
||||
#include "utilities/parser.h"
|
||||
@@ -179,6 +180,7 @@ 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 *> >;
|
||||
using sound_sequence = std::vector<sound_source *>;
|
||||
using eventlauncher_sequence = std::vector<TEventLauncher *>;
|
||||
using memorycell_sequence = std::vector<TMemCell *>;
|
||||
@@ -196,6 +198,10 @@ public:
|
||||
path_sequence m_paths; // path pieces
|
||||
instance_sequence m_instancesopaque;
|
||||
instance_sequence m_instancetranslucent;
|
||||
// batched instance buckets keyed by shared TModel3d*; populated alongside
|
||||
// m_instancesopaque for nodes whose TAnimModel::m_instanceable == true.
|
||||
// The renderer uses these to amortise per-model state setup across many instances.
|
||||
instance_bucket_map m_instancebuckets_opaque;
|
||||
traction_sequence m_traction;
|
||||
sound_sequence m_sounds;
|
||||
eventlauncher_sequence m_eventlaunchers;
|
||||
|
||||
Reference in New Issue
Block a user