From b1413d6d00e59f0ab98ad128a972b8624daf4edb Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Sat, 4 Apr 2026 03:42:50 +0300 Subject: [PATCH] Procedural texture implementation --- src/features/editScene/CMakeLists.txt | 8 + src/features/editScene/EditorApp.cpp | 37 +++ src/features/editScene/EditorApp.hpp | 2 + .../components/ProceduralTexture.hpp | 145 +++++++++ .../components/ProceduralTextureModule.cpp | 34 ++ src/features/editScene/gizmo/Gizmo.cpp | 38 ++- src/features/editScene/gizmo/Gizmo.hpp | 5 + .../editScene/systems/EditorUISystem.cpp | 16 + .../editScene/systems/EditorUISystem.hpp | 5 + .../systems/ProceduralTextureSystem.cpp | 136 ++++++++ .../systems/ProceduralTextureSystem.hpp | 38 +++ .../editScene/systems/SceneSerializer.cpp | 87 +++++ .../editScene/systems/SceneSerializer.hpp | 2 + .../editScene/ui/ProceduralTextureEditor.cpp | 305 ++++++++++++++++++ .../editScene/ui/ProceduralTextureEditor.hpp | 31 ++ 15 files changed, 880 insertions(+), 9 deletions(-) create mode 100644 src/features/editScene/components/ProceduralTexture.hpp create mode 100644 src/features/editScene/components/ProceduralTextureModule.cpp create mode 100644 src/features/editScene/systems/ProceduralTextureSystem.cpp create mode 100644 src/features/editScene/systems/ProceduralTextureSystem.hpp create mode 100644 src/features/editScene/ui/ProceduralTextureEditor.cpp create mode 100644 src/features/editScene/ui/ProceduralTextureEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 2fb66da..b6c2c0e 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -19,6 +19,7 @@ set(EDITSCENE_SOURCES systems/CameraSystem.cpp systems/LodSystem.cpp systems/StaticGeometrySystem.cpp + systems/ProceduralTextureSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -29,11 +30,13 @@ set(EDITSCENE_SOURCES ui/LodSettingsEditor.cpp ui/StaticGeometryEditor.cpp ui/StaticGeometryMemberEditor.cpp + ui/ProceduralTextureEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp components/CameraModule.cpp components/LodModule.cpp components/StaticGeometryModule.cpp + components/ProceduralTextureModule.cpp camera/EditorCamera.cpp gizmo/Gizmo.cpp physics/physics.cpp @@ -53,7 +56,9 @@ set(EDITSCENE_HEADERS components/LodSettings.hpp components/StaticGeometry.hpp components/StaticGeometryMember.hpp + components/ProceduralTexture.hpp systems/EditorUISystem.hpp + systems/ProceduralTextureSystem.hpp systems/StaticGeometrySystem.hpp systems/SceneSerializer.hpp systems/PhysicsSystem.hpp @@ -71,6 +76,9 @@ set(EDITSCENE_HEADERS ui/CameraEditor.hpp ui/LodEditor.hpp ui/LodSettingsEditor.hpp + ui/StaticGeometryEditor.hpp + ui/StaticGeometryMemberEditor.hpp + ui/ProceduralTextureEditor.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 5603159..bd6d4d3 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -6,6 +6,7 @@ #include "systems/CameraSystem.hpp" #include "systems/LodSystem.hpp" #include "systems/StaticGeometrySystem.hpp" +#include "systems/ProceduralTextureSystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -19,6 +20,7 @@ #include "components/LodSettings.hpp" #include "components/StaticGeometry.hpp" #include "components/StaticGeometryMember.hpp" +#include "components/ProceduralTexture.hpp" #include #include @@ -69,6 +71,29 @@ EditorApp::EditorApp() EditorApp::~EditorApp() { + // Shutdown UI system first (cleans up gizmo while SceneManager is still valid) + if (m_uiSystem) { + m_uiSystem->shutdown(); + } + + // Delete all editor entities before OGRE cleanup + // This ensures all components with Ogre resources are cleaned up while SceneManager exists + m_world.query().each([&](flecs::entity e, EditorMarkerComponent) { + e.destruct(); + }); + + // Release all systems + m_proceduralTextureSystem.reset(); + m_staticGeometrySystem.reset(); + m_lodSystem.reset(); + m_cameraSystem.reset(); + m_lightSystem.reset(); + m_physicsSystem.reset(); + m_imguiListener.reset(); + m_uiSystem.reset(); + m_camera.reset(); + + // Now OGRE can shut down safely // Singletons are managed by OGRE, don't delete them } @@ -135,6 +160,10 @@ void EditorApp::setup() // Setup StaticGeometry system m_staticGeometrySystem = std::make_unique(m_world, m_sceneMgr); m_staticGeometrySystem->initialize(); + + // Setup ProceduralTexture system + m_proceduralTextureSystem = std::make_unique(m_world, m_sceneMgr); + m_proceduralTextureSystem->initialize(); // Add default entities to UI cache for (auto &e : m_defaultEntities) { @@ -179,6 +208,9 @@ void EditorApp::setupECS() // Register StaticGeometry components m_world.component(); m_world.component(); + + // Register ProceduralTexture component + m_world.component(); } void EditorApp::createDefaultEntities() @@ -326,6 +358,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) if (m_staticGeometrySystem) { m_staticGeometrySystem->update(); } + + // Update ProceduralTexture system + if (m_proceduralTextureSystem) { + m_proceduralTextureSystem->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 1c29128..11bf914 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -18,6 +18,7 @@ class EditorLightSystem; class EditorCameraSystem; class EditorLodSystem; class StaticGeometrySystem; +class ProceduralTextureSystem; /** * RenderTargetListener for ImGui frame management @@ -88,6 +89,7 @@ private: std::unique_ptr m_cameraSystem; std::unique_ptr m_lodSystem; std::unique_ptr m_staticGeometrySystem; + std::unique_ptr m_proceduralTextureSystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/ProceduralTexture.hpp b/src/features/editScene/components/ProceduralTexture.hpp new file mode 100644 index 0000000..095e36b --- /dev/null +++ b/src/features/editScene/components/ProceduralTexture.hpp @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include +#include +#include + +/** + * @brief Information about a named rectangle in the texture atlas + */ +struct TextureRectInfo { + std::string name; + float u1, v1; // Top-left UV (0-1 range) + float u2, v2; // Bottom-right UV (0-1 range) + + TextureRectInfo() : u1(0), v1(0), u2(1), v2(1) {} + TextureRectInfo(const std::string& n, float left, float top, float right, float bottom) + : name(n), u1(left), v1(top), u2(right), v2(bottom) {} +}; + +/** + * @brief Component for procedural texture generation with 10x10 colored rectangles + * + * Uses OgreProcedural to generate a texture with a grid of colored rectangles. + * Each rectangle color is individually selectable. + * Also supports named rectangles for texture atlas/UV mapping. + */ +struct ProceduralTextureComponent { + // Texture name for Ogre resource + std::string textureName; + + // Grid dimensions (default 10x10) + static constexpr int GRID_SIZE = 10; + static constexpr int RECT_COUNT = GRID_SIZE * GRID_SIZE; + + // Colors for each rectangle (stored as float4: r, g, b, a) + std::array colors; + + // Named rectangles for texture atlas (name -> rect info) + std::map namedRects; + + // Texture size (default 512x512) + int textureSize = 512; + + // Whether the texture needs regeneration + bool dirty = true; + + // Whether the texture has been generated + bool generated = false; + + // Pointer to the generated Ogre texture + Ogre::TexturePtr ogreTexture; + + ProceduralTextureComponent() { + // Initialize with default colors (checkerboard pattern) + for (int i = 0; i < RECT_COUNT; ++i) { + int row = i / GRID_SIZE; + int col = i % GRID_SIZE; + bool isWhite = (row + col) % 2 == 0; + + colors[i * 4 + 0] = isWhite ? 1.0f : 0.0f; // R + colors[i * 4 + 1] = isWhite ? 1.0f : 0.0f; // G + colors[i * 4 + 2] = isWhite ? 1.0f : 0.0f; // B + colors[i * 4 + 3] = 1.0f; // A + } + } + + // Get color for a specific rectangle + Ogre::ColourValue getColor(int index) const { + if (index < 0 || index >= RECT_COUNT) return Ogre::ColourValue::White; + return Ogre::ColourValue( + colors[index * 4 + 0], + colors[index * 4 + 1], + colors[index * 4 + 2], + colors[index * 4 + 3] + ); + } + + // Set color for a specific rectangle + void setColor(int index, const Ogre::ColourValue& color) { + if (index < 0 || index >= RECT_COUNT) return; + colors[index * 4 + 0] = color.r; + colors[index * 4 + 1] = color.g; + colors[index * 4 + 2] = color.b; + colors[index * 4 + 3] = color.a; + dirty = true; + } + + // Calculate UV coordinates for a rectangle index + void getRectUVs(int index, float& u1, float& v1, float& u2, float& v2) const { + int row = index / GRID_SIZE; + int col = index % GRID_SIZE; + float cellSize = 1.0f / GRID_SIZE; + + u1 = col * cellSize; + v1 = row * cellSize; + u2 = (col + 1) * cellSize; + v2 = (row + 1) * cellSize; + } + + // Add a named rectangle + bool addNamedRect(const std::string& name, int rectIndex) { + if (name.empty() || rectIndex < 0 || rectIndex >= RECT_COUNT) return false; + + float u1, v1, u2, v2; + getRectUVs(rectIndex, u1, v1, u2, v2); + + namedRects[name] = TextureRectInfo(name, u1, v1, u2, v2); + return true; + } + + // Remove a named rectangle + bool removeNamedRect(const std::string& name) { + auto it = namedRects.find(name); + if (it != namedRects.end()) { + namedRects.erase(it); + return true; + } + return false; + } + + // Get named rectangle info + const TextureRectInfo* getNamedRect(const std::string& name) const { + auto it = namedRects.find(name); + if (it != namedRects.end()) { + return &it->second; + } + return nullptr; + } + + // Check if a name exists + bool hasNamedRect(const std::string& name) const { + return namedRects.find(name) != namedRects.end(); + } + + // Get all named rectangles + const std::map& getAllNamedRects() const { + return namedRects; + } + + void markDirty() { + dirty = true; + } +}; diff --git a/src/features/editScene/components/ProceduralTextureModule.cpp b/src/features/editScene/components/ProceduralTextureModule.cpp new file mode 100644 index 0000000..c553161 --- /dev/null +++ b/src/features/editScene/components/ProceduralTextureModule.cpp @@ -0,0 +1,34 @@ +#include "ProceduralTexture.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/ProceduralTextureEditor.hpp" + +// Register ProceduralTexture component +REGISTER_COMPONENT("Procedural Texture", ProceduralTextureComponent, ProceduralTextureEditor) +{ + registry.registerComponent( + "Procedural Texture", + std::make_unique(), + // Adder + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [sceneMgr](flecs::entity e) { + if (e.has()) { + auto& texture = e.get_mut(); + // Clean up Ogre texture - wrap in try/catch since TextureManager may be shutting down + if (texture.ogreTexture) { + try { + if (Ogre::TextureManager::getSingletonPtr()) { + Ogre::TextureManager::getSingleton().remove(texture.ogreTexture); + } + } catch (...) {} + texture.ogreTexture.reset(); + } + e.remove(); + } + } + ); +} diff --git a/src/features/editScene/gizmo/Gizmo.cpp b/src/features/editScene/gizmo/Gizmo.cpp index 02adc36..06abcfe 100644 --- a/src/features/editScene/gizmo/Gizmo.cpp +++ b/src/features/editScene/gizmo/Gizmo.cpp @@ -41,34 +41,54 @@ Gizmo::Gizmo(Ogre::SceneManager *sceneMgr) m_gizmoNode->setVisible(false); } -Gizmo::~Gizmo() +void Gizmo::shutdown() { + if (!m_sceneMgr) { + return; // Already shutdown + } + // Detach objects from node first to avoid double-delete - if (m_gizmoNode) { - if (m_axisX) m_gizmoNode->detachObject(m_axisX); - if (m_axisY) m_gizmoNode->detachObject(m_axisY); - if (m_axisZ) m_gizmoNode->detachObject(m_axisZ); + // Use try-catch since Ogre objects may be invalid during app shutdown + if (m_gizmoNode && m_axisX) { + try { m_gizmoNode->detachObject(m_axisX); } catch (...) {} + } + if (m_gizmoNode && m_axisY) { + try { m_gizmoNode->detachObject(m_axisY); } catch (...) {} + } + if (m_gizmoNode && m_axisZ) { + try { m_gizmoNode->detachObject(m_axisZ); } catch (...) {} } // Now destroy the manual objects if (m_axisX) { - m_sceneMgr->destroyManualObject(m_axisX); + try { m_sceneMgr->destroyManualObject(m_axisX); } catch (...) {} m_axisX = nullptr; } if (m_axisY) { - m_sceneMgr->destroyManualObject(m_axisY); + try { m_sceneMgr->destroyManualObject(m_axisY); } catch (...) {} m_axisY = nullptr; } if (m_axisZ) { - m_sceneMgr->destroyManualObject(m_axisZ); + try { m_sceneMgr->destroyManualObject(m_axisZ); } catch (...) {} m_axisZ = nullptr; } // Finally destroy the node if (m_gizmoNode) { - m_sceneMgr->destroySceneNode(m_gizmoNode); + try { m_sceneMgr->destroySceneNode(m_gizmoNode); } catch (...) {} m_gizmoNode = nullptr; } + + // Mark scene manager as invalid + m_sceneMgr = nullptr; +} + +Gizmo::~Gizmo() +{ + // If shutdown wasn't called, do cleanup now + if (m_sceneMgr) { + shutdown(); + } } void Gizmo::attachTo(flecs::entity entity) diff --git a/src/features/editScene/gizmo/Gizmo.hpp b/src/features/editScene/gizmo/Gizmo.hpp index 074550a..b97e886 100644 --- a/src/features/editScene/gizmo/Gizmo.hpp +++ b/src/features/editScene/gizmo/Gizmo.hpp @@ -27,6 +27,11 @@ public: Gizmo(Ogre::SceneManager *sceneMgr); ~Gizmo(); + + /** + * Shutdown and cleanup - must be called before SceneManager is destroyed + */ + void shutdown(); /** * Attach to an entity's transform diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 59e6bf6..ad06d1e 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -10,6 +10,7 @@ #include "../components/Lod.hpp" #include "../components/StaticGeometry.hpp" #include "../components/StaticGeometryMember.hpp" +#include "../components/ProceduralTexture.hpp" #include "../components/LodSettings.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" @@ -36,6 +37,14 @@ EditorUISystem::EditorUISystem(flecs::world &world, EditorUISystem::~EditorUISystem() = default; +void EditorUISystem::shutdown() +{ + // Shutdown gizmo before SceneManager is destroyed + if (m_gizmo) { + m_gizmo->shutdown(); + } +} + bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay) { if (m_gizmo) { @@ -554,6 +563,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity) m_componentRegistry.render(entity, member); componentCount++; } + + // Render ProceduralTexture if present + if (entity.has()) { + auto &texture = entity.get_mut(); + m_componentRegistry.render(entity, texture); + componentCount++; + } // Show message if no components if (componentCount == 0) { diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index 6aedc86..24af38a 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -62,6 +62,11 @@ public: * Get the gizmo for external interaction */ Gizmo *getGizmo() const { return m_gizmo.get(); } + + /** + * Shutdown UI system - must be called before SceneManager destruction + */ + void shutdown(); /** * Get/set SceneNode parenting mode diff --git a/src/features/editScene/systems/ProceduralTextureSystem.cpp b/src/features/editScene/systems/ProceduralTextureSystem.cpp new file mode 100644 index 0000000..56d0ee2 --- /dev/null +++ b/src/features/editScene/systems/ProceduralTextureSystem.cpp @@ -0,0 +1,136 @@ +#include "ProceduralTextureSystem.hpp" +#include "../components/ProceduralTexture.hpp" +#include +#include +#include +#include +#include + +ProceduralTextureSystem::ProceduralTextureSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_query(world.query()) +{ +} + +ProceduralTextureSystem::~ProceduralTextureSystem() = default; + +void ProceduralTextureSystem::initialize() +{ + if (m_initialized) return; + m_initialized = true; + + Ogre::LogManager::getSingleton().logMessage("ProceduralTextureSystem initialized"); +} + +void ProceduralTextureSystem::update() +{ + if (!m_initialized) return; + + m_query.each([&](flecs::entity entity, ProceduralTextureComponent& component) { + if (component.dirty || !component.generated) { + generateTexture(entity, component); + } + }); +} + +void ProceduralTextureSystem::generateTexture(flecs::entity entity, ProceduralTextureComponent& component) +{ + try { + // Generate unique texture name if not set + if (component.textureName.empty()) { + component.textureName = "ProceduralTex_" + std::to_string(entity.id()) + "_" + std::to_string(m_generatedCount++); + } + + const int gridSize = ProceduralTextureComponent::GRID_SIZE; + const int rectCount = ProceduralTextureComponent::RECT_COUNT; + const int texSize = component.textureSize; + const int rectSize = texSize / gridSize; + + // Create a buffer for the final texture + Ogre::Image finalImage; + finalImage.loadDynamicImage( + new Ogre::uchar[texSize * texSize * 4], // Allocate buffer + texSize, texSize, 1, + Ogre::PF_R8G8B8A8, true // Auto-delete buffer + ); + + // Clear to transparent + std::memset(finalImage.getData(), 0, texSize * texSize * 4); + + // Generate each rectangle using OgreProcedural::Solid and composite into final image + for (int i = 0; i < rectCount; ++i) { + int row = i / gridSize; + int col = i % gridSize; + + // Create a small texture buffer for this rectangle + Procedural::TextureBufferPtr rectBuffer(new Procedural::TextureBuffer(rectSize)); + + // Get color for this rectangle + Ogre::ColourValue color = component.getColor(i); + + // Generate solid color using OgreProcedural + Procedural::Solid(rectBuffer).setColour(color).process(); + + // Get the image data from the buffer + Ogre::Image& rectImage = rectBuffer->getImage(); + + // Copy rectangle into final image + int destX = col * rectSize; + int destY = row * rectSize; + + for (int y = 0; y < rectSize; ++y) { + for (int x = 0; x < rectSize; ++x) { + Ogre::ColourValue pixel = rectImage.getColourAt(x, y, 0); + finalImage.setColourAt(pixel, destX + x, destY + y, 0); + } + } + } + + // Destroy old texture if exists + if (component.ogreTexture) { + Ogre::TextureManager::getSingleton().remove(component.ogreTexture); + component.ogreTexture.reset(); + } + + // Create Ogre texture from image + component.ogreTexture = Ogre::TextureManager::getSingleton().loadImage( + component.textureName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, + finalImage, + Ogre::TEX_TYPE_2D, + 0, // numMipmaps + 1.0f // gamma + ); + + component.generated = true; + component.dirty = false; + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralTexture: Generated '" + component.textureName + + "' (" + std::to_string(texSize) + "x" + std::to_string(texSize) + ")"); + + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralTexture ERROR: " + e.getDescription()); + component.dirty = true; // Will retry + } +} + +void ProceduralTextureSystem::regenerateTexture(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) { + return; + } + + entity.get_mut().markDirty(); +} + +std::string ProceduralTextureSystem::getTextureName(flecs::entity entity) +{ + if (!entity.is_alive() || !entity.has()) { + return ""; + } + + return entity.get().textureName; +} diff --git a/src/features/editScene/systems/ProceduralTextureSystem.hpp b/src/features/editScene/systems/ProceduralTextureSystem.hpp new file mode 100644 index 0000000..d1295c4 --- /dev/null +++ b/src/features/editScene/systems/ProceduralTextureSystem.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace Ogre { + class SceneManager; +} + +class ProceduralTextureSystem { +public: + ProceduralTextureSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~ProceduralTextureSystem(); + + // Initialize the system + void initialize(); + + // Main update - generates dirty textures + void update(); + + // Force regenerate a specific texture + void regenerateTexture(flecs::entity entity); + + // Get the generated texture name for an entity + std::string getTextureName(flecs::entity entity); + +private: + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + + flecs::query m_query; + + bool m_initialized = false; + int m_generatedCount = 0; + + // Generate the texture for a component + void generateTexture(flecs::entity entity, struct ProceduralTextureComponent& component); +}; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index d5ebf63..8242d73 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -11,6 +11,7 @@ #include "../components/LodSettings.hpp" #include "../components/StaticGeometry.hpp" #include "../components/StaticGeometryMember.hpp" +#include "../components/ProceduralTexture.hpp" #include "EditorUISystem.hpp" #include #include @@ -156,6 +157,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["staticGeometryMember"] = serializeStaticGeometryMember(entity); } + if (entity.has()) { + json["proceduralTexture"] = serializeProceduralTexture(entity); + } + // Serialize children json["children"] = nlohmann::json::array(); entity.children([&](flecs::entity child) { @@ -233,6 +238,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit deserializeStaticGeometryMember(entity, json["staticGeometryMember"]); } + if (json.contains("proceduralTexture")) { + deserializeProceduralTexture(entity, json["proceduralTexture"]); + } + // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); @@ -946,3 +955,81 @@ void SceneSerializer::deserializeStaticGeometryMember(flecs::entity entity, cons entity.set(member); } + +nlohmann::json SceneSerializer::serializeProceduralTexture(flecs::entity entity) +{ + auto& texture = entity.get(); + nlohmann::json json; + + json["textureName"] = texture.textureName; + json["textureSize"] = texture.textureSize; + + // Serialize colors array + nlohmann::json colorsJson = nlohmann::json::array(); + for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) { + nlohmann::json color; + color["r"] = texture.colors[i * 4 + 0]; + color["g"] = texture.colors[i * 4 + 1]; + color["b"] = texture.colors[i * 4 + 2]; + color["a"] = texture.colors[i * 4 + 3]; + colorsJson.push_back(color); + } + json["colors"] = colorsJson; + + // Serialize named rectangles + nlohmann::json namedRectsJson = nlohmann::json::array(); + for (const auto& pair : texture.namedRects) { + const auto& rect = pair.second; + nlohmann::json rectJson; + rectJson["name"] = rect.name; + rectJson["u1"] = rect.u1; + rectJson["v1"] = rect.v1; + rectJson["u2"] = rect.u2; + rectJson["v2"] = rect.v2; + namedRectsJson.push_back(rectJson); + } + json["namedRects"] = namedRectsJson; + + return json; +} + +void SceneSerializer::deserializeProceduralTexture(flecs::entity entity, const nlohmann::json& json) +{ + ProceduralTextureComponent texture; + + texture.textureName = json.value("textureName", ""); + texture.textureSize = json.value("textureSize", 512); + + // Deserialize colors array + if (json.contains("colors") && json["colors"].is_array()) { + int i = 0; + for (const auto& colorJson : json["colors"]) { + if (i >= ProceduralTextureComponent::RECT_COUNT) break; + texture.colors[i * 4 + 0] = colorJson.value("r", 1.0f); + texture.colors[i * 4 + 1] = colorJson.value("g", 1.0f); + texture.colors[i * 4 + 2] = colorJson.value("b", 1.0f); + texture.colors[i * 4 + 3] = colorJson.value("a", 1.0f); + ++i; + } + } + + // Deserialize named rectangles + if (json.contains("namedRects") && json["namedRects"].is_array()) { + for (const auto& rectJson : json["namedRects"]) { + std::string name = rectJson.value("name", ""); + if (!name.empty()) { + TextureRectInfo rect; + rect.name = name; + rect.u1 = rectJson.value("u1", 0.0f); + rect.v1 = rectJson.value("v1", 0.0f); + rect.u2 = rectJson.value("u2", 1.0f); + rect.v2 = rectJson.value("v2", 1.0f); + texture.namedRects[name] = rect; + } + } + } + + texture.dirty = true; + + entity.set(texture); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 0a25da5..d3cf1f6 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -50,6 +50,7 @@ private: nlohmann::json serializeLod(flecs::entity entity); nlohmann::json serializeStaticGeometry(flecs::entity entity); nlohmann::json serializeStaticGeometryMember(flecs::entity entity); + nlohmann::json serializeProceduralTexture(flecs::entity entity); // Component deserialization void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); @@ -63,6 +64,7 @@ private: 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); + void deserializeProceduralTexture(flecs::entity entity, const nlohmann::json& json); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/ui/ProceduralTextureEditor.cpp b/src/features/editScene/ui/ProceduralTextureEditor.cpp new file mode 100644 index 0000000..c5dea88 --- /dev/null +++ b/src/features/editScene/ui/ProceduralTextureEditor.cpp @@ -0,0 +1,305 @@ +#include "ProceduralTextureEditor.hpp" +#include "../components/ProceduralTexture.hpp" +#include +#include +#include +#include +#include + +void ProceduralTextureEditor::renderColorButton(int index, ProceduralTextureComponent &texture) +{ + Ogre::ColourValue color = texture.getColor(index); + + // Create a unique ID for this button + ImGui::PushID(index); + + // Show color as a small colored button + ImVec4 buttonColor(color.r, color.g, color.b, color.a); + ImGui::ColorButton("##color", buttonColor, ImGuiColorEditFlags_NoTooltip, ImVec2(24, 24)); + + // Handle click + if (ImGui::IsItemClicked()) { + m_selectedRect = index; + } + + // Show tooltip with rectangle index + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rect %d: (%.2f, %.2f, %.2f)", index, color.r, color.g, color.b); + } + + ImGui::PopID(); +} + +void ProceduralTextureEditor::renderColorGrid(flecs::entity entity, ProceduralTextureComponent &texture) +{ + const int gridSize = ProceduralTextureComponent::GRID_SIZE; + + ImGui::Text("10x10 Grid - Click a rectangle to edit its color or add to atlas:"); + + // Calculate grid width for centering + float buttonSize = 24.0f; + float spacing = 2.0f; + float gridWidth = gridSize * buttonSize + (gridSize - 1) * spacing; + + // Get window width for centering + float windowWidth = ImGui::GetContentRegionAvail().x; + float startX = ImGui::GetCursorPosX(); + float offset = (windowWidth - gridWidth) * 0.5f; + if (offset < 0) offset = 0; + + // Render the grid - each row needs its own offset + for (int row = 0; row < gridSize; ++row) { + // Set cursor position for this row + ImGui::SetCursorPosX(startX + offset); + + for (int col = 0; col < gridSize; ++col) { + int index = row * gridSize + col; + renderColorButton(index, texture); + + if (col < gridSize - 1) { + ImGui::SameLine(0, spacing); + } + } + } + + // Color picker and naming for selected rectangle + if (m_selectedRect >= 0 && m_selectedRect < ProceduralTextureComponent::RECT_COUNT) { + ImGui::Separator(); + ImGui::Text("Editing Rectangle %d (Row %d, Col %d)", + m_selectedRect, + m_selectedRect / gridSize, + m_selectedRect % gridSize); + + // UV coordinates display + float u1, v1, u2, v2; + texture.getRectUVs(m_selectedRect, u1, v1, u2, v2); + ImGui::TextDisabled("UVs: (%.3f, %.3f) - (%.3f, %.3f)", u1, v1, u2, v2); + + Ogre::ColourValue currentColor = texture.getColor(m_selectedRect); + float colorArray[4] = { currentColor.r, currentColor.g, currentColor.b, currentColor.a }; + + if (ImGui::ColorEdit4("Color", colorArray)) { + Ogre::ColourValue newColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); + texture.setColor(m_selectedRect, newColor); + } + + // Add to atlas section + ImGui::Separator(); + ImGui::Text("Add to Texture Atlas:"); + + ImGui::InputText("Name", m_nameBuffer, sizeof(m_nameBuffer)); + ImGui::SameLine(); + + if (ImGui::Button("Add")) { + if (strlen(m_nameBuffer) > 0) { + if (texture.hasNamedRect(m_nameBuffer)) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Name already exists!"); + } else { + texture.addNamedRect(m_nameBuffer, m_selectedRect); + m_nameBuffer[0] = '\0'; // Clear buffer + } + } + } + + if (ImGui::Button("Deselect")) { + m_selectedRect = -1; + m_nameBuffer[0] = '\0'; + } + + ImGui::SameLine(); + + // Preset colors + ImGui::Text("Presets:"); + ImGui::SameLine(); + + const ImVec4 presets[] = { + ImVec4(1, 1, 1, 1), // White + ImVec4(0, 0, 0, 1), // Black + ImVec4(1, 0, 0, 1), // Red + ImVec4(0, 1, 0, 1), // Green + ImVec4(0, 0, 1, 1), // Blue + ImVec4(1, 1, 0, 1), // Yellow + ImVec4(1, 0, 1, 1), // Magenta + ImVec4(0, 1, 1, 1), // Cyan + ImVec4(0.5f, 0.5f, 0.5f, 1), // Gray + }; + + for (size_t i = 0; i < IM_ARRAYSIZE(presets); ++i) { + ImGui::PushID((int)i); + if (ImGui::ColorButton("##preset", presets[i], ImGuiColorEditFlags_NoTooltip, ImVec2(20, 20))) { + Ogre::ColourValue presetColor(presets[i].x, presets[i].y, presets[i].z, presets[i].w); + texture.setColor(m_selectedRect, presetColor); + } + ImGui::PopID(); + if (i < IM_ARRAYSIZE(presets) - 1) { + ImGui::SameLine(0, 2); + } + } + } +} + +void ProceduralTextureEditor::renderNamedRects(flecs::entity entity, ProceduralTextureComponent &texture) +{ + ImGui::Separator(); + ImGui::Text("Texture Atlas (Named Rectangles):"); + + const auto& namedRects = texture.getAllNamedRects(); + + if (namedRects.empty()) { + ImGui::TextDisabled("No named rectangles. Select a rectangle above and add it to the atlas."); + } else { + // Table header + if (ImGui::BeginTable("NamedRects", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("UV Coordinates"); + ImGui::TableSetupColumn("Action"); + ImGui::TableHeadersRow(); + + for (const auto& pair : namedRects) { + const auto& rect = pair.second; + ImGui::TableNextRow(); + + // Name column + ImGui::TableNextColumn(); + ImGui::Text("%s", rect.name.c_str()); + + // UVs column + ImGui::TableNextColumn(); + ImGui::Text("(%.3f, %.3f) - (%.3f, %.3f)", rect.u1, rect.v1, rect.u2, rect.v2); + + // Action column + ImGui::TableNextColumn(); + ImGui::PushID(rect.name.c_str()); + if (ImGui::Button("Delete")) { + texture.removeNamedRect(rect.name); + } + ImGui::PopID(); + } + + ImGui::EndTable(); + } + + ImGui::TextDisabled("Total: %zu named rectangles", namedRects.size()); + } +} + +bool ProceduralTextureEditor::renderComponent(flecs::entity entity, ProceduralTextureComponent &texture) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Procedural Texture", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Texture info + if (texture.generated) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Generated"); + ImGui::Text("Texture: %s", texture.textureName.c_str()); + ImGui::Text("Size: %dx%d", texture.textureSize, texture.textureSize); + + // Save PNG button + if (ImGui::Button("Save as PNG...")) { + ImGui::OpenPopup("SaveTexturePopup"); + } + + // Save popup + if (ImGui::BeginPopup("SaveTexturePopup")) { + static char filename[256] = "procedural_texture.png"; + ImGui::InputText("Filename", filename, sizeof(filename)); + + if (ImGui::Button("Save", ImVec2(120, 0))) { + try { + if (texture.ogreTexture) { + Ogre::Image image; + texture.ogreTexture->convertToImage(image); + image.save(filename); + Ogre::LogManager::getSingleton().logMessage( + "ProceduralTexture: Saved to " + std::string(filename)); + } + } catch (const Ogre::Exception& e) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralTexture ERROR: Failed to save: " + e.getDescription()); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } else if (texture.dirty) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Needs Generation"); + } else { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1), "Status: Not Generated"); + } + + ImGui::Separator(); + + // Texture size selector + const int sizes[] = { 128, 256, 512, 1024, 2048 }; + int currentSizeIndex = 2; // Default 512 + for (int i = 0; i < IM_ARRAYSIZE(sizes); ++i) { + if (sizes[i] == texture.textureSize) { + currentSizeIndex = i; + break; + } + } + + const char* sizeNames[] = { "128x128", "256x256", "512x512", "1024x1024", "2048x2048" }; + if (ImGui::Combo("Texture Size", ¤tSizeIndex, sizeNames, IM_ARRAYSIZE(sizeNames))) { + texture.textureSize = sizes[currentSizeIndex]; + texture.markDirty(); + modified = true; + } + + ImGui::Separator(); + + // Color grid + renderColorGrid(entity, texture); + + // Named rectangles list + renderNamedRects(entity, texture); + + ImGui::Separator(); + + // Global actions + if (ImGui::Button("Randomize Colors")) { + for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) { + Ogre::ColourValue randomColor( + (float)rand() / RAND_MAX, + (float)rand() / RAND_MAX, + (float)rand() / RAND_MAX, + 1.0f + ); + texture.setColor(i, randomColor); + } + modified = true; + } + + ImGui::SameLine(); + + if (ImGui::Button("Reset to Checkerboard")) { + for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) { + int row = i / ProceduralTextureComponent::GRID_SIZE; + int col = i % ProceduralTextureComponent::GRID_SIZE; + bool isWhite = (row + col) % 2 == 0; + Ogre::ColourValue color(isWhite ? 1.0f : 0.0f, isWhite ? 1.0f : 0.0f, isWhite ? 1.0f : 0.0f, 1.0f); + texture.setColor(i, color); + } + modified = true; + } + + ImGui::SameLine(); + + if (ImGui::Button("All White")) { + for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) { + texture.setColor(i, Ogre::ColourValue::White); + } + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/ProceduralTextureEditor.hpp b/src/features/editScene/ui/ProceduralTextureEditor.hpp new file mode 100644 index 0000000..6aee1d6 --- /dev/null +++ b/src/features/editScene/ui/ProceduralTextureEditor.hpp @@ -0,0 +1,31 @@ +#ifndef EDITSCENE_PROCEDURALTEXTUREEDITOR_HPP +#define EDITSCENE_PROCEDURALTEXTUREEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/ProceduralTexture.hpp" +#include + +/** + * Editor for ProceduralTextureComponent + * Shows a 10x10 grid of color pickers for each rectangle + * Plus named rectangle management for texture atlas + */ +class ProceduralTextureEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, ProceduralTextureComponent &texture) override; + const char *getName() const override { return "Procedural Texture"; } + +private: + void renderColorGrid(flecs::entity entity, ProceduralTextureComponent &texture); + void renderColorButton(int index, ProceduralTextureComponent &texture); + void renderNamedRects(flecs::entity entity, ProceduralTextureComponent &texture); + + // Currently selected color for editing + int m_selectedRect = -1; + + // Buffer for new rectangle name + char m_nameBuffer[256] = {}; +}; + +#endif // EDITSCENE_PROCEDURALTEXTUREEDITOR_HPP