mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-03 12:00:22 +01:00
Merge pull request #195 from MaSzyna-EU07/fix/instancing
Improve instancing rendering speed
This commit is contained in:
@@ -403,8 +403,8 @@ namespace {
|
|||||||
// TSubModel tree, which would make batched rendering unsafe. Most animations
|
// TSubModel tree, which would make batched rendering unsafe. Most animations
|
||||||
// loaded from .t3d files are global functions of time (clocks, wind, sky) and
|
// 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-
|
// only transform the local modelview matrix — those are safe to share. Camera-
|
||||||
// relative billboards also operate purely on the local matrix using whatever
|
// relative billboards are not: they're computed on the cpu from the modelview of
|
||||||
// modelview the caller pushed, which is exactly per-instance behaviour.
|
// the drawn instance, which in a batch is only the submodel-local chain.
|
||||||
// The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ /
|
// The runtime SetRotate/SetTranslate animations (at_Rotate / at_RotateXYZ /
|
||||||
// at_Translate) are tied to per-instance iAnimOwner and are unsafe to share.
|
// at_Translate) are tied to per-instance iAnimOwner and are unsafe to share.
|
||||||
// at_Undefined is the type assigned to .t3d submodels declared with `anim: true`
|
// at_Undefined is the type assigned to .t3d submodels declared with `anim: true`
|
||||||
@@ -425,17 +425,40 @@ bool anim_type_unsafe_for_instancing( TAnimType a ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns true if this animation type depends only on global state (simulation
|
||||||
|
// time, wind, sky) and so gives every instance of the model the same local
|
||||||
|
// transform. The batch evaluates it once and the per-instance placement comes
|
||||||
|
// from the instance matrix, the result is the same as with per-instance drawing.
|
||||||
|
bool anim_type_shared_by_instances( TAnimType a ) {
|
||||||
|
switch( a ) {
|
||||||
|
case TAnimType::at_SecondsJump:
|
||||||
|
case TAnimType::at_MinutesJump:
|
||||||
|
case TAnimType::at_HoursJump:
|
||||||
|
case TAnimType::at_Hours24Jump:
|
||||||
|
case TAnimType::at_Seconds:
|
||||||
|
case TAnimType::at_Minutes:
|
||||||
|
case TAnimType::at_Hours:
|
||||||
|
case TAnimType::at_Hours24:
|
||||||
|
case TAnimType::at_Wind:
|
||||||
|
case TAnimType::at_Sky:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// recursively walks a submodel tree and returns true if any submodel declares
|
// recursively walks a submodel tree and returns true if any submodel declares
|
||||||
// an animation type that's unsafe to batch, OR carries the runtime "needs
|
// an animation type that's unsafe to batch, OR carries the runtime "needs
|
||||||
// animation matrix" flag (iFlags bit 0x4000), which is set whenever the
|
// animation matrix" flag (iFlags bit 0x4000) for anything else than an animation
|
||||||
// submodel was tagged as animatable in the .t3d file or had WillBeAnimated()
|
// shared by all instances. The flag is set whenever the submodel was tagged as
|
||||||
// called on it during model load. Either signal means the submodel may receive
|
// animatable in the .t3d file (any `anim:` other than `false`) or had
|
||||||
|
// WillBeAnimated() called on it during model load. Such a submodel may receive
|
||||||
// per-instance event-driven animation commands at runtime, which the GPU-
|
// per-instance event-driven animation commands at runtime, which the GPU-
|
||||||
// instanced path (one shared submodel tree across all instances) cannot serve.
|
// instanced path (one shared submodel tree across all instances) cannot serve.
|
||||||
bool submodel_tree_blocks_instancing( TSubModel const *Sub ) {
|
bool submodel_tree_blocks_instancing( TSubModel const *Sub ) {
|
||||||
if( Sub == nullptr ) { return false; }
|
if( Sub == nullptr ) { return false; }
|
||||||
if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; }
|
if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; }
|
||||||
if( ( Sub->iFlags & 0x4000 ) != 0 ) { return true; }
|
if( ( ( Sub->iFlags & 0x4000 ) != 0 ) && ( false == anim_type_shared_by_instances( Sub->b_Anim ) ) ) { return true; }
|
||||||
if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; }
|
if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; }
|
||||||
if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; }
|
if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; }
|
||||||
return false;
|
return false;
|
||||||
@@ -478,6 +501,9 @@ std::shared_ptr<TAnimContainer> TAnimModel::AddContainer(std::string const &Name
|
|||||||
auto tmp = std::make_shared<TAnimContainer>();
|
auto tmp = std::make_shared<TAnimContainer>();
|
||||||
tmp->Init(tsb);
|
tmp->Init(tsb);
|
||||||
m_animlist.push_back(tmp);
|
m_animlist.push_back(tmp);
|
||||||
|
// 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;
|
||||||
return tmp;
|
return tmp;
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -638,6 +664,23 @@ void TAnimModel::RaPrepare()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glm::mat4 const &TAnimModel::rotation_scale() {
|
||||||
|
|
||||||
|
if( ( false == m_rotationscalevalid ) || ( vAngle != m_rotationscaleangles ) || ( m_scale != m_rotationscalescale ) ) {
|
||||||
|
// same order as the per-instance render path
|
||||||
|
glm::mat4 transform( 1.0f );
|
||||||
|
if( vAngle.y != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); }
|
||||||
|
if( vAngle.x != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); }
|
||||||
|
if( vAngle.z != 0.0f ) { transform = glm::rotate( transform, glm::radians( vAngle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); }
|
||||||
|
if( m_scale != glm::vec3( 1.0f ) ) { transform = glm::scale( transform, m_scale ); }
|
||||||
|
m_rotationscale = transform;
|
||||||
|
m_rotationscaleangles = vAngle;
|
||||||
|
m_rotationscalescale = m_scale;
|
||||||
|
m_rotationscalevalid = true;
|
||||||
|
}
|
||||||
|
return m_rotationscale;
|
||||||
|
}
|
||||||
|
|
||||||
int TAnimModel::Flags()
|
int TAnimModel::Flags()
|
||||||
{ // informacja dla TGround, czy ma być w Render, RenderAlpha, czy RenderMixed
|
{ // informacja dla TGround, czy ma być w Render, RenderAlpha, czy RenderMixed
|
||||||
int i = pModel ? pModel->Flags() : 0; // pobranie flag całego modelu
|
int i = pModel ? pModel->Flags() : 0; // pobranie flag całego modelu
|
||||||
@@ -688,6 +731,8 @@ std::optional<std::tuple<float, float, std::optional<glm::vec3>> > TAnimModel::L
|
|||||||
void TAnimModel::SkinSet( int const Index, material_handle const Material ) {
|
void TAnimModel::SkinSet( int const Index, material_handle const Material ) {
|
||||||
|
|
||||||
m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TAnimModel::AnimUpdate(double dt)
|
void TAnimModel::AnimUpdate(double dt)
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ public:
|
|||||||
public:
|
public:
|
||||||
// methods
|
// methods
|
||||||
void RaPrepare(); // ustawienie animacji egzemplarza na wzorcu
|
void RaPrepare(); // ustawienie animacji egzemplarza na wzorcu
|
||||||
|
// rotation and scale of the instance as a matrix: rotate y * rotate x * rotate z * scale
|
||||||
|
glm::mat4 const &rotation_scale();
|
||||||
void RaAnimate( unsigned int const Framestamp ); // przeliczenie animacji egzemplarza
|
void RaAnimate( unsigned int const Framestamp ); // przeliczenie animacji egzemplarza
|
||||||
|
|
||||||
// radius() subclass details, calculates node's bounding radius
|
// radius() subclass details, calculates node's bounding radius
|
||||||
@@ -190,6 +192,12 @@ public:
|
|||||||
TModel3d *pModel { nullptr };
|
TModel3d *pModel { nullptr };
|
||||||
glm::vec3 vAngle; // bazowe obroty egzemplarza względem osi
|
glm::vec3 vAngle; // bazowe obroty egzemplarza względem osi
|
||||||
glm::vec3 m_scale { 1.0f, 1.0f, 1.0f }; // per-axis scale (see Scale() accessors above)
|
glm::vec3 m_scale { 1.0f, 1.0f, 1.0f }; // per-axis scale (see Scale() accessors above)
|
||||||
|
// rotation and scale part of the instance transform, cached for the instanced render path.
|
||||||
|
// rebuilt by rotation_scale() whenever the angles or the scale differ from the ones it was built from
|
||||||
|
glm::mat4 m_rotationscale { 1.0f };
|
||||||
|
glm::vec3 m_rotationscaleangles { 0.0f, 0.0f, 0.0f };
|
||||||
|
glm::vec3 m_rotationscalescale { 1.0f, 1.0f, 1.0f };
|
||||||
|
bool m_rotationscalevalid { false };
|
||||||
material_data m_materialdata;
|
material_data m_materialdata;
|
||||||
|
|
||||||
std::string asText; // tekst dla wyświetlacza znakowego
|
std::string asText; // tekst dla wyświetlacza znakowego
|
||||||
|
|||||||
@@ -215,13 +215,34 @@ opengl33_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry,
|
|||||||
GLsizei const inst_count = static_cast<GLsizei>(InstanceCount);
|
GLsizei const inst_count = static_cast<GLsizei>(InstanceCount);
|
||||||
|
|
||||||
if( chunkrecord.index_count > 0 ) {
|
if( chunkrecord.index_count > 0 ) {
|
||||||
m_vao->bind();
|
// GLES 3.0 context doesn't provide the base vertex variant, same fallbacks as in draw_()
|
||||||
::glDrawElementsInstancedBaseVertex(
|
if( glDrawElementsInstancedBaseVertex ) {
|
||||||
chunk.type,
|
m_vao->bind();
|
||||||
chunkrecord.index_count, GL_UNSIGNED_INT,
|
::glDrawElementsInstancedBaseVertex(
|
||||||
reinterpret_cast<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ),
|
chunk.type,
|
||||||
inst_count,
|
chunkrecord.index_count, GL_UNSIGNED_INT,
|
||||||
chunkrecord.vertex_offset );
|
reinterpret_cast<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ),
|
||||||
|
inst_count,
|
||||||
|
chunkrecord.vertex_offset );
|
||||||
|
}
|
||||||
|
else if( glDrawElementsInstancedBaseVertexOES ) {
|
||||||
|
m_vao->bind();
|
||||||
|
::glDrawElementsInstancedBaseVertexOES(
|
||||||
|
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 {
|
||||||
|
setup_attrib( chunkrecord.vertex_offset );
|
||||||
|
m_vao->bind();
|
||||||
|
::glDrawElementsInstanced(
|
||||||
|
chunk.type,
|
||||||
|
chunkrecord.index_count, GL_UNSIGNED_INT,
|
||||||
|
reinterpret_cast<void const *>( chunkrecord.index_offset * sizeof( gfx::basic_index ) ),
|
||||||
|
inst_count );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_vao->bind();
|
m_vao->bind();
|
||||||
|
|||||||
@@ -2382,8 +2382,11 @@ void opengl33_renderer::Render(scene::basic_region *Region)
|
|||||||
|
|
||||||
m_sectionqueue.clear();
|
m_sectionqueue.clear();
|
||||||
m_cellqueue.clear();
|
m_cellqueue.clear();
|
||||||
// discard last pass's accumulated instance buckets before this pass starts
|
// discard last pass's accumulated instance buckets before this pass starts.
|
||||||
m_frame_instance_buckets.clear();
|
// the vectors themselves are kept, so their allocations get reused by the following passes
|
||||||
|
for( auto &bucket : m_frame_instance_buckets ) {
|
||||||
|
bucket.second.clear();
|
||||||
|
}
|
||||||
// build a list of region sections to render
|
// build a list of region sections to render
|
||||||
glm::vec3 const cameraposition{m_renderpass.pass_camera.position()};
|
glm::vec3 const cameraposition{m_renderpass.pass_camera.position()};
|
||||||
auto const camerax = static_cast<int>(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2));
|
auto const camerax = static_cast<int>(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2));
|
||||||
@@ -2802,10 +2805,11 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite
|
|||||||
// still happens inside Render_Instanced(), so correctness is unchanged -- only
|
// still happens inside Render_Instanced(), so correctness is unchanged -- only
|
||||||
// the number of calls and glBufferSubData round-trips drops sharply.
|
// the number of calls and glBufferSubData round-trips drops sharply.
|
||||||
// (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.)
|
// (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.)
|
||||||
for( auto const &bucket : m_frame_instance_buckets ) {
|
for( auto &bucket : m_frame_instance_buckets ) {
|
||||||
|
if( bucket.second.empty() ) { continue; }
|
||||||
Render_Instanced( bucket.first.pModel, bucket.second );
|
Render_Instanced( bucket.first.pModel, bucket.second );
|
||||||
|
bucket.second.clear();
|
||||||
}
|
}
|
||||||
m_frame_instance_buckets.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void opengl33_renderer::Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end)
|
void opengl33_renderer::Draw_Geometry(std::vector<gfx::geometrybank_handle>::iterator begin, std::vector<gfx::geometrybank_handle>::iterator end)
|
||||||
@@ -3022,15 +3026,46 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
|
|||||||
if( Model == nullptr ) { return; }
|
if( Model == nullptr ) { return; }
|
||||||
if( Instances.empty() ) { return; }
|
if( Instances.empty() ) { return; }
|
||||||
|
|
||||||
// 1. Visibility / distance cull. Build parallel arrays of surviving
|
// LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist)
|
||||||
// instances and their precomputed camera-space root modelview matrices.
|
// range, so the distinct range bounds split distance into bands with constant LOD. The bucket
|
||||||
// m_instance_modelviews is a persistent member reused across every
|
// collects instances from every visible cell, so a single fSquareDist for all of them would draw
|
||||||
// Render_Instanced() call: clear() drops the contents but keeps the
|
// far instances at the LOD of the nearest one. Each instance is assigned to its band and every
|
||||||
// allocated capacity, so after the first few frames this stops calling
|
// band is drawn with its own fSquareDist, like in Render_Sleepers(). The grouping is a counting
|
||||||
// malloc/free entirely (the reserve() below becomes a no-op once the
|
// sort, linear in the instance count.
|
||||||
// buffer has grown to the largest batch encountered).
|
// the same walk also checks whether any piece of the model can get into the shadow map. Models
|
||||||
m_instance_modelviews.clear();
|
// whose materials are all excluded by the shadow rank cutoff (e.g. grass) would otherwise go through
|
||||||
m_instance_modelviews.reserve( Instances.size() );
|
// the whole per-instance culling and the batched draws in every shadow pass, only for every submodel
|
||||||
|
// to be skipped in Render(TSubModel*)
|
||||||
|
auto const *skins { Instances.front() != nullptr && Instances.front()->Material() != nullptr ? Instances.front()->Material()->replacable_skins : nullptr };
|
||||||
|
bool castsshadow { m_renderpass.draw_mode != rendermode::shadows };
|
||||||
|
m_instance_lodbounds.clear();
|
||||||
|
m_instance_lodpending.clear();
|
||||||
|
m_instance_lodpending.push_back( Model->Root );
|
||||||
|
while( false == m_instance_lodpending.empty() ) {
|
||||||
|
auto const *submodel = m_instance_lodpending.back();
|
||||||
|
m_instance_lodpending.pop_back();
|
||||||
|
if( submodel == nullptr ) { continue; }
|
||||||
|
if( submodel->fSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); }
|
||||||
|
if( submodel->fSquareMaxDist < std::numeric_limits<float>::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); }
|
||||||
|
if( ( false == castsshadow ) && ( submodel->eType < TP_ROTATOR ) ) {
|
||||||
|
auto const material { submodel->m_material >= 0 ? submodel->m_material : ( skins != nullptr ? skins[ -submodel->m_material ] : null_handle ) };
|
||||||
|
castsshadow = ( Material( material ).shadow_rank <= Global.gfx_shadow_rank_cutoff );
|
||||||
|
}
|
||||||
|
m_instance_lodpending.push_back( submodel->Child );
|
||||||
|
m_instance_lodpending.push_back( submodel->Next );
|
||||||
|
}
|
||||||
|
if( false == castsshadow ) { return; }
|
||||||
|
std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() );
|
||||||
|
m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() );
|
||||||
|
auto const bandcount { m_instance_lodbounds.size() + 1 };
|
||||||
|
|
||||||
|
// 1. Visibility / distance cull. Collect surviving instances with their precomputed
|
||||||
|
// camera-space root modelview matrices and lod bands. m_instance_survivors and the other
|
||||||
|
// scratch vectors are persistent members reused across every Render_Instanced() call:
|
||||||
|
// clear() drops the contents but keeps the allocated capacity, so after the first few
|
||||||
|
// frames this stops calling malloc/free entirely.
|
||||||
|
m_instance_survivors.clear();
|
||||||
|
m_instance_survivors.reserve( Instances.size() );
|
||||||
|
|
||||||
// Pull the current pass camera/view transform once. We use the current GL
|
// 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
|
// modelview matrix as the view matrix because at the point Render_Instanced
|
||||||
@@ -3039,11 +3074,14 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
|
|||||||
glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW );
|
glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW );
|
||||||
|
|
||||||
bool prepared_shared_state = false; // RaPrepare() runs once per bucket
|
bool prepared_shared_state = false; // RaPrepare() runs once per bucket
|
||||||
float closest_distancesquared = std::numeric_limits<float>::max();
|
|
||||||
material_data const *batch_material { nullptr };
|
material_data const *batch_material { nullptr };
|
||||||
|
float farthest_distancesquared { 0.f };
|
||||||
|
|
||||||
for( auto *Instance : Instances ) {
|
for( auto *Instance : Instances ) {
|
||||||
if( Instance == nullptr ) { continue; }
|
if( Instance == nullptr ) { continue; }
|
||||||
|
// 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 == Instance->m_visible ) { continue; }
|
||||||
if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; }
|
if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { continue; }
|
||||||
|
|
||||||
@@ -3076,78 +3114,103 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
|
|||||||
batch_material = Instance->Material();
|
batch_material = Instance->Material();
|
||||||
}
|
}
|
||||||
Instance->m_framestamp = m_framestamp;
|
Instance->m_framestamp = m_framestamp;
|
||||||
closest_distancesquared = std::min<float>( closest_distancesquared, static_cast<float>(distancesquared) );
|
|
||||||
|
|
||||||
// Build the camera-relative root modelview for this instance:
|
// Build the camera-relative root modelview for this instance:
|
||||||
// mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale)
|
// mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale)
|
||||||
// The scale is folded in here so the GPU-instanced path produces visually
|
// The rotation and scale part is cached by the instance, the translation goes into its last
|
||||||
// identical output to the regular per-instance path: each instance gets
|
// column. The scale is folded in so the GPU-instanced path produces visually identical output
|
||||||
// its own (translate × rotate × scale) baked into instance_modelview[i],
|
// to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local.
|
||||||
// which the shader applies via effective_modelview = instance_mv * model_local.
|
glm::mat4 root { Instance->rotation_scale() };
|
||||||
glm::dvec3 const offset = Instance->location() - m_renderpass.pass_camera.position();
|
root[ 3 ] = glm::vec4( glm::vec3( Instance->location() - m_renderpass.pass_camera.position() ), 1.f );
|
||||||
glm::mat4 mv = view_matrix;
|
|
||||||
mv = glm::translate( mv, glm::vec3( offset ) );
|
auto const distance { static_cast<float>( distancesquared ) };
|
||||||
auto const &angle = Instance->vAngle;
|
farthest_distancesquared = std::max( farthest_distancesquared, distance );
|
||||||
if( angle.y != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); }
|
m_instance_survivors.push_back( {
|
||||||
if( angle.x != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); }
|
view_matrix * root,
|
||||||
if( angle.z != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); }
|
distance,
|
||||||
auto const &scale = Instance->Scale();
|
static_cast<std::uint32_t>( std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), distance ) - m_instance_lodbounds.begin() ) } );
|
||||||
if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) {
|
|
||||||
mv = glm::scale( mv, scale );
|
|
||||||
}
|
|
||||||
m_instance_modelviews.emplace_back( mv );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if( m_instance_modelviews.empty() ) { return; }
|
if( m_instance_survivors.empty() ) { return; }
|
||||||
|
|
||||||
|
// group the modelviews by lod band into a contiguous array, for the UBO upload. every distance in
|
||||||
|
// a band selects the same lod; the nearest one represents the band.
|
||||||
|
// within a band the instances are additionally ordered roughly near-to-far, in distance slots. dense
|
||||||
|
// alpha tested foliage then gets the most out of the early depth test, as the nearest instances fill
|
||||||
|
// the depth buffer first and hide the fragments of those behind them before they get shaded.
|
||||||
|
// both levels are done with a single counting sort, linear in the instance count
|
||||||
|
auto const distanceslots { 16 };
|
||||||
|
auto const keycount { bandcount * distanceslots };
|
||||||
|
auto const slotscale { distanceslots / std::sqrt( std::max( farthest_distancesquared, 1.f ) ) };
|
||||||
|
m_instance_bandstarts.assign( keycount + 1, 0 );
|
||||||
|
m_instance_banddistances.assign( bandcount, std::numeric_limits<float>::max() );
|
||||||
|
for( auto &survivor : m_instance_survivors ) {
|
||||||
|
m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared );
|
||||||
|
auto const slot { std::min( distanceslots - 1, static_cast<int>( std::sqrt( survivor.distancesquared ) * slotscale ) ) };
|
||||||
|
survivor.band = static_cast<std::uint32_t>( survivor.band * distanceslots + slot ); // from now on it's the sort key
|
||||||
|
++m_instance_bandstarts[ survivor.band + 1 ];
|
||||||
|
}
|
||||||
|
for( std::size_t key = 1; key <= keycount; ++key ) {
|
||||||
|
m_instance_bandstarts[ key ] += m_instance_bandstarts[ key - 1 ];
|
||||||
|
}
|
||||||
|
m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 );
|
||||||
|
m_instance_modelviews.resize( m_instance_survivors.size() );
|
||||||
|
for( auto const &survivor : m_instance_survivors ) {
|
||||||
|
m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 );
|
||||||
|
alpha ^= 0x0F0F000F;
|
||||||
|
|
||||||
// 2. Walk the submodel tree once per sub-batch. The submodel-local matrix
|
// 2. Walk the submodel tree once per sub-batch. The submodel-local matrix
|
||||||
// stack starts at identity; the per-instance camera transform comes from
|
// stack starts at identity; the per-instance camera transform comes from
|
||||||
// instance_modelview[gl_InstanceID] in the shader.
|
// instance_modelview[gl_InstanceID] in the shader.
|
||||||
std::size_t const total = m_instance_modelviews.size();
|
for( std::size_t band = 0; band < bandcount; ++band ) {
|
||||||
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].
|
std::size_t offset_idx = m_instance_bandstarts[ band * distanceslots ];
|
||||||
instance_ubo->update(
|
std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ];
|
||||||
reinterpret_cast<uint8_t const *>( m_instance_modelviews.data() + offset_idx ),
|
while( offset_idx < band_end ) {
|
||||||
0,
|
std::size_t const this_batch = std::min<std::size_t>( band_end - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
|
||||||
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
|
|
||||||
|
|
||||||
// 2b. Push identity onto the matrix stack so submodel transforms
|
// 2a. Upload N modelviews to instance_ubo[0..N-1].
|
||||||
// accumulate from identity, not from camera/instance space.
|
instance_ubo->update(
|
||||||
::glPushMatrix();
|
reinterpret_cast<uint8_t const *>( m_instance_modelviews.data() + offset_idx ),
|
||||||
::glLoadIdentity();
|
0,
|
||||||
|
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
|
||||||
|
|
||||||
// 2c. Configure the existing TModel3d/TSubModel render path. Setting
|
// 2b. Push identity onto the matrix stack so submodel transforms
|
||||||
// m_current_instance_count routes draw(handle) calls to draw_instanced.
|
// accumulate from identity, not from camera/instance space.
|
||||||
m_current_instance_count = this_batch;
|
::glPushMatrix();
|
||||||
|
::glLoadIdentity();
|
||||||
|
|
||||||
Model->Root->fSquareDist = closest_distancesquared; // shared global, used by submodel LOD
|
// 2c. Configure the existing TModel3d/TSubModel render path. Setting
|
||||||
auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 );
|
// m_current_instance_count routes draw(handle) calls to draw_instanced.
|
||||||
alpha ^= 0x0F0F000F;
|
m_current_instance_count = this_batch;
|
||||||
Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha );
|
|
||||||
Model->Root->pRoot = Model;
|
|
||||||
|
|
||||||
Render( Model->Root );
|
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;
|
||||||
|
|
||||||
m_current_instance_count = 0;
|
Render( Model->Root );
|
||||||
|
|
||||||
::glPopMatrix();
|
m_current_instance_count = 0;
|
||||||
|
|
||||||
// 2d. Restore instance_modelview[0] to identity so subsequent
|
::glPopMatrix();
|
||||||
// non-instanced draws continue to compute identity * modelview.
|
|
||||||
{
|
// 2d. Restore instance_modelview[0] to identity so subsequent
|
||||||
glm::mat4 const identity( 1.0f );
|
// non-instanced draws continue to compute identity * modelview.
|
||||||
instance_ubo->update( reinterpret_cast<uint8_t const *>( &identity ), 0, sizeof( identity ) );
|
{
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset_idx += this_batch;
|
|
||||||
++m_renderpass.draw_stats.instanced_drawcalls;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_renderpass.draw_stats.instances += static_cast<int>( total );
|
m_renderpass.draw_stats.instances += static_cast<int>( m_instance_survivors.size() );
|
||||||
m_renderpass.draw_stats.models += static_cast<int>( total );
|
m_renderpass.draw_stats.models += static_cast<int>( m_instance_survivors.size() );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the
|
// Renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) using the
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ class opengl33_renderer : public gfx_renderer {
|
|||||||
// cell's buckets are merged here keyed by (TModel3d*, skins), so that
|
// cell's buckets are merged here keyed by (TModel3d*, skins), so that
|
||||||
// Render_Instanced() runs once per unique model across the whole pass instead
|
// 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
|
// of once per cell -- collapsing many tiny instanced draws into a few large
|
||||||
// batches. Reused every pass; cleared at the top of Render(scene::basic_region*).
|
// 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;
|
scene::basic_cell::instance_bucket_map m_frame_instance_buckets;
|
||||||
renderpass_config m_colorpass; // parametrs of most recent color pass
|
renderpass_config m_colorpass; // parametrs of most recent color pass
|
||||||
std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages
|
std::array<renderpass_config, 3> m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages
|
||||||
@@ -426,6 +426,21 @@ class opengl33_renderer : public gfx_renderer {
|
|||||||
// reused across calls -- clear() retains capacity, so once it has grown to
|
// reused across calls -- clear() retains capacity, so once it has grown to
|
||||||
// the largest batch seen, steady-state frames perform no allocation here.
|
// the largest batch seen, steady-state frames perform no allocation here.
|
||||||
std::vector<glm::mat4> m_instance_modelviews;
|
std::vector<glm::mat4> m_instance_modelviews;
|
||||||
|
// further persistent scratch buffers for Render_Instanced(): culled instances paired with their
|
||||||
|
// squared distance, and the lod bounds of the drawn model which split them into lod bands
|
||||||
|
struct instance_survivor {
|
||||||
|
glm::mat4 modelview;
|
||||||
|
float distancesquared;
|
||||||
|
std::uint32_t band;
|
||||||
|
};
|
||||||
|
std::vector<instance_survivor> m_instance_survivors;
|
||||||
|
std::vector<float> m_instance_lodbounds;
|
||||||
|
std::vector<TSubModel const *> m_instance_lodpending;
|
||||||
|
// per sort key (lod band and distance slot): first index in m_instance_modelviews (with end sentinel)
|
||||||
|
// and fill cursor; per lod band: nearest distance
|
||||||
|
std::vector<std::size_t> m_instance_bandstarts;
|
||||||
|
std::vector<std::size_t> m_instance_bandcursors;
|
||||||
|
std::vector<float> m_instance_banddistances;
|
||||||
gl::scene_ubs scene_ubs;
|
gl::scene_ubs scene_ubs;
|
||||||
gl::model_ubs model_ubs;
|
gl::model_ubs model_ubs;
|
||||||
gl::light_ubs light_ubs;
|
gl::light_ubs light_ubs;
|
||||||
|
|||||||
@@ -442,22 +442,18 @@ basic_cell::erase( TAnimModel *Instance ) {
|
|||||||
[=]( TAnimModel *instance ) {
|
[=]( TAnimModel *instance ) {
|
||||||
return instance == Instance; } ),
|
return instance == Instance; } ),
|
||||||
std::end( m_instancesopaque ) );
|
std::end( m_instancesopaque ) );
|
||||||
// also remove from the per-(pModel, skins) instance bucket if present
|
// also remove from the per-(pModel, skins) instance bucket if present.
|
||||||
if( Instance->m_instanceable && Instance->Model() != nullptr ) {
|
// NOTE: searched by pointer rather than by key: the instance may have lost its instanceable
|
||||||
instance_bucket_key key;
|
// status or changed its skins since it was inserted, which would leave a dangling pointer behind
|
||||||
key.pModel = Instance->Model();
|
for( auto bucket = m_instancebuckets_opaque.begin(); bucket != m_instancebuckets_opaque.end(); ) {
|
||||||
auto const *mat = Instance->Material();
|
bucket->second.erase(
|
||||||
if( mat != nullptr ) {
|
std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ),
|
||||||
for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; }
|
std::end( bucket->second ) );
|
||||||
|
if( bucket->second.empty() ) {
|
||||||
|
bucket = m_instancebuckets_opaque.erase( bucket );
|
||||||
}
|
}
|
||||||
auto bucket = m_instancebuckets_opaque.find( key );
|
else {
|
||||||
if( bucket != m_instancebuckets_opaque.end() ) {
|
++bucket;
|
||||||
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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user