diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 25028a8d..bf7d3727 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,71 @@ 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. + { + auto const wrap360 = [](float v) { + v = std::fmod(v, 360.0f); + if (v < 0.0f) { v += 360.0f; } + return v; + }; + glm::vec3 angles{ + wrap360(picked->Angles().x), + wrap360(picked->Angles().y), + wrap360(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/gl/glsl_common.cpp b/gl/glsl_common.cpp index 032f7e21..26c31000 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -99,9 +99,14 @@ void gl::glsl_common_setup() return instance_modelview[gl_InstanceID] * modelview; } mat3 effective_modelviewnormal() { - // instance_modelview is rotation+translation only (no scale), so its - // upper-3x3 is its own inverse-transpose (rotation matrix). - return mat3(instance_modelview[gl_InstanceID]) * modelviewnormal; + // 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 diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index dec24bc6..5db4bbe9 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; } @@ -677,20 +695,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 @@ -700,18 +719,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 ? @@ -748,11 +778,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 bf7d73da..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 diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 19ae7343..bf46f3b7 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -709,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 @@ -2914,8 +2915,32 @@ 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; } @@ -2987,7 +3012,11 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( closest_distancesquared, static_cast(distancesquared) ); // Build the camera-relative root modelview for this instance: - // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) + // 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 ) ); @@ -2995,6 +3024,10 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorScale(); + if( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ) { + mv = glm::scale( mv, scale ); + } instance_modelviews.emplace_back( mv ); } @@ -4028,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/scene/scene.h b/scene/scene.h index 9b2b955f..7b53783b 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -46,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; 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 );