Procedural texture implementation

This commit is contained in:
2026-04-04 03:42:50 +03:00
parent f785339852
commit b1413d6d00
15 changed files with 880 additions and 9 deletions

View File

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

View File

@@ -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 <OgreRTShaderSystem.h>
#include <imgui.h>
@@ -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<EditorMarkerComponent>().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<StaticGeometrySystem>(m_world, m_sceneMgr);
m_staticGeometrySystem->initialize();
// Setup ProceduralTexture system
m_proceduralTextureSystem = std::make_unique<ProceduralTextureSystem>(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<StaticGeometryComponent>();
m_world.component<StaticGeometryMemberComponent>();
// Register ProceduralTexture component
m_world.component<ProceduralTextureComponent>();
}
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;

View File

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

View File

@@ -0,0 +1,145 @@
#pragma once
#include <string>
#include <array>
#include <vector>
#include <map>
#include <Ogre.h>
/**
* @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<float, RECT_COUNT * 4> colors;
// Named rectangles for texture atlas (name -> rect info)
std::map<std::string, TextureRectInfo> 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<std::string, TextureRectInfo>& getAllNamedRects() const {
return namedRects;
}
void markDirty() {
dirty = true;
}
};

View File

@@ -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<ProceduralTextureComponent>(
"Procedural Texture",
std::make_unique<ProceduralTextureEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<ProceduralTextureComponent>()) {
e.set<ProceduralTextureComponent>({});
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<ProceduralTextureComponent>()) {
auto& texture = e.get_mut<ProceduralTextureComponent>();
// 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<ProceduralTextureComponent>();
}
}
);
}

View File

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

View File

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

View File

@@ -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<StaticGeometryMemberComponent>(entity, member);
componentCount++;
}
// Render ProceduralTexture if present
if (entity.has<ProceduralTextureComponent>()) {
auto &texture = entity.get_mut<ProceduralTextureComponent>();
m_componentRegistry.render<ProceduralTextureComponent>(entity, texture);
componentCount++;
}
// Show message if no components
if (componentCount == 0) {

View File

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

View File

@@ -0,0 +1,136 @@
#include "ProceduralTextureSystem.hpp"
#include "../components/ProceduralTexture.hpp"
#include <OgreTextureManager.h>
#include <OgreImage.h>
#include <OgreLogManager.h>
#include <ProceduralTextureGenerator.h>
#include <ProceduralTextureBuffer.h>
ProceduralTextureSystem::ProceduralTextureSystem(flecs::world& world, Ogre::SceneManager* sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_query(world.query<ProceduralTextureComponent>())
{
}
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<ProceduralTextureComponent>()) {
return;
}
entity.get_mut<ProceduralTextureComponent>().markDirty();
}
std::string ProceduralTextureSystem::getTextureName(flecs::entity entity)
{
if (!entity.is_alive() || !entity.has<ProceduralTextureComponent>()) {
return "";
}
return entity.get<ProceduralTextureComponent>().textureName;
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <flecs.h>
#include <memory>
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<struct ProceduralTextureComponent> m_query;
bool m_initialized = false;
int m_generatedCount = 0;
// Generate the texture for a component
void generateTexture(flecs::entity entity, struct ProceduralTextureComponent& component);
};

View File

@@ -11,6 +11,7 @@
#include "../components/LodSettings.hpp"
#include "../components/StaticGeometry.hpp"
#include "../components/StaticGeometryMember.hpp"
#include "../components/ProceduralTexture.hpp"
#include "EditorUISystem.hpp"
#include <random>
#include <fstream>
@@ -156,6 +157,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["staticGeometryMember"] = serializeStaticGeometryMember(entity);
}
if (entity.has<ProceduralTextureComponent>()) {
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<StaticGeometryMemberComponent>(member);
}
nlohmann::json SceneSerializer::serializeProceduralTexture(flecs::entity entity)
{
auto& texture = entity.get<ProceduralTextureComponent>();
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<ProceduralTextureComponent>(texture);
}

View File

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

View File

@@ -0,0 +1,305 @@
#include "ProceduralTextureEditor.hpp"
#include "../components/ProceduralTexture.hpp"
#include <imgui.h>
#include <OgreColourValue.h>
#include <OgreTextureManager.h>
#include <OgreImage.h>
#include <OgreLogManager.h>
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", &currentSizeIndex, 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;
}

View File

@@ -0,0 +1,31 @@
#ifndef EDITSCENE_PROCEDURALTEXTUREEDITOR_HPP
#define EDITSCENE_PROCEDURALTEXTUREEDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/ProceduralTexture.hpp"
#include <flecs.h>
/**
* Editor for ProceduralTextureComponent
* Shows a 10x10 grid of color pickers for each rectangle
* Plus named rectangle management for texture atlas
*/
class ProceduralTextureEditor : public ComponentEditor<ProceduralTextureComponent> {
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