mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-17 22:39:17 +02:00
Merge pull request #107 from MaSzyna-EU07/opengl-instancing
Opengl instancing + node::model scale
This commit is contained in:
@@ -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()
|
||||
@@ -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<TAnimModel *>(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()
|
||||
{
|
||||
|
||||
|
||||
@@ -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<text_line> m_grouplines;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 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()
|
||||
|
||||
32
gl/ubo.h
32
gl/ubo.h
@@ -3,9 +3,18 @@
|
||||
#include "object.h"
|
||||
#include "bindable.h"
|
||||
#include "buffer.h"
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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];
|
||||
|
||||
@@ -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 <x> <y> <z>` (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<TAnimContainer> 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 <rotation.y> ...` 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"
|
||||
|
||||
@@ -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 <factor>` / `scale <x> <y> <z>` 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<std::shared_ptr<TAnimContainer>> m_animlist;
|
||||
|
||||
@@ -169,6 +189,7 @@ public:
|
||||
std::shared_ptr<TAnimContainer> 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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <typename Iterator_>
|
||||
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 <typename Iterator_>
|
||||
void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) {
|
||||
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
|
||||
void
|
||||
opengl33_vaogeometrybank::release_() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -142,6 +142,7 @@ bool opengl33_renderer::Init(GLFWwindow *Window)
|
||||
scene_ubo = std::make_unique<gl::ubo>(sizeof(gl::scene_ubs), 0);
|
||||
model_ubo = std::make_unique<gl::ubo>(sizeof(gl::model_ubs), 1, GL_STREAM_DRAW);
|
||||
light_ubo = std::make_unique<gl::ubo>(sizeof(gl::light_ubs), 2);
|
||||
instance_ubo = std::make_unique<gl::ubo>(sizeof(gl::instance_ubs), 3, GL_STREAM_DRAW);
|
||||
|
||||
// better initialize with 0 to not crash driver/whole system
|
||||
// when we forget
|
||||
@@ -153,6 +154,14 @@ bool opengl33_renderer::Init(GLFWwindow *Window)
|
||||
model_ubo->update(model_ubs);
|
||||
scene_ubo->update(scene_ubs);
|
||||
|
||||
// initialize instance_ubo slot 0 to identity. This is the matrix sampled by
|
||||
// gl_InstanceID==0 in non-instanced draws, so existing rendering paths
|
||||
// continue to multiply effective_modelview = identity * modelview = modelview.
|
||||
{
|
||||
glm::mat4 identity( 1.0f );
|
||||
instance_ubo->update( reinterpret_cast<uint8_t const*>(&identity), 0, sizeof(identity) );
|
||||
}
|
||||
|
||||
int samples = 1 << Global.iMultisampling;
|
||||
if (!Global.gfx_usegles && samples > 1)
|
||||
glEnable(GL_MULTISAMPLE);
|
||||
@@ -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<void *>(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<gfx::geometrybank_handle>::iterator it, std::vector<gfx::geometrybank_handle>::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<TAnimModel *> 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<glm::mat4> instance_modelviews;
|
||||
instance_modelviews.reserve( Instances.size() );
|
||||
|
||||
// Pull the current pass camera/view transform once. We use the current GL
|
||||
// modelview matrix as the view matrix because at the point Render_Instanced
|
||||
// is called, OpenGLMatrices is set to the camera view (scene root) — no
|
||||
// per-instance transform has been pushed yet.
|
||||
glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW );
|
||||
|
||||
bool prepared_shared_state = false; // RaPrepare() runs once per bucket
|
||||
float closest_distancesquared = std::numeric_limits<float>::max();
|
||||
material_data const *batch_material { nullptr };
|
||||
|
||||
for( auto *Instance : Instances ) {
|
||||
if( Instance == nullptr ) { continue; }
|
||||
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<float>( closest_distancesquared, static_cast<float>(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<std::size_t>( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH );
|
||||
|
||||
// 2a. Upload N modelviews to instance_ubo[0..N-1].
|
||||
instance_ubo->update(
|
||||
reinterpret_cast<uint8_t const *>( instance_modelviews.data() + offset_idx ),
|
||||
0,
|
||||
static_cast<int>( this_batch * sizeof( glm::mat4 ) ) );
|
||||
|
||||
// 2b. Push identity onto the matrix stack so submodel transforms
|
||||
// accumulate from identity, not from camera/instance space.
|
||||
::glPushMatrix();
|
||||
::glLoadIdentity();
|
||||
|
||||
// 2c. Configure the existing TModel3d/TSubModel render path. Setting
|
||||
// m_current_instance_count routes draw(handle) calls to draw_instanced.
|
||||
m_current_instance_count = this_batch;
|
||||
|
||||
Model->Root->fSquareDist = closest_distancesquared; // shared global, used by submodel LOD
|
||||
auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 );
|
||||
alpha ^= 0x0F0F000F;
|
||||
Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha );
|
||||
Model->Root->pRoot = Model;
|
||||
|
||||
Render( Model->Root );
|
||||
|
||||
m_current_instance_count = 0;
|
||||
|
||||
::glPopMatrix();
|
||||
|
||||
// 2d. Restore instance_modelview[0] to identity so subsequent
|
||||
// non-instanced draws continue to compute identity * modelview.
|
||||
{
|
||||
glm::mat4 const identity( 1.0f );
|
||||
instance_ubo->update( reinterpret_cast<uint8_t const *>( &identity ), 0, sizeof( identity ) );
|
||||
}
|
||||
|
||||
offset_idx += this_batch;
|
||||
++m_renderpass.draw_stats.instanced_drawcalls;
|
||||
}
|
||||
|
||||
m_renderpass.draw_stats.instances += static_cast<int>( total );
|
||||
m_renderpass.draw_stats.models += static_cast<int>( 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,8 @@ class opengl33_renderer : public gfx_renderer {
|
||||
int lines{0};
|
||||
int particles{0};
|
||||
int drawcalls{0};
|
||||
int instanced_drawcalls{0}; // drawcalls issued via instanced render path (one per TModel3d batch)
|
||||
int instances{0}; // total instances drawn via instanced render path
|
||||
int triangles{0};
|
||||
|
||||
debug_stats& operator+=( const debug_stats& Right ) {
|
||||
@@ -174,6 +176,8 @@ class opengl33_renderer : public gfx_renderer {
|
||||
lines += Right.lines;
|
||||
particles += Right.particles;
|
||||
drawcalls += Right.drawcalls;
|
||||
instanced_drawcalls += Right.instanced_drawcalls;
|
||||
instances += Right.instances;
|
||||
return *this; }
|
||||
};
|
||||
|
||||
@@ -266,6 +270,12 @@ class opengl33_renderer : public gfx_renderer {
|
||||
void Render(cell_sequence::iterator First, cell_sequence::iterator Last);
|
||||
void Render(scene::shape_node const &Shape, bool const Ignorerange);
|
||||
void Render(TAnimModel *Instance);
|
||||
// batched render path for many TAnimModel instances that share the same TModel3d.
|
||||
// Caller guarantees every instance has m_instanceable == true (no per-instance lights,
|
||||
// no submodel animation, no replacable skins). Each call counts as one
|
||||
// instanced_drawcall in draw_stats and contributes Instances.size() to the
|
||||
// instances counter. Instances are still individually frustum/distance culled.
|
||||
void Render_Instanced( TModel3d *Model, std::vector<TAnimModel *> const &Instances );
|
||||
bool Render(TDynamicObject *Dynamic);
|
||||
bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle);
|
||||
bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance);
|
||||
@@ -381,6 +391,18 @@ class opengl33_renderer : public gfx_renderer {
|
||||
std::unique_ptr<gl::ubo> scene_ubo;
|
||||
std::unique_ptr<gl::ubo> model_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::model_ubs model_ubs;
|
||||
gl::light_ubs light_ubs;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <array>
|
||||
#include <stack>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <map>
|
||||
|
||||
#include "utilities/parser.h"
|
||||
@@ -45,6 +46,11 @@ struct scratch_data {
|
||||
struct location_data {
|
||||
|
||||
std::stack<glm::dvec3> 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<glm::vec3> scale;
|
||||
glm::vec3 rotation;
|
||||
} location;
|
||||
|
||||
@@ -179,6 +185,30 @@ public:
|
||||
using linesnode_sequence = std::vector<lines_node>;
|
||||
using traction_sequence = std::vector<TTraction *>;
|
||||
using instance_sequence = 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 eventlauncher_sequence = std::vector<TEventLauncher *>;
|
||||
using memorycell_sequence = std::vector<TMemCell *>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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<world_vertex> vertices; // world space source data of the geometry
|
||||
gfx::userdata_array userdata;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 <x> <y> <z>` (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 <factor>` 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<double>( 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<double>( s.x );
|
||||
Location.y *= static_cast<double>( s.y );
|
||||
Location.z *= static_cast<double>( s.z );
|
||||
}
|
||||
if( false == Scratchpad.location.offset.empty() ) {
|
||||
Location += Scratchpad.location.offset.top();
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
9
stdafx.h
9
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 <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/matrix_transform_2d.hpp>
|
||||
@@ -105,6 +108,12 @@
|
||||
#include <glm/gtx/norm.hpp>
|
||||
#include <glm/gtx/string_cast.hpp>
|
||||
|
||||
#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"
|
||||
|
||||
Reference in New Issue
Block a user