Added StaticGeometry support

This commit is contained in:
2026-04-04 02:12:38 +03:00
parent 9e72a48457
commit c2cbd0974d
17 changed files with 1224 additions and 1 deletions

View File

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

View File

@@ -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 <OgreRTShaderSystem.h>
#include <imgui.h>
@@ -128,6 +131,10 @@ void EditorApp::setup()
// Setup LOD system
m_lodSystem = std::make_unique<EditorLodSystem>(m_world, m_sceneMgr);
m_lodSystem->initialize();
// Setup StaticGeometry system
m_staticGeometrySystem = std::make_unique<StaticGeometrySystem>(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<LodComponent>();
m_world.component<LodSettingsComponent>();
// Register StaticGeometry components
m_world.component<StaticGeometryComponent>();
m_world.component<StaticGeometryMemberComponent>();
}
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;

View File

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

View File

@@ -0,0 +1,43 @@
#pragma once
#include <string>
#include <memory>
#include <vector>
#include <Ogre.h>
/**
* @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<std::uint64_t> memberEntityIds;
void markDirty() {
dirty = true;
}
};

View File

@@ -0,0 +1,45 @@
#pragma once
#include <string>
#include <flecs.h>
/**
* @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++;
}
};

View File

@@ -0,0 +1,60 @@
#include "StaticGeometry.hpp"
#include "StaticGeometryMember.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/StaticGeometryEditor.hpp"
#include "../ui/StaticGeometryMemberEditor.hpp"
#include <Ogre.h>
#include <random>
// 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<StaticGeometryComponent>(
"StaticGeometry Region",
std::make_unique<StaticGeometryEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<StaticGeometryComponent>()) {
auto region = StaticGeometryComponent();
region.regionId = generateRegionId();
region.regionName = "Region_" + Ogre::StringConverter::toString(e.id());
e.set<StaticGeometryComponent>(region);
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<StaticGeometryComponent>()) {
e.remove<StaticGeometryComponent>();
}
}
);
}
// Register StaticGeometryMember component
REGISTER_COMPONENT("StaticGeometry Member", StaticGeometryMemberComponent, StaticGeometryMemberEditor)
{
registry.registerComponent<StaticGeometryMemberComponent>(
"StaticGeometry Member",
std::make_unique<StaticGeometryMemberEditor>(sceneMgr),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<StaticGeometryMemberComponent>()) {
e.set<StaticGeometryMemberComponent>({});
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<StaticGeometryMemberComponent>()) {
e.remove<StaticGeometryMemberComponent>();
}
}
);
}

View File

@@ -1,5 +1,6 @@
#include "Gizmo.hpp"
#include "../components/Transform.hpp"
#include "../components/StaticGeometryMember.hpp"
#include <cmath>
// 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<StaticGeometryMemberComponent>()) {
m_attachedEntity.get_mut<StaticGeometryMemberComponent>().markDirty();
}
// Update gizmo to follow (use derived position)
m_gizmoNode->setPosition(transform.node->_getDerivedPosition());

View File

@@ -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<TransformComponent>()) {
auto &transform = entity.get_mut<TransformComponent>();
m_componentRegistry.render<TransformComponent>(entity, transform);
if (m_componentRegistry.render<TransformComponent>(entity, transform)) {
// Transform changed - mark StaticGeometryMember dirty if present
if (entity.has<StaticGeometryMemberComponent>()) {
entity.get_mut<StaticGeometryMemberComponent>().markDirty();
}
}
componentCount++;
}
@@ -533,6 +540,20 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
m_componentRegistry.render<LodComponent>(entity, lod);
componentCount++;
}
// Render StaticGeometry Region if present
if (entity.has<StaticGeometryComponent>()) {
auto &region = entity.get_mut<StaticGeometryComponent>();
m_componentRegistry.render<StaticGeometryComponent>(entity, region);
componentCount++;
}
// Render StaticGeometry Member if present
if (entity.has<StaticGeometryMemberComponent>()) {
auto &member = entity.get_mut<StaticGeometryMemberComponent>();
m_componentRegistry.render<StaticGeometryMemberComponent>(entity, member);
componentCount++;
}
// Show message if no components
if (componentCount == 0) {

View File

@@ -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 <random>
#include <fstream>
#include <iostream>
@@ -145,6 +148,14 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["lod"] = serializeLod(entity);
}
if (entity.has<StaticGeometryComponent>()) {
json["staticGeometry"] = serializeStaticGeometry(entity);
}
if (entity.has<StaticGeometryMemberComponent>()) {
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<StaticGeometryComponent>();
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<StaticGeometryMemberComponent>();
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<StaticGeometryComponent>(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<StaticGeometryMemberComponent>(member);
}

View File

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

View File

@@ -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 <OgreSceneManager.h>
#include <OgreEntity.h>
#include <OgreMeshManager.h>
#include <OgreLogManager.h>
#include <OgreVector3.h>
#include <OgreQuaternion.h>
#include <OGRE/MeshLodGenerator/OgreMeshLodGenerator.h>
#include <OgreDistanceLodStrategy.h>
#include <OgrePixelCountLodStrategy.h>
StaticGeometrySystem::StaticGeometrySystem(flecs::world& world, Ogre::SceneManager* sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_regionQuery(world.query<StaticGeometryComponent>())
, m_memberQuery(world.query<StaticGeometryMemberComponent>())
{
}
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<TransformComponent>()) {
const auto& transform = entity.get<TransformComponent>();
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<StaticGeometryComponent>()) {
member.regionEntity.get_mut<StaticGeometryComponent>().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<std::pair<flecs::entity, StaticGeometryMemberComponent*>> 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<TransformComponent>()) {
const auto& transform = memberEntity.get<TransformComponent>();
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<LodSettingsComponent>()) {
const auto& lodSettings = member->lodSettingsEntity.get<LodSettingsComponent>();
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<StaticGeometryComponent>()) {
return;
}
auto& region = regionEntity.get_mut<StaticGeometryComponent>();
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<StaticGeometryComponent>()) {
return;
}
auto& region = regionEntity.get_mut<StaticGeometryComponent>();
destroyOgreStaticGeometry(region);
}
Ogre::StaticGeometry* StaticGeometrySystem::getStaticGeometry(flecs::entity regionEntity)
{
if (!regionEntity.is_alive() || !regionEntity.has<StaticGeometryComponent>()) {
return nullptr;
}
return regionEntity.get<StaticGeometryComponent>().staticGeometry;
}
bool StaticGeometrySystem::isRegionDirty(flecs::entity regionEntity)
{
if (!regionEntity.is_alive() || !regionEntity.has<StaticGeometryComponent>()) {
return false;
}
const auto& region = regionEntity.get<StaticGeometryComponent>();
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<StaticGeometryComponent>()) {
return 0;
}
return regionEntity.get<StaticGeometryComponent>().memberEntityIds.size();
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <flecs.h>
#include <memory>
#include <Ogre.h>
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<struct StaticGeometryComponent> m_regionQuery;
flecs::query<struct StaticGeometryMemberComponent> 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);
};

View File

@@ -4,6 +4,10 @@
#include <imgui.h>
#include <flecs.h>
// Forward declarations for component editor templates
struct StaticGeometryComponent;
struct StaticGeometryMemberComponent;
/**
* Base interface for component editors
*/

View File

@@ -0,0 +1,99 @@
#include "StaticGeometryEditor.hpp"
#include "../components/StaticGeometryMember.hpp"
#include "../components/LodSettings.hpp"
#include <imgui.h>
#include <OgreStaticGeometry.h>
bool StaticGeometryEditor::renderComponent(flecs::entity entity, StaticGeometryComponent &region)
{
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<StaticGeometryMemberComponent>().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", &region.castShadows)) {
region.markDirty();
changed = true;
}
if (ImGui::DragFloat("Rendering Distance", &region.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", &region.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;
}

View File

@@ -0,0 +1,18 @@
#ifndef EDITSCENE_STATICGEOMETRYEDITOR_HPP
#define EDITSCENE_STATICGEOMETRYEDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/StaticGeometry.hpp"
#include <flecs.h>
/**
* Editor for StaticGeometryComponent (region)
*/
class StaticGeometryEditor : public ComponentEditor<StaticGeometryComponent> {
public:
bool renderComponent(flecs::entity entity, StaticGeometryComponent &region) override;
const char *getName() const override { return "StaticGeometry Region"; }
};
#endif // EDITSCENE_STATICGEOMETRYEDITOR_HPP

View File

@@ -0,0 +1,363 @@
#include "StaticGeometryMemberEditor.hpp"
#include "../components/StaticGeometry.hpp"
#include "../components/LodSettings.hpp"
#include <imgui.h>
#include <OgreResourceGroupManager.h>
#include <algorithm>
#include <cstring>
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<flecs::entity> regionEntities;
std::vector<std::string> regionIds;
std::vector<std::string> regionNames;
int currentIndex = -1;
world.query<StaticGeometryComponent>().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<flecs::entity> settingsEntities;
std::vector<std::string> settingsIds;
std::vector<std::string> 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<LodSettingsComponent>().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<LodSettingsComponent>();
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;
}

View File

@@ -0,0 +1,35 @@
#ifndef EDITSCENE_STATICGEOMETRYMEMBEREDITOR_HPP
#define EDITSCENE_STATICGEOMETRYMEMBEREDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/StaticGeometryMember.hpp"
#include <OgreSceneManager.h>
#include <flecs.h>
#include <vector>
#include <string>
/**
* Editor for StaticGeometryMemberComponent
*/
class StaticGeometryMemberEditor : public ComponentEditor<StaticGeometryMemberComponent> {
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<std::string> m_meshFiles;
bool m_meshesScanned = false;
bool m_showMeshBrowser = false;
char m_searchBuffer[256] = {};
};
#endif // EDITSCENE_STATICGEOMETRYMEMBEREDITOR_HPP