From c2cbd0974d22527da1aac47646c309b53134bbe2 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Sat, 4 Apr 2026 02:12:38 +0300 Subject: [PATCH] Added StaticGeometry support --- src/features/editScene/CMakeLists.txt | 7 + src/features/editScene/EditorApp.cpp | 16 + src/features/editScene/EditorApp.hpp | 2 + .../editScene/components/StaticGeometry.hpp | 43 +++ .../components/StaticGeometryMember.hpp | 45 +++ .../components/StaticGeometryModule.cpp | 60 +++ src/features/editScene/gizmo/Gizmo.cpp | 6 + .../editScene/systems/EditorUISystem.cpp | 23 +- .../editScene/systems/SceneSerializer.cpp | 87 +++++ .../editScene/systems/SceneSerializer.hpp | 4 + .../systems/StaticGeometrySystem.cpp | 359 +++++++++++++++++ .../systems/StaticGeometrySystem.hpp | 54 +++ src/features/editScene/ui/ComponentEditor.hpp | 4 + .../editScene/ui/StaticGeometryEditor.cpp | 99 +++++ .../editScene/ui/StaticGeometryEditor.hpp | 18 + .../ui/StaticGeometryMemberEditor.cpp | 363 ++++++++++++++++++ .../ui/StaticGeometryMemberEditor.hpp | 35 ++ 17 files changed, 1224 insertions(+), 1 deletion(-) create mode 100644 src/features/editScene/components/StaticGeometry.hpp create mode 100644 src/features/editScene/components/StaticGeometryMember.hpp create mode 100644 src/features/editScene/components/StaticGeometryModule.cpp create mode 100644 src/features/editScene/systems/StaticGeometrySystem.cpp create mode 100644 src/features/editScene/systems/StaticGeometrySystem.hpp create mode 100644 src/features/editScene/ui/StaticGeometryEditor.cpp create mode 100644 src/features/editScene/ui/StaticGeometryEditor.hpp create mode 100644 src/features/editScene/ui/StaticGeometryMemberEditor.cpp create mode 100644 src/features/editScene/ui/StaticGeometryMemberEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 553ed05..b69245c 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -17,6 +17,7 @@ set(EDITSCENE_SOURCES systems/LightSystem.cpp systems/CameraSystem.cpp systems/LodSystem.cpp + systems/StaticGeometrySystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -25,10 +26,13 @@ set(EDITSCENE_SOURCES ui/CameraEditor.cpp ui/LodEditor.cpp ui/LodSettingsEditor.cpp + ui/StaticGeometryEditor.cpp + ui/StaticGeometryMemberEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp components/LodModule.cpp + components/StaticGeometryModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -46,7 +50,10 @@ set(EDITSCENE_HEADERS components/Camera.hpp components/Lod.hpp components/LodSettings.hpp + components/StaticGeometry.hpp + components/StaticGeometryMember.hpp systems/EditorUISystem.hpp + systems/StaticGeometrySystem.hpp systems/SceneSerializer.hpp systems/PhysicsSystem.hpp systems/LightSystem.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 79bf3c2..5603159 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -5,6 +5,7 @@ #include "systems/LightSystem.hpp" #include "systems/CameraSystem.hpp" #include "systems/LodSystem.hpp" +#include "systems/StaticGeometrySystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -16,6 +17,8 @@ #include "components/Camera.hpp" #include "components/Lod.hpp" #include "components/LodSettings.hpp" +#include "components/StaticGeometry.hpp" +#include "components/StaticGeometryMember.hpp" #include #include @@ -128,6 +131,10 @@ void EditorApp::setup() // Setup LOD system m_lodSystem = std::make_unique(m_world, m_sceneMgr); m_lodSystem->initialize(); + + // Setup StaticGeometry system + m_staticGeometrySystem = std::make_unique(m_world, m_sceneMgr); + m_staticGeometrySystem->initialize(); // Add default entities to UI cache for (auto &e : m_defaultEntities) { @@ -168,6 +175,10 @@ void EditorApp::setupECS() // Register LOD components m_world.component(); m_world.component(); + + // Register StaticGeometry components + m_world.component(); + m_world.component(); } void EditorApp::createDefaultEntities() @@ -310,6 +321,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) if (m_lodSystem) { m_lodSystem->update(); } + + // Update StaticGeometry system + if (m_staticGeometrySystem) { + m_staticGeometrySystem->update(); + } // Don't call base class - it crashes when iterating input listeners return true; diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index 93ba53a..1c29128 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -17,6 +17,7 @@ class EditorPhysicsSystem; class EditorLightSystem; class EditorCameraSystem; class EditorLodSystem; +class StaticGeometrySystem; /** * RenderTargetListener for ImGui frame management @@ -86,6 +87,7 @@ private: std::unique_ptr m_lightSystem; std::unique_ptr m_cameraSystem; std::unique_ptr m_lodSystem; + std::unique_ptr m_staticGeometrySystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/StaticGeometry.hpp b/src/features/editScene/components/StaticGeometry.hpp new file mode 100644 index 0000000..e98b888 --- /dev/null +++ b/src/features/editScene/components/StaticGeometry.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +/** + * @brief Component for a StaticGeometry region/batch + * + * Entities with StaticGeometryMemberComponent reference this region. + * The system collects all members and builds the batched geometry. + */ +struct StaticGeometryComponent { + // Persistent ID for serialization and member references + std::string regionId; + + // Region name for the Ogre::StaticGeometry object + std::string regionName; + + // The actual Ogre StaticGeometry object + Ogre::StaticGeometry* staticGeometry = nullptr; + + // Build configuration + float renderingDistance = 0.0f; // 0 = infinite + bool castShadows = true; + + // Optimization settings + float regionDimensions = 1000.0f; // Size of each region cell + std::uint32_t visibilityFlags = 0xFFFFFFFF; + + // State + bool built = false; + bool dirty = true; // Needs rebuild + unsigned int buildVersion = 0; // Incremented on each build + + // List of member entity IDs (for tracking) + std::vector memberEntityIds; + + void markDirty() { + dirty = true; + } +}; diff --git a/src/features/editScene/components/StaticGeometryMember.hpp b/src/features/editScene/components/StaticGeometryMember.hpp new file mode 100644 index 0000000..a877468 --- /dev/null +++ b/src/features/editScene/components/StaticGeometryMember.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +/** + * @brief Component for entities that should be batched into StaticGeometry + * + * This component stores mesh data and references the StaticGeometry region entity. + * The actual Ogre::Entity is NOT created on a SceneNode - instead, the data is + * used to add geometry to the StaticGeometry batch. + * + * Position and Orientation are taken from TransformComponent's SceneNode (_getDerived*). + * Scale is also taken from TransformComponent. + */ +struct StaticGeometryMemberComponent { + // Reference to the StaticGeometry region entity (runtime only) + flecs::entity regionEntity; + + // Persistent ID for serialization (references StaticGeometryComponent::regionId) + std::string regionId; + + // Mesh data (stored here, not in RenderableComponent) + std::string meshName; + std::string materialName; // Empty = use mesh default + + // LOD settings reference (must be same for all members in a region) + std::string lodSettingsId; + flecs::entity lodSettingsEntity; + + // Configuration + bool castShadows = true; + bool visible = true; + + // Dirty flag - triggers region rebuild when changed + bool dirty = true; + + // Version tracking for transform changes + unsigned int transformVersion = 0; + + void markDirty() { + dirty = true; + transformVersion++; + } +}; diff --git a/src/features/editScene/components/StaticGeometryModule.cpp b/src/features/editScene/components/StaticGeometryModule.cpp new file mode 100644 index 0000000..7fe7e70 --- /dev/null +++ b/src/features/editScene/components/StaticGeometryModule.cpp @@ -0,0 +1,60 @@ +#include "StaticGeometry.hpp" +#include "StaticGeometryMember.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/StaticGeometryEditor.hpp" +#include "../ui/StaticGeometryMemberEditor.hpp" +#include +#include + +// Helper to generate unique ID +static std::string generateRegionId() { + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_int_distribution<> dis(1000, 9999); + return "region_" + std::to_string(dis(gen)); +} + +// Register StaticGeometry (region) component +REGISTER_COMPONENT("StaticGeometry Region", StaticGeometryComponent, StaticGeometryEditor) +{ + registry.registerComponent( + "StaticGeometry Region", + std::make_unique(), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + auto region = StaticGeometryComponent(); + region.regionId = generateRegionId(); + region.regionName = "Region_" + Ogre::StringConverter::toString(e.id()); + e.set(region); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register StaticGeometryMember component +REGISTER_COMPONENT("StaticGeometry Member", StaticGeometryMemberComponent, StaticGeometryMemberEditor) +{ + registry.registerComponent( + "StaticGeometry Member", + std::make_unique(sceneMgr), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/gizmo/Gizmo.cpp b/src/features/editScene/gizmo/Gizmo.cpp index 08dde42..02adc36 100644 --- a/src/features/editScene/gizmo/Gizmo.cpp +++ b/src/features/editScene/gizmo/Gizmo.cpp @@ -1,5 +1,6 @@ #include "Gizmo.hpp" #include "../components/Transform.hpp" +#include "../components/StaticGeometryMember.hpp" #include // Simple colors for axes - not using vertex colors since RTSS has issues @@ -262,6 +263,11 @@ bool Gizmo::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDe transform.applyToNode(); + // Mark StaticGeometryMember dirty if present + if (m_attachedEntity.has()) { + m_attachedEntity.get_mut().markDirty(); + } + // Update gizmo to follow (use derived position) m_gizmoNode->setPosition(transform.node->_getDerivedPosition()); diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index c8ba4ae..59e6bf6 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -8,6 +8,8 @@ #include "../components/Light.hpp" #include "../components/Camera.hpp" #include "../components/Lod.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/StaticGeometryMember.hpp" #include "../components/LodSettings.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" @@ -481,7 +483,12 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render Transform first if present (it's the base component) if (entity.has()) { auto &transform = entity.get_mut(); - m_componentRegistry.render(entity, transform); + if (m_componentRegistry.render(entity, transform)) { + // Transform changed - mark StaticGeometryMember dirty if present + if (entity.has()) { + entity.get_mut().markDirty(); + } + } componentCount++; } @@ -533,6 +540,20 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, lod); componentCount++; } + + // Render StaticGeometry Region if present + if (entity.has()) { + auto ®ion = entity.get_mut(); + m_componentRegistry.render(entity, region); + componentCount++; + } + + // Render StaticGeometry Member if present + if (entity.has()) { + auto &member = entity.get_mut(); + m_componentRegistry.render(entity, member); + componentCount++; + } // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 96142e4..d5ebf63 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -9,7 +9,10 @@ #include "../components/Camera.hpp" #include "../components/Lod.hpp" #include "../components/LodSettings.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/StaticGeometryMember.hpp" #include "EditorUISystem.hpp" +#include #include #include @@ -145,6 +148,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["lod"] = serializeLod(entity); } + if (entity.has()) { + json["staticGeometry"] = serializeStaticGeometry(entity); + } + + if (entity.has()) { + json["staticGeometryMember"] = serializeStaticGeometryMember(entity); + } + // Serialize children json["children"] = nlohmann::json::array(); entity.children([&](flecs::entity child) { @@ -214,6 +225,14 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeLod(entity, json["lod"]); } + if (json.contains("staticGeometry")) { + deserializeStaticGeometry(entity, json["staticGeometry"]); + } + + if (json.contains("staticGeometryMember")) { + deserializeStaticGeometryMember(entity, json["staticGeometryMember"]); + } + // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); @@ -859,3 +878,71 @@ void SceneSerializer::deserializeLod(flecs::entity entity, const nlohmann::json& Ogre::StringConverter::toString(lod.settingsEntity.id()) + ", hasValid=" + (lod.hasValidSettings() ? "true" : "false")); } + +nlohmann::json SceneSerializer::serializeStaticGeometry(flecs::entity entity) +{ + auto& region = entity.get(); + nlohmann::json json; + + json["regionId"] = region.regionId; + json["regionName"] = region.regionName; + json["renderingDistance"] = region.renderingDistance; + json["castShadows"] = region.castShadows; + json["regionDimensions"] = region.regionDimensions; + json["visibilityFlags"] = region.visibilityFlags; + + return json; +} + +nlohmann::json SceneSerializer::serializeStaticGeometryMember(flecs::entity entity) +{ + auto& member = entity.get(); + nlohmann::json json; + + json["regionId"] = member.regionId; + json["meshName"] = member.meshName; + json["materialName"] = member.materialName; + json["lodSettingsId"] = member.lodSettingsId; + json["castShadows"] = member.castShadows; + json["visible"] = member.visible; + + return json; +} + +void SceneSerializer::deserializeStaticGeometry(flecs::entity entity, const nlohmann::json& json) +{ + StaticGeometryComponent region; + + region.regionId = json.value("regionId", ""); + if (region.regionId.empty()) { + // Generate new ID if missing + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_int_distribution<> dis(1000, 9999); + region.regionId = "region_" + std::to_string(dis(gen)); + } + + region.regionName = json.value("regionName", ""); + region.renderingDistance = json.value("renderingDistance", 0.0f); + region.castShadows = json.value("castShadows", true); + region.regionDimensions = json.value("regionDimensions", 1000.0f); + region.visibilityFlags = json.value("visibilityFlags", 0xFFFFFFFF); + region.dirty = true; + + entity.set(region); +} + +void SceneSerializer::deserializeStaticGeometryMember(flecs::entity entity, const nlohmann::json& json) +{ + StaticGeometryMemberComponent member; + + member.regionId = json.value("regionId", ""); + member.meshName = json.value("meshName", ""); + member.materialName = json.value("materialName", ""); + member.lodSettingsId = json.value("lodSettingsId", ""); + member.castShadows = json.value("castShadows", true); + member.visible = json.value("visible", true); + member.dirty = true; + + entity.set(member); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 6cf8433..0a25da5 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -48,6 +48,8 @@ private: nlohmann::json serializeCamera(flecs::entity entity); nlohmann::json serializeLodSettings(flecs::entity entity); nlohmann::json serializeLod(flecs::entity entity); + nlohmann::json serializeStaticGeometry(flecs::entity entity); + nlohmann::json serializeStaticGeometryMember(flecs::entity entity); // Component deserialization void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); @@ -59,6 +61,8 @@ private: void deserializeCamera(flecs::entity entity, const nlohmann::json& json); void deserializeLodSettings(flecs::entity entity, const nlohmann::json& json); void deserializeLod(flecs::entity entity, const nlohmann::json& json); + void deserializeStaticGeometry(flecs::entity entity, const nlohmann::json& json); + void deserializeStaticGeometryMember(flecs::entity entity, const nlohmann::json& json); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/systems/StaticGeometrySystem.cpp b/src/features/editScene/systems/StaticGeometrySystem.cpp new file mode 100644 index 0000000..fcbc719 --- /dev/null +++ b/src/features/editScene/systems/StaticGeometrySystem.cpp @@ -0,0 +1,359 @@ +#include "StaticGeometrySystem.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/StaticGeometryMember.hpp" +#include "../components/Transform.hpp" +#include "../components/Lod.hpp" +#include "../components/LodSettings.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +StaticGeometrySystem::StaticGeometrySystem(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_regionQuery(world.query()) + , m_memberQuery(world.query()) +{ +} + +StaticGeometrySystem::~StaticGeometrySystem() = default; + +void StaticGeometrySystem::initialize() +{ + if (m_initialized) return; + m_initialized = true; +} + +void StaticGeometrySystem::update() +{ + if (!m_initialized) return; + + // First pass: resolve region references for members and detect transform changes + m_memberQuery.each([&](flecs::entity entity, StaticGeometryMemberComponent& member) { + if (!member.regionId.empty() && !member.regionEntity.is_alive()) { + // Try to find the region entity by regionId + m_regionQuery.each([&](flecs::entity e, StaticGeometryComponent& region) { + if (region.regionId == member.regionId) { + member.regionEntity = e; + } + }); + } + + // Check if transform has changed (using version tracking) + if (entity.has()) { + const auto& transform = entity.get(); + if (transform.node) { + // Store current transform version in member for comparison + // For now, we rely on the member.dirty flag being set externally + // when TransformComponent changes + } + } + + // Mark region dirty if member is dirty + if (member.dirty && member.regionEntity.is_alive()) { + if (member.regionEntity.has()) { + member.regionEntity.get_mut().markDirty(); + } + } + }); + + // Second pass: rebuild dirty regions + m_regionQuery.each([&](flecs::entity entity, StaticGeometryComponent& region) { + if (region.dirty || !region.built) { + buildRegion(entity, region); + } + }); +} + +void StaticGeometrySystem::buildRegion(flecs::entity regionEntity, StaticGeometryComponent& region) +{ + if (!regionEntity.is_alive()) return; + + // Destroy existing static geometry + destroyOgreStaticGeometry(region); + + // Generate unique region name if not set + if (region.regionName.empty()) { + region.regionName = "StaticRegion_" + std::to_string(regionEntity.id()); + } + + // Create new StaticGeometry + region.staticGeometry = m_sceneMgr->createStaticGeometry(region.regionName); + region.staticGeometry->setCastShadows(region.castShadows); + + if (region.renderingDistance > 0.0f) { + region.staticGeometry->setRenderingDistance(region.renderingDistance); + } + + region.staticGeometry->setRegionDimensions(Ogre::Vector3(region.regionDimensions)); + region.staticGeometry->setVisibilityFlags(region.visibilityFlags); + + // Collect all members for this region + std::vector> members; + m_memberQuery.each([&](flecs::entity memberEntity, StaticGeometryMemberComponent& member) { + if (member.regionEntity == regionEntity && member.visible) { + members.emplace_back(memberEntity, &member); + } + }); + + // Track member IDs + region.memberEntityIds.clear(); + region.memberEntityIds.reserve(members.size()); + + // Check LOD consistency across members + std::string regionLodSettingsId; + flecs::entity regionLodSettingsEntity; + bool lodConsistent = true; + + for (auto& [memberEntity, member] : members) { + if (!member->lodSettingsId.empty()) { + if (regionLodSettingsId.empty()) { + regionLodSettingsId = member->lodSettingsId; + regionLodSettingsEntity = member->lodSettingsEntity; + } else if (regionLodSettingsId != member->lodSettingsId) { + lodConsistent = false; + } + } + } + + if (!lodConsistent) { + Ogre::LogManager::getSingleton().logMessage( + "StaticGeometry WARNING: Region '" + region.regionName + + "' has members with different LOD Settings! Using first found.", + Ogre::LML_WARNING); + } + + // Get LOD generator + Ogre::MeshLodGenerator* lodGenerator = nullptr; + try { + lodGenerator = Ogre::MeshLodGenerator::getSingletonPtr(); + } catch (...) { + // Generator not created yet + } + + // Add each member's geometry + for (auto& [memberEntity, member] : members) { + if (member->meshName.empty()) continue; + + try { + // Check if mesh exists + if (!Ogre::MeshManager::getSingleton().resourceExists(member->meshName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME)) { + continue; + } + + // Get global position and orientation from TransformComponent if available + Ogre::Vector3 position = Ogre::Vector3::ZERO; + Ogre::Quaternion orientation = Ogre::Quaternion::IDENTITY; + Ogre::Vector3 scale = Ogre::Vector3::UNIT_SCALE; + + if (memberEntity.has()) { + const auto& transform = memberEntity.get(); + if (transform.node) { + // Use derived (global) position and orientation + position = transform.node->_getDerivedPosition(); + orientation = transform.node->_getDerivedOrientation(); + scale = transform.node->getScale(); + } + } + + // Get the mesh and apply LOD if configured + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load( + member->meshName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + + // Apply LOD if this member uses LOD and we have valid settings + if (lodGenerator && !member->lodSettingsId.empty() && + member->lodSettingsEntity.is_alive() && + member->lodSettingsEntity.has()) { + + const auto& lodSettings = member->lodSettingsEntity.get(); + if (!lodSettings.lodLevels.empty()) { + // Build LOD config + Ogre::LodConfig lodConfig; + lodConfig.mesh = mesh; + + // Set strategy + switch (lodSettings.strategy) { + case LodSettingsComponent::Strategy::Distance: + lodConfig.strategy = Ogre::DistanceLodBoxStrategy::getSingletonPtr(); + break; + case LodSettingsComponent::Strategy::PixelCount: + case LodSettingsComponent::Strategy::EdgePixelCount: + lodConfig.strategy = Ogre::PixelCountLodStrategy::getSingletonPtr(); + break; + } + + // Build LOD levels + for (const auto& levelConfig : lodSettings.lodLevels) { + Ogre::LodLevel level; + level.distance = levelConfig.distance; + + if (levelConfig.useManualMesh && !levelConfig.manualMeshName.empty()) { + level.manualMeshName = levelConfig.manualMeshName; + } else { + switch (levelConfig.reductionMethod) { + case LodLevelConfig::ReductionMethod::Proportional: + level.reductionMethod = Ogre::LodLevel::VRM_PROPORTIONAL; + break; + case LodLevelConfig::ReductionMethod::Constant: + level.reductionMethod = Ogre::LodLevel::VRM_CONSTANT; + break; + case LodLevelConfig::ReductionMethod::CollapseCost: + level.reductionMethod = Ogre::LodLevel::VRM_COLLAPSE_COST; + break; + } + level.reductionValue = levelConfig.reductionValue; + } + lodConfig.levels.push_back(level); + } + + // Apply advanced settings + lodConfig.advanced.useCompression = lodSettings.useCompression; + lodConfig.advanced.useVertexNormals = lodSettings.useVertexNormals; + lodConfig.advanced.preventPunchingHoles = lodSettings.preventPunchingHoles; + lodConfig.advanced.preventBreakingLines = lodSettings.preventBreakingLines; + + // Generate LOD levels + lodGenerator->generateLodLevels(lodConfig); + } + } + + // Create a unique entity name for this instance + std::string entityName = region.regionName + "_" + std::to_string(memberEntity.id()) + "_" + member->meshName; + + // Create temporary entity + Ogre::Entity* tempEntity = m_sceneMgr->createEntity(entityName, mesh); + + // Apply material override if specified + if (!member->materialName.empty()) { + tempEntity->setMaterialName(member->materialName); + } + + // Set shadow casting + tempEntity->setCastShadows(member->castShadows); + + // Add to static geometry using global transform + region.staticGeometry->addEntity(tempEntity, + position, + orientation, + scale); + + // Destroy the temporary entity - the geometry is now copied + m_sceneMgr->destroyEntity(tempEntity); + + // Track this member + region.memberEntityIds.push_back(memberEntity.id()); + member->dirty = false; + + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "StaticGeometry: Failed to add mesh " + member->meshName + + ": " + e.getDescription()); + } + } + + // Build the static geometry + try { + region.staticGeometry->build(); + region.built = true; + region.dirty = false; + region.buildVersion++; + + Ogre::LogManager::getSingleton().logMessage( + "StaticGeometry: Built region '" + region.regionName + + "' with " + std::to_string(members.size()) + " entities"); + + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "StaticGeometry: Failed to build region '" + region.regionName + + "': " + e.getDescription()); + destroyOgreStaticGeometry(region); + region.built = false; + region.dirty = true; + } +} + +void StaticGeometrySystem::destroyOgreStaticGeometry(StaticGeometryComponent& region) +{ + if (region.staticGeometry) { + m_sceneMgr->destroyStaticGeometry(region.staticGeometry); + region.staticGeometry = nullptr; + } + region.built = false; +} + +void StaticGeometrySystem::rebuildRegion(flecs::entity regionEntity) +{ + if (!regionEntity.is_alive() || !regionEntity.has()) { + return; + } + + auto& region = regionEntity.get_mut(); + region.markDirty(); + + // Also mark all members as dirty to force refresh + m_memberQuery.each([&](flecs::entity e, StaticGeometryMemberComponent& member) { + if (member.regionEntity == regionEntity) { + member.markDirty(); + } + }); +} + +void StaticGeometrySystem::destroyRegion(flecs::entity regionEntity) +{ + if (!regionEntity.is_alive() || !regionEntity.has()) { + return; + } + + auto& region = regionEntity.get_mut(); + destroyOgreStaticGeometry(region); +} + +Ogre::StaticGeometry* StaticGeometrySystem::getStaticGeometry(flecs::entity regionEntity) +{ + if (!regionEntity.is_alive() || !regionEntity.has()) { + return nullptr; + } + + return regionEntity.get().staticGeometry; +} + +bool StaticGeometrySystem::isRegionDirty(flecs::entity regionEntity) +{ + if (!regionEntity.is_alive() || !regionEntity.has()) { + return false; + } + + const auto& region = regionEntity.get(); + if (region.dirty) return true; + + // Check if any members are dirty + return areMembersDirty(regionEntity); +} + +bool StaticGeometrySystem::areMembersDirty(flecs::entity regionEntity) +{ + bool dirty = false; + m_memberQuery.each([&](flecs::entity e, StaticGeometryMemberComponent& member) { + if (member.regionEntity == regionEntity && member.dirty) { + dirty = true; + } + }); + return dirty; +} + +std::size_t StaticGeometrySystem::getMemberCount(flecs::entity regionEntity) +{ + if (!regionEntity.is_alive() || !regionEntity.has()) { + return 0; + } + + return regionEntity.get().memberEntityIds.size(); +} diff --git a/src/features/editScene/systems/StaticGeometrySystem.hpp b/src/features/editScene/systems/StaticGeometrySystem.hpp new file mode 100644 index 0000000..e983e8e --- /dev/null +++ b/src/features/editScene/systems/StaticGeometrySystem.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +class StaticGeometrySystem { +public: + StaticGeometrySystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~StaticGeometrySystem(); + + // Initialize the system + void initialize(); + + // Main update - processes dirty regions and rebuilds + void update(); + + // Force rebuild of a specific region + void rebuildRegion(flecs::entity regionEntity); + + // Destroy a region's StaticGeometry + void destroyRegion(flecs::entity regionEntity); + + // Get the Ogre::StaticGeometry for a region (if built) + Ogre::StaticGeometry* getStaticGeometry(flecs::entity regionEntity); + + // Check if a region needs rebuild + bool isRegionDirty(flecs::entity regionEntity); + + // Get member count for a region + std::size_t getMemberCount(flecs::entity regionEntity); + +private: + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + // Queries + flecs::query m_regionQuery; + flecs::query m_memberQuery; + + bool m_initialized = false; + + // Build a region's StaticGeometry + void buildRegion(flecs::entity regionEntity, struct StaticGeometryComponent& region); + + // Destroy Ogre StaticGeometry object + void destroyOgreStaticGeometry(struct StaticGeometryComponent& region); + + // Check if any members are dirty + bool areMembersDirty(flecs::entity regionEntity); + + // Validate region exists and is valid + bool isValidRegion(flecs::entity regionEntity); +}; diff --git a/src/features/editScene/ui/ComponentEditor.hpp b/src/features/editScene/ui/ComponentEditor.hpp index 3e2bcf8..efe1f6c 100644 --- a/src/features/editScene/ui/ComponentEditor.hpp +++ b/src/features/editScene/ui/ComponentEditor.hpp @@ -4,6 +4,10 @@ #include #include +// Forward declarations for component editor templates +struct StaticGeometryComponent; +struct StaticGeometryMemberComponent; + /** * Base interface for component editors */ diff --git a/src/features/editScene/ui/StaticGeometryEditor.cpp b/src/features/editScene/ui/StaticGeometryEditor.cpp new file mode 100644 index 0000000..84f6367 --- /dev/null +++ b/src/features/editScene/ui/StaticGeometryEditor.cpp @@ -0,0 +1,99 @@ +#include "StaticGeometryEditor.hpp" +#include "../components/StaticGeometryMember.hpp" +#include "../components/LodSettings.hpp" +#include +#include + +bool StaticGeometryEditor::renderComponent(flecs::entity entity, StaticGeometryComponent ®ion) +{ + bool changed = false; + + ImGui::Text("Region ID: %s", region.regionId.empty() ? "(none)" : region.regionId.c_str()); + + // Region name + char nameBuf[256]; + strncpy(nameBuf, region.regionName.c_str(), sizeof(nameBuf) - 1); + nameBuf[sizeof(nameBuf) - 1] = '\0'; + if (ImGui::InputText("Region Name", nameBuf, sizeof(nameBuf))) { + region.regionName = nameBuf; + region.markDirty(); + changed = true; + } + + ImGui::Separator(); + + // Check LOD consistency + std::string regionLodId; + bool lodConsistent = true; + int lodMemberCount = 0; + + entity.world().query().each([&](flecs::entity e, StaticGeometryMemberComponent& member) { + if (member.regionEntity == entity && !member.lodSettingsId.empty()) { + lodMemberCount++; + if (regionLodId.empty()) { + regionLodId = member.lodSettingsId; + } else if (regionLodId != member.lodSettingsId) { + lodConsistent = false; + } + } + }); + + if (lodMemberCount > 0) { + if (lodConsistent) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "LOD: Consistent (%s)", regionLodId.c_str()); + } else { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "LOD: INCONSISTENT!"); + ImGui::TextWrapped("All members must use the same LOD Settings!"); + } + } + + ImGui::Separator(); + + // Build status + if (region.built) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Built (v%d)", region.buildVersion); + ImGui::Text("Members: %zu", region.memberEntityIds.size()); + } else if (region.dirty) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Needs Rebuild"); + } else { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1), "Status: Not Built"); + } + + ImGui::Separator(); + + // Settings + if (ImGui::Checkbox("Cast Shadows", ®ion.castShadows)) { + region.markDirty(); + changed = true; + } + + if (ImGui::DragFloat("Rendering Distance", ®ion.renderingDistance, 10.0f, 0.0f, 10000.0f)) { + region.markDirty(); + changed = true; + } + if (region.renderingDistance == 0.0f) { + ImGui::SameLine(); + ImGui::TextDisabled("(infinite)"); + } + + if (ImGui::DragFloat("Region Dimensions", ®ion.regionDimensions, 10.0f, 100.0f, 10000.0f)) { + region.markDirty(); + changed = true; + } + + ImGui::Separator(); + + // Manual rebuild button + if (ImGui::Button("Rebuild Now", ImVec2(120, 0))) { + region.markDirty(); + changed = true; + } + ImGui::SameLine(); + if (ImGui::Button("Clear Geometry", ImVec2(120, 0))) { + region.built = false; + region.dirty = true; + changed = true; + } + + return changed; +} diff --git a/src/features/editScene/ui/StaticGeometryEditor.hpp b/src/features/editScene/ui/StaticGeometryEditor.hpp new file mode 100644 index 0000000..265074f --- /dev/null +++ b/src/features/editScene/ui/StaticGeometryEditor.hpp @@ -0,0 +1,18 @@ +#ifndef EDITSCENE_STATICGEOMETRYEDITOR_HPP +#define EDITSCENE_STATICGEOMETRYEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/StaticGeometry.hpp" +#include + +/** + * Editor for StaticGeometryComponent (region) + */ +class StaticGeometryEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, StaticGeometryComponent ®ion) override; + const char *getName() const override { return "StaticGeometry Region"; } +}; + +#endif // EDITSCENE_STATICGEOMETRYEDITOR_HPP diff --git a/src/features/editScene/ui/StaticGeometryMemberEditor.cpp b/src/features/editScene/ui/StaticGeometryMemberEditor.cpp new file mode 100644 index 0000000..2b61639 --- /dev/null +++ b/src/features/editScene/ui/StaticGeometryMemberEditor.cpp @@ -0,0 +1,363 @@ +#include "StaticGeometryMemberEditor.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/LodSettings.hpp" +#include +#include +#include +#include + +StaticGeometryMemberEditor::StaticGeometryMemberEditor(Ogre::SceneManager *sceneMgr) + : m_sceneMgr(sceneMgr) +{ +} + +void StaticGeometryMemberEditor::scanMeshFiles() +{ + if (m_meshesScanned) + return; + + m_meshFiles.clear(); + + // Get all resource groups + Ogre::ResourceGroupManager &rgm = Ogre::ResourceGroupManager::getSingleton(); + Ogre::StringVector groups = rgm.getResourceGroups(); + + for (const auto &group : groups) { + // Get file info for this group + Ogre::FileInfoListPtr fileList = rgm.findResourceFileInfo(group, "*"); + + if (fileList) { + for (const auto &fileInfo : *fileList) { + const std::string &filename = fileInfo.filename; + // Check for supported extensions + if (filename.size() > 5) { + std::string ext = filename.substr(filename.find_last_of('.') + 1); + // Convert to lowercase + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext == "mesh" || ext == "gltf" || ext == "glb") { + m_meshFiles.push_back(filename); + } + } + } + } + } + + // Sort and remove duplicates + std::sort(m_meshFiles.begin(), m_meshFiles.end()); + m_meshFiles.erase(std::unique(m_meshFiles.begin(), m_meshFiles.end()), + m_meshFiles.end()); + + m_meshesScanned = true; +} + +bool StaticGeometryMemberEditor::matchesSearch(const std::string &meshName, + const std::string &search) +{ + if (search.empty()) + return true; + + std::string lowerMesh = meshName; + std::string lowerSearch = search; + std::transform(lowerMesh.begin(), lowerMesh.end(), lowerMesh.begin(), ::tolower); + std::transform(lowerSearch.begin(), lowerSearch.end(), lowerSearch.begin(), ::tolower); + + return lowerMesh.find(lowerSearch) != std::string::npos; +} + +void StaticGeometryMemberEditor::renderMeshBrowser(StaticGeometryMemberComponent &member) +{ + if (ImGui::BeginPopupModal("Mesh Browser (StaticGeometry)", &m_showMeshBrowser, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Search:"); + ImGui::SameLine(); + ImGui::InputText("##search", m_searchBuffer, sizeof(m_searchBuffer)); + + ImGui::Separator(); + + // Show filtered mesh list + ImGui::BeginChild("MeshList", ImVec2(400, 300), true); + + std::string searchStr(m_searchBuffer); + + for (const auto &meshName : m_meshFiles) { + if (!matchesSearch(meshName, searchStr)) + continue; + + // Highlight current selection + bool isCurrent = (member.meshName == meshName); + if (isCurrent) { + ImGui::PushStyleColor(ImGuiCol_Text, + ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + } + + if (ImGui::Selectable(meshName.c_str(), isCurrent)) { + member.meshName = meshName; + member.markDirty(); + } + + if (isCurrent) { + ImGui::PopStyleColor(); + } + } + + if (m_meshFiles.empty()) { + ImGui::TextDisabled("No mesh files found"); + } + + ImGui::EndChild(); + + ImGui::Separator(); + + // Show count + int visibleCount = 0; + for (const auto &meshName : m_meshFiles) { + if (matchesSearch(meshName, searchStr)) + visibleCount++; + } + ImGui::Text("Showing %d of %zu meshes", visibleCount, m_meshFiles.size()); + + // Buttons + if (ImGui::Button("Select", ImVec2(120, 0))) { + m_showMeshBrowser = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + m_showMeshBrowser = false; + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Refresh", ImVec2(120, 0))) { + m_meshesScanned = false; + scanMeshFiles(); + } + + ImGui::EndPopup(); + } +} + +void StaticGeometryMemberEditor::renderRegionSelector(flecs::entity entity, StaticGeometryMemberComponent& member) +{ + // Get the world from the entity + flecs::world world = entity.world(); + + // Collect all region entities + std::vector regionEntities; + std::vector regionIds; + std::vector regionNames; + + int currentIndex = -1; + world.query().each([&](flecs::entity e, StaticGeometryComponent& region) { + regionEntities.push_back(e); + regionIds.push_back(region.regionId); + regionNames.push_back(region.regionName.empty() ? "Region " + std::to_string(e.id()) : region.regionName); + + if (member.regionEntity == e || member.regionId == region.regionId) { + currentIndex = (int)regionEntities.size() - 1; + } + }); + + // Default to first region if none selected + if (currentIndex == -1 && !regionEntities.empty()) { + currentIndex = 0; + member.regionEntity = regionEntities[0]; + member.regionId = regionIds[0]; + member.markDirty(); + } + + // Build combo string + std::string comboItems; + for (size_t i = 0; i < regionNames.size(); ++i) { + if (i > 0) comboItems += '\0'; + comboItems += regionNames[i] + " (" + regionIds[i] + ")"; + } + comboItems += '\0'; + + int newIndex = currentIndex < 0 ? 0 : currentIndex; + if (ImGui::Combo("Region", &newIndex, comboItems.c_str())) { + if (newIndex >= 0 && newIndex < (int)regionEntities.size()) { + member.regionEntity = regionEntities[newIndex]; + member.regionId = regionIds[newIndex]; + member.markDirty(); + } + } + + if (regionEntities.empty()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "No StaticGeometry regions! Create one first."); + } +} + +void StaticGeometryMemberEditor::renderLodSettingsSelector(flecs::entity entity, StaticGeometryMemberComponent& member) +{ + // Get the world from the entity + flecs::world world = entity.world(); + + // Collect all LOD settings entities + std::vector settingsEntities; + std::vector settingsIds; + std::vector displayNames; + + int currentIndex = -1; + int noneIndex = -1; + + // Add "None" option + displayNames.push_back("None (no LOD)"); + settingsEntities.push_back(flecs::entity::null()); + settingsIds.push_back(""); + noneIndex = 0; + + world.query().each([&](flecs::entity e, LodSettingsComponent& settings) { + settingsEntities.push_back(e); + settingsIds.push_back(settings.settingsId); + + std::string displayName = settings.settingsId; + if (displayName.empty()) { + displayName = "LOD Settings " + std::to_string(e.id()); + } + displayNames.push_back(displayName); + + if (settings.settingsId == member.lodSettingsId) { + currentIndex = (int)settingsEntities.size() - 1; + member.lodSettingsEntity = e; + } + }); + + // If no selection but has ID, try to find it + if (currentIndex == -1 && !member.lodSettingsId.empty()) { + for (size_t i = 0; i < settingsIds.size(); ++i) { + if (settingsIds[i] == member.lodSettingsId) { + currentIndex = (int)i; + member.lodSettingsEntity = settingsEntities[i]; + break; + } + } + } + + // Default to "None" if not found + if (currentIndex == -1) { + currentIndex = noneIndex; + } + + // Build combo string + std::string comboItems; + for (size_t i = 0; i < displayNames.size(); ++i) { + if (i > 0) comboItems += '\0'; + comboItems += displayNames[i]; + } + comboItems += '\0'; + + int newIndex = currentIndex; + if (ImGui::Combo("LOD Settings", &newIndex, comboItems.c_str())) { + if (newIndex >= 0 && newIndex < (int)settingsEntities.size()) { + if (newIndex == noneIndex) { + member.lodSettingsId.clear(); + member.lodSettingsEntity = flecs::entity::null(); + } else { + member.lodSettingsId = settingsIds[newIndex]; + member.lodSettingsEntity = settingsEntities[newIndex]; + } + member.markDirty(); + } + } + + // Show warning if no LOD settings available + if (settingsEntities.size() <= 1) { + ImGui::TextDisabled("Create a LOD Settings entity to enable LOD"); + } + + // Show info about selected settings + if (!member.lodSettingsId.empty() && member.lodSettingsEntity.is_alive()) { + const auto& settings = member.lodSettingsEntity.get(); + ImGui::TextDisabled("Levels: %zu, Strategy: %s", + settings.lodLevels.size(), + settings.strategy == LodSettingsComponent::Strategy::Distance ? "Distance" : + settings.strategy == LodSettingsComponent::Strategy::PixelCount ? "Pixel Count" : "Edge Pixel"); + } +} + +bool StaticGeometryMemberEditor::renderComponent(flecs::entity entity, StaticGeometryMemberComponent &member) +{ + bool changed = false; + + // Region selection + renderRegionSelector(entity, member); + + ImGui::Separator(); + + // LOD Settings selection + renderLodSettingsSelector(entity, member); + + ImGui::Separator(); + + // Mesh selection with browser + char meshBuf[256]; + std::strncpy(meshBuf, member.meshName.c_str(), sizeof(meshBuf) - 1); + meshBuf[sizeof(meshBuf) - 1] = '\0'; + + if (ImGui::InputText("Mesh Name", meshBuf, sizeof(meshBuf))) { + member.meshName = meshBuf; + member.markDirty(); + changed = true; + } + + ImGui::SameLine(); + if (ImGui::Button("Browse...")) { + scanMeshFiles(); + m_showMeshBrowser = true; + m_searchBuffer[0] = '\0'; + } + + // Mesh Browser Popup + if (m_showMeshBrowser) { + ImGui::OpenPopup("Mesh Browser (StaticGeometry)"); + } + + renderMeshBrowser(member); + + ImGui::Separator(); + + // Material input (simple text input) + char matBuf[256]; + std::strncpy(matBuf, member.materialName.c_str(), sizeof(matBuf) - 1); + matBuf[sizeof(matBuf) - 1] = '\0'; + + if (ImGui::InputText("Material Override", matBuf, sizeof(matBuf))) { + member.materialName = matBuf; + member.markDirty(); + changed = true; + } + ImGui::TextDisabled("(Empty = use mesh default material)"); + + ImGui::Separator(); + + // Options + if (ImGui::Checkbox("Cast Shadows", &member.castShadows)) { + member.markDirty(); + changed = true; + } + + if (ImGui::Checkbox("Visible", &member.visible)) { + member.markDirty(); + changed = true; + } + + // Info notes + ImGui::Separator(); + ImGui::TextDisabled("Note: Position/Orientation uses Transform component's SceneNode."); + ImGui::TextDisabled("LOD: All members in a region must use the same LOD Settings!"); + + // Status + ImGui::Separator(); + if (member.dirty) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Modified (needs rebuild)"); + } else { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Up to date"); + } + + return changed; +} diff --git a/src/features/editScene/ui/StaticGeometryMemberEditor.hpp b/src/features/editScene/ui/StaticGeometryMemberEditor.hpp new file mode 100644 index 0000000..1e21b39 --- /dev/null +++ b/src/features/editScene/ui/StaticGeometryMemberEditor.hpp @@ -0,0 +1,35 @@ +#ifndef EDITSCENE_STATICGEOMETRYMEMBEREDITOR_HPP +#define EDITSCENE_STATICGEOMETRYMEMBEREDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/StaticGeometryMember.hpp" +#include +#include +#include +#include + +/** + * Editor for StaticGeometryMemberComponent + */ +class StaticGeometryMemberEditor : public ComponentEditor { +public: + StaticGeometryMemberEditor(Ogre::SceneManager *sceneMgr = nullptr); + bool renderComponent(flecs::entity entity, StaticGeometryMemberComponent &member) override; + const char *getName() const override { return "StaticGeometry Member"; } + +private: + void renderRegionSelector(flecs::entity entity, StaticGeometryMemberComponent& member); + void renderLodSettingsSelector(flecs::entity entity, StaticGeometryMemberComponent& member); + void renderMeshBrowser(StaticGeometryMemberComponent &member); + void scanMeshFiles(); + bool matchesSearch(const std::string &meshName, const std::string &search); + + Ogre::SceneManager *m_sceneMgr; + std::vector m_meshFiles; + bool m_meshesScanned = false; + bool m_showMeshBrowser = false; + char m_searchBuffer[256] = {}; +}; + +#endif // EDITSCENE_STATICGEOMETRYMEMBEREDITOR_HPP