Fixed LOD support

This commit is contained in:
2026-04-04 00:42:27 +03:00
parent ebd875feac
commit 9e72a48457
15 changed files with 1158 additions and 1 deletions

View File

@@ -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

View File

@@ -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 <OgreRTShaderSystem.h>
#include <imgui.h>
@@ -121,6 +124,10 @@ void EditorApp::setup()
// Setup camera system
m_cameraSystem = std::make_unique<EditorCameraSystem>(m_world, m_sceneMgr);
// Setup LOD system
m_lodSystem = std::make_unique<EditorLodSystem>(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<LightComponent>();
m_world.component<CameraComponent>();
// Register LOD components
m_world.component<LodComponent>();
m_world.component<LodSettingsComponent>();
}
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;

View File

@@ -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<EditorPhysicsSystem> m_physicsSystem;
std::unique_ptr<EditorLightSystem> m_lightSystem;
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
std::unique_ptr<EditorLodSystem> m_lodSystem;
// State
uint16_t m_currentModifiers;

View File

@@ -0,0 +1,47 @@
#ifndef EDITSCENE_LOD_HPP
#define EDITSCENE_LOD_HPP
#pragma once
#include <Ogre.h>
#include <flecs.h>
#include <string>
#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<LodSettingsComponent>();
}
};
#endif // EDITSCENE_LOD_HPP

View File

@@ -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<LodSettingsComponent>(
"LOD Settings",
std::make_unique<LodSettingsEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<LodSettingsComponent>()) {
auto settings = LodSettingsComponent();
// Create default LOD levels
settings.createDefaultLevels();
e.set<LodSettingsComponent>(settings);
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<LodSettingsComponent>()) {
e.remove<LodSettingsComponent>();
}
}
);
}
// Register Lod component
REGISTER_COMPONENT("LOD", LodComponent, LodEditor)
{
registry.registerComponent<LodComponent>(
"LOD",
std::make_unique<LodEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<LodComponent>()) {
// LOD requires Renderable
if (!e.has<RenderableComponent>()) {
// Auto-add renderable if missing (will need mesh assignment later)
e.set<RenderableComponent>({});
}
e.set<LodComponent>({});
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<LodComponent>()) {
e.remove<LodComponent>();
}
}
);
}

View File

@@ -0,0 +1,107 @@
#ifndef EDITSCENE_LODSETTINGS_HPP
#define EDITSCENE_LODSETTINGS_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
/**
* 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<LodLevelConfig> 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<float>(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

View File

@@ -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<PhysicsColliderComponent>())
indicators += " [C]";
if (entity.has<LodSettingsComponent>())
indicators += " [LODS]";
if (entity.has<LodComponent>())
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<PhysicsColliderComponent>(entity, collider);
componentCount++;
}
// Render LOD Settings if present
if (entity.has<LodSettingsComponent>()) {
auto &lodSettings = entity.get_mut<LodSettingsComponent>();
m_componentRegistry.render<LodSettingsComponent>(entity, lodSettings);
componentCount++;
}
// Render LOD if present
if (entity.has<LodComponent>()) {
auto &lod = entity.get_mut<LodComponent>();
m_componentRegistry.render<LodComponent>(entity, lod);
componentCount++;
}
// Show message if no components
if (componentCount == 0) {

View File

@@ -0,0 +1,250 @@
#include "LodSystem.hpp"
#include "../components/Lod.hpp"
#include "../components/LodSettings.hpp"
#include "../components/Renderable.hpp"
#include <OgreLogManager.h>
#include <OGRE/MeshLodGenerator/OgreMeshLodGenerator.h>
#include <OgreDistanceLodStrategy.h>
#include <OgrePixelCountLodStrategy.h>
EditorLodSystem::EditorLodSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_lodQuery(world.query<LodComponent, RenderableComponent>())
, m_settingsQuery(world.query<LodSettingsComponent>())
{
}
EditorLodSystem::~EditorLodSystem() = default;
void EditorLodSystem::initialize()
{
if (m_initialized)
return;
// Create the LOD generator singleton
m_lodGenerator = std::make_unique<Ogre::MeshLodGenerator>();
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<LodSettingsComponent>().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<LodSettingsComponent>()) {
const auto &settings =
lod.settingsEntity.get<LodSettingsComponent>();
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<LodSettingsComponent>().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<LodSettingsComponent>();
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<LodSettingsComponent>()) {
lod.lodApplied = false;
lod.dirty = false;
return;
}
// Get the settings
const auto &settings = lod.settingsEntity.get<LodSettingsComponent>();
// 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<LodSettingsComponent>()) {
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;
}

View File

@@ -0,0 +1,60 @@
#ifndef EDITSCENE_LODSYSTEM_HPP
#define EDITSCENE_LODSYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <OGRE/MeshLodGenerator/OgreMeshLodGenerator.h>
/**
* 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<Ogre::MeshLodGenerator> m_lodGenerator;
// Query for entities with LOD and renderable
flecs::query<class LodComponent, class RenderableComponent> m_lodQuery;
// Query for settings entities
flecs::query<class LodSettingsComponent> m_settingsQuery;
bool m_initialized = false;
};
#endif // EDITSCENE_LODSYSTEM_HPP

View File

@@ -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 <fstream>
#include <iostream>
@@ -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<LodSettingsComponent>()) {
json["lodSettings"] = serializeLodSettings(entity);
}
if (entity.has<LodComponent>()) {
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<CameraComponent>(camera);
}
nlohmann::json SceneSerializer::serializeLodSettings(flecs::entity entity)
{
auto& settings = entity.get<LodSettingsComponent>();
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<LodComponent>();
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<LodSettingsComponent>(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<LodSettingsComponent>().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<LodComponent>(lod);
Ogre::LogManager::getSingleton().logMessage(
"Deserialize LOD: Final settingsEntity=" +
Ogre::StringConverter::toString(lod.settingsEntity.id()) +
", hasValid=" + (lod.hasValidSettings() ? "true" : "false"));
}

View File

@@ -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;

View File

@@ -0,0 +1,149 @@
#include "LodEditor.hpp"
#include "../components/LodSettings.hpp"
#include <imgui.h>
#include <vector>
#include <string>
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<flecs::entity> settingsEntities;
std::vector<std::string> settingsIds;
std::vector<std::string> displayNames;
int currentIndex = -1;
world.query<LodSettingsComponent>().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<int>(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<int>(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<int>(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<LodSettingsComponent>();
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;
}

View File

@@ -0,0 +1,22 @@
#ifndef EDITSCENE_LODEDITOR_HPP
#define EDITSCENE_LODEDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/Lod.hpp"
#include <flecs.h>
/**
* Editor for LodComponent with combo box selector for LOD Settings
*/
class LodEditor : public ComponentEditor<LodComponent> {
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

View File

@@ -0,0 +1,199 @@
#include "LodSettingsEditor.hpp"
#include <imgui.h>
#include <cstring>
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<int>(level.reductionMethod);
if (ImGui::Combo("Method", &currentMethod, methods, IM_ARRAYSIZE(methods))) {
level.reductionMethod = static_cast<LodLevelConfig::ReductionMethod>(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<int*>(&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<int>(settings.strategy);
if (ImGui::Combo("Strategy", &currentStrategy, strategies, IM_ARRAYSIZE(strategies))) {
settings.strategy = static_cast<LodSettingsComponent::Strategy>(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<int>(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;
}

View File

@@ -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<LodSettingsComponent> {
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