mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-22 15:09:19 +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_LIGHTS " + std::to_string(MAX_LIGHTS) + "U\n" +
|
||||||
"#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" +
|
"#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" +
|
||||||
"#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "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(
|
R"STRING(
|
||||||
const uint LIGHT_SPOT = 0U;
|
const uint LIGHT_SPOT = 0U;
|
||||||
const uint LIGHT_POINT = 1U;
|
const uint LIGHT_POINT = 1U;
|
||||||
@@ -78,5 +79,31 @@ void gl::glsl_common_setup()
|
|||||||
vec4 wiper_timer_return;
|
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";
|
)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 sampler2DShadow;\n";
|
||||||
str += "precision highp sampler2DArrayShadow;\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 += "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);
|
str += read_file(basedir + filename);
|
||||||
@@ -293,6 +299,9 @@ void gl::program::init()
|
|||||||
|
|
||||||
if ((index = glGetUniformBlockIndex(*this, "light_ubo")) != GL_INVALID_INDEX)
|
if ((index = glGetUniformBlockIndex(*this, "light_ubo")) != GL_INVALID_INDEX)
|
||||||
glUniformBlockBinding(*this, index, 2);
|
glUniformBlockBinding(*this, index, 2);
|
||||||
|
|
||||||
|
if ((index = glGetUniformBlockIndex(*this, "instance_ubo")) != GL_INVALID_INDEX)
|
||||||
|
glUniformBlockBinding(*this, index, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
gl::program::program()
|
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");
|
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
|
struct light_element_ubs
|
||||||
{
|
{
|
||||||
enum type_e
|
enum type_e
|
||||||
|
|||||||
@@ -325,6 +325,13 @@ geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units con
|
|||||||
return draw_( Geometry, Units, Streams );
|
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
|
// frees subclass-specific resources associated with the bank, typically called when the bank wasn't in use for a period of time
|
||||||
void
|
void
|
||||||
geometry_bank::release() {
|
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 );
|
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
|
// provides direct access to index data of specfied chunk
|
||||||
gfx::index_array const &
|
gfx::index_array const &
|
||||||
geometrybank_manager::indices( gfx::geometry_handle const &Geometry ) 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;
|
auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool;
|
||||||
// draws geometry stored in specified chunk
|
// 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;
|
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
|
// draws geometry stored in supplied list of chunks
|
||||||
template <typename Iterator_>
|
template <typename Iterator_>
|
||||||
auto draw( Iterator_ First, Iterator_ Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) ->std::size_t {
|
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;
|
virtual void replace_( gfx::geometry_handle const &Geometry ) = 0;
|
||||||
// draw() subclass details
|
// draw() subclass details
|
||||||
virtual auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t = 0;
|
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
|
// resource release subclass details
|
||||||
virtual void release_() = 0;
|
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;
|
auto append( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool;
|
||||||
// draws geometry stored in specified chunk
|
// draws geometry stored in specified chunk
|
||||||
void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams );
|
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_>
|
template <typename Iterator_>
|
||||||
void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) {
|
void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) {
|
||||||
while( First != Last ) {
|
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
|
// release () subclass details
|
||||||
void
|
void
|
||||||
opengl33_vaogeometrybank::release_() {
|
opengl33_vaogeometrybank::release_() {
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ private:
|
|||||||
// draw() subclass details
|
// draw() subclass details
|
||||||
auto
|
auto
|
||||||
draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override;
|
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
|
// release() subclass details
|
||||||
void
|
void
|
||||||
release_() override;
|
release_() override;
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ bool opengl33_renderer::Init(GLFWwindow *Window)
|
|||||||
scene_ubo = std::make_unique<gl::ubo>(sizeof(gl::scene_ubs), 0);
|
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);
|
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);
|
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
|
// better initialize with 0 to not crash driver/whole system
|
||||||
// when we forget
|
// when we forget
|
||||||
@@ -153,6 +154,14 @@ bool opengl33_renderer::Init(GLFWwindow *Window)
|
|||||||
model_ubo->update(model_ubs);
|
model_ubo->update(model_ubs);
|
||||||
scene_ubo->update(scene_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;
|
int samples = 1 << Global.iMultisampling;
|
||||||
if (!Global.gfx_usegles && samples > 1)
|
if (!Global.gfx_usegles && samples > 1)
|
||||||
glEnable(GL_MULTISAMPLE);
|
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?
|
// TBD, TODO: refactor in to a method to reuse in branch below?
|
||||||
// opaque parts of instanced models -- batched path first
|
// opaque parts of instanced models -- batched path first
|
||||||
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
|
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
|
// remaining (non-instanceable) opaque instance nodes go through the per-node path
|
||||||
for (auto *instance : cell->m_instancesopaque)
|
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 ) {
|
if( Global.reflectiontune.fidelity >= 1 ) {
|
||||||
// opaque parts of instanced models -- batched path first
|
// opaque parts of instanced models -- batched path first
|
||||||
for( auto const &bucket : cell->m_instancebuckets_opaque ) {
|
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 ) {
|
for( auto *instance : cell->m_instancesopaque ) {
|
||||||
if( instance->m_instanceable ) { continue; }
|
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_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW));
|
||||||
model_ubo->update(model_ubs);
|
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)
|
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.
|
// True GPU-instanced render path for a group of TAnimModel instances sharing the same
|
||||||
// Per-instance state (location, vAngle, distance/visibility cull) still applies; what's
|
// TModel3d. The submodel tree is walked ONCE per batch; at every submodel that draws
|
||||||
// amortised is the lack of any per-instance animation/light prep. The TModel3d submodel
|
// geometry we issue a single glDrawElementsInstancedBaseVertex(N) covering all visible
|
||||||
// chain has no animation flags (verified by TAnimModel::update_instanceable_flag), so
|
// instances. The vertex shader multiplies effective_modelview = instance_modelview[gl_InstanceID]
|
||||||
// no static-mutable submodel state is touched across instances.
|
// * 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 )
|
void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimModel *> const &Instances )
|
||||||
{
|
{
|
||||||
if( Model == nullptr ) { return; }
|
if( Model == nullptr ) { return; }
|
||||||
if( Instances.empty() ) { return; }
|
if( Instances.empty() ) { return; }
|
||||||
|
|
||||||
int rendered_count = 0;
|
// 1. Visibility / distance cull. Build parallel arrays of surviving
|
||||||
bool prepared_shared_state = false; // RaPrepare()'s side-effects on shared seasonal-variant
|
// instances and their precomputed camera-space root modelview matrices.
|
||||||
// submodels only need to happen once per pModel per frame
|
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 ) {
|
for( auto *Instance : Instances ) {
|
||||||
if( Instance == nullptr ) { continue; }
|
if( Instance == nullptr ) { continue; }
|
||||||
@@ -2931,9 +2962,7 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
|
|||||||
break;
|
break;
|
||||||
case rendermode::reflections:
|
case rendermode::reflections:
|
||||||
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) );
|
distancesquared = glm::length2( ( Instance->location() - m_renderpass.pass_camera.position() ) );
|
||||||
if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) {
|
if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET );
|
distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET );
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -2949,34 +2978,76 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector<TAnimMode
|
|||||||
auto const radiussquared { Instance->radius() * Instance->radius() };
|
auto const radiussquared { Instance->radius() * Instance->radius() };
|
||||||
if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; }
|
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 ) {
|
if( !prepared_shared_state ) {
|
||||||
Instance->RaPrepare();
|
Instance->RaPrepare();
|
||||||
prepared_shared_state = true;
|
prepared_shared_state = true;
|
||||||
|
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) );
|
||||||
|
|
||||||
// per-instance modelview is pushed via the standard matrix stack so the
|
// Build the camera-relative root modelview for this instance:
|
||||||
// existing shader path picks it up through model_ubs.modelview without
|
// mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles)
|
||||||
// any shader changes. State sharing across the batch is implicit: the
|
glm::dvec3 const offset = Instance->location() - m_renderpass.pass_camera.position();
|
||||||
// material binding, shader binding, VAO binding and uniform layouts done
|
glm::mat4 mv = view_matrix;
|
||||||
// inside Render(TSubModel*) all hit the OpenGL driver's hot cache because
|
mv = glm::translate( mv, glm::vec3( offset ) );
|
||||||
// every iteration submits the same draw structure with only the modelview
|
auto const &angle = Instance->vAngle;
|
||||||
// differing. That alone halves the CPU cost in driver validation paths.
|
if( angle.y != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.y ), glm::vec3( 0.f, 1.f, 0.f ) ); }
|
||||||
Render( Model, Instance->Material(), distancesquared,
|
if( angle.x != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.x ), glm::vec3( 1.f, 0.f, 0.f ) ); }
|
||||||
Instance->location() - m_renderpass.pass_camera.position(),
|
if( angle.z != 0.0f ) { mv = glm::rotate( mv, glm::radians( angle.z ), glm::vec3( 0.f, 0.f, 1.f ) ); }
|
||||||
Instance->vAngle );
|
instance_modelviews.emplace_back( mv );
|
||||||
++rendered_count;
|
|
||||||
++m_renderpass.draw_stats.models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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)
|
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> scene_ubo;
|
||||||
std::unique_ptr<gl::ubo> model_ubo;
|
std::unique_ptr<gl::ubo> model_ubo;
|
||||||
std::unique_ptr<gl::ubo> light_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::scene_ubs scene_ubs;
|
||||||
gl::model_ubs model_ubs;
|
gl::model_ubs model_ubs;
|
||||||
gl::light_ubs light_ubs;
|
gl::light_ubs light_ubs;
|
||||||
|
|||||||
@@ -374,12 +374,20 @@ basic_cell::insert( TAnimModel *Instance ) {
|
|||||||
if( alpha & flags & 0x1F1F001F ) {
|
if( alpha & flags & 0x1F1F001F ) {
|
||||||
// opaque pieces
|
// opaque pieces
|
||||||
m_instancesopaque.emplace_back( Instance );
|
m_instancesopaque.emplace_back( Instance );
|
||||||
// additionally route instanceable nodes into a per-pModel bucket so the
|
// additionally route instanceable nodes into a per-(pModel, skins) bucket so
|
||||||
// renderer can amortise material/state setup across all instances of the
|
// the renderer can amortise material/state setup across many instances of the
|
||||||
// same TModel3d. The flat list is kept too for fallback paths
|
// same TModel3d AND issue a single GPU-instanced draw call per submodel.
|
||||||
// (picking, reflections at low fidelity, etc.).
|
// 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 ) {
|
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
|
// 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 ) {
|
[=]( TAnimModel *instance ) {
|
||||||
return instance == Instance; } ),
|
return instance == Instance; } ),
|
||||||
std::end( m_instancesopaque ) );
|
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 ) {
|
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() ) {
|
if( bucket != m_instancebuckets_opaque.end() ) {
|
||||||
bucket->second.erase(
|
bucket->second.erase(
|
||||||
std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ),
|
std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ),
|
||||||
|
|||||||
@@ -180,7 +180,30 @@ public:
|
|||||||
using linesnode_sequence = std::vector<lines_node>;
|
using linesnode_sequence = std::vector<lines_node>;
|
||||||
using traction_sequence = std::vector<TTraction *>;
|
using traction_sequence = std::vector<TTraction *>;
|
||||||
using instance_sequence = std::vector<TAnimModel *>;
|
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 sound_sequence = std::vector<sound_source *>;
|
||||||
using eventlauncher_sequence = std::vector<TEventLauncher *>;
|
using eventlauncher_sequence = std::vector<TEventLauncher *>;
|
||||||
using memorycell_sequence = std::vector<TMemCell *>;
|
using memorycell_sequence = std::vector<TMemCell *>;
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ out vec2 f_coord;
|
|||||||
|
|
||||||
void main()
|
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;
|
f_coord = v_coord;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,20 +23,23 @@ out vec4 UserData;
|
|||||||
|
|
||||||
void main()
|
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_normal_raw = v_normal;
|
||||||
f_coord = v_coord;
|
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) {
|
for (uint idx = 0U ; idx < MAX_CASCADES ; ++idx) {
|
||||||
f_light_pos[idx] = lightview[idx] * f_pos;
|
f_light_pos[idx] = lightview[idx] * f_pos;
|
||||||
}
|
}
|
||||||
f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0);
|
f_clip_pos = projection * f_pos;
|
||||||
f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0);
|
f_clip_future_pos = (projection * future) * f_pos;
|
||||||
|
|
||||||
gl_Position = f_clip_pos;
|
gl_Position = f_clip_pos;
|
||||||
gl_PointSize = param[1].x;
|
gl_PointSize = param[1].x;
|
||||||
|
|
||||||
vec3 T = normalize(modelviewnormal * v_tangent.xyz);
|
vec3 T = normalize(mvn * v_tangent.xyz);
|
||||||
vec3 N = f_normal;
|
vec3 N = f_normal;
|
||||||
vec3 B = normalize(cross(N, T));
|
vec3 B = normalize(cross(N, T));
|
||||||
f_tbn = mat3(T, B, N);
|
f_tbn = mat3(T, B, N);
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ layout(location = 0) in vec3 v_vert;
|
|||||||
|
|
||||||
void main()
|
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