diff --git a/CMakeLists.txt b/CMakeLists.txt index 8806a7dc..28436635 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,7 @@ option(WITH_BETTER_RENDERER "Experimental multi-backend renderer based on NVRHI" option(GENERATE_PDB "Generate executable with program debugging symbols" ON) option(ENABLE_MCC "Enable multicore compilation" ON) option(WITHDUMPGEN "Enable generating DMP files on crash" ON) +option(ENABLE_AVX2 "Enable AVX2 instruction set (requires AVX2-capable CPU, Haswell/2013 or newer)" ON) set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) @@ -508,7 +509,6 @@ else() target_compile_definitions(${PROJECT_NAME} PUBLIC WITH_DISCORD_RPC=0) endif() - find_package(OpenAL REQUIRED) if (TARGET OpenAL::OpenAL) target_link_libraries(${PROJECT_NAME} OpenAL::OpenAL) @@ -582,4 +582,13 @@ endif() if(ENABLE_MCC AND MSVC) message(STATUS "Enabling multi-processor compilation for MSVC.") add_compile_options(/MP) +endif() + +if(ENABLE_AVX2) + message(STATUS "Enabling AVX2 instruction set.") + if (MSVC) + target_compile_options(${PROJECT_NAME} PRIVATE /arch:AVX2) + else() + target_compile_options(${PROJECT_NAME} PRIVATE -mavx2) + endif() endif() \ No newline at end of file diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 25028a8d..2afcaa36 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -20,7 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editoruilayer.h" #include "rendering/renderer.h" -void itemproperties_panel::update(scene::basic_node const *Node) +void itemproperties_panel::update(scene::basic_node *Node) { m_node = Node; @@ -290,12 +290,66 @@ void itemproperties_panel::render() { ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); } + // transform editor (position/rotation/scale) — TAnimModel only + render_transform_editor(); // group section render_group(); } ImGui::End(); } +// In-place editor for position (double precision), rotation (degrees, 0-360), +// and uniform scale (per-axis float, 1.000) of a picked TAnimModel. +// Other node subclasses don't expose these knobs through the same API, so the +// editor short-circuits when the bound node isn't a TAnimModel. +void itemproperties_panel::render_transform_editor() +{ + if (m_node == nullptr) { return; } + if (typeid(*m_node) != typeid(TAnimModel)) { return; } + auto *picked = static_cast(m_node); + + if (false == ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) + { + return; + } + + // Position — full double precision via DragScalarN. World coordinates can grow + // large; %.6f gives sub-millimetre resolution at typical scenario distances. + { + glm::dvec3 location = picked->location(); + double pos[3] = { location.x, location.y, location.z }; + if (ImGui::DragScalarN("position", ImGuiDataType_Double, pos, 3, 0.05f, nullptr, nullptr, "%.6f")) + { + picked->location(glm::dvec3(pos[0], pos[1], pos[2])); + } + } + + // Rotation — wrapped into [0,360) for display; slider clamps drags to that range. + { + glm::vec3 angles{ + clamp_circular(picked->Angles().x), + clamp_circular(picked->Angles().y), + clamp_circular(picked->Angles().z)}; + if (ImGui::DragFloat3("rotation (deg)", &angles.x, 0.5f, 0.0f, 360.0f, "%.3f")) + { + picked->Angles(angles); + } + } + + // Scale — per-axis float, 1.000 display. Clamped to a reasonable positive range. + { + glm::vec3 scale = picked->Scale(); + if (ImGui::DragFloat3("scale (x,y,z)", &scale.x, 0.01f, 0.001f, 100.0f, "%.3f")) + { + picked->Scale(scale); + } + } + + if (ImGui::Button("reset rotation")) { picked->Angles(glm::vec3(0.0f)); } + ImGui::SameLine(); + if (ImGui::Button("reset scale")) { picked->Scale(glm::vec3(1.0f)); } +} + bool itemproperties_panel::render_group() { diff --git a/application/editoruipanels.h b/application/editoruipanels.h index c90f3eda..41da7173 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -32,16 +32,22 @@ class itemproperties_panel : public ui_panel public: itemproperties_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {} - void update(scene::basic_node const *Node); + // non-const node pointer so the transform editor in render() can mutate + // position/rotation/scale of TAnimModel nodes in place. Other node types + // are still treated as read-only. + void update(scene::basic_node *Node); void render() override; private: // methods void update_group(); bool render_group(); + // renders DragFloat3/DragScalarN widgets for position, rotation and scale + // of the currently bound TAnimModel; no-op for other node subclasses. + void render_transform_editor(); // members - scene::basic_node const *m_node{nullptr}; // scene node bound to the panel + scene::basic_node *m_node{nullptr}; // scene node bound to the panel scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel std::string m_groupprefix; std::vector m_grouplines; diff --git a/betterRenderer/CMakeLists.txt b/betterRenderer/CMakeLists.txt index 1d60abd2..803ab1cd 100644 --- a/betterRenderer/CMakeLists.txt +++ b/betterRenderer/CMakeLists.txt @@ -67,7 +67,11 @@ target_include_directories(${LIBMANUL_NAME} PRIVATE target_compile_definitions(${LIBMANUL_NAME} PRIVATE GLM_ENABLE_EXPERIMENTAL - GLM_FORCE_SWIZZLE) + GLM_FORCE_SWIZZLE + GLM_FORCE_CTOR_INIT + GLM_FORCE_INLINE + GLM_FORCE_DEFAULT_ALIGNED_GENTYPES + GLM_FORCE_INTRINSICS) target_compile_definitions(${LIBMANUL_NAME} PRIVATE _USE_MATH_DEFINES) @@ -99,12 +103,13 @@ else () target_compile_definitions(${LIBMANUL_NAME} PUBLIC "LIBMANUL_WITH_VULKAN=0") endif () -## For double-precision vector goodness -#if (WIN32) -# target_compile_options(${LIBMANUL_NAME} PRIVATE "/arch:AVX2") -#elseif (UNIX) -# target_compile_options(${LIBMANUL_NAME} PRIVATE "-mavx2") -#endif () +if(ENABLE_AVX2) + if (WIN32) + target_compile_options(${LIBMANUL_NAME} PRIVATE "/arch:AVX2") + elseif (UNIX) + target_compile_options(${LIBMANUL_NAME} PRIVATE "-mavx2") + endif () +endif() target_sources(${LIBMANUL_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/eu07_source/register.cpp") set_target_properties(nvrhi yaml-cpp fmt EnTT glfw yaml-cpp-parse yaml-cpp-read yaml-cpp-sandbox PROPERTIES FOLDER "libraries") diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 2ca344f4..26c31000 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -14,6 +14,7 @@ void gl::glsl_common_setup() "#define MAX_LIGHTS " + std::to_string(MAX_LIGHTS) + "U\n" + "#define MAX_CASCADES " + std::to_string(MAX_CASCADES) + "U\n" + "#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" + + "#define MAX_INSTANCES_PER_BATCH " + std::to_string(MAX_INSTANCES_PER_BATCH) + "U\n" + R"STRING( const uint LIGHT_SPOT = 0U; const uint LIGHT_POINT = 1U; @@ -78,5 +79,36 @@ void gl::glsl_common_setup() vec4 wiper_timer_return; }; + // Per-instance modelview matrices for GPU-instanced draws. + // The UBO itself is declared in every stage so the binding stays valid, + // but the helper functions reference gl_InstanceID which is vertex-only, + // so they are guarded behind STAGE_VERTEX. + layout (std140) uniform instance_ubo + { + mat4 instance_modelview[MAX_INSTANCES_PER_BATCH]; + }; + + #ifdef STAGE_VERTEX + // For non-instanced draws gl_InstanceID == 0 and slot 0 is permanently set + // to identity, so effective_modelview() reduces to model_ubo.modelview. + // For instanced draws the C++ side uploads camera-space root transforms for + // instances 0..N-1 and sets model_ubo.modelview to the submodel-local chain + // (computed from identity, NOT from the camera/instance), so the product + // gives the correct final transform per instance per submodel. + mat4 effective_modelview() { + return instance_modelview[gl_InstanceID] * modelview; + } + mat3 effective_modelviewnormal() { + // instance_modelview can include per-instance scale (uniform or per-axis), + // so its upper-3x3 is NOT its own inverse-transpose in the non-uniform + // case. Compute the correct normal matrix via transpose(inverse(...)). + // For pure rotation+translation this still produces the rotation matrix + // (one extra mat3 inverse per vertex — modern GPUs handle it cheaply). + mat3 instance_mv3 = mat3(instance_modelview[gl_InstanceID]); + mat3 instance_normal = transpose(inverse(instance_mv3)); + return instance_normal * modelviewnormal; + } + #endif + )STRING"; } diff --git a/gl/shader.cpp b/gl/shader.cpp index edc52a54..248b50f1 100644 --- a/gl/shader.cpp +++ b/gl/shader.cpp @@ -106,6 +106,12 @@ std::pair gl::shader::process_source(const std::string &fil str += "precision highp sampler2DShadow;\n"; str += "precision highp sampler2DArrayShadow;\n"; } + // expose the shader stage to glsl_common so vertex-only built-ins + // (gl_InstanceID, effective_modelview, etc.) can be guarded out of + // fragment/geometry compilations. + if (type == GL_VERTEX_SHADER) str += "#define STAGE_VERTEX 1\n"; + else if (type == GL_FRAGMENT_SHADER) str += "#define STAGE_FRAGMENT 1\n"; + else if (type == GL_GEOMETRY_SHADER) str += "#define STAGE_GEOMETRY 1\n"; str += "vec4 FBOUT(vec4 x) { return " + (Global.gfx_shadergamma ? std::string("vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a)") : std::string("x")) + "; }\n"; str += read_file(basedir + filename); @@ -293,6 +299,9 @@ void gl::program::init() if ((index = glGetUniformBlockIndex(*this, "light_ubo")) != GL_INVALID_INDEX) glUniformBlockBinding(*this, index, 2); + + if ((index = glGetUniformBlockIndex(*this, "instance_ubo")) != GL_INVALID_INDEX) + glUniformBlockBinding(*this, index, 3); } gl::program::program() diff --git a/gl/ubo.h b/gl/ubo.h index 8bac33b7..b22b17e1 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -3,9 +3,18 @@ #include "object.h" #include "bindable.h" #include "buffer.h" +#include #define UBS_PAD(x) uint8_t PAD[x] +// 12-byte vec3 for structs that must match an exact binary layout (UBOs, vertex data). +// Stays packed regardless of GLM_FORCE_DEFAULT_ALIGNED_GENTYPES. +// Guard prevents redefinition when stdafx.h also defines it. +#ifndef PACKED_VEC3_DEFINED +#define PACKED_VEC3_DEFINED +using packed_vec3 = glm::vec<3, float, glm::packed_highp>; +#endif + namespace gl { class ubo : public buffer @@ -43,7 +52,7 @@ namespace gl glm::mat4 projection; glm::mat4 inv_view; glm::mat4 lightview[MAX_CASCADES]; - glm::vec3 cascade_end; + packed_vec3 cascade_end; float time; glm::vec4 rain_params; glm::vec4 wiper_pos; @@ -78,6 +87,17 @@ namespace gl static_assert(sizeof(model_ubs) == 208 + 16 * MAX_PARAMS, "bad size of ubs"); + // maximum number of instances per single GPU-instanced draw call. + // 256 mat4 = 16 KiB, the guaranteed minimum UBO size. Bigger batches must split. + const size_t MAX_INSTANCES_PER_BATCH = 256; + + struct instance_ubs + { + glm::mat4 instance_modelview[MAX_INSTANCES_PER_BATCH]; + }; + + static_assert(sizeof(instance_ubs) == 64 * MAX_INSTANCES_PER_BATCH, "bad size of instance_ubs"); + struct light_element_ubs { enum type_e @@ -88,13 +108,13 @@ namespace gl HEADLIGHTS }; - glm::vec3 pos; + packed_vec3 pos; type_e type; - glm::vec3 dir; + packed_vec3 dir; float in_cutoff; - glm::vec3 color; + packed_vec3 color; float out_cutoff; float linear; @@ -113,10 +133,10 @@ namespace gl struct light_ubs { - glm::vec3 ambient; + packed_vec3 ambient; UBS_PAD(4); - glm::vec3 fog_color; + packed_vec3 fog_color; uint32_t lights_count; light_element_ubs lights[MAX_LIGHTS]; diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index f6c95345..b7c140d0 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -273,6 +273,7 @@ TAnimModel::is_keyword( std::string const &Token ) const { || ( Token == "lights" ) || ( Token == "lightcolors" ) || ( Token == "angles" ) + || ( Token == "scale" ) || ( Token == "notransition" ); } @@ -369,6 +370,23 @@ bool TAnimModel::Load(cParser *parser, bool ter) >> vAngle[ 2 ]; } + if( token == "scale" ) { + // Per-node scale: `scale ` (always three tokens, mirroring + // the `angles` syntax). For uniform scaling, write the same value + // three times (e.g. `scale 2 2 2`). Combines multiplicatively with + // any active scale block from the scenariostateserializer (which is + // applied at deserialize_model time before Load() is called, so + // m_scale already reflects the outer block when we arrive here). + parser->getTokens( 3 ); + glm::vec3 factor; + *parser >> factor.x >> factor.y >> factor.z; + if( factor.x > 0.0f && factor.y > 0.0f && factor.z > 0.0f ) { + m_scale.x *= factor.x; + m_scale.y *= factor.y; + m_scale.z *= factor.z; + } + } + if( token == "notransition" ) { m_transition = false; } @@ -376,9 +394,80 @@ 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. +// at_Undefined is the type assigned to .t3d submodels declared with `anim: true` +// (a generic "this submodel is animatable" hint) and to anything else that +// doesn't match a recognised animation keyword — these are driven by event- +// triggered SetRotate/SetTranslate at runtime, which would silently break if +// the model were batched. +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 + case TAnimType::at_Undefined: // `anim: true` / unknown — driven by events + 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, OR carries the runtime "needs +// animation matrix" flag (iFlags bit 0x4000), which is set whenever the +// submodel was tagged as animatable in the .t3d file or had WillBeAnimated() +// called on it during model load. Either signal means the submodel may receive +// per-instance event-driven animation commands at runtime, which the GPU- +// instanced path (one shared submodel tree across all instances) cannot serve. +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( ( Sub->iFlags & 0x4000 ) != 0 ) { 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 TAnimModel::AddContainer(std::string const &Name) { // dodanie sterowania submodelem dla egzemplarza if (!pModel) @@ -616,20 +705,21 @@ void TAnimModel::AnimUpdate(double dt) }); } -// radius() subclass details, calculates node's bounding radius +// radius() subclass details, calculates node's bounding radius. +// For non-uniform scale we use the largest axis factor so the bounding sphere +// fully contains the scaled model — undersizing would cause incorrect culling. float TAnimModel::radius_() { - return ( - pModel ? - pModel->bounding_radius() : - 0.f ); + if( pModel == nullptr ) { return 0.f; } + float const max_scale = std::max( { m_scale.x, m_scale.y, m_scale.z } ); + return pModel->bounding_radius() * max_scale; } // serialize() subclass details, sends content of the subclass to provided stream void TAnimModel::serialize_( std::ostream &Output ) const { - + // TODO: implement } // deserialize() subclass details, restores content of the subclass from provided stream @@ -639,18 +729,29 @@ TAnimModel::deserialize_( std::istream &Input ) { // TODO: implement } -// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream. +// Smart export: omit fields that match defaults so reloaded scenarios stay clean. +// - if X and Z rotation are zero, fold Y rotation into the 4th token slot +// (the legacy `node ... model X Y Z ...` format) and skip the +// `angles` block entirely +// - if any axis of rotation needs all three components, emit `angles X Y Z` +// - emit `scale X Y Z` only when m_scale isn't (1,1,1) void TAnimModel::export_as_text_( std::ostream &Output ) const { + // header Output << "model "; - // location and rotation - Output << std::fixed << std::setprecision(3) // ustawienie dokładnie 3 cyfr po przecinku - << location().x << ' ' - << location().y << ' ' - << location().z << ' '; - Output - << "0 " ; + + // location and rotation. The 4th token after location is a legacy + // shorthand for the Y rotation. We use it (and skip the angles block) + // whenever the rotation is purely around Y, which is the common case. + bool const xz_rotation_zero = ( vAngle.x == 0.0f && vAngle.z == 0.0f ); + Output << std::fixed << std::setprecision( 3 ) + << location().x << ' ' + << location().y << ' ' + << location().z << ' ' + << ( xz_rotation_zero ? vAngle.y : 0.0f ) << ' '; + // 3d shape auto modelfile { ( pModel ? @@ -687,11 +788,22 @@ TAnimModel::export_as_text_( std::ostream &Output ) const { if( false == m_transition ) { Output << "notransition" << ' '; } - // footer - Output << "angles " - << vAngle.x << ' ' - << vAngle.y << ' ' - << vAngle.z << ' '; + // angles directive only when X or Z are rotated — otherwise the Y angle + // already lives in the 4th token slot above. + if( false == xz_rotation_zero ) { + Output << "angles " + << vAngle.x << ' ' + << vAngle.y << ' ' + << vAngle.z << ' '; + } + // scale directive only when actually scaled — keeps default-scale models + // from being polluted with redundant `scale 1 1 1` entries on every save. + if( m_scale.x != 1.0f || m_scale.y != 1.0f || m_scale.z != 1.0f ) { + Output << "scale " + << m_scale.x << ' ' + << m_scale.y << ' ' + << m_scale.z << ' '; + } // footer Output << "endmodel" diff --git a/model/AnimModel.h b/model/AnimModel.h index 2d470e67..13f15a90 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -143,6 +143,26 @@ public: glm::vec3 Angles() const { return vAngle; } + // per-axis scale, applied between rotation and the submodel-local transform chain. + // (1,1,1) = unchanged. Set by the `scale`/`endscale` scenario directives or the + // optional `scale ` / `scale ` token inside a `node model` block. + // Per-axis values let you stretch a model along a single dimension; uniform input + // (single float) broadcasts to all three axes. + inline + void + Scale( glm::vec3 const &Factor ) { + m_scale = glm::vec3( + Factor.x > 0.0f ? Factor.x : 1.0f, + Factor.y > 0.0f ? Factor.y : 1.0f, + Factor.z > 0.0f ? Factor.z : 1.0f ); } + inline + void + Scale( float const Factor ) { + Scale( glm::vec3( Factor ) ); } + inline + glm::vec3 const & + Scale() const { + return m_scale; } // members std::list> m_animlist; @@ -169,6 +189,7 @@ public: std::shared_ptr pRoot; // pojemniki sterujące, tylko dla aniomowanych submodeli TModel3d *pModel { nullptr }; 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) material_data m_materialdata; std::string asText; // tekst dla wyświetlacza znakowego @@ -189,6 +210,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; }; diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index 137dfca2..fcd731dd 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -325,6 +325,13 @@ geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units con return draw_( Geometry, Units, Streams ); } +// draws geometry stored in specified chunk InstanceCount times via instanced draw call +std::size_t +geometry_bank::draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) { + if( InstanceCount == 0 ) { return 0; } + return draw_instanced_( Geometry, Units, InstanceCount, Streams ); +} + // frees subclass-specific resources associated with the bank, typically called when the bank wasn't in use for a period of time void geometry_bank::release() { @@ -416,6 +423,19 @@ geometrybank_manager::draw( gfx::geometry_handle const &Geometry, unsigned int c m_primitivecount += bankrecord.first->draw( Geometry, m_units, Streams ); } +// draws geometry stored in specified chunk InstanceCount times via instanced draw call +void +geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams ) { + + if( Geometry == null_handle ) { return; } + if( InstanceCount == 0 ) { return; } + + auto &bankrecord = bank( Geometry ); + + bankrecord.second = m_garbagecollector.timestamp(); + m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, Streams ); +} + // provides direct access to index data of specfied chunk gfx::index_array const & geometrybank_manager::indices( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 9828d578..39735e00 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -119,6 +119,8 @@ public: auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool; // draws geometry stored in specified chunk auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; + // draws geometry stored in specified chunk N times via glDrawElementsInstanced* + auto draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in supplied list of chunks template auto draw( Iterator_ First, Iterator_ Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) ->std::size_t { @@ -180,6 +182,11 @@ private: virtual void replace_( gfx::geometry_handle const &Geometry ) = 0; // draw() subclass details virtual auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t = 0; + // draw_instanced() subclass details. Default implementation falls back to N regular draws. + virtual auto draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t { + std::size_t count { 0 }; + for( std::size_t i = 0; i < InstanceCount; ++i ) { count += draw_( Geometry, Units, Streams ); } + return count; } // resource release subclass details virtual void release_() = 0; }; @@ -208,6 +215,9 @@ public: auto append( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; // draws geometry stored in specified chunk void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams ); + // draws geometry stored in specified chunk InstanceCount times via GPU instancing. + // The shader reads per-instance modelview matrices from instance_ubo[gl_InstanceID]. + void draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ); template void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) { while( First != Last ) { diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 28ad6b1b..91c9a8dd 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -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(m_vertex_count * sizeof(gfx::basic_vertex) + chunkrecord.vertex_offset * sizeof(gfx::vertex_userdata)), + static_cast(chunkrecord.vertex_count * sizeof(gfx::vertex_userdata)) ); + } + chunkrecord.is_good = true; + } + + GLsizei const inst_count = static_cast(InstanceCount); + + if( chunkrecord.index_count > 0 ) { + m_vao->bind(); + ::glDrawElementsInstancedBaseVertex( + chunk.type, + chunkrecord.index_count, GL_UNSIGNED_INT, + reinterpret_cast( chunkrecord.index_offset * sizeof( gfx::basic_index ) ), + inst_count, + chunkrecord.vertex_offset ); + } + else { + m_vao->bind(); + ::glDrawArraysInstanced( chunk.type, chunkrecord.vertex_offset, chunkrecord.vertex_count, inst_count ); + } + + auto const vertexcount { ( chunkrecord.index_count > 0 ? chunkrecord.index_count : chunkrecord.vertex_count ) }; + std::size_t prims = 0; + switch( chunk.type ) { + case GL_TRIANGLES: prims = vertexcount / 3; break; + case GL_TRIANGLE_STRIP: prims = ( vertexcount > 2 ? vertexcount - 2 : 0 ); break; + default: prims = 0; break; + } + return prims * InstanceCount; +} + // release () subclass details void opengl33_vaogeometrybank::release_() { diff --git a/rendering/opengl33geometrybank.h b/rendering/opengl33geometrybank.h index 7dd54ba8..f5cd7e37 100644 --- a/rendering/opengl33geometrybank.h +++ b/rendering/opengl33geometrybank.h @@ -53,6 +53,9 @@ private: // draw() subclass details auto draw_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override; + // draw_instanced() subclass details — issues glDrawElementsInstancedBaseVertex + auto + draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t override; // release() subclass details void release_() override; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 43fef6b4..bf46f3b7 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -142,6 +142,7 @@ bool opengl33_renderer::Init(GLFWwindow *Window) scene_ubo = std::make_unique(sizeof(gl::scene_ubs), 0); model_ubo = std::make_unique(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW); light_ubo = std::make_unique(sizeof(gl::light_ubs), 2); + instance_ubo = std::make_unique(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(&identity), 0, sizeof(identity) ); + } + int samples = 1 << Global.iMultisampling; if (!Global.gfx_usegles && samples > 1) glEnable(GL_MULTISAMPLE); @@ -653,6 +662,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 ) @@ -690,6 +709,7 @@ void opengl33_renderer::draw_debug_ui() ImGui::Image(reinterpret_cast(m_pick_tex->id), ImGui::GetContentRegionAvail(), ImVec2(0, 1.0), ImVec2(1.0, 0)); } ImGui::End(); + } // runs jobs needed to generate graphics for specified render pass @@ -2659,9 +2679,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.pModel, 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 +2702,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.pModel, bucket.second ); + } for( auto *instance : cell->m_instancesopaque ) { + if( instance->m_instanceable ) { continue; } Render( instance ); } } @@ -2695,7 +2724,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); @@ -2730,7 +2761,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::iterator it, std::vector::iterator end) @@ -2877,13 +2915,174 @@ void opengl33_renderer::Render(TAnimModel *Instance) Instance->RaPrepare(); if (Instance->pModel) { - // renderowanie rekurencyjne submodeli - Render(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); + // per-instance uniform scale: applied between the rotation and the + // submodel-local transform chain. Pushing it on the matrix stack here + // means Render(TModel3d, ..., position, angle) doesn't need a new + // signature and the same scale flows naturally into Render_Alpha and + // any other recursive callers. + auto const &scale = Instance->Scale(); + bool const scaled = ( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ); + if( scaled ) { + // Per-axis scale applied between rotation and the submodel-local + // chain: build the per-instance transform here so we can inject + // glScalef before the submodel walk. + ::glPushMatrix(); + auto const Position = Instance->location() - m_renderpass.pass_camera.position(); + auto const Angle = Instance->vAngle; + ::glTranslated( Position.x, Position.y, Position.z ); + if( Angle.y != 0.0 ) ::glRotated( Angle.y, 0.f, 1.f, 0.f ); + if( Angle.x != 0.0 ) ::glRotated( Angle.x, 1.f, 0.f, 0.f ); + if( Angle.z != 0.0 ) ::glRotated( Angle.z, 0.f, 0.f, 1.f ); + ::glScalef( scale.x, scale.y, scale.z ); + Render( Instance->pModel, Instance->Material(), distancesquared ); + ::glPopMatrix(); + } + else { + // fast path for unscaled (the common case): existing behaviour + Render(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); + } // debug data ++m_renderpass.draw_stats.models; } } +// 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 const &Instances ) +{ + if( Model == nullptr ) { return; } + if( Instances.empty() ) { return; } + + // 1. Visibility / distance cull. Build parallel arrays of surviving + // instances and their precomputed camera-space root modelview matrices. + std::vector 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::max(); + material_data const *batch_material { nullptr }; + + 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; } + + if( !prepared_shared_state ) { + Instance->RaPrepare(); + prepared_shared_state = true; + batch_material = Instance->Material(); + } + Instance->m_framestamp = m_framestamp; + closest_distancesquared = std::min( closest_distancesquared, static_cast(distancesquared) ); + + // Build the camera-relative root modelview for this instance: + // 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 + // identical output to the regular per-instance path: each instance gets + // its own (translate × rotate × scale) baked into instance_modelview[i], + // which the shader applies via effective_modelview = instance_mv * model_local. + 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 ) ); } + auto const &scale = Instance->Scale(); + if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) { + mv = glm::scale( mv, scale ); + } + instance_modelviews.emplace_back( mv ); + } + + 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( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); + + // 2a. Upload N modelviews to instance_ubo[0..N-1]. + instance_ubo->update( + reinterpret_cast( instance_modelviews.data() + offset_idx ), + 0, + static_cast( 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( &identity ), 0, sizeof( identity ) ); + } + + offset_idx += this_batch; + ++m_renderpass.draw_stats.instanced_drawcalls; + } + + m_renderpass.draw_stats.instances += static_cast( total ); + m_renderpass.draw_stats.models += static_cast( total ); +} + bool opengl33_renderer::Render(TDynamicObject *Dynamic) { glDebug("Render TDynamicObject"); @@ -3101,7 +3300,7 @@ bool opengl33_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li auto const luminance { Global.fLuminance * ( Dynamic->fShade > 0.0f ? Dynamic->fShade : 1.0f ) }; if( Lightlevel > 0.f ) { // crude way to light the cabin, until we have something more complete in place - light_ubs.ambient += ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ); + light_ubs.ambient += packed_vec3( ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ) ); light_ubo->update( light_ubs ); } @@ -3862,8 +4061,26 @@ void opengl33_renderer::Render_Alpha(TAnimModel *Instance) Instance->RaPrepare(); if (Instance->pModel) { - // renderowanie rekurencyjne submodeli - Render_Alpha(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); + auto const &scale = Instance->Scale(); + bool const scaled = ( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ); + if( scaled ) { + // scaled instance: build the per-instance transform locally and call + // the distance-only Render_Alpha so we can inject glScalef before + // the submodel walk. Mirror of the opaque path in Render(TAnimModel*). + ::glPushMatrix(); + auto const Position = Instance->location() - m_renderpass.pass_camera.position(); + auto const Angle = Instance->vAngle; + ::glTranslated( Position.x, Position.y, Position.z ); + if( Angle.y != 0.0 ) ::glRotated( Angle.y, 0.f, 1.f, 0.f ); + if( Angle.x != 0.0 ) ::glRotated( Angle.x, 1.f, 0.f, 0.f ); + if( Angle.z != 0.0 ) ::glRotated( Angle.z, 0.f, 0.f, 1.f ); + ::glScalef( scale.x, scale.y, scale.z ); + Render_Alpha( Instance->pModel, Instance->Material(), distancesquared ); + ::glPopMatrix(); + } + else { + Render_Alpha(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); + } } } diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index c761ef49..7cfa187e 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -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 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); @@ -381,6 +391,18 @@ class opengl33_renderer : public gfx_renderer { std::unique_ptr scene_ubo; std::unique_ptr model_ubo; std::unique_ptr 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 instance_ubo; + // when > 0, the renderer is inside a Render_Instanced() call. The draw(handle) + // method routes to glDrawElementsInstanced* with this many instances rather + // than a single regular draw. The vertex shader reads per-instance modelview + // from instance_ubo[gl_InstanceID]. Reset to 0 by Render_Instanced() on exit. + std::size_t m_current_instance_count { 0 }; gl::scene_ubs scene_ubs; gl::model_ubs model_ubs; gl::light_ubs light_ubs; diff --git a/scene/scene.cpp b/scene/scene.cpp index 1bc8674a..c2100449 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -374,6 +374,21 @@ basic_cell::insert( TAnimModel *Instance ) { if( alpha & flags & 0x1F1F001F ) { // opaque pieces m_instancesopaque.emplace_back( Instance ); + // additionally route instanceable nodes into a per-(pModel, skins) bucket so + // the renderer can amortise material/state setup across many instances of the + // same TModel3d AND issue a single GPU-instanced draw call per submodel. + // Sub-bucketing on skin set ensures every batch has consistent textures — + // instances of the same pModel with different replacable skins land in + // different buckets and render via separate (correct) instanced draws. + if( Instance->m_instanceable && Instance->Model() != nullptr ) { + 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 enclose_area( Instance ); @@ -438,6 +453,24 @@ basic_cell::erase( TAnimModel *Instance ) { [=]( TAnimModel *instance ) { return instance == Instance; } ), std::end( m_instancesopaque ) ); + // also remove from the per-(pModel, skins) instance bucket if present + if( Instance->m_instanceable && Instance->Model() != nullptr ) { + instance_bucket_key key; + key.pModel = Instance->Model(); + auto const *mat = Instance->Material(); + if( mat != nullptr ) { + for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } + } + auto bucket = m_instancebuckets_opaque.find( key ); + if( bucket != m_instancebuckets_opaque.end() ) { + bucket->second.erase( + std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), + std::end( bucket->second ) ); + if( bucket->second.empty() ) { + m_instancebuckets_opaque.erase( bucket ); + } + } + } } // TODO: update cell bounding area } diff --git a/scene/scene.h b/scene/scene.h index f63a7f9e..7b53783b 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include "utilities/parser.h" @@ -45,6 +46,11 @@ struct scratch_data { struct location_data { std::stack offset; + // per-axis scale stack — mirrors `offset` for the `scale`/`endscale` + // scenario directives. Effective scale at any nesting depth is the + // component-wise product of all stack entries (outer `scale 2 2 2` × + // inner `scale 1.5 1.5 1.5` yields (3,3,3)). Empty stack means (1,1,1). + std::stack scale; glm::vec3 rotation; } location; @@ -179,6 +185,30 @@ public: using linesnode_sequence = std::vector; using traction_sequence = std::vector; using instance_sequence = std::vector; + // 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 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()( k.pModel ); + for( auto s : k.skins ) { + // boost-style hash combine + h ^= std::hash()( s ) + 0x9e3779b9 + ( h << 6 ) + ( h >> 2 ); + } + return h; + } + }; + using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector, instance_bucket_key_hash >; using sound_sequence = std::vector; using eventlauncher_sequence = std::vector; using memorycell_sequence = std::vector; @@ -196,6 +226,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; diff --git a/scene/scenenode.h b/scene/scenenode.h index eac74651..10be9c1b 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -48,7 +48,7 @@ operator!=( lighting_data const &Left, lighting_data const &Right ) { namespace scene { struct bounding_area { - glm::highp_dvec3 center; // mid point of the rectangle + glm::dvec3 center; // mid point of the rectangle float radius { -1.0f }; // radius of the bounding sphere bounding_area() = default; @@ -94,7 +94,7 @@ public: material_handle material { null_handle }; lighting_data lighting; // geometry data - glm::highp_dvec3 origin; // world position of the relative coordinate system origin + glm::dvec3 origin; // world position of the relative coordinate system origin gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer std::vector vertices; // world space source data of the geometry gfx::userdata_array userdata; diff --git a/shaders/simpleuv.vert b/shaders/simpleuv.vert index f3cd0ea2..673130ae 100644 --- a/shaders/simpleuv.vert +++ b/shaders/simpleuv.vert @@ -7,6 +7,6 @@ out vec2 f_coord; void main() { - gl_Position = (projection * modelview) * vec4(v_vert, 1.0f); + gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0f); f_coord = v_coord; } diff --git a/shaders/vertex.vert b/shaders/vertex.vert index feadbe1e..5326545e 100644 --- a/shaders/vertex.vert +++ b/shaders/vertex.vert @@ -23,24 +23,27 @@ out vec4 UserData; void main() { - f_normal = normalize(modelviewnormal * v_normal); + mat4 mv = effective_modelview(); + mat3 mvn = effective_modelviewnormal(); + + f_normal = normalize(mvn * v_normal); f_normal_raw = v_normal; f_coord = v_coord; - f_pos = modelview * vec4(v_vert, 1.0); + f_pos = mv * vec4(v_vert, 1.0); for (uint idx = 0U ; idx < MAX_CASCADES ; ++idx) { f_light_pos[idx] = lightview[idx] * f_pos; - } - f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0); - f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0); - + } + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + gl_Position = f_clip_pos; gl_PointSize = param[1].x; - vec3 T = normalize(modelviewnormal * v_tangent.xyz); + vec3 T = normalize(mvn * v_tangent.xyz); vec3 N = f_normal; vec3 B = normalize(cross(N, T)); f_tbn = mat3(T, B, N); - + mat3 TBN = transpose(f_tbn); // TangentLightPos = TBN * f_light_pos.xyz; // TangentViewPos = TBN * vec3(0.0, 0.0, 0.0); diff --git a/shaders/vertexonly.vert b/shaders/vertexonly.vert index c121b09b..370b4aa3 100644 --- a/shaders/vertexonly.vert +++ b/shaders/vertexonly.vert @@ -4,5 +4,5 @@ layout(location = 0) in vec3 v_vert; void main() { - gl_Position = (projection * modelview) * vec4(v_vert, 1.0); + gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0); } diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 0b2a9a8d..7277e338 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -93,6 +93,8 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { { "node", &state_serializer::deserialize_node }, { "origin", &state_serializer::deserialize_origin }, { "endorigin", &state_serializer::deserialize_endorigin }, + { "scale", &state_serializer::deserialize_scale }, + { "endscale", &state_serializer::deserialize_endscale }, { "rotate", &state_serializer::deserialize_rotate }, { "sky", &state_serializer::deserialize_sky }, { "test", &state_serializer::deserialize_test }, @@ -664,6 +666,43 @@ state_serializer::deserialize_endorigin( cParser &Input, scene::scratch_data &Sc } } +void +state_serializer::deserialize_scale( cParser &Input, scene::scratch_data &Scratchpad ) { + // Syntax: `scale ` (three tokens, mirroring `rotate`/`angles`). + // For uniform scaling write the same value three times (e.g. `scale 2 2 2`). + // Affects both: + // 1. positions of nodes inside the block (transform() multiplies offset by scale) + // 2. the per-instance m_scale stamped onto each TAnimModel created inside the block + // The two together let you scale a multi-node-model group built around a common + // origin: positions of the parts spread out by the factor AND each part is itself + // scaled by the same factor, preserving the visual shape of the assembly. + glm::vec3 factor; + Input.getTokens( 3 ); + Input >> factor.x >> factor.y >> factor.z; + if( factor.x <= 0.0f || factor.y <= 0.0f || factor.z <= 0.0f ) { + ErrorLog( "Bad scale: non-positive scale factor in file \"" + + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + "); scale (1,1,1) used" ); + factor = glm::vec3( 1.0f ); + } + // scales compose component-wise, mirroring how origin offsets compose additively. + glm::vec3 const parent = ( + Scratchpad.location.scale.empty() ? + glm::vec3( 1.0f ) : + Scratchpad.location.scale.top() ); + Scratchpad.location.scale.emplace( factor * parent ); +} + +void +state_serializer::deserialize_endscale( cParser &Input, scene::scratch_data &Scratchpad ) { + + if( false == Scratchpad.location.scale.empty() ) { + Scratchpad.location.scale.pop(); + } + else { + ErrorLog( "Bad scale: endscale instruction with empty scale stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); + } +} + void state_serializer::deserialize_rotate( cParser &Input, scene::scratch_data &Scratchpad ) { @@ -915,6 +954,12 @@ state_serializer::deserialize_model( cParser &Input, scene::scratch_data &Scratc auto *instance = new TAnimModel( Nodedata ); instance->Angles( Scratchpad.location.rotation + rotation ); // dostosowanie do pochylania linii + // pick up the scale active at this point in the scenario stream — outer + // `scale`/`endscale` blocks compose multiplicatively in the scratchpad. + // Load() may further multiply this by an inline `scale ` token. + if( false == Scratchpad.location.scale.empty() ) { + instance->Scale( Scratchpad.location.scale.top() ); + } if( instance->Load( &Input, false ) ) { instance->location( transform( location, Scratchpad ) ); @@ -1091,7 +1136,7 @@ state_serializer::skip_until( cParser &Input, std::string const &Token ) { } } -// transforms provided location by specifed rotation and offset +// transforms provided location by specifed rotation, scale and offset glm::dvec3 state_serializer::transform( glm::dvec3 Location, scene::scratch_data const &Scratchpad ) { @@ -1099,6 +1144,17 @@ state_serializer::transform( glm::dvec3 Location, scene::scratch_data const &Scr auto const rotation = glm::radians( Scratchpad.location.rotation ); Location = glm::rotateY( Location, rotation.y ); // Ra 2014-11: uwzględnienie rotacji } + // Scale applies in local origin space — positions inside a `scale 2 2 2` block + // are pushed twice as far from the local origin along each axis, so a + // multi-node-model group (e.g. a building made of separate node models built + // around a shared origin) ends up looking uniformly scaled rather than just + // having one piece grow. Per-axis values stretch the assembly anisotropically. + if( false == Scratchpad.location.scale.empty() ) { + auto const &s = Scratchpad.location.scale.top(); + Location.x *= static_cast( s.x ); + Location.y *= static_cast( s.y ); + Location.z *= static_cast( s.z ); + } if( false == Scratchpad.location.offset.empty() ) { Location += Scratchpad.location.offset.top(); } diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index fb0dd4ed..dea1e20e 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -65,6 +65,8 @@ private: void deserialize_node( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_endorigin( cParser &Input, scene::scratch_data &Scratchpad ); + void deserialize_scale( cParser &Input, scene::scratch_data &Scratchpad ); + void deserialize_endscale( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_rotate( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_sky( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_test( cParser &Input, scene::scratch_data &Scratchpad ); diff --git a/stdafx.h b/stdafx.h index 8138b138..fec02883 100644 --- a/stdafx.h +++ b/stdafx.h @@ -93,6 +93,9 @@ #define GLM_FORCE_SWIZZLE #define GLM_ENABLE_EXPERIMENTAL #define GLM_FORCE_CTOR_INIT +#define GLM_FORCE_INLINE +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#define GLM_FORCE_INTRINSICS #include #include #include @@ -105,6 +108,12 @@ #include #include +#ifndef PACKED_VEC3_DEFINED +#define PACKED_VEC3_DEFINED +// 12-byte vec3 that stays packed regardless of GLM_FORCE_DEFAULT_ALIGNED_GENTYPES. +// Use in structs that must match an exact binary layout (UBOs, vertex data, serialized formats). +using packed_vec3 = glm::vec<3, float, glm::packed_highp>; +#endif #include "rendering/openglmatrixstack.h"