From 9e72a48457919ccf4122d8dd42ed83075a5ff31e Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Sat, 4 Apr 2026 00:42:27 +0300 Subject: [PATCH] Fixed LOD support --- src/features/editScene/CMakeLists.txt | 12 +- src/features/editScene/EditorApp.cpp | 16 ++ src/features/editScene/EditorApp.hpp | 2 + src/features/editScene/components/Lod.hpp | 47 ++++ .../editScene/components/LodModule.cpp | 56 ++++ .../editScene/components/LodSettings.hpp | 107 ++++++++ .../editScene/systems/EditorUISystem.cpp | 20 ++ src/features/editScene/systems/LodSystem.cpp | 250 ++++++++++++++++++ src/features/editScene/systems/LodSystem.hpp | 60 +++++ .../editScene/systems/SceneSerializer.cpp | 195 ++++++++++++++ .../editScene/systems/SceneSerializer.hpp | 4 + src/features/editScene/ui/LodEditor.cpp | 149 +++++++++++ src/features/editScene/ui/LodEditor.hpp | 22 ++ .../editScene/ui/LodSettingsEditor.cpp | 199 ++++++++++++++ .../editScene/ui/LodSettingsEditor.hpp | 20 ++ 15 files changed, 1158 insertions(+), 1 deletion(-) create mode 100644 src/features/editScene/components/Lod.hpp create mode 100644 src/features/editScene/components/LodModule.cpp create mode 100644 src/features/editScene/components/LodSettings.hpp create mode 100644 src/features/editScene/systems/LodSystem.cpp create mode 100644 src/features/editScene/systems/LodSystem.hpp create mode 100644 src/features/editScene/ui/LodEditor.cpp create mode 100644 src/features/editScene/ui/LodEditor.hpp create mode 100644 src/features/editScene/ui/LodSettingsEditor.cpp create mode 100644 src/features/editScene/ui/LodSettingsEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index b8b2221..553ed05 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -1,7 +1,7 @@ project(editScene) set(CMAKE_CXX_STANDARD 17) -find_package(OGRE REQUIRED COMPONENTS Bites Overlay CONFIG) +find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator CONFIG) find_package(flecs REQUIRED CONFIG) find_package(nlohmann_json REQUIRED) find_package(SDL2 REQUIRED) @@ -16,15 +16,19 @@ set(EDITSCENE_SOURCES systems/PhysicsSystem.cpp systems/LightSystem.cpp systems/CameraSystem.cpp + systems/LodSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp ui/RigidBodyEditor.cpp ui/LightEditor.cpp ui/CameraEditor.cpp + ui/LodEditor.cpp + ui/LodSettingsEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp + components/LodModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -40,11 +44,14 @@ set(EDITSCENE_HEADERS components/RigidBody.hpp components/Light.hpp components/Camera.hpp + components/Lod.hpp + components/LodSettings.hpp systems/EditorUISystem.hpp systems/SceneSerializer.hpp systems/PhysicsSystem.hpp systems/LightSystem.hpp systems/CameraSystem.hpp + systems/LodSystem.hpp ui/ComponentEditor.hpp ui/ComponentRegistry.hpp ui/ComponentRegistration.hpp @@ -54,6 +61,8 @@ set(EDITSCENE_HEADERS ui/RigidBodyEditor.hpp ui/LightEditor.hpp ui/CameraEditor.hpp + ui/LodEditor.hpp + ui/LodSettingsEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp physics/physics.h @@ -68,6 +77,7 @@ target_link_libraries(editSceneEditor OgreMain OgreBites OgreOverlay + OgreMeshLodGenerator flecs::flecs_static nlohmann_json::nlohmann_json Jolt::Jolt diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 9aef223..79bf3c2 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -4,6 +4,7 @@ #include "systems/PhysicsSystem.hpp" #include "systems/LightSystem.hpp" #include "systems/CameraSystem.hpp" +#include "systems/LodSystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -13,6 +14,8 @@ #include "components/RigidBody.hpp" #include "components/Light.hpp" #include "components/Camera.hpp" +#include "components/Lod.hpp" +#include "components/LodSettings.hpp" #include #include @@ -121,6 +124,10 @@ void EditorApp::setup() // Setup camera system m_cameraSystem = std::make_unique(m_world, m_sceneMgr); + + // Setup LOD system + m_lodSystem = std::make_unique(m_world, m_sceneMgr); + m_lodSystem->initialize(); // Add default entities to UI cache for (auto &e : m_defaultEntities) { @@ -157,6 +164,10 @@ void EditorApp::setupECS() // Register light and camera components m_world.component(); m_world.component(); + + // Register LOD components + m_world.component(); + m_world.component(); } void EditorApp::createDefaultEntities() @@ -294,6 +305,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) if (m_cameraSystem) { m_cameraSystem->update(); } + + // Update LOD system + if (m_lodSystem) { + m_lodSystem->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 17f01e6..93ba53a 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -16,6 +16,7 @@ class EditorCamera; class EditorPhysicsSystem; class EditorLightSystem; class EditorCameraSystem; +class EditorLodSystem; /** * RenderTargetListener for ImGui frame management @@ -84,6 +85,7 @@ private: std::unique_ptr m_physicsSystem; std::unique_ptr m_lightSystem; std::unique_ptr m_cameraSystem; + std::unique_ptr m_lodSystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/Lod.hpp b/src/features/editScene/components/Lod.hpp new file mode 100644 index 0000000..cc2003f --- /dev/null +++ b/src/features/editScene/components/Lod.hpp @@ -0,0 +1,47 @@ +#ifndef EDITSCENE_LOD_HPP +#define EDITSCENE_LOD_HPP +#pragma once + +#include +#include +#include +#include "LodSettings.hpp" + +/** + * LodComponent - Per-entity LOD configuration + * + * This component references a LodSettingsComponent by its settingsId + * for persistent identification across scene loads. + */ +struct LodComponent { + // Reference to the settings by ID (persistent across scene loads) + std::string settingsId; + + // Runtime entity reference (resolved from settingsId) + flecs::entity settingsEntity = flecs::entity::null(); + + // Per-entity distance multiplier (scales all LOD distances) + float distanceMultiplier = 1.0f; + + // Whether LOD has been applied to the mesh + bool lodApplied = false; + + // Last settings version that was applied + uint32_t appliedVersion = 0; + + // Dirty flag for regenerating LOD + bool dirty = true; + + void markDirty() { dirty = true; } + + // Helper to check if LOD needs to be regenerated + bool needsUpdate() const { return dirty; } + + // Helper to check if settings reference is valid + bool hasValidSettings() const { + return settingsEntity.is_alive() && + settingsEntity.has(); + } +}; + +#endif // EDITSCENE_LOD_HPP diff --git a/src/features/editScene/components/LodModule.cpp b/src/features/editScene/components/LodModule.cpp new file mode 100644 index 0000000..9cdc155 --- /dev/null +++ b/src/features/editScene/components/LodModule.cpp @@ -0,0 +1,56 @@ +#include "Lod.hpp" +#include "LodSettings.hpp" +#include "Renderable.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/LodEditor.hpp" +#include "../ui/LodSettingsEditor.hpp" + +// Register LodSettings component +REGISTER_COMPONENT("LOD Settings", LodSettingsComponent, LodSettingsEditor) +{ + registry.registerComponent( + "LOD Settings", + std::make_unique(), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + auto settings = LodSettingsComponent(); + // Create default LOD levels + settings.createDefaultLevels(); + e.set(settings); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Register Lod component +REGISTER_COMPONENT("LOD", LodComponent, LodEditor) +{ + registry.registerComponent( + "LOD", + std::make_unique(), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + // LOD requires Renderable + if (!e.has()) { + // Auto-add renderable if missing (will need mesh assignment later) + e.set({}); + } + e.set({}); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/components/LodSettings.hpp b/src/features/editScene/components/LodSettings.hpp new file mode 100644 index 0000000..059ef5d --- /dev/null +++ b/src/features/editScene/components/LodSettings.hpp @@ -0,0 +1,107 @@ +#ifndef EDITSCENE_LODSETTINGS_HPP +#define EDITSCENE_LODSETTINGS_HPP +#pragma once + +#include +#include +#include + +/** + * LOD Level configuration - represents one LOD level + */ +struct LodLevelConfig { + // Distance at which to switch to this LOD level + float distance = 100.0f; + + // Reduction method + enum class ReductionMethod { + Proportional, // 0.0-1.0 percentage of vertices to remove + Constant, // Exact vertex count to remove + CollapseCost // Collapse cost threshold + }; + ReductionMethod reductionMethod = ReductionMethod::Proportional; + + // Reduction value (meaning depends on method) + float reductionValue = 0.5f; // 50% reduction by default + + // Optional: manual mesh name (if using manual LOD) + std::string manualMeshName; + + bool useManualMesh = false; +}; + +/** + * LodSettingsComponent - Shareable LOD configuration + * + * This component can be attached to a "settings" entity and referenced + * by multiple entities with LodComponent for shared LOD settings. + */ +struct LodSettingsComponent { + // Unique identifier for this LOD settings (for referencing across scenes) + std::string settingsId; + + // Strategy for determining LOD level based on distance + enum class Strategy { + Distance, // Distance from camera + PixelCount, // Screen-space pixel count + EdgePixelCount // Screen-space edge pixel count + }; + Strategy strategy = Strategy::Distance; + + // LOD levels (from highest quality to lowest) + // Level 0 is always the original mesh + std::vector lodLevels; + + // Advanced options + bool useCompression = true; // Compress index buffers + bool useVertexNormals = true; // Use normals for quality + bool preventPunchingHoles = false; // Prevent destroying triangles + bool preventBreakingLines = false; // Prevent destroying lines + bool useBackgroundQueue = false; // Generate LODs in background + + float outsideWeight = 0.0f; // Weight for outside faces (0 = disabled) + float outsideWalkAngle = 0.0f; // Angle for outside walking (-1 to 1) + + // Flag to track if settings have been modified + bool dirty = true; + + // Version counter for tracking changes + uint32_t version = 1; + + void markDirty() { + dirty = true; + version++; + } + + // Helper to add a proportional reduction level + void addProportionalLevel(float distance, float reductionPercent) { + LodLevelConfig level; + level.distance = distance; + level.reductionMethod = LodLevelConfig::ReductionMethod::Proportional; + level.reductionValue = reductionPercent; + lodLevels.push_back(level); + markDirty(); + } + + // Helper to add a constant reduction level + void addConstantLevel(float distance, int verticesToRemove) { + LodLevelConfig level; + level.distance = distance; + level.reductionMethod = LodLevelConfig::ReductionMethod::Constant; + level.reductionValue = static_cast(verticesToRemove); + lodLevels.push_back(level); + markDirty(); + } + + // Helper to create default LOD levels for a mesh + void createDefaultLevels(float baseDistance = 100.0f) { + lodLevels.clear(); + // LOD 1: 50% reduction at base distance + addProportionalLevel(baseDistance, 0.5f); + // LOD 2: 75% reduction at 2x distance + addProportionalLevel(baseDistance * 2.0f, 0.75f); + markDirty(); + } +}; + +#endif // EDITSCENE_LODSETTINGS_HPP diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 22f6a70..c8ba4ae 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -7,6 +7,8 @@ #include "../components/RigidBody.hpp" #include "../components/Light.hpp" #include "../components/Camera.hpp" +#include "../components/Lod.hpp" +#include "../components/LodSettings.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" #include "../ui/PhysicsColliderEditor.hpp" @@ -361,6 +363,10 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) indicators += " [RB]"; if (entity.has()) indicators += " [C]"; + if (entity.has()) + indicators += " [LODS]"; + if (entity.has()) + indicators += " [LOD]"; snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(), indicators.c_str(), (unsigned long long)entity.id()); @@ -513,6 +519,20 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, collider); componentCount++; } + + // Render LOD Settings if present + if (entity.has()) { + auto &lodSettings = entity.get_mut(); + m_componentRegistry.render(entity, lodSettings); + componentCount++; + } + + // Render LOD if present + if (entity.has()) { + auto &lod = entity.get_mut(); + m_componentRegistry.render(entity, lod); + componentCount++; + } // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/LodSystem.cpp b/src/features/editScene/systems/LodSystem.cpp new file mode 100644 index 0000000..d85dfe4 --- /dev/null +++ b/src/features/editScene/systems/LodSystem.cpp @@ -0,0 +1,250 @@ +#include "LodSystem.hpp" +#include "../components/Lod.hpp" +#include "../components/LodSettings.hpp" +#include "../components/Renderable.hpp" +#include +#include +#include +#include + +EditorLodSystem::EditorLodSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_lodQuery(world.query()) + , m_settingsQuery(world.query()) +{ +} + +EditorLodSystem::~EditorLodSystem() = default; + +void EditorLodSystem::initialize() +{ + if (m_initialized) + return; + + // Create the LOD generator singleton + m_lodGenerator = std::make_unique(); + + m_initialized = true; +} + +void EditorLodSystem::update() +{ + if (!m_initialized) + return; + + // Process entities with LOD components + m_lodQuery.each([&](flecs::entity entity, LodComponent &lod, + RenderableComponent &renderable) { + // Try to resolve settings entity if we have an ID but no valid entity + if (!lod.settingsId.empty() && !lod.hasValidSettings()) { + m_world.query().each( + [&](flecs::entity e, + LodSettingsComponent &settings) { + if (settings.settingsId == + lod.settingsId) { + lod.settingsEntity = e; + } + }); + } + + // Fallback: if we have a valid settingsEntity but no settingsId, get the ID from the settings + if (lod.settingsId.empty() && lod.settingsEntity.is_alive() && + lod.settingsEntity.has()) { + const auto &settings = + lod.settingsEntity.get(); + if (!settings.settingsId.empty()) { + lod.settingsId = settings.settingsId; + } + } + + // Final fallback: if settingsId is still empty, try to use any available LOD settings + if (lod.settingsId.empty() && !lod.hasValidSettings()) { + m_world.query().each( + [&](flecs::entity e, + LodSettingsComponent &settings) { + if (lod.settingsEntity == + flecs::entity::null()) { + lod.settingsEntity = e; + lod.settingsId = + settings.settingsId; + } + }); + } + + // Check if we need to apply/update LOD + bool needsUpdate = lod.dirty; + + // Check if settings have been modified + if (lod.hasValidSettings()) { + const auto &settings = + lod.settingsEntity.get(); + if (settings.version != lod.appliedVersion) { + needsUpdate = true; + } + } + + // Check if mesh changed + if (!lod.lodApplied && !renderable.meshName.empty()) { + needsUpdate = true; + } + + if (needsUpdate) { + applyLod(entity, lod, renderable); + } + }); +} + +void EditorLodSystem::applyLod(flecs::entity entity, LodComponent &lod, + RenderableComponent &renderable) +{ + // Skip if no settings entity + if (!lod.settingsEntity.is_alive() || + !lod.settingsEntity.has()) { + lod.lodApplied = false; + lod.dirty = false; + return; + } + + // Get the settings + const auto &settings = lod.settingsEntity.get(); + + // Skip if no LOD levels configured + if (settings.lodLevels.empty()) { + lod.lodApplied = false; + lod.dirty = false; + return; + } + + // Need a loaded mesh + if (renderable.meshName.empty() || !renderable.entity) { + lod.dirty = false; // Will retry when mesh is loaded + return; + } + + try { + Ogre::MeshPtr mesh = renderable.entity->getMesh(); + if (!mesh || !mesh->isLoaded()) { + lod.dirty = false; + return; + } + + // Build Ogre LOD config + Ogre::LodConfig lodConfig; + buildOgreLodConfig(mesh, settings, lodConfig); + + // Apply distance multiplier + if (lod.distanceMultiplier != 1.0f) { + for (auto &level : lodConfig.levels) { + level.distance *= lod.distanceMultiplier; + } + } + + // Generate LOD levels + Ogre::LogManager::getSingleton().logMessage( + "Generating LOD levels for: " + renderable.meshName); + m_lodGenerator->generateLodLevels(lodConfig); + + lod.lodApplied = true; + lod.appliedVersion = settings.version; + lod.dirty = false; + + } catch (const Ogre::Exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "ERROR: Failed to generate LOD for " + + renderable.meshName + ": " + e.getDescription()); + lod.dirty = false; // Don't retry immediately + } +} + +void EditorLodSystem::buildOgreLodConfig(Ogre::MeshPtr mesh, + const LodSettingsComponent &settings, + Ogre::LodConfig &outConfig) +{ + outConfig.mesh = mesh; + + // Set strategy + switch (settings.strategy) { + case LodSettingsComponent::Strategy::Distance: + outConfig.strategy = + Ogre::DistanceLodBoxStrategy::getSingletonPtr(); + break; + case LodSettingsComponent::Strategy::PixelCount: + outConfig.strategy = + Ogre::PixelCountLodStrategy::getSingletonPtr(); + break; + case LodSettingsComponent::Strategy::EdgePixelCount: + // Fallback to pixel count + outConfig.strategy = + Ogre::PixelCountLodStrategy::getSingletonPtr(); + break; + } + + // Build LOD levels + outConfig.levels.clear(); + for (const auto &levelConfig : settings.lodLevels) { + Ogre::LodLevel level; + level.distance = levelConfig.distance; + + if (levelConfig.useManualMesh && + !levelConfig.manualMeshName.empty()) { + level.manualMeshName = levelConfig.manualMeshName; + } else { + // Set reduction method + 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; + default: + level.reductionMethod = + Ogre::LodLevel::VRM_PROPORTIONAL; + break; + } + level.reductionValue = levelConfig.reductionValue; + } + + outConfig.levels.push_back(level); + } + + // Advanced settings + outConfig.advanced.useCompression = settings.useCompression; + outConfig.advanced.useVertexNormals = settings.useVertexNormals; + outConfig.advanced.preventPunchingHoles = settings.preventPunchingHoles; + outConfig.advanced.preventBreakingLines = settings.preventBreakingLines; + outConfig.advanced.useBackgroundQueue = settings.useBackgroundQueue; + outConfig.advanced.outsideWeight = settings.outsideWeight; + outConfig.advanced.outsideWalkAngle = settings.outsideWalkAngle; +} + +void EditorLodSystem::regenerateLodForSettings(flecs::entity settingsEntity) +{ + if (!settingsEntity.is_alive() || + !settingsEntity.has()) { + return; + } + + // Mark all LOD components using these settings as dirty + m_lodQuery.each([&](flecs::entity entity, LodComponent &lod, + RenderableComponent &renderable) { + if (lod.settingsEntity == settingsEntity) { + lod.markDirty(); + } + }); +} + +bool EditorLodSystem::hasLodGenerated(Ogre::MeshPtr mesh) const +{ + if (!mesh) + return false; + return mesh->getNumLodLevels() > 1; +} diff --git a/src/features/editScene/systems/LodSystem.hpp b/src/features/editScene/systems/LodSystem.hpp new file mode 100644 index 0000000..0918f6d --- /dev/null +++ b/src/features/editScene/systems/LodSystem.hpp @@ -0,0 +1,60 @@ +#ifndef EDITSCENE_LODSYSTEM_HPP +#define EDITSCENE_LODSYSTEM_HPP +#pragma once + +#include +#include +#include + +/** + * LOD System - manages LOD generation for meshes + * + * This system: + * 1. Tracks entities with LodComponent and RenderableComponent + * 2. Applies LOD settings from LodSettingsComponent when meshes are loaded + * 3. Regenerates LOD when settings change + */ +class EditorLodSystem { +public: + EditorLodSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~EditorLodSystem(); + + // Initialize the LOD generator + void initialize(); + + // Update - process pending LOD operations + void update(); + + // Force regenerate LOD for all meshes using specific settings + void regenerateLodForSettings(flecs::entity settingsEntity); + + // Check if a mesh has LOD generated + bool hasLodGenerated(Ogre::MeshPtr mesh) const; + +private: + // Apply LOD to a renderable entity + void applyLod(flecs::entity entity, + class LodComponent& lod, + class RenderableComponent& renderable); + + // Convert our config to Ogre::LodConfig + void buildOgreLodConfig(Ogre::MeshPtr mesh, + const class LodSettingsComponent& settings, + Ogre::LodConfig& outConfig); + + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + // The Ogre LOD generator + std::unique_ptr m_lodGenerator; + + // Query for entities with LOD and renderable + flecs::query m_lodQuery; + + // Query for settings entities + flecs::query m_settingsQuery; + + bool m_initialized = false; +}; + +#endif // EDITSCENE_LODSYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 6845866..96142e4 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -7,6 +7,8 @@ #include "../components/PhysicsCollider.hpp" #include "../components/Light.hpp" #include "../components/Camera.hpp" +#include "../components/Lod.hpp" +#include "../components/LodSettings.hpp" #include "EditorUISystem.hpp" #include #include @@ -84,6 +86,8 @@ bool SceneSerializer::loadFromFile(const std::string& filepath, EditorUISystem* // Load entities if (scene.contains("entities") && scene["entities"].is_array()) { + Ogre::LogManager::getSingleton().logMessage( + "SceneSerializer: Loading " + Ogre::StringConverter::toString(scene["entities"].size()) + " entities"); for (const auto& entityJson : scene["entities"]) { deserializeEntity(entityJson, flecs::entity::null(), uiSystem); } @@ -133,6 +137,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["camera"] = serializeCamera(entity); } + if (entity.has()) { + json["lodSettings"] = serializeLodSettings(entity); + } + + if (entity.has()) { + json["lod"] = serializeLod(entity); + } + // Serialize children json["children"] = nlohmann::json::array(); entity.children([&](flecs::entity child) { @@ -192,6 +204,16 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeCamera(entity, json["camera"]); } + if (json.contains("lodSettings")) { + Ogre::LogManager::getSingleton().logMessage("SceneSerializer: Found lodSettings for entity " + Ogre::StringConverter::toString(entity.id())); + deserializeLodSettings(entity, json["lodSettings"]); + } + + if (json.contains("lod")) { + Ogre::LogManager::getSingleton().logMessage("SceneSerializer: Found lod for entity " + Ogre::StringConverter::toString(entity.id())); + deserializeLod(entity, json["lod"]); + } + // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); @@ -664,3 +686,176 @@ void SceneSerializer::deserializeCamera(flecs::entity entity, const nlohmann::js entity.set(camera); } + + +nlohmann::json SceneSerializer::serializeLodSettings(flecs::entity entity) +{ + auto& settings = entity.get(); + nlohmann::json json; + + // Serialize settings ID + json["settingsId"] = settings.settingsId; + + // Serialize strategy + switch (settings.strategy) { + case LodSettingsComponent::Strategy::Distance: + json["strategy"] = "distance"; + break; + case LodSettingsComponent::Strategy::PixelCount: + json["strategy"] = "pixelCount"; + break; + case LodSettingsComponent::Strategy::EdgePixelCount: + json["strategy"] = "edgePixelCount"; + break; + } + + // Serialize LOD levels + json["lodLevels"] = nlohmann::json::array(); + for (const auto& level : settings.lodLevels) { + nlohmann::json levelJson; + levelJson["distance"] = level.distance; + + switch (level.reductionMethod) { + case LodLevelConfig::ReductionMethod::Proportional: + levelJson["reductionMethod"] = "proportional"; + break; + case LodLevelConfig::ReductionMethod::Constant: + levelJson["reductionMethod"] = "constant"; + break; + case LodLevelConfig::ReductionMethod::CollapseCost: + levelJson["reductionMethod"] = "collapseCost"; + break; + } + + levelJson["reductionValue"] = level.reductionValue; + levelJson["useManualMesh"] = level.useManualMesh; + levelJson["manualMeshName"] = level.manualMeshName; + + json["lodLevels"].push_back(levelJson); + } + + // Serialize advanced options + json["useCompression"] = settings.useCompression; + json["useVertexNormals"] = settings.useVertexNormals; + json["preventPunchingHoles"] = settings.preventPunchingHoles; + json["preventBreakingLines"] = settings.preventBreakingLines; + json["useBackgroundQueue"] = settings.useBackgroundQueue; + json["outsideWeight"] = settings.outsideWeight; + json["outsideWalkAngle"] = settings.outsideWalkAngle; + + return json; +} + +nlohmann::json SceneSerializer::serializeLod(flecs::entity entity) +{ + auto& lod = entity.get(); + nlohmann::json json; + + // Serialize settings ID (persistent across scene loads) + json["settingsId"] = lod.settingsId; + + json["distanceMultiplier"] = lod.distanceMultiplier; + + return json; +} + +void SceneSerializer::deserializeLodSettings(flecs::entity entity, const nlohmann::json& json) +{ + LodSettingsComponent settings; + + // Deserialize settings ID + settings.settingsId = json.value("settingsId", ""); + Ogre::LogManager::getSingleton().logMessage( + "Deserialize LodSettings: loaded settingsId=" + settings.settingsId); + + // Auto-generate ID if empty + if (settings.settingsId.empty()) { + settings.settingsId = "lod_settings_" + std::to_string(entity.id()); + Ogre::LogManager::getSingleton().logMessage( + "Deserialize LodSettings: auto-generated settingsId=" + settings.settingsId); + } + + // Deserialize strategy + std::string strategy = json.value("strategy", "distance"); + if (strategy == "distance") { + settings.strategy = LodSettingsComponent::Strategy::Distance; + } else if (strategy == "pixelCount") { + settings.strategy = LodSettingsComponent::Strategy::PixelCount; + } else if (strategy == "edgePixelCount") { + settings.strategy = LodSettingsComponent::Strategy::EdgePixelCount; + } + + // Deserialize LOD levels + settings.lodLevels.clear(); + if (json.contains("lodLevels") && json["lodLevels"].is_array()) { + for (const auto& levelJson : json["lodLevels"]) { + LodLevelConfig level; + level.distance = levelJson.value("distance", 100.0f); + + std::string method = levelJson.value("reductionMethod", "proportional"); + if (method == "proportional") { + level.reductionMethod = LodLevelConfig::ReductionMethod::Proportional; + } else if (method == "constant") { + level.reductionMethod = LodLevelConfig::ReductionMethod::Constant; + } else if (method == "collapseCost") { + level.reductionMethod = LodLevelConfig::ReductionMethod::CollapseCost; + } + + level.reductionValue = levelJson.value("reductionValue", 0.5f); + level.useManualMesh = levelJson.value("useManualMesh", false); + level.manualMeshName = levelJson.value("manualMeshName", ""); + + settings.lodLevels.push_back(level); + } + } + + // Deserialize advanced options + settings.useCompression = json.value("useCompression", true); + settings.useVertexNormals = json.value("useVertexNormals", true); + settings.preventPunchingHoles = json.value("preventPunchingHoles", false); + settings.preventBreakingLines = json.value("preventBreakingLines", false); + settings.useBackgroundQueue = json.value("useBackgroundQueue", false); + settings.outsideWeight = json.value("outsideWeight", 0.0f); + settings.outsideWalkAngle = json.value("outsideWalkAngle", 0.0f); + + // Mark as dirty so LOD system will apply settings + settings.markDirty(); + + entity.set(settings); +} + +void SceneSerializer::deserializeLod(flecs::entity entity, const nlohmann::json& json) +{ + LodComponent lod; + + // Deserialize settings ID + lod.settingsId = json.value("settingsId", ""); + Ogre::LogManager::getSingleton().logMessage( + "Deserialize LOD: settingsId=" + lod.settingsId); + + // Try to find the settings entity by ID + if (!lod.settingsId.empty()) { + m_world.query().each([&](flecs::entity e, LodSettingsComponent& settings) { + Ogre::LogManager::getSingleton().logMessage( + "Deserialize LOD: checking settings entity " + + Ogre::StringConverter::toString(e.id()) + + " with settingsId=" + settings.settingsId); + if (settings.settingsId == lod.settingsId) { + Ogre::LogManager::getSingleton().logMessage( + "Deserialize LOD: MATCH FOUND!"); + lod.settingsEntity = e; + } + }); + } + + lod.distanceMultiplier = json.value("distanceMultiplier", 1.0f); + lod.lodApplied = false; + lod.dirty = true; + + entity.set(lod); + + Ogre::LogManager::getSingleton().logMessage( + "Deserialize LOD: Final settingsEntity=" + + Ogre::StringConverter::toString(lod.settingsEntity.id()) + + ", hasValid=" + (lod.hasValidSettings() ? "true" : "false")); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 03fd5fd..6cf8433 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -46,6 +46,8 @@ private: nlohmann::json serializeCollider(flecs::entity entity); nlohmann::json serializeLight(flecs::entity entity); nlohmann::json serializeCamera(flecs::entity entity); + nlohmann::json serializeLodSettings(flecs::entity entity); + nlohmann::json serializeLod(flecs::entity entity); // Component deserialization void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); @@ -55,6 +57,8 @@ private: void deserializeCollider(flecs::entity entity, const nlohmann::json& json); void deserializeLight(flecs::entity entity, const nlohmann::json& json); 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); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/ui/LodEditor.cpp b/src/features/editScene/ui/LodEditor.cpp new file mode 100644 index 0000000..cac5389 --- /dev/null +++ b/src/features/editScene/ui/LodEditor.cpp @@ -0,0 +1,149 @@ +#include "LodEditor.hpp" +#include "../components/LodSettings.hpp" +#include +#include +#include + +void LodEditor::resolveSettingsEntity(LodComponent& lod) +{ + // If we have a settingsId but no valid entity, try to resolve it + if (!lod.settingsId.empty() && !lod.hasValidSettings()) { + // This would need access to the world to query - handled in renderSettingsSelector + } +} + +void LodEditor::renderSettingsSelector(flecs::entity entity, LodComponent& lod) +{ + // Get the world from the entity + flecs::world world = entity.world(); + + // Collect all entities with LodSettingsComponent + std::vector settingsEntities; + std::vector settingsIds; + std::vector displayNames; + int currentIndex = -1; + + world.query().each([&](flecs::entity e, LodSettingsComponent& settings) { + settingsEntities.push_back(e); + settingsIds.push_back(settings.settingsId); + + // Build display name + std::string displayName = settings.settingsId; + if (displayName.empty()) { + displayName = "LOD Settings " + std::to_string(e.id()); + } + displayNames.push_back(displayName); + + // Check if this is the currently selected settings + if (settings.settingsId == lod.settingsId) { + currentIndex = static_cast(settingsEntities.size()) - 1; + lod.settingsEntity = e; // Update runtime reference + } + }); + + if (settingsEntities.empty()) { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "No LOD Settings entities found!"); + ImGui::Text("Create an entity with LOD Settings component first."); + return; + } + + // Build combo items string (null-terminated) + std::string comboItems; + for (size_t i = 0; i < displayNames.size(); i++) { + if (i > 0) comboItems += '\0'; + comboItems += displayNames[i]; + } + comboItems += '\0'; + + // Show "None" if no selection + if (currentIndex < 0) { + // Try to find by entity reference as fallback + for (size_t i = 0; i < settingsEntities.size(); i++) { + if (settingsEntities[i] == lod.settingsEntity) { + currentIndex = static_cast(i); + lod.settingsId = settingsIds[i]; // Update ID from entity + break; + } + } + + // If still not found, show -1 (None) + if (currentIndex < 0) { + // Prepend "None" option + comboItems = "None\0" + comboItems; + currentIndex = 0; + } + } + + int newIndex = currentIndex; + if (ImGui::Combo("LOD Settings", &newIndex, comboItems.c_str())) { + if (newIndex >= 0 && newIndex < static_cast(settingsEntities.size())) { + lod.settingsId = settingsIds[newIndex]; + lod.settingsEntity = settingsEntities[newIndex]; + lod.markDirty(); + } else if (newIndex == 0 && currentIndex < 0) { + // Selected "None" + lod.settingsId.clear(); + lod.settingsEntity = flecs::entity::null(); + lod.markDirty(); + } + } + + // Show info about selected settings + if (lod.hasValidSettings()) { + const auto& settings = lod.settingsEntity.get(); + ImGui::Text("Levels: %zu", settings.lodLevels.size()); + ImGui::Text("Strategy: %s", + settings.strategy == LodSettingsComponent::Strategy::Distance ? "Distance" : + settings.strategy == LodSettingsComponent::Strategy::PixelCount ? "Pixel Count" : "Edge Pixel"); + } + + // Show the persistent ID + if (!lod.settingsId.empty()) { + ImGui::TextDisabled("Settings ID: %s", lod.settingsId.c_str()); + } +} + +bool LodEditor::renderComponent(flecs::entity entity, LodComponent &lod) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("LOD", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Settings selector with combo box + renderSettingsSelector(entity, lod); + + ImGui::Separator(); + + // Distance multiplier + if (ImGui::DragFloat("Distance Multiplier", &lod.distanceMultiplier, 0.1f, 0.1f, 10.0f)) { + lod.markDirty(); + modified = true; + } + ImGui::SetItemTooltip("Scales all LOD distances for this entity"); + + ImGui::Separator(); + + // Status + if (lod.lodApplied) { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "LOD Applied"); + ImGui::Text("Settings Version: %u", lod.appliedVersion); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "LOD Not Applied"); + } + + if (lod.dirty) { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "(update pending)"); + } + + // Force regenerate button + if (ImGui::Button("Force Regenerate LOD")) { + lod.markDirty(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/LodEditor.hpp b/src/features/editScene/ui/LodEditor.hpp new file mode 100644 index 0000000..70520d2 --- /dev/null +++ b/src/features/editScene/ui/LodEditor.hpp @@ -0,0 +1,22 @@ +#ifndef EDITSCENE_LODEDITOR_HPP +#define EDITSCENE_LODEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/Lod.hpp" +#include + +/** + * Editor for LodComponent with combo box selector for LOD Settings + */ +class LodEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, LodComponent &lod) override; + const char *getName() const override { return "LOD"; } + +private: + void renderSettingsSelector(flecs::entity entity, LodComponent& lod); + void resolveSettingsEntity(LodComponent& lod); +}; + +#endif // EDITSCENE_LODEDITOR_HPP diff --git a/src/features/editScene/ui/LodSettingsEditor.cpp b/src/features/editScene/ui/LodSettingsEditor.cpp new file mode 100644 index 0000000..495d1ea --- /dev/null +++ b/src/features/editScene/ui/LodSettingsEditor.cpp @@ -0,0 +1,199 @@ +#include "LodSettingsEditor.hpp" +#include +#include + +void LodSettingsEditor::renderLodLevel(LodLevelConfig& level, int index) +{ + ImGui::PushID(index); + + if (ImGui::TreeNode("Level", "LOD Level %d", index + 1)) { + // Distance + if (ImGui::DragFloat("Distance", &level.distance, 1.0f, 0.0f, 10000.0f)) { + // Mark settings as dirty through parent + } + + // Manual mesh option + if (ImGui::Checkbox("Use Manual Mesh", &level.useManualMesh)) { + // + } + + if (level.useManualMesh) { + char meshName[256]; + std::strncpy(meshName, level.manualMeshName.c_str(), sizeof(meshName) - 1); + meshName[sizeof(meshName) - 1] = '\0'; + if (ImGui::InputText("Mesh Name", meshName, sizeof(meshName))) { + level.manualMeshName = meshName; + } + } else { + // Reduction method + const char* methods[] = { "Proportional", "Constant", "Collapse Cost" }; + int currentMethod = static_cast(level.reductionMethod); + if (ImGui::Combo("Method", ¤tMethod, methods, IM_ARRAYSIZE(methods))) { + level.reductionMethod = static_cast(currentMethod); + } + + // Reduction value + switch (level.reductionMethod) { + case LodLevelConfig::ReductionMethod::Proportional: + if (ImGui::SliderFloat("Reduction %", &level.reductionValue, 0.0f, 1.0f, "%.2f")) { + // + } + ImGui::TextDisabled("0.0 = no reduction, 1.0 = all vertices removed"); + break; + + case LodLevelConfig::ReductionMethod::Constant: + if (ImGui::DragInt("Vertices to Remove", reinterpret_cast(&level.reductionValue), 1, 0, 100000)) { + // + } + break; + + case LodLevelConfig::ReductionMethod::CollapseCost: + if (ImGui::DragFloat("Max Collapse Cost", &level.reductionValue, 0.1f, 0.0f, 1000.0f)) { + // + } + break; + } + } + + // Remove button + if (ImGui::Button("Remove Level")) { + // This is tricky with ImGui - we mark for removal instead + level.distance = -1.0f; // Mark for removal + } + + ImGui::TreePop(); + } + + ImGui::PopID(); +} + +bool LodSettingsEditor::renderComponent(flecs::entity entity, LodSettingsComponent &settings) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("LOD Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Settings ID (for referencing from LOD components) + char idBuffer[256]; + std::strncpy(idBuffer, settings.settingsId.c_str(), sizeof(idBuffer) - 1); + idBuffer[sizeof(idBuffer) - 1] = '\0'; + if (ImGui::InputText("Settings ID", idBuffer, sizeof(idBuffer))) { + settings.settingsId = idBuffer; + // Note: We don't mark dirty here as this doesn't affect LOD generation + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Unique identifier used by LOD components to reference these settings"); + } + + // Auto-generate ID button if empty + if (settings.settingsId.empty()) { + if (ImGui::Button("Generate ID")) { + settings.settingsId = "lod_settings_" + std::to_string(entity.id()); + modified = true; + } + } + + ImGui::Separator(); + + // Strategy selector + const char* strategies[] = { "Distance", "Pixel Count", "Edge Pixel Count" }; + int currentStrategy = static_cast(settings.strategy); + if (ImGui::Combo("Strategy", ¤tStrategy, strategies, IM_ARRAYSIZE(strategies))) { + settings.strategy = static_cast(currentStrategy); + settings.markDirty(); + modified = true; + } + + ImGui::Separator(); + + // LOD Levels + ImGui::Text("LOD Levels (%zu configured):", settings.lodLevels.size()); + + // Remove marked levels + settings.lodLevels.erase( + std::remove_if(settings.lodLevels.begin(), settings.lodLevels.end(), + [](const LodLevelConfig& level) { return level.distance < 0.0f; }), + settings.lodLevels.end()); + + // Render levels + for (size_t i = 0; i < settings.lodLevels.size(); i++) { + bool levelModified = false; + float oldDistance = settings.lodLevels[i].distance; + + renderLodLevel(settings.lodLevels[i], static_cast(i)); + + if (settings.lodLevels[i].distance != oldDistance) { + levelModified = true; + } + + if (levelModified) { + settings.markDirty(); + modified = true; + } + } + + // Add level button + if (ImGui::Button("Add LOD Level")) { + float distance = settings.lodLevels.empty() ? 100.0f : + settings.lodLevels.back().distance * 2.0f; + settings.addProportionalLevel(distance, 0.5f); + modified = true; + } + + ImGui::SameLine(); + + // Auto-configure button + if (ImGui::Button("Reset to Defaults")) { + settings.createDefaultLevels(); + modified = true; + } + + ImGui::Separator(); + + // Advanced options + if (ImGui::TreeNode("Advanced Options")) { + if (ImGui::Checkbox("Use Compression", &settings.useCompression)) { + settings.markDirty(); + modified = true; + } + ImGui::SetItemTooltip("Compress index buffers for smaller memory footprint"); + + if (ImGui::Checkbox("Use Vertex Normals", &settings.useVertexNormals)) { + settings.markDirty(); + modified = true; + } + ImGui::SetItemTooltip("Use vertex normals for better quality (slower generation)"); + + if (ImGui::Checkbox("Prevent Punching Holes", &settings.preventPunchingHoles)) { + settings.markDirty(); + modified = true; + } + + if (ImGui::Checkbox("Prevent Breaking Lines", &settings.preventBreakingLines)) { + settings.markDirty(); + modified = true; + } + + if (ImGui::Checkbox("Background Queue", &settings.useBackgroundQueue)) { + settings.markDirty(); + modified = true; + } + ImGui::SetItemTooltip("Generate LODs in background (requires manual injection)"); + + ImGui::TreePop(); + } + + // Info + ImGui::Separator(); + ImGui::Text("Version: %u", settings.version); + if (settings.dirty) { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "Settings modified - will apply to meshes"); + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/LodSettingsEditor.hpp b/src/features/editScene/ui/LodSettingsEditor.hpp new file mode 100644 index 0000000..98c6715 --- /dev/null +++ b/src/features/editScene/ui/LodSettingsEditor.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_LODSETTINGSEDITOR_HPP +#define EDITSCENE_LODSETTINGSEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/LodSettings.hpp" + +/** + * Editor for LodSettingsComponent + */ +class LodSettingsEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, LodSettingsComponent &settings) override; + const char *getName() const override { return "LOD Settings"; } + +private: + void renderLodLevel(LodLevelConfig& level, int index); +}; + +#endif // EDITSCENE_LODSETTINGSEDITOR_HPP