diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index b6c2c0e..fbfb98e 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -20,6 +20,8 @@ set(EDITSCENE_SOURCES systems/LodSystem.cpp systems/StaticGeometrySystem.cpp systems/ProceduralTextureSystem.cpp + systems/ProceduralMaterialSystem.cpp + systems/ProceduralMeshSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -31,12 +33,18 @@ set(EDITSCENE_SOURCES ui/StaticGeometryEditor.cpp ui/StaticGeometryMemberEditor.cpp ui/ProceduralTextureEditor.cpp + ui/ProceduralMaterialEditor.cpp + ui/PrimitiveEditor.cpp + ui/TriangleBufferEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp components/LodModule.cpp components/StaticGeometryModule.cpp components/ProceduralTextureModule.cpp + components/ProceduralMaterialModule.cpp + components/PrimitiveModule.cpp + components/TriangleBufferModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -57,7 +65,12 @@ set(EDITSCENE_HEADERS components/StaticGeometry.hpp components/StaticGeometryMember.hpp components/ProceduralTexture.hpp + components/ProceduralMaterial.hpp + components/Primitive.hpp + components/TriangleBuffer.hpp systems/EditorUISystem.hpp + systems/ProceduralMaterialSystem.hpp + systems/ProceduralMeshSystem.hpp systems/ProceduralTextureSystem.hpp systems/StaticGeometrySystem.hpp systems/SceneSerializer.hpp @@ -79,6 +92,9 @@ set(EDITSCENE_HEADERS ui/StaticGeometryEditor.hpp ui/StaticGeometryMemberEditor.hpp ui/ProceduralTextureEditor.hpp + ui/ProceduralMaterialEditor.hpp + ui/PrimitiveEditor.hpp + ui/TriangleBufferEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp physics/physics.h diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index bd6d4d3..33f3e05 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -7,6 +7,8 @@ #include "systems/LodSystem.hpp" #include "systems/StaticGeometrySystem.hpp" #include "systems/ProceduralTextureSystem.hpp" +#include "systems/ProceduralMaterialSystem.hpp" +#include "systems/ProceduralMeshSystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -21,6 +23,9 @@ #include "components/StaticGeometry.hpp" #include "components/StaticGeometryMember.hpp" #include "components/ProceduralTexture.hpp" +#include "components/ProceduralMaterial.hpp" +#include "components/Primitive.hpp" +#include "components/TriangleBuffer.hpp" #include #include @@ -83,6 +88,8 @@ EditorApp::~EditorApp() }); // Release all systems + m_proceduralMeshSystem.reset(); + m_proceduralMaterialSystem.reset(); m_proceduralTextureSystem.reset(); m_staticGeometrySystem.reset(); m_lodSystem.reset(); @@ -164,6 +171,14 @@ void EditorApp::setup() // Setup ProceduralTexture system m_proceduralTextureSystem = std::make_unique(m_world, m_sceneMgr); m_proceduralTextureSystem->initialize(); + + // Setup ProceduralMaterial system + m_proceduralMaterialSystem = std::make_unique(m_world, m_sceneMgr); + m_proceduralMaterialSystem->initialize(); + + // Setup ProceduralMesh system + m_proceduralMeshSystem = std::make_unique(m_world, m_sceneMgr); + m_proceduralMeshSystem->initialize(); // Add default entities to UI cache for (auto &e : m_defaultEntities) { @@ -211,6 +226,13 @@ void EditorApp::setupECS() // Register ProceduralTexture component m_world.component(); + + // Register ProceduralMaterial component + m_world.component(); + + // Register Primitive and TriangleBuffer components + m_world.component(); + m_world.component(); } void EditorApp::createDefaultEntities() @@ -363,6 +385,16 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) if (m_proceduralTextureSystem) { m_proceduralTextureSystem->update(); } + + // Update ProceduralMaterial system + if (m_proceduralMaterialSystem) { + m_proceduralMaterialSystem->update(); + } + + // Update ProceduralMesh system + if (m_proceduralMeshSystem) { + m_proceduralMeshSystem->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 11bf914..969b43b 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -19,6 +19,8 @@ class EditorCameraSystem; class EditorLodSystem; class StaticGeometrySystem; class ProceduralTextureSystem; +class ProceduralMaterialSystem; +class ProceduralMeshSystem; /** * RenderTargetListener for ImGui frame management @@ -90,6 +92,8 @@ private: std::unique_ptr m_lodSystem; std::unique_ptr m_staticGeometrySystem; std::unique_ptr m_proceduralTextureSystem; + std::unique_ptr m_proceduralMaterialSystem; + std::unique_ptr m_proceduralMeshSystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/Primitive.hpp b/src/features/editScene/components/Primitive.hpp new file mode 100644 index 0000000..1cc2428 --- /dev/null +++ b/src/features/editScene/components/Primitive.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +/** + * @brief Base component for procedural primitives that feed into TriangleBuffer + */ +struct PrimitiveComponent { + // Type of primitive + enum class Type { + None, + Box, + Plane + }; + + Type type = Type::None; + + // Box parameters + float boxSizeX = 1.0f; + float boxSizeY = 1.0f; + float boxSizeZ = 1.0f; + + // Plane parameters + float planeSizeX = 1.0f; + float planeSizeY = 1.0f; + int planeSegmentsX = 1; + int planeSegmentsY = 1; + + // Dirty flag - triggers TriangleBuffer rebuild + bool dirty = true; + + // Track last seen transform version to detect gizmo movements + unsigned int lastTransformVersion = 0; + + void markDirty() { dirty = true; } +}; diff --git a/src/features/editScene/components/PrimitiveModule.cpp b/src/features/editScene/components/PrimitiveModule.cpp new file mode 100644 index 0000000..b008b85 --- /dev/null +++ b/src/features/editScene/components/PrimitiveModule.cpp @@ -0,0 +1,24 @@ +#include "Primitive.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/PrimitiveEditor.hpp" + +// Register Primitive component +REGISTER_COMPONENT("Primitive", PrimitiveComponent, PrimitiveEditor) +{ + registry.registerComponent( + "Primitive", + std::make_unique(), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/components/ProceduralMaterial.hpp b/src/features/editScene/components/ProceduralMaterial.hpp new file mode 100644 index 0000000..075a5d1 --- /dev/null +++ b/src/features/editScene/components/ProceduralMaterial.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +/** + * @brief Component for creating procedural Ogre materials + * + * Creates a named material that uses ProceduralTexture as diffuse/albedo map. + * The material can be referenced by name for use with meshes/entities. + */ +struct ProceduralMaterialComponent { + // Material name for Ogre resource + std::string materialName; + + // Reference to entity with ProceduralTextureComponent (for diffuse) + flecs::entity diffuseTextureEntity; + + // Whether the material needs regeneration + bool dirty = true; + + // Whether the material has been created + bool created = false; + + // Pointer to the created Ogre material + Ogre::MaterialPtr ogreMaterial; + + // Material properties + float ambient[3] = {0.2f, 0.2f, 0.2f}; // Ambient color (RGB 0-1) + float diffuse[3] = {1.0f, 1.0f, 1.0f}; // Diffuse color multiplier (RGB 0-1) + float specular[3] = {0.0f, 0.0f, 0.0f}; // Specular color (RGB 0-1) + float shininess = 32.0f; // Specular shininess + float roughness = 0.5f; // Roughness (0-1, for PBR-style) + + void markDirty() { + dirty = true; + } +}; diff --git a/src/features/editScene/components/ProceduralMaterialModule.cpp b/src/features/editScene/components/ProceduralMaterialModule.cpp new file mode 100644 index 0000000..27dd746 --- /dev/null +++ b/src/features/editScene/components/ProceduralMaterialModule.cpp @@ -0,0 +1,35 @@ +#include "ProceduralMaterial.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/ProceduralMaterialEditor.hpp" +#include + +// Register ProceduralMaterial component +REGISTER_COMPONENT("Procedural Material", ProceduralMaterialComponent, ProceduralMaterialEditor) +{ + registry.registerComponent( + "Procedural Material", + std::make_unique(sceneMgr), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + auto& material = e.get_mut(); + // Clean up Ogre material - wrap in try/catch since MaterialManager may be shutting down + if (material.ogreMaterial) { + try { + if (Ogre::MaterialManager::getSingletonPtr()) { + Ogre::MaterialManager::getSingleton().remove(material.ogreMaterial); + } + } catch (...) {} + material.ogreMaterial.reset(); + } + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/components/Transform.hpp b/src/features/editScene/components/Transform.hpp index 7a7b011..75e591e 100644 --- a/src/features/editScene/components/Transform.hpp +++ b/src/features/editScene/components/Transform.hpp @@ -12,6 +12,9 @@ struct TransformComponent { Ogre::Vector3 position = Ogre::Vector3::ZERO; Ogre::Quaternion rotation = Ogre::Quaternion::IDENTITY; Ogre::Vector3 scale = Ogre::Vector3::UNIT_SCALE; + + // Version tracking for change detection + unsigned int version = 0; /** * Apply component values to the scene node @@ -36,6 +39,11 @@ struct TransformComponent { scale = node->getScale(); } } + + /** + * Mark transform as changed (increment version) + */ + void markChanged() { version++; } }; #endif // EDITSCENE_TRANSFORM_HPP diff --git a/src/features/editScene/components/TriangleBuffer.hpp b/src/features/editScene/components/TriangleBuffer.hpp new file mode 100644 index 0000000..45b8dd1 --- /dev/null +++ b/src/features/editScene/components/TriangleBuffer.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +/** + * @brief Component that holds a procedural triangle buffer + * + * The buffer contents are NOT serialized - only the configuration. + * The buffer is rebuilt from Primitive children when needed. + */ +struct TriangleBufferComponent { + // The actual triangle buffer (runtime only, not serialized) + // Using shared_ptr because flecs needs to copy components + std::shared_ptr buffer; + + // Reference to entity with ProceduralTexture for UV mapping + flecs::entity textureEntity; + + // Name of the rectangle in the texture atlas to use for UVs + std::string textureRectName; + + // Reference to entity with ProceduralMaterial for material + flecs::entity materialEntity; + + // Whether to attach to StaticGeometry instead of SceneNode + bool useStaticGeometry = false; + + // Reference to StaticGeometry region entity (if useStaticGeometry is true) + flecs::entity staticGeometryEntity; + + // Mesh name for the generated mesh + std::string meshName; + + // Whether the buffer needs rebuilding + bool dirty = true; + + // Whether the buffer has been converted to a mesh + bool meshCreated = false; + + // Pointer to the created Ogre entity (if not using StaticGeometry) + Ogre::Entity* ogreEntity = nullptr; + + void markDirty() { dirty = true; } +}; diff --git a/src/features/editScene/components/TriangleBufferModule.cpp b/src/features/editScene/components/TriangleBufferModule.cpp new file mode 100644 index 0000000..12fca6d --- /dev/null +++ b/src/features/editScene/components/TriangleBufferModule.cpp @@ -0,0 +1,40 @@ +#include "TriangleBuffer.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/TriangleBufferEditor.hpp" +#include + +// Register TriangleBuffer component +REGISTER_COMPONENT("Triangle Buffer", TriangleBufferComponent, TriangleBufferEditor) +{ + registry.registerComponent( + "Triangle Buffer", + std::make_unique(sceneMgr), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + auto& tb = e.get_mut(); + // Clean up Ogre entity and mesh + if (tb.ogreEntity && sceneMgr) { + try { + sceneMgr->destroyEntity(tb.ogreEntity); + } catch (...) {} + tb.ogreEntity = nullptr; + } + if (!tb.meshName.empty()) { + try { + if (Ogre::MeshManager::getSingletonPtr()) { + Ogre::MeshManager::getSingleton().remove(tb.meshName); + } + } catch (...) {} + } + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/gizmo/Gizmo.cpp b/src/features/editScene/gizmo/Gizmo.cpp index 06abcfe..82ca54f 100644 --- a/src/features/editScene/gizmo/Gizmo.cpp +++ b/src/features/editScene/gizmo/Gizmo.cpp @@ -282,6 +282,7 @@ bool Gizmo::onMouseMoved(const Ogre::Ray &mouseRay, const Ogre::Vector2 &mouseDe } transform.applyToNode(); + transform.markChanged(); // Mark StaticGeometryMember dirty if present if (m_attachedEntity.has()) { diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index ad06d1e..08b29ea 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -11,6 +11,9 @@ #include "../components/StaticGeometry.hpp" #include "../components/StaticGeometryMember.hpp" #include "../components/ProceduralTexture.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/Primitive.hpp" +#include "../components/TriangleBuffer.hpp" #include "../components/LodSettings.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" @@ -570,6 +573,27 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, texture); componentCount++; } + + // Render ProceduralMaterial if present + if (entity.has()) { + auto &material = entity.get_mut(); + m_componentRegistry.render(entity, material); + componentCount++; + } + + // Render Primitive if present + if (entity.has()) { + auto &primitive = entity.get_mut(); + m_componentRegistry.render(entity, primitive); + componentCount++; + } + + // Render TriangleBuffer if present + if (entity.has()) { + auto &tb = entity.get_mut(); + m_componentRegistry.render(entity, tb); + componentCount++; + } // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/ProceduralMaterialSystem.cpp b/src/features/editScene/systems/ProceduralMaterialSystem.cpp new file mode 100644 index 0000000..d8a3563 --- /dev/null +++ b/src/features/editScene/systems/ProceduralMaterialSystem.cpp @@ -0,0 +1,125 @@ +#include "ProceduralMaterialSystem.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/ProceduralTexture.hpp" +#include +#include +#include +#include +#include + +ProceduralMaterialSystem::ProceduralMaterialSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_query(world.query()) +{ +} + +ProceduralMaterialSystem::~ProceduralMaterialSystem() = default; + +void ProceduralMaterialSystem::initialize() +{ + if (m_initialized) return; + m_initialized = true; + + Ogre::LogManager::getSingleton().logMessage("ProceduralMaterialSystem initialized"); +} + +void ProceduralMaterialSystem::update() +{ + if (!m_initialized) return; + + m_query.each([&](flecs::entity entity, ProceduralMaterialComponent& component) { + // Check if we need to recreate (dirty or not created yet) + if (component.dirty || !component.created) { + createMaterial(entity, component); + } + }); +} + +void ProceduralMaterialSystem::createMaterial(flecs::entity entity, ProceduralMaterialComponent& component) +{ + try { + // Generate unique material name if not set + if (component.materialName.empty()) { + component.materialName = "ProceduralMat_" + std::to_string(entity.id()) + "_" + std::to_string(m_createdCount++); + } + + // Destroy old material if exists + if (component.ogreMaterial) { + destroyMaterial(component); + } + + // Create new material + component.ogreMaterial = Ogre::MaterialManager::getSingleton().create( + component.materialName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + + // Get the default technique and pass + Ogre::Technique* technique = component.ogreMaterial->getTechnique(0); + Ogre::Pass* pass = technique->getPass(0); + + // Set material colors + pass->setAmbient(component.ambient[0], component.ambient[1], component.ambient[2]); + pass->setDiffuse(component.diffuse[0], component.diffuse[1], component.diffuse[2], 1.0f); + pass->setSpecular(component.specular[0], component.specular[1], component.specular[2], 1.0f); + pass->setShininess(component.shininess); + + // Add diffuse texture if we have a reference to a ProceduralTexture entity + if (component.diffuseTextureEntity.is_alive() && + component.diffuseTextureEntity.has()) { + + const auto& textureComp = component.diffuseTextureEntity.get(); + if (textureComp.generated && !textureComp.textureName.empty()) { + Ogre::TextureUnitState* texUnit = pass->createTextureUnitState(); + texUnit->setTextureName(textureComp.textureName); + texUnit->setTextureAddressingMode(Ogre::TAM_CLAMP); + } + } + + component.created = true; + component.dirty = false; + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMaterial: Created '" + component.materialName + "'"); + + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMaterial ERROR: " + e.getDescription()); + component.dirty = true; // Will retry + } +} + +void ProceduralMaterialSystem::destroyMaterial(ProceduralMaterialComponent& component) +{ + if (component.ogreMaterial) { + try { + Ogre::MaterialManager::getSingleton().remove(component.ogreMaterial); + } catch (...) { + // Ignore errors during cleanup + } + component.ogreMaterial.reset(); + } +} + +void ProceduralMaterialSystem::recreateMaterial(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) { + return; + } + + entity.get_mut().markDirty(); +} + +std::string ProceduralMaterialSystem::getMaterialName(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) { + return ""; + } + + return entity.get().materialName; +} + +bool ProceduralMaterialSystem::materialExists(const std::string& name) +{ + return Ogre::MaterialManager::getSingleton().resourceExists(name); +} diff --git a/src/features/editScene/systems/ProceduralMaterialSystem.hpp b/src/features/editScene/systems/ProceduralMaterialSystem.hpp new file mode 100644 index 0000000..eca25f3 --- /dev/null +++ b/src/features/editScene/systems/ProceduralMaterialSystem.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace Ogre { + class SceneManager; +} + +class ProceduralMaterialSystem { +public: + ProceduralMaterialSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~ProceduralMaterialSystem(); + + // Initialize the system + void initialize(); + + // Main update - creates dirty materials + void update(); + + // Force recreate a specific material + void recreateMaterial(flecs::entity entity); + + // Get the material name for an entity + std::string getMaterialName(flecs::entity entity); + + // Check if material exists + bool materialExists(const std::string& name); + +private: + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + flecs::query m_query; + + bool m_initialized = false; + int m_createdCount = 0; + + // Create the Ogre material + void createMaterial(flecs::entity entity, struct ProceduralMaterialComponent& component); + + // Destroy the Ogre material + void destroyMaterial(struct ProceduralMaterialComponent& component); +}; diff --git a/src/features/editScene/systems/ProceduralMeshSystem.cpp b/src/features/editScene/systems/ProceduralMeshSystem.cpp new file mode 100644 index 0000000..4699759 --- /dev/null +++ b/src/features/editScene/systems/ProceduralMeshSystem.cpp @@ -0,0 +1,347 @@ +#include "ProceduralMeshSystem.hpp" +#include "../components/TriangleBuffer.hpp" +#include "../components/Primitive.hpp" +#include "../components/ProceduralTexture.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/Transform.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/StaticGeometryMember.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +ProceduralMeshSystem::ProceduralMeshSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_triangleBufferQuery(world.query()) + , m_primitiveQuery(world.query()) +{ +} + +ProceduralMeshSystem::~ProceduralMeshSystem() = default; + +void ProceduralMeshSystem::initialize() +{ + if (m_initialized) return; + m_initialized = true; + + Ogre::LogManager::getSingleton().logMessage("ProceduralMeshSystem initialized"); +} + +void ProceduralMeshSystem::update() +{ + if (!m_initialized) return; + + // First pass: mark TriangleBuffers dirty if any child primitives are dirty + m_triangleBufferQuery.each([&](flecs::entity entity, TriangleBufferComponent& tb) { + if (arePrimitivesDirty(entity)) { + tb.markDirty(); + } + }); + + // Second pass: rebuild dirty triangle buffers + m_triangleBufferQuery.each([&](flecs::entity entity, TriangleBufferComponent& tb) { + if (tb.dirty) { + buildTriangleBuffer(entity, tb); + if (tb.buffer && hasValidTriangles(tb.buffer.get())) { + convertToMesh(entity, tb); + } + } + }); +} + +bool ProceduralMeshSystem::arePrimitivesDirty(flecs::entity parent) +{ + bool dirty = false; + + // Check all children for PrimitiveComponent + parent.children([&](flecs::entity child) { + if (child.has()) { + const auto& prim = child.get(); + if (prim.dirty) { + dirty = true; + return; + } + // Check if transform has changed + if (child.has()) { + const auto& transform = child.get(); + // Get stored transform version from primitive's user data or check directly + // We'll use a simple approach: store last known version in the primitive + auto& primMut = child.get_mut(); + if (transform.version != primMut.lastTransformVersion) { + dirty = true; + } + } + } + }); + + return dirty; +} + +void ProceduralMeshSystem::buildTriangleBuffer(flecs::entity entity, TriangleBufferComponent& tb) +{ + // Create new triangle buffer + tb.buffer = std::make_shared(); + + // Collect all primitive children + std::vector> primitives; + + entity.children([&](flecs::entity child) { + if (child.has()) { + auto& prim = child.get_mut(); + primitives.emplace_back(child, &prim); + prim.dirty = false; // Mark as processed + } + }); + + // Build each primitive into the triangle buffer + for (auto& [primEntity, prim] : primitives) { + if (prim->type == PrimitiveComponent::Type::None) continue; + + // Get transform from the primitive entity + Ogre::Vector3 position = Ogre::Vector3::ZERO; + Ogre::Quaternion orientation = Ogre::Quaternion::IDENTITY; + + if (primEntity.has()) { + const auto& transform = primEntity.get(); + if (transform.node) { + position = transform.node->getPosition(); + orientation = transform.node->getOrientation(); + } + } + + try { + Procedural::TriangleBuffer primBuffer; + + switch (prim->type) { + case PrimitiveComponent::Type::Box: { + Procedural::BoxGenerator boxGen; + boxGen.setSizeX(prim->boxSizeX); + boxGen.setSizeY(prim->boxSizeY); + boxGen.setSizeZ(prim->boxSizeZ); + boxGen.addToTriangleBuffer(primBuffer); + break; + } + case PrimitiveComponent::Type::Plane: { + Procedural::PlaneGenerator planeGen; + planeGen.setSizeX(prim->planeSizeX); + planeGen.setSizeY(prim->planeSizeY); + planeGen.setNumSegX(prim->planeSegmentsX); + planeGen.setNumSegY(prim->planeSegmentsY); + planeGen.addToTriangleBuffer(primBuffer); + break; + } + default: + break; + } + + // Transform vertices + auto& vertices = primBuffer.getVertices(); + for (auto& vertex : vertices) { + Ogre::Vector3 pos = vertex.mPosition; + pos = orientation * pos + position; + vertex.mPosition = pos; + } + + // Merge into main buffer + tb.buffer->append(primBuffer); + + // Update transform version tracking + if (primEntity.has()) { + const auto& transform = primEntity.get(); + prim->lastTransformVersion = transform.version; + } + + } catch (const std::exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMesh: Failed to build primitive: " + Ogre::String(e.what())); + } + } + + tb.dirty = false; + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMesh: Built triangle buffer with " + + std::to_string(tb.buffer->getVertices().size()) + " vertices"); +} + +bool ProceduralMeshSystem::hasValidTriangles(Procedural::TriangleBuffer* buffer) +{ + if (!buffer) return false; + + const auto& indices = buffer->getIndices(); + if (indices.size() < 3) return false; + + // Check for at least one non-degenerate triangle + for (size_t i = 0; i + 2 < indices.size(); i += 3) { + const auto& v0 = buffer->getVertices()[indices[i]]; + const auto& v1 = buffer->getVertices()[indices[i + 1]]; + const auto& v2 = buffer->getVertices()[indices[i + 2]]; + + // Check if triangle has non-zero area + Ogre::Vector3 e1 = v1.mPosition - v0.mPosition; + Ogre::Vector3 e2 = v2.mPosition - v0.mPosition; + + if (e1.crossProduct(e2).squaredLength() > 0.0001f) { + return true; // Found valid triangle + } + } + + return false; +} + +void ProceduralMeshSystem::applyUVMapping(Procedural::TriangleBuffer* buffer, + ProceduralTextureComponent& texture, + const std::string& rectName) +{ + if (!buffer || rectName.empty()) return; + + const TextureRectInfo* rect = texture.getNamedRect(rectName); + if (!rect) return; + + // Calculate UV mapping with margin + const float margin = 0.01f; + float uRange = (rect->u2 - rect->u1) * (1.0f - 2.0f * margin); + float vRange = (rect->v2 - rect->v1) * (1.0f - 2.0f * margin); + float uOffset = rect->u1 + (rect->u2 - rect->u1) * margin; + float vOffset = rect->v1 + (rect->v2 - rect->v1) * margin; + + auto& vertices = buffer->getVertices(); + for (auto& vertex : vertices) { + // Map UV from [0,1] to [rect.u1, rect.u2] with margin + vertex.mUV.x = uOffset + vertex.mUV.x * uRange; + vertex.mUV.y = vOffset + vertex.mUV.y * vRange; + } +} + +void ProceduralMeshSystem::destroyMesh(TriangleBufferComponent& tb) +{ + if (tb.ogreEntity) { + try { + m_sceneMgr->destroyEntity(tb.ogreEntity); + } catch (...) {} + tb.ogreEntity = nullptr; + } + + if (!tb.meshName.empty()) { + try { + Ogre::MeshManager::getSingleton().remove(tb.meshName); + } catch (...) {} + } + + tb.meshCreated = false; +} + +void ProceduralMeshSystem::convertToMesh(flecs::entity entity, TriangleBufferComponent& tb) +{ + if (!tb.buffer) return; + + // Apply UV mapping if texture and rect are specified + if (tb.textureEntity.is_alive() && tb.textureEntity.has()) { + auto& texture = tb.textureEntity.get_mut(); + applyUVMapping(tb.buffer.get(), texture, tb.textureRectName); + } + + // Generate mesh name + if (tb.meshName.empty()) { + tb.meshName = "ProceduralMesh_" + std::to_string(entity.id()) + "_" + std::to_string(m_meshCount++); + } + + // Destroy old mesh if exists + destroyMesh(tb); + + try { + // Create Ogre mesh from triangle buffer + Ogre::MeshPtr mesh = tb.buffer->transformToMesh(tb.meshName); + + // Apply material if specified + if (tb.materialEntity.is_alive() && tb.materialEntity.has()) { + const auto& material = tb.materialEntity.get(); + if (material.created && !material.materialName.empty()) { + mesh->getSubMesh(0)->setMaterialName(material.materialName); + } + } + + // Either attach to SceneNode or StaticGeometry + if (tb.useStaticGeometry && tb.staticGeometryEntity.is_alive()) { + // Add to StaticGeometry as a member + attachToStaticGeometry(entity, tb); + } else { + // Attach to entity's SceneNode + if (entity.has()) { + auto& transform = entity.get_mut(); + if (transform.node) { + tb.ogreEntity = m_sceneMgr->createEntity(tb.meshName); + transform.node->attachObject(tb.ogreEntity); + } + } + } + + tb.meshCreated = true; + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMesh: Created mesh '" + tb.meshName + "'"); + + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMesh ERROR: Failed to create mesh: " + e.getDescription()); + } +} + +void ProceduralMeshSystem::rebuildTriangleBuffer(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) { + return; + } + + entity.get_mut().markDirty(); +} + +void ProceduralMeshSystem::attachToStaticGeometry(flecs::entity entity, TriangleBufferComponent& tb) +{ + if (!tb.staticGeometryEntity.is_alive() || !tb.staticGeometryEntity.has()) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMesh: StaticGeometry entity invalid or missing StaticGeometryComponent"); + return; + } + + auto& region = tb.staticGeometryEntity.get_mut(); + + // Get or create StaticGeometryMemberComponent on the TriangleBuffer entity + StaticGeometryMemberComponent* member = nullptr; + if (entity.has()) { + member = &entity.get_mut(); + } else { + // Add the component + entity.add(); + member = &entity.get_mut(); + } + + // Configure the member + member->regionEntity = tb.staticGeometryEntity; + member->regionId = region.regionId; + member->meshName = tb.meshName; + member->castShadows = true; + member->visible = true; + member->dirty = true; + + // Get material name if available + if (tb.materialEntity.is_alive() && tb.materialEntity.has()) { + const auto& material = tb.materialEntity.get(); + if (material.created && !material.materialName.empty()) { + member->materialName = material.materialName; + } + } + + // Mark the region as dirty to trigger rebuild + region.markDirty(); + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMesh: Attached mesh '" + tb.meshName + "' to StaticGeometry region '" + + region.regionName + "'"); +} diff --git a/src/features/editScene/systems/ProceduralMeshSystem.hpp b/src/features/editScene/systems/ProceduralMeshSystem.hpp new file mode 100644 index 0000000..b226a0d --- /dev/null +++ b/src/features/editScene/systems/ProceduralMeshSystem.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +namespace Ogre { + class SceneManager; +} + +namespace Procedural { + class TriangleBuffer; +} + +class ProceduralMeshSystem { +public: + ProceduralMeshSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~ProceduralMeshSystem(); + + // Initialize the system + void initialize(); + + // Main update - builds dirty triangle buffers and converts to meshes + void update(); + + // Force rebuild of a specific triangle buffer + void rebuildTriangleBuffer(flecs::entity entity); + + // Check if triangle buffer has valid triangles (non-degenerate) + bool hasValidTriangles(Procedural::TriangleBuffer* buffer); + +private: + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + flecs::query m_triangleBufferQuery; + flecs::query m_primitiveQuery; + + bool m_initialized = false; + int m_meshCount = 0; + + // Build triangle buffer from primitive children + void buildTriangleBuffer(flecs::entity entity, struct TriangleBufferComponent& tb); + + // Convert triangle buffer to Ogre mesh + void convertToMesh(flecs::entity entity, struct TriangleBufferComponent& tb); + + // Apply UV mapping from named rectangle + void applyUVMapping(Procedural::TriangleBuffer* buffer, struct ProceduralTextureComponent& texture, + const std::string& rectName); + + // Destroy existing mesh/entity + void destroyMesh(struct TriangleBufferComponent& tb); + + // Check if any primitive children are dirty + bool arePrimitivesDirty(flecs::entity parent); + + // Attach mesh to StaticGeometry region + void attachToStaticGeometry(flecs::entity entity, struct TriangleBufferComponent& tb); +}; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 8242d73..6a91c04 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -12,6 +12,9 @@ #include "../components/StaticGeometry.hpp" #include "../components/StaticGeometryMember.hpp" #include "../components/ProceduralTexture.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/Primitive.hpp" +#include "../components/TriangleBuffer.hpp" #include "EditorUISystem.hpp" #include #include @@ -161,6 +164,18 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["proceduralTexture"] = serializeProceduralTexture(entity); } + if (entity.has()) { + json["proceduralMaterial"] = serializeProceduralMaterial(entity); + } + + if (entity.has()) { + json["primitive"] = serializePrimitive(entity); + } + + if (entity.has()) { + json["triangleBuffer"] = serializeTriangleBuffer(entity); + } + // Serialize children json["children"] = nlohmann::json::array(); entity.children([&](flecs::entity child) { @@ -242,6 +257,18 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeProceduralTexture(entity, json["proceduralTexture"]); } + if (json.contains("proceduralMaterial")) { + deserializeProceduralMaterial(entity, json["proceduralMaterial"]); + } + + if (json.contains("primitive")) { + deserializePrimitive(entity, json["primitive"]); + } + + if (json.contains("triangleBuffer")) { + deserializeTriangleBuffer(entity, json["triangleBuffer"]); + } + // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); @@ -1033,3 +1060,147 @@ void SceneSerializer::deserializeProceduralTexture(flecs::entity entity, const n entity.set(texture); } + +nlohmann::json SceneSerializer::serializeProceduralMaterial(flecs::entity entity) +{ + auto& material = entity.get(); + nlohmann::json json; + + json["materialName"] = material.materialName; + json["diffuseTextureEntity"] = material.diffuseTextureEntity.id(); + + json["ambient"] = {{"r", material.ambient[0]}, {"g", material.ambient[1]}, {"b", material.ambient[2]}}; + json["diffuse"] = {{"r", material.diffuse[0]}, {"g", material.diffuse[1]}, {"b", material.diffuse[2]}}; + json["specular"] = {{"r", material.specular[0]}, {"g", material.specular[1]}, {"b", material.specular[2]}}; + json["shininess"] = material.shininess; + json["roughness"] = material.roughness; + + return json; +} + +void SceneSerializer::deserializeProceduralMaterial(flecs::entity entity, const nlohmann::json& json) +{ + ProceduralMaterialComponent material; + + material.materialName = json.value("materialName", ""); + + // Store the entity ID for now - resolution will happen at runtime + if (json.contains("diffuseTextureEntity")) { + uint64_t textureEntityId = json["diffuseTextureEntity"]; + if (textureEntityId != 0) { + // Will resolve to actual entity in update loop + material.diffuseTextureEntity = m_world.entity(textureEntityId); + } + } + + if (json.contains("ambient")) { + material.ambient[0] = json["ambient"].value("r", 0.2f); + material.ambient[1] = json["ambient"].value("g", 0.2f); + material.ambient[2] = json["ambient"].value("b", 0.2f); + } + + if (json.contains("diffuse")) { + material.diffuse[0] = json["diffuse"].value("r", 1.0f); + material.diffuse[1] = json["diffuse"].value("g", 1.0f); + material.diffuse[2] = json["diffuse"].value("b", 1.0f); + } + + if (json.contains("specular")) { + material.specular[0] = json["specular"].value("r", 0.0f); + material.specular[1] = json["specular"].value("g", 0.0f); + material.specular[2] = json["specular"].value("b", 0.0f); + } + + material.shininess = json.value("shininess", 32.0f); + material.roughness = json.value("roughness", 0.5f); + + material.dirty = true; + + entity.set(material); +} + +nlohmann::json SceneSerializer::serializePrimitive(flecs::entity entity) +{ + auto& prim = entity.get(); + nlohmann::json json; + + json["type"] = static_cast(prim.type); + + // Box parameters + json["boxSizeX"] = prim.boxSizeX; + json["boxSizeY"] = prim.boxSizeY; + json["boxSizeZ"] = prim.boxSizeZ; + + // Plane parameters + json["planeSizeX"] = prim.planeSizeX; + json["planeSizeY"] = prim.planeSizeY; + json["planeSegmentsX"] = prim.planeSegmentsX; + json["planeSegmentsY"] = prim.planeSegmentsY; + + return json; +} + +void SceneSerializer::deserializePrimitive(flecs::entity entity, const nlohmann::json& json) +{ + PrimitiveComponent prim; + + prim.type = static_cast(json.value("type", 0)); + + prim.boxSizeX = json.value("boxSizeX", 1.0f); + prim.boxSizeY = json.value("boxSizeY", 1.0f); + prim.boxSizeZ = json.value("boxSizeZ", 1.0f); + + prim.planeSizeX = json.value("planeSizeX", 1.0f); + prim.planeSizeY = json.value("planeSizeY", 1.0f); + prim.planeSegmentsX = json.value("planeSegmentsX", 1); + prim.planeSegmentsY = json.value("planeSegmentsY", 1); + + prim.dirty = true; + + entity.set(prim); +} + +nlohmann::json SceneSerializer::serializeTriangleBuffer(flecs::entity entity) +{ + auto& tb = entity.get(); + nlohmann::json json; + + // Only serialize configuration, not the buffer contents + json["meshName"] = tb.meshName; + json["textureEntity"] = tb.textureEntity.id(); + json["textureRectName"] = tb.textureRectName; + json["materialEntity"] = tb.materialEntity.id(); + json["useStaticGeometry"] = tb.useStaticGeometry; + json["staticGeometryEntity"] = tb.staticGeometryEntity.id(); + + return json; +} + +void SceneSerializer::deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json) +{ + TriangleBufferComponent tb; + + tb.meshName = json.value("meshName", ""); + tb.textureRectName = json.value("textureRectName", ""); + tb.useStaticGeometry = json.value("useStaticGeometry", false); + + // Store entity IDs for runtime resolution + if (json.contains("textureEntity")) { + uint64_t id = json["textureEntity"]; + if (id != 0) tb.textureEntity = m_world.entity(id); + } + + if (json.contains("materialEntity")) { + uint64_t id = json["materialEntity"]; + if (id != 0) tb.materialEntity = m_world.entity(id); + } + + if (json.contains("staticGeometryEntity")) { + uint64_t id = json["staticGeometryEntity"]; + if (id != 0) tb.staticGeometryEntity = m_world.entity(id); + } + + tb.dirty = true; + + entity.set(tb); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index d3cf1f6..3fa821f 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -51,6 +51,9 @@ private: nlohmann::json serializeStaticGeometry(flecs::entity entity); nlohmann::json serializeStaticGeometryMember(flecs::entity entity); nlohmann::json serializeProceduralTexture(flecs::entity entity); + nlohmann::json serializeProceduralMaterial(flecs::entity entity); + nlohmann::json serializePrimitive(flecs::entity entity); + nlohmann::json serializeTriangleBuffer(flecs::entity entity); // Component deserialization void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); @@ -65,6 +68,9 @@ private: void deserializeStaticGeometry(flecs::entity entity, const nlohmann::json& json); void deserializeStaticGeometryMember(flecs::entity entity, const nlohmann::json& json); void deserializeProceduralTexture(flecs::entity entity, const nlohmann::json& json); + void deserializeProceduralMaterial(flecs::entity entity, const nlohmann::json& json); + void deserializePrimitive(flecs::entity entity, const nlohmann::json& json); + void deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/ui/PrimitiveEditor.cpp b/src/features/editScene/ui/PrimitiveEditor.cpp new file mode 100644 index 0000000..b2c40da --- /dev/null +++ b/src/features/editScene/ui/PrimitiveEditor.cpp @@ -0,0 +1,83 @@ +#include "PrimitiveEditor.hpp" +#include + +bool PrimitiveEditor::renderComponent(flecs::entity entity, PrimitiveComponent &primitive) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Primitive", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Type selector + const char* typeNames[] = {"None", "Box", "Plane"}; + int currentType = static_cast(primitive.type); + + if (ImGui::Combo("Type", ¤tType, typeNames, IM_ARRAYSIZE(typeNames))) { + primitive.type = static_cast(currentType); + primitive.markDirty(); + modified = true; + } + + ImGui::Separator(); + + // Type-specific parameters + switch (primitive.type) { + case PrimitiveComponent::Type::Box: { + ImGui::Text("Box Dimensions:"); + if (ImGui::DragFloat("Size X", &primitive.boxSizeX, 0.01f, 0.001f, 100.0f)) { + primitive.markDirty(); + modified = true; + } + if (ImGui::DragFloat("Size Y", &primitive.boxSizeY, 0.01f, 0.001f, 100.0f)) { + primitive.markDirty(); + modified = true; + } + if (ImGui::DragFloat("Size Z", &primitive.boxSizeZ, 0.01f, 0.001f, 100.0f)) { + primitive.markDirty(); + modified = true; + } + break; + } + case PrimitiveComponent::Type::Plane: { + ImGui::Text("Plane Dimensions:"); + if (ImGui::DragFloat("Size X", &primitive.planeSizeX, 0.01f, 0.001f, 100.0f)) { + primitive.markDirty(); + modified = true; + } + if (ImGui::DragFloat("Size Y", &primitive.planeSizeY, 0.01f, 0.001f, 100.0f)) { + primitive.markDirty(); + modified = true; + } + ImGui::Separator(); + ImGui::Text("Segments:"); + if (ImGui::DragInt("Segments X", &primitive.planeSegmentsX, 1, 1, 100)) { + primitive.markDirty(); + modified = true; + } + if (ImGui::DragInt("Segments Y", &primitive.planeSegmentsY, 1, 1, 100)) { + primitive.markDirty(); + modified = true; + } + break; + } + default: + ImGui::TextDisabled("Select a primitive type to configure"); + break; + } + + ImGui::Separator(); + + // Status + if (primitive.dirty) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Modified (needs rebuild)"); + } else { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Up to date"); + } + + ImGui::TextDisabled("Note: Add this entity as child of a TriangleBuffer entity"); + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/PrimitiveEditor.hpp b/src/features/editScene/ui/PrimitiveEditor.hpp new file mode 100644 index 0000000..3045c40 --- /dev/null +++ b/src/features/editScene/ui/PrimitiveEditor.hpp @@ -0,0 +1,18 @@ +#ifndef EDITSCENE_PRIMITIVEEDITOR_HPP +#define EDITSCENE_PRIMITIVEEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/Primitive.hpp" +#include + +/** + * Editor for PrimitiveComponent (Box, Plane, etc.) + */ +class PrimitiveEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, PrimitiveComponent &primitive) override; + const char *getName() const override { return "Primitive"; } +}; + +#endif // EDITSCENE_PRIMITIVEEDITOR_HPP diff --git a/src/features/editScene/ui/ProceduralMaterialEditor.cpp b/src/features/editScene/ui/ProceduralMaterialEditor.cpp new file mode 100644 index 0000000..df54e91 --- /dev/null +++ b/src/features/editScene/ui/ProceduralMaterialEditor.cpp @@ -0,0 +1,171 @@ +#include "ProceduralMaterialEditor.hpp" +#include "../components/ProceduralTexture.hpp" +#include + +ProceduralMaterialEditor::ProceduralMaterialEditor(Ogre::SceneManager* sceneMgr) + : m_sceneMgr(sceneMgr) +{ +} + +void ProceduralMaterialEditor::renderTextureSelector(flecs::entity entity, ProceduralMaterialComponent &material) +{ + // Get the world from the entity + flecs::world world = entity.world(); + + // Collect all entities with ProceduralTextureComponent + std::vector textureEntities; + std::vector textureNames; + std::vector displayNames; + + int currentIndex = -1; + int noneIndex = 0; + + // Add "None" option + displayNames.push_back("None"); + textureEntities.push_back(flecs::entity::null()); + textureNames.push_back(""); + + world.query().each([&](flecs::entity e, ProceduralTextureComponent& texture) { + textureEntities.push_back(e); + + std::string name = texture.textureName.empty() ? + "Texture " + std::to_string(e.id()) : texture.textureName; + textureNames.push_back(name); + + // Build display name + std::string display = name; + if (texture.generated) { + display += " (generated)"; + } else { + display += " (pending)"; + } + displayNames.push_back(display); + + if (material.diffuseTextureEntity == e) { + currentIndex = (int)textureEntities.size() - 1; + } + }); + + // 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("Diffuse Texture", &newIndex, comboItems.c_str())) { + if (newIndex >= 0 && newIndex < (int)textureEntities.size()) { + if (newIndex == noneIndex) { + material.diffuseTextureEntity = flecs::entity::null(); + } else { + material.diffuseTextureEntity = textureEntities[newIndex]; + } + material.markDirty(); + } + } + + // Show warning if no textures available + if (textureEntities.size() <= 1) { + ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), "Create a Procedural Texture first!"); + } +} + +bool ProceduralMaterialEditor::renderComponent(flecs::entity entity, ProceduralMaterialComponent &material) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Procedural Material", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Material info + if (material.created) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Created"); + ImGui::Text("Material: %s", material.materialName.c_str()); + } else if (material.dirty) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Needs Creation"); + } else { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1), "Status: Not Created"); + } + + ImGui::Separator(); + + // Material name + char nameBuf[256]; + strncpy(nameBuf, material.materialName.c_str(), sizeof(nameBuf) - 1); + nameBuf[sizeof(nameBuf) - 1] = '\0'; + + if (ImGui::InputText("Material Name", nameBuf, sizeof(nameBuf))) { + material.materialName = nameBuf; + material.markDirty(); + modified = true; + } + ImGui::TextDisabled("(Leave empty for auto-generated name)"); + + ImGui::Separator(); + + // Diffuse texture selector + renderTextureSelector(entity, material); + + ImGui::Separator(); + + // Material colors + ImGui::Text("Material Properties:"); + + if (ImGui::ColorEdit3("Ambient", material.ambient)) { + material.markDirty(); + modified = true; + } + + if (ImGui::ColorEdit3("Diffuse Multiplier", material.diffuse)) { + material.markDirty(); + modified = true; + } + + if (ImGui::ColorEdit3("Specular", material.specular)) { + material.markDirty(); + modified = true; + } + + if (ImGui::SliderFloat("Shininess", &material.shininess, 1.0f, 128.0f)) { + material.markDirty(); + modified = true; + } + + if (ImGui::SliderFloat("Roughness", &material.roughness, 0.0f, 1.0f)) { + material.markDirty(); + modified = true; + } + + ImGui::Separator(); + + // Recreate button + if (ImGui::Button("Recreate Material")) { + material.markDirty(); + modified = true; + } + + ImGui::SameLine(); + + // Reset to defaults + if (ImGui::Button("Reset to Defaults")) { + material.ambient[0] = 0.2f; material.ambient[1] = 0.2f; material.ambient[2] = 0.2f; + material.diffuse[0] = 1.0f; material.diffuse[1] = 1.0f; material.diffuse[2] = 1.0f; + material.specular[0] = 0.0f; material.specular[1] = 0.0f; material.specular[2] = 0.0f; + material.shininess = 32.0f; + material.roughness = 0.5f; + material.markDirty(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/ProceduralMaterialEditor.hpp b/src/features/editScene/ui/ProceduralMaterialEditor.hpp new file mode 100644 index 0000000..e8b3f13 --- /dev/null +++ b/src/features/editScene/ui/ProceduralMaterialEditor.hpp @@ -0,0 +1,25 @@ +#ifndef EDITSCENE_PROCEDURALMATERIALEDITOR_HPP +#define EDITSCENE_PROCEDURALMATERIALEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/ProceduralMaterial.hpp" +#include +#include + +/** + * Editor for ProceduralMaterialComponent + */ +class ProceduralMaterialEditor : public ComponentEditor { +public: + ProceduralMaterialEditor(Ogre::SceneManager* sceneMgr = nullptr); + bool renderComponent(flecs::entity entity, ProceduralMaterialComponent &material) override; + const char *getName() const override { return "Procedural Material"; } + +private: + void renderTextureSelector(flecs::entity entity, ProceduralMaterialComponent &material); + + Ogre::SceneManager* m_sceneMgr; +}; + +#endif // EDITSCENE_PROCEDURALMATERIALEDITOR_HPP diff --git a/src/features/editScene/ui/TransformEditor.cpp b/src/features/editScene/ui/TransformEditor.cpp index 9227ed6..ed2ed0a 100644 --- a/src/features/editScene/ui/TransformEditor.cpp +++ b/src/features/editScene/ui/TransformEditor.cpp @@ -126,6 +126,7 @@ bool TransformEditor::renderComponent(flecs::entity entity, // Apply changes to scene node if (modified && transform.node) { transform.applyToNode(); + transform.markChanged(); } ImGui::Unindent(); diff --git a/src/features/editScene/ui/TriangleBufferEditor.cpp b/src/features/editScene/ui/TriangleBufferEditor.cpp new file mode 100644 index 0000000..99904b9 --- /dev/null +++ b/src/features/editScene/ui/TriangleBufferEditor.cpp @@ -0,0 +1,283 @@ +#include "TriangleBufferEditor.hpp" +#include "../components/ProceduralTexture.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/StaticGeometry.hpp" +#include "../components/Primitive.hpp" +#include + +TriangleBufferEditor::TriangleBufferEditor(Ogre::SceneManager* sceneMgr) + : m_sceneMgr(sceneMgr) +{ +} + +void TriangleBufferEditor::renderTextureSelector(flecs::entity entity, TriangleBufferComponent &tb) +{ + flecs::world world = entity.world(); + + std::vector textureEntities; + std::vector textureNames; + + int currentIndex = -1; + int noneIndex = 0; + + textureEntities.push_back(flecs::entity::null()); + textureNames.push_back("None"); + + world.query().each([&](flecs::entity e, ProceduralTextureComponent& tex) { + textureEntities.push_back(e); + std::string name = tex.textureName.empty() ? "Texture " + std::to_string(e.id()) : tex.textureName; + textureNames.push_back(name); + + if (tb.textureEntity == e) { + currentIndex = (int)textureEntities.size() - 1; + } + }); + + if (currentIndex == -1) currentIndex = noneIndex; + + std::string comboItems; + for (size_t i = 0; i < textureNames.size(); ++i) { + if (i > 0) comboItems += '\0'; + comboItems += textureNames[i]; + } + comboItems += '\0'; + + int newIndex = currentIndex; + if (ImGui::Combo("Texture Source", &newIndex, comboItems.c_str())) { + if (newIndex == noneIndex) { + tb.textureEntity = flecs::entity::null(); + tb.textureRectName.clear(); + } else { + tb.textureEntity = textureEntities[newIndex]; + } + tb.markDirty(); + } +} + +void TriangleBufferEditor::renderRectSelector(flecs::entity entity, TriangleBufferComponent &tb) +{ + if (!tb.textureEntity.is_alive() || !tb.textureEntity.has()) { + ImGui::TextDisabled("Select a texture first"); + return; + } + + const auto& texture = tb.textureEntity.get(); + const auto& namedRects = texture.getAllNamedRects(); + + if (namedRects.empty()) { + ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), "No named rectangles in texture!"); + ImGui::TextDisabled("Add named rectangles in the Procedural Texture component"); + return; + } + + std::vector rectNames; + int currentIndex = -1; + int noneIndex = 0; + + rectNames.push_back("None (use full texture)"); + + for (const auto& pair : namedRects) { + rectNames.push_back(pair.first); + if (tb.textureRectName == pair.first) { + currentIndex = (int)rectNames.size() - 1; + } + } + + if (currentIndex == -1) currentIndex = noneIndex; + + std::string comboItems; + for (size_t i = 0; i < rectNames.size(); ++i) { + if (i > 0) comboItems += '\0'; + comboItems += rectNames[i]; + } + comboItems += '\0'; + + int newIndex = currentIndex; + if (ImGui::Combo("Texture Rectangle", &newIndex, comboItems.c_str())) { + if (newIndex == noneIndex) { + tb.textureRectName.clear(); + } else { + tb.textureRectName = rectNames[newIndex]; + } + tb.markDirty(); + } +} + +void TriangleBufferEditor::renderMaterialSelector(flecs::entity entity, TriangleBufferComponent &tb) +{ + flecs::world world = entity.world(); + + std::vector materialEntities; + std::vector materialNames; + + int currentIndex = -1; + int noneIndex = 0; + + materialEntities.push_back(flecs::entity::null()); + materialNames.push_back("None"); + + world.query().each([&](flecs::entity e, ProceduralMaterialComponent& mat) { + materialEntities.push_back(e); + std::string name = mat.materialName.empty() ? "Material " + std::to_string(e.id()) : mat.materialName; + if (mat.created) name += " (created)"; + else name += " (pending)"; + materialNames.push_back(name); + + if (tb.materialEntity == e) { + currentIndex = (int)materialEntities.size() - 1; + } + }); + + if (currentIndex == -1) currentIndex = noneIndex; + + std::string comboItems; + for (size_t i = 0; i < materialNames.size(); ++i) { + if (i > 0) comboItems += '\0'; + comboItems += materialNames[i]; + } + comboItems += '\0'; + + int newIndex = currentIndex; + if (ImGui::Combo("Material", &newIndex, comboItems.c_str())) { + if (newIndex == noneIndex) { + tb.materialEntity = flecs::entity::null(); + } else { + tb.materialEntity = materialEntities[newIndex]; + } + tb.markDirty(); + } +} + +void TriangleBufferEditor::renderStaticGeometrySelector(flecs::entity entity, TriangleBufferComponent &tb) +{ + flecs::world world = entity.world(); + + if (!tb.useStaticGeometry) { + return; + } + + std::vector regionEntities; + std::vector regionNames; + + int currentIndex = -1; + int noneIndex = 0; + + regionEntities.push_back(flecs::entity::null()); + regionNames.push_back("None"); + + world.query().each([&](flecs::entity e, StaticGeometryComponent& region) { + regionEntities.push_back(e); + std::string name = region.regionName.empty() ? "Region " + std::to_string(e.id()) : region.regionName; + regionNames.push_back(name); + + if (tb.staticGeometryEntity == e) { + currentIndex = (int)regionEntities.size() - 1; + } + }); + + if (currentIndex == -1) currentIndex = noneIndex; + + std::string comboItems; + for (size_t i = 0; i < regionNames.size(); ++i) { + if (i > 0) comboItems += '\0'; + comboItems += regionNames[i]; + } + comboItems += '\0'; + + int newIndex = currentIndex; + if (ImGui::Combo("StaticGeometry Region", &newIndex, comboItems.c_str())) { + if (newIndex == noneIndex) { + tb.staticGeometryEntity = flecs::entity::null(); + } else { + tb.staticGeometryEntity = regionEntities[newIndex]; + } + tb.markDirty(); + } + + if (regionEntities.size() <= 1) { + ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), "No StaticGeometry regions! Create one first."); + } +} + +bool TriangleBufferEditor::renderComponent(flecs::entity entity, TriangleBufferComponent &tb) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Triangle Buffer", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Status + if (tb.meshCreated) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Mesh Created"); + ImGui::Text("Mesh: %s", tb.meshName.c_str()); + } else if (tb.dirty) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Needs Rebuild"); + } else { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1), "Status: Empty"); + } + + ImGui::Separator(); + + // Mesh name + char nameBuf[256]; + strncpy(nameBuf, tb.meshName.c_str(), sizeof(nameBuf) - 1); + nameBuf[sizeof(nameBuf) - 1] = '\0'; + + if (ImGui::InputText("Mesh Name", nameBuf, sizeof(nameBuf))) { + tb.meshName = nameBuf; + tb.markDirty(); + modified = true; + } + + ImGui::Separator(); + + // Texture and UV mapping + ImGui::Text("Texture Mapping:"); + renderTextureSelector(entity, tb); + renderRectSelector(entity, tb); + ImGui::TextDisabled("UVs will be mapped to rectangle with 0.01 margin"); + + ImGui::Separator(); + + // Material + ImGui::Text("Material:"); + renderMaterialSelector(entity, tb); + + ImGui::Separator(); + + // Output mode + ImGui::Text("Output:"); + if (ImGui::Checkbox("Use StaticGeometry", &tb.useStaticGeometry)) { + tb.markDirty(); + modified = true; + } + + if (tb.useStaticGeometry) { + renderStaticGeometrySelector(entity, tb); + } + + ImGui::Separator(); + + // Rebuild button + if (ImGui::Button("Force Rebuild")) { + tb.markDirty(); + modified = true; + } + + ImGui::Separator(); + + // Info + ImGui::TextDisabled("Add Primitive components as children to build geometry"); + + // Count children primitives + int primCount = 0; + entity.children([&](flecs::entity child) { + if (child.has()) primCount++; + }); + ImGui::Text("Child primitives: %d", primCount); + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/TriangleBufferEditor.hpp b/src/features/editScene/ui/TriangleBufferEditor.hpp new file mode 100644 index 0000000..5bdbd73 --- /dev/null +++ b/src/features/editScene/ui/TriangleBufferEditor.hpp @@ -0,0 +1,28 @@ +#ifndef EDITSCENE_TRIANGLEBUFFEREDITOR_HPP +#define EDITSCENE_TRIANGLEBUFFEREDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/TriangleBuffer.hpp" +#include +#include + +/** + * Editor for TriangleBufferComponent + */ +class TriangleBufferEditor : public ComponentEditor { +public: + TriangleBufferEditor(Ogre::SceneManager* sceneMgr = nullptr); + bool renderComponent(flecs::entity entity, TriangleBufferComponent &tb) override; + const char *getName() const override { return "Triangle Buffer"; } + +private: + void renderTextureSelector(flecs::entity entity, TriangleBufferComponent &tb); + void renderRectSelector(flecs::entity entity, TriangleBufferComponent &tb); + void renderMaterialSelector(flecs::entity entity, TriangleBufferComponent &tb); + void renderStaticGeometrySelector(flecs::entity entity, TriangleBufferComponent &tb); + + Ogre::SceneManager* m_sceneMgr; +}; + +#endif // EDITSCENE_TRIANGLEBUFFEREDITOR_HPP