Normal display tool implemented

This commit is contained in:
2026-04-24 21:12:02 +03:00
parent 1d2c330481
commit 2b3482da88
9 changed files with 656 additions and 191 deletions

View File

@@ -30,6 +30,7 @@ set(EDITSCENE_SOURCES
systems/ProceduralMaterialSystem.cpp
systems/ProceduralMeshSystem.cpp
systems/CellGridSystem.cpp
systems/NormalDebugSystem.cpp
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
systems/StartupMenuSystem.cpp
@@ -149,6 +150,7 @@ set(EDITSCENE_HEADERS
systems/PlayerControllerSystem.hpp
systems/EditorUISystem.hpp
systems/CellGridSystem.hpp
systems/NormalDebugSystem.hpp
systems/RoomLayoutSystem.hpp
systems/FurnitureLibrary.hpp
systems/ProceduralMaterialSystem.hpp

View File

@@ -19,6 +19,7 @@
#include "systems/NavMeshSystem.hpp"
#include "systems/CharacterSystem.hpp"
#include "systems/CellGridSystem.hpp"
#include "systems/NormalDebugSystem.hpp"
#include "systems/RoomLayoutSystem.hpp"
#include "systems/StartupMenuSystem.hpp"
#include "systems/PlayerControllerSystem.hpp"
@@ -243,12 +244,12 @@ void EditorApp::setup()
m_world, m_physicsSystem->getPhysicsWrapper());
m_buoyancySystem->initialize();
m_sunSystem = std::make_unique<EditorSunSystem>(m_world, m_sceneMgr);
m_skyboxSystem =
std::make_unique<EditorSkyboxSystem>(m_world, m_sceneMgr);
m_waterPlaneSystem =
std::make_unique<EditorWaterPlaneSystem>(m_world,
m_sceneMgr);
m_sunSystem =
std::make_unique<EditorSunSystem>(m_world, m_sceneMgr);
m_skyboxSystem = std::make_unique<EditorSkyboxSystem>(
m_world, m_sceneMgr);
m_waterPlaneSystem = std::make_unique<EditorWaterPlaneSystem>(
m_world, m_sceneMgr);
// Apply debug setting if it was set before system creation
if (m_debugBuoyancy) {
@@ -305,12 +306,11 @@ void EditorApp::setup()
m_world, m_sceneMgr);
m_animationTreeSystem->initialize();
m_behaviorTreeSystem = std::make_unique<BehaviorTreeSystem>(
m_world, m_sceneMgr,
m_animationTreeSystem.get());
m_world, m_sceneMgr, m_animationTreeSystem.get());
// Setup NavMesh system
m_navMeshSystem = std::make_unique<NavMeshSystem>(
m_world, m_sceneMgr);
m_navMeshSystem =
std::make_unique<NavMeshSystem>(m_world, m_sceneMgr);
// Setup Character physics system
m_characterSystem =
@@ -324,8 +324,17 @@ void EditorApp::setup()
// Wire CellGridSystem into NavMeshSystem so it can collect
// batched frame/furniture geometry from StaticGeometry.
m_navMeshSystem->setCellGridSystem(
m_cellGridSystem.get());
m_navMeshSystem->setCellGridSystem(m_cellGridSystem.get());
// Setup NormalDebug system (disabled by default)
m_normalDebugSystem = std::make_unique<NormalDebugSystem>(
m_world, m_sceneMgr, m_cellGridSystem.get());
// Wire NormalDebugSystem into UI for toggle
if (m_uiSystem) {
m_uiSystem->setNormalDebugSystem(
m_normalDebugSystem.get());
}
// Setup RoomLayout system
m_roomLayoutSystem =
@@ -694,8 +703,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
if (m_animationTreeSystem) {
m_animationTreeSystem->update(evt.timeSinceLastFrame);
if (m_behaviorTreeSystem)
m_behaviorTreeSystem->update(
evt.timeSinceLastFrame);
m_behaviorTreeSystem->update(evt.timeSinceLastFrame);
}
if (m_proceduralMeshSystem) {
m_proceduralMeshSystem->update();
@@ -709,6 +717,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
m_cellGridSystem->update();
}
/* --- Normal debug visualization (after geometry is built) --- */
if (m_normalDebugSystem) {
m_normalDebugSystem->update();
}
/* --- NavMesh builds after static geometry is ready --- */
if (m_navMeshSystem) {
m_navMeshSystem->update(evt.timeSinceLastFrame);

View File

@@ -34,6 +34,7 @@ class BuoyancySystem;
class EditorSunSystem;
class EditorSkyboxSystem;
class EditorWaterPlaneSystem;
class NormalDebugSystem;
class EditorApp;
/**
@@ -217,6 +218,7 @@ private:
std::unique_ptr<NavMeshSystem> m_navMeshSystem;
std::unique_ptr<CharacterSystem> m_characterSystem;
std::unique_ptr<CellGridSystem> m_cellGridSystem;
std::unique_ptr<NormalDebugSystem> m_normalDebugSystem;
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
// Game systems

View File

@@ -11,6 +11,7 @@ FileSystem=resources/buildings/parts/pier
FileSystem=resources/buildings/parts/furniture
FileSystem=resources/vehicles
FileSystem=resources/fonts
FileSystem=resources/debug
[Popular]
FileSystem=resources/materials/programs

View File

@@ -6,186 +6,241 @@
#include "../components/RigidBody.hpp"
#include "../components/PhysicsCollider.hpp"
namespace Procedural {
class TriangleBuffer;
namespace Procedural
{
class TriangleBuffer;
}
namespace Ogre {
class StaticGeometry;
namespace Ogre
{
class StaticGeometry;
}
class CellGridSystem {
public:
CellGridSystem(flecs::world& world, Ogre::SceneManager* sceneMgr);
~CellGridSystem();
void initialize();
void update();
// Force rebuild of a specific cell grid
void rebuildCellGrid(flecs::entity entity);
// Create/Update town material
void updateTownMaterial(flecs::entity townEntity);
// Expose frame/furniture placements for navmesh generation
struct PlacementInfo {
std::string meshName;
Ogre::Vector3 position;
Ogre::Quaternion rotation;
};
std::vector<PlacementInfo> getFramePlacements(flecs::entity entity) const;
std::vector<PlacementInfo> getFurniturePlacements(flecs::entity entity) const;
CellGridSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
~CellGridSystem();
void initialize();
void update();
// Force rebuild of a specific cell grid
void rebuildCellGrid(flecs::entity entity);
// Create/Update town material
void updateTownMaterial(flecs::entity townEntity);
// Expose frame/furniture placements for navmesh generation
struct PlacementInfo {
std::string meshName;
Ogre::Vector3 position;
Ogre::Quaternion rotation;
};
std::vector<PlacementInfo>
getFramePlacements(flecs::entity entity) const;
std::vector<PlacementInfo>
getFurniturePlacements(flecs::entity entity) const;
// Plaza mesh storage per district entity
struct PlazaData {
std::string meshName;
Ogre::Entity *entity = nullptr;
// Track texture dependency for automatic rebuild when texture changes
flecs::entity textureEntity = flecs::entity::null();
unsigned int textureVersion = 0;
flecs::entity physicsParentEntity = flecs::entity::null();
};
// Lot base mesh storage per lot entity
struct LotBaseData {
std::string meshName;
Ogre::Entity *entity = nullptr;
// Track texture dependency for automatic rebuild when texture changes
flecs::entity textureEntity = flecs::entity::null();
unsigned int textureVersion = 0;
flecs::entity physicsParentEntity = flecs::entity::null();
};
// Expose plaza and lot base mesh data for debug visualization
const std::unordered_map<uint64_t, PlazaData> &getPlazaMeshes() const
{
return m_plazaMeshes;
}
const std::unordered_map<uint64_t, LotBaseData> &
getLotBaseMeshes() const
{
return m_lotBaseMeshes;
}
private:
flecs::world& m_world;
Ogre::SceneManager* m_sceneMgr;
flecs::query<struct CellGridComponent> m_cellGridQuery;
flecs::query<struct TownComponent> m_townQuery;
flecs::query<struct DistrictComponent> m_districtQuery;
flecs::query<struct LotComponent> m_lotQuery;
bool m_initialized = false;
int m_meshCount = 0;
std::vector<flecs::entity> m_pendingPhysicsBuilds;
// Build geometry from cell grid
void buildCellGrid(flecs::entity entity, struct CellGridComponent& grid);
// Create triangle buffers for different parts
void buildFloorsAndCeilings(const struct CellGridComponent& grid, Procedural::TriangleBuffer& floorTb, Procedural::TriangleBuffer& ceilingTb);
void buildWalls(const struct CellGridComponent& grid,
Procedural::TriangleBuffer& extWallTb,
Procedural::TriangleBuffer& intWallTb,
Procedural::TriangleBuffer& intWindowsTb);
void buildDoors(const struct CellGridComponent& grid, Procedural::TriangleBuffer& extDoorsTb, Procedural::TriangleBuffer& intDoorsTb);
void buildWindows(const struct CellGridComponent& grid, Procedural::TriangleBuffer& extWindowsTb, Procedural::TriangleBuffer& intWindowsTb);
void buildCorners(const struct CellGridComponent& grid, Procedural::TriangleBuffer& extWallTb);
void buildInternalCorners(const struct CellGridComponent& grid, Procedural::TriangleBuffer& intWallTb);
// Build roofs
void buildRoofs(flecs::entity lotEntity, const struct CellGridComponent& grid,
Procedural::TriangleBuffer& roofTopTb, Procedural::TriangleBuffer& roofSideTb);
// Build plaza for district
void buildDistrictPlaza(flecs::entity entity, struct DistrictComponent& district);
// Build lot base geometry
void buildLotBase(flecs::entity entity, struct LotComponent& lot);
// Build furniture meshes
void buildFurniture(flecs::entity entity, const struct CellGridComponent& grid);
void destroyFurniture(flecs::entity entity);
// Build window and door frames (3D frame meshes)
void buildFrames(flecs::entity entity, const struct CellGridComponent& grid, const std::string& materialName);
void createWindowFrameMeshes(const struct CellGridComponent& grid, const std::string& materialName,
const std::string& meshPrefix, flecs::entity materialEntity);
void createDoorFrameMeshes(const struct CellGridComponent& grid, const std::string& materialName,
const std::string& meshPrefix, flecs::entity materialEntity);
void placeWindowFrames(flecs::entity entity, const struct CellGridComponent& grid, Ogre::SceneNode* parentNode, std::vector<Ogre::Entity*>& frameEntities);
void placeDoorFrames(flecs::entity entity, const struct CellGridComponent& grid, Ogre::SceneNode* parentNode, std::vector<Ogre::Entity*>& frameEntities);
void destroyFrames(flecs::entity entity);
// StaticGeometry versions (batched for better performance)
void placeWindowFramesInStaticGeometry(flecs::entity entity, const struct CellGridComponent& grid,
const Ogre::Vector3& parentPos, const Ogre::Quaternion& parentRot, const Ogre::Vector3& parentScale,
Ogre::StaticGeometry* staticGeom, int& frameCount);
void placeDoorFramesInStaticGeometry(flecs::entity entity, const struct CellGridComponent& grid,
const Ogre::Vector3& parentPos, const Ogre::Quaternion& parentRot, const Ogre::Vector3& parentScale,
Ogre::StaticGeometry* staticGeom, int& frameCount);
// Convert triangle buffer to mesh
Ogre::MeshPtr convertToMesh(const std::string& name, Procedural::TriangleBuffer& tb, const std::string& materialName);
// Generate LOD levels for a mesh (shared settings for all procedural editor meshes)
void generateLodForMesh(Ogre::MeshPtr mesh);
// Apply UV mapping from color rects
void applyUVMapping(Procedural::TriangleBuffer& tb, flecs::entity entity, const std::string& rectName);
// Apply UV mapping using texture coordinates directly
void applyUVMappingToBuffer(Procedural::TriangleBuffer& tb, const std::string& rectName, flecs::entity materialEntity);
// Destroy existing mesh
void destroyCellGridMeshes(struct CellGridComponent& grid);
void destroyCellGridMeshes(flecs::entity entity);
// Physics helpers
void addPhysicsCollider(flecs::entity physicsParent,
const std::string& meshName,
const Ogre::Vector3& position,
const Ogre::Quaternion& rotation);
void buildPhysicsColliders(flecs::entity entity);
void destroyPhysicsColliders(flecs::entity entity);
// Mesh storage per entity
struct MeshData {
struct ColliderPlacement {
std::string meshName;
Ogre::Vector3 position;
Ogre::Quaternion rotation;
};
std::string floorMesh;
std::string ceilingMesh;
std::string extWallMesh;
std::string intWallMesh;
std::string intWindowsMesh;
std::vector<std::string> doorMeshes;
std::vector<std::string> windowMeshes;
std::string roofMesh;
std::string roofSideMesh;
std::vector<Ogre::Entity*> entities;
// Track texture dependency for automatic rebuild when texture changes
flecs::entity textureEntity = flecs::entity::null();
unsigned int textureVersion = 0;
// Frame meshes (unique per CellGrid for cellSize/cellHeight adaptation)
std::string externalWindowFrameMesh;
std::string internalWindowFrameMesh;
std::string externalDoorFrameMesh;
std::string internalDoorFrameMesh;
// Frame entities attached to the SceneNode (so they move with transform)
std::vector<Ogre::Entity*> frameEntities;
// StaticGeometry for frames (batches all frames into single draw call)
Ogre::StaticGeometry* frameStaticGeometry = nullptr;
// Furniture entities
std::vector<Ogre::Entity*> furnitureEntities;
// Physics placement data for compound shape
std::vector<ColliderPlacement> framePlacements;
std::vector<ColliderPlacement> furniturePlacements;
std::vector<flecs::entity> isolatedFurnitureEntities;
flecs::entity physicsParentEntity = flecs::entity::null();
};
std::unordered_map<uint64_t, MeshData> m_entityMeshes;
// Plaza mesh storage per district entity
struct PlazaData {
std::string meshName;
Ogre::Entity* entity = nullptr;
// Track texture dependency for automatic rebuild when texture changes
flecs::entity textureEntity = flecs::entity::null();
unsigned int textureVersion = 0;
flecs::entity physicsParentEntity = flecs::entity::null();
};
std::unordered_map<uint64_t, PlazaData> m_plazaMeshes;
// Lot base mesh storage per lot entity
struct LotBaseData {
std::string meshName;
Ogre::Entity* entity = nullptr;
// Track texture dependency for automatic rebuild when texture changes
flecs::entity textureEntity = flecs::entity::null();
unsigned int textureVersion = 0;
flecs::entity physicsParentEntity = flecs::entity::null();
};
std::unordered_map<uint64_t, LotBaseData> m_lotBaseMeshes;
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
flecs::query<struct CellGridComponent> m_cellGridQuery;
flecs::query<struct TownComponent> m_townQuery;
flecs::query<struct DistrictComponent> m_districtQuery;
flecs::query<struct LotComponent> m_lotQuery;
bool m_initialized = false;
int m_meshCount = 0;
std::vector<flecs::entity> m_pendingPhysicsBuilds;
// Build geometry from cell grid
void buildCellGrid(flecs::entity entity,
struct CellGridComponent &grid);
// Create triangle buffers for different parts
void buildFloorsAndCeilings(const struct CellGridComponent &grid,
Procedural::TriangleBuffer &floorTb,
Procedural::TriangleBuffer &ceilingTb);
void buildWalls(const struct CellGridComponent &grid,
Procedural::TriangleBuffer &extWallTb,
Procedural::TriangleBuffer &intWallTb,
Procedural::TriangleBuffer &intWindowsTb);
void buildDoors(const struct CellGridComponent &grid,
Procedural::TriangleBuffer &extDoorsTb,
Procedural::TriangleBuffer &intDoorsTb);
void buildWindows(const struct CellGridComponent &grid,
Procedural::TriangleBuffer &extWindowsTb,
Procedural::TriangleBuffer &intWindowsTb);
void buildCorners(const struct CellGridComponent &grid,
Procedural::TriangleBuffer &extWallTb);
void buildInternalCorners(const struct CellGridComponent &grid,
Procedural::TriangleBuffer &intWallTb);
// Build roofs
void buildRoofs(flecs::entity lotEntity,
const struct CellGridComponent &grid,
Procedural::TriangleBuffer &roofTopTb,
Procedural::TriangleBuffer &roofSideTb);
// Build plaza for district
void buildDistrictPlaza(flecs::entity entity,
struct DistrictComponent &district);
// Build lot base geometry
void buildLotBase(flecs::entity entity, struct LotComponent &lot);
// Build furniture meshes
void buildFurniture(flecs::entity entity,
const struct CellGridComponent &grid);
void destroyFurniture(flecs::entity entity);
// Build window and door frames (3D frame meshes)
void buildFrames(flecs::entity entity,
const struct CellGridComponent &grid,
const std::string &materialName);
void createWindowFrameMeshes(const struct CellGridComponent &grid,
const std::string &materialName,
const std::string &meshPrefix,
flecs::entity materialEntity);
void createDoorFrameMeshes(const struct CellGridComponent &grid,
const std::string &materialName,
const std::string &meshPrefix,
flecs::entity materialEntity);
void placeWindowFrames(flecs::entity entity,
const struct CellGridComponent &grid,
Ogre::SceneNode *parentNode,
std::vector<Ogre::Entity *> &frameEntities);
void placeDoorFrames(flecs::entity entity,
const struct CellGridComponent &grid,
Ogre::SceneNode *parentNode,
std::vector<Ogre::Entity *> &frameEntities);
void destroyFrames(flecs::entity entity);
// StaticGeometry versions (batched for better performance)
void placeWindowFramesInStaticGeometry(
flecs::entity entity, const struct CellGridComponent &grid,
const Ogre::Vector3 &parentPos,
const Ogre::Quaternion &parentRot,
const Ogre::Vector3 &parentScale,
Ogre::StaticGeometry *staticGeom, int &frameCount);
void placeDoorFramesInStaticGeometry(
flecs::entity entity, const struct CellGridComponent &grid,
const Ogre::Vector3 &parentPos,
const Ogre::Quaternion &parentRot,
const Ogre::Vector3 &parentScale,
Ogre::StaticGeometry *staticGeom, int &frameCount);
// Convert triangle buffer to mesh
Ogre::MeshPtr convertToMesh(const std::string &name,
Procedural::TriangleBuffer &tb,
const std::string &materialName);
// Generate LOD levels for a mesh (shared settings for all procedural editor meshes)
void generateLodForMesh(Ogre::MeshPtr mesh);
// Apply UV mapping from color rects
void applyUVMapping(Procedural::TriangleBuffer &tb,
flecs::entity entity, const std::string &rectName);
// Apply UV mapping using texture coordinates directly
void applyUVMappingToBuffer(Procedural::TriangleBuffer &tb,
const std::string &rectName,
flecs::entity materialEntity);
// Destroy existing mesh
void destroyCellGridMeshes(struct CellGridComponent &grid);
void destroyCellGridMeshes(flecs::entity entity);
// Physics helpers
void addPhysicsCollider(flecs::entity physicsParent,
const std::string &meshName,
const Ogre::Vector3 &position,
const Ogre::Quaternion &rotation);
void buildPhysicsColliders(flecs::entity entity);
void destroyPhysicsColliders(flecs::entity entity);
// Mesh storage per entity
struct MeshData {
struct ColliderPlacement {
std::string meshName;
Ogre::Vector3 position;
Ogre::Quaternion rotation;
};
std::string floorMesh;
std::string ceilingMesh;
std::string extWallMesh;
std::string intWallMesh;
std::string intWindowsMesh;
std::vector<std::string> doorMeshes;
std::vector<std::string> windowMeshes;
std::string roofMesh;
std::string roofSideMesh;
std::vector<Ogre::Entity *> entities;
// Track texture dependency for automatic rebuild when texture changes
flecs::entity textureEntity = flecs::entity::null();
unsigned int textureVersion = 0;
// Frame meshes (unique per CellGrid for cellSize/cellHeight adaptation)
std::string externalWindowFrameMesh;
std::string internalWindowFrameMesh;
std::string externalDoorFrameMesh;
std::string internalDoorFrameMesh;
// Frame entities attached to the SceneNode (so they move with transform)
std::vector<Ogre::Entity *> frameEntities;
// StaticGeometry for frames (batches all frames into single draw call)
Ogre::StaticGeometry *frameStaticGeometry = nullptr;
// Furniture entities
std::vector<Ogre::Entity *> furnitureEntities;
// Physics placement data for compound shape
std::vector<ColliderPlacement> framePlacements;
std::vector<ColliderPlacement> furniturePlacements;
std::vector<flecs::entity> isolatedFurnitureEntities;
flecs::entity physicsParentEntity = flecs::entity::null();
};
std::unordered_map<uint64_t, MeshData> m_entityMeshes;
// Plaza mesh storage per district entity
std::unordered_map<uint64_t, PlazaData> m_plazaMeshes;
// Lot base mesh storage per lot entity
std::unordered_map<uint64_t, LotBaseData> m_lotBaseMeshes;
};

View File

@@ -41,6 +41,7 @@
#include "PhysicsSystem.hpp"
#include "BuoyancySystem.hpp"
#include "NavMeshSystem.hpp"
#include "NormalDebugSystem.hpp"
#include <imgui.h>
#include <algorithm>
#include <filesystem>
@@ -322,6 +323,22 @@ void EditorUISystem::renderHierarchyWindow()
}
}
// Normal debug visualization toggle
if (m_normalDebugSystem) {
bool normalDebug =
m_normalDebugSystem->isEnabled();
if (ImGui::Checkbox(
"Show Polygon Normals",
&normalDebug)) {
m_normalDebugSystem->setEnabled(
normalDebug);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Show vertex normals for plaza and lot base geometry");
}
}
ImGui::EndMenu();
}
ImGui::EndMenuBar();
@@ -753,7 +770,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
// Render AnimationTreeTemplate if present
if (entity.has<AnimationTreeTemplate>()) {
auto &templ = entity.get_mut<AnimationTreeTemplate>();
m_componentRegistry.render<AnimationTreeTemplate>(entity, templ);
m_componentRegistry.render<AnimationTreeTemplate>(entity,
templ);
componentCount++;
}
@@ -860,7 +878,8 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
auto &nav = entity.get_mut<NavMeshComponent>();
if (m_componentRegistry.render<NavMeshComponent>(entity, nav)) {
if (nav.debugDraw && NavMeshSystem::getInstance())
NavMeshSystem::getInstance()->setDebugDraw(entity, nav.debugDraw);
NavMeshSystem::getInstance()->setDebugDraw(
entity, nav.debugDraw);
}
componentCount++;
}

View File

@@ -14,6 +14,7 @@
// Forward declarations
class EditorPhysicsSystem;
class BuoyancySystem;
class NormalDebugSystem;
namespace Ogre
{
@@ -124,6 +125,14 @@ public:
m_buoyancySystem = buoyancy;
}
/**
* Set normal debug system for toggle
*/
void setNormalDebugSystem(NormalDebugSystem *normalDebug)
{
m_normalDebugSystem = normalDebug;
}
/**
* Enable/disable editor UI rendering
*/
@@ -215,6 +224,9 @@ private:
// Buoyancy system reference (for configuration)
BuoyancySystem *m_buoyancySystem = nullptr;
// Normal debug system reference (for toggle)
NormalDebugSystem *m_normalDebugSystem = nullptr;
// Editor UI enabled flag
bool m_editorUIEnabled = true;

View File

@@ -0,0 +1,290 @@
#include "NormalDebugSystem.hpp"
#include "CellGridSystem.hpp"
#include "../components/CellGrid.hpp"
#include "../components/Transform.hpp"
#include <OgreManualObject.h>
#include <OgreSceneNode.h>
#include <OgreEntity.h>
#include <OgreSubMesh.h>
#include <OgreMesh.h>
#include <OgreLogManager.h>
#include <OgreStringConverter.h>
// Normal line length in world units
static const float NORMAL_LINE_LENGTH = 0.5f;
NormalDebugSystem::NormalDebugSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr,
CellGridSystem *cellGridSystem)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_cellGridSystem(cellGridSystem)
{
// Create ManualObject for normal lines
m_manualObj = m_sceneMgr->createManualObject("NormalDebugLines");
m_manualObj->setDynamic(true);
// Create scene node for the debug visualization
m_node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(
"NormalDebugNode");
m_node->attachObject(m_manualObj);
m_node->setVisible(false); // Disabled by default
}
NormalDebugSystem::~NormalDebugSystem()
{
if (m_node) {
m_node->detachAllObjects();
m_sceneMgr->destroySceneNode(m_node);
m_node = nullptr;
}
if (m_manualObj) {
m_sceneMgr->destroyManualObject(m_manualObj);
m_manualObj = nullptr;
}
}
void NormalDebugSystem::setEnabled(bool enabled)
{
m_enabled = enabled;
if (m_node) {
m_node->setVisible(enabled);
}
if (enabled) {
rebuildAll();
} else {
// Clear the manual object
if (m_manualObj) {
m_manualObj->clear();
}
}
}
void NormalDebugSystem::update()
{
if (!m_enabled || !m_manualObj || !m_cellGridSystem) {
return;
}
// Check if plaza/lot entities have changed by comparing cached IDs
bool needsRebuild = false;
// Get current plaza entities from CellGridSystem
const auto &plazaMeshes = m_cellGridSystem->getPlazaMeshes();
std::vector<uint64_t> currentPlazaEntities;
for (const auto &pair : plazaMeshes) {
currentPlazaEntities.push_back(pair.first);
}
if (currentPlazaEntities != m_cachedPlazaEntities) {
needsRebuild = true;
m_cachedPlazaEntities = currentPlazaEntities;
}
// Get current lot base entities from CellGridSystem
const auto &lotBaseMeshes = m_cellGridSystem->getLotBaseMeshes();
std::vector<uint64_t> currentLotEntities;
for (const auto &pair : lotBaseMeshes) {
currentLotEntities.push_back(pair.first);
}
if (currentLotEntities != m_cachedLotEntities) {
needsRebuild = true;
m_cachedLotEntities = currentLotEntities;
}
if (needsRebuild) {
rebuildAll();
}
}
void NormalDebugSystem::rebuildAll()
{
if (!m_manualObj || !m_cellGridSystem) {
return;
}
m_manualObj->clear();
const auto &plazaMeshes = m_cellGridSystem->getPlazaMeshes();
const auto &lotBaseMeshes = m_cellGridSystem->getLotBaseMeshes();
// Count total normals to draw
size_t totalNormals = 0;
for (const auto &pair : plazaMeshes) {
if (pair.second.entity) {
Ogre::MeshPtr mesh = pair.second.entity->getMesh();
if (mesh.isNull())
continue;
for (unsigned int i = 0; i < mesh->getNumSubMeshes();
++i) {
Ogre::SubMesh *sub = mesh->getSubMesh(i);
if (sub && sub->vertexData) {
totalNormals +=
sub->vertexData->vertexCount;
}
}
}
}
for (const auto &pair : lotBaseMeshes) {
if (pair.second.entity) {
Ogre::MeshPtr mesh = pair.second.entity->getMesh();
if (mesh.isNull())
continue;
for (unsigned int i = 0; i < mesh->getNumSubMeshes();
++i) {
Ogre::SubMesh *sub = mesh->getSubMesh(i);
if (sub && sub->vertexData) {
totalNormals +=
sub->vertexData->vertexCount;
}
}
}
}
if (totalNormals == 0) {
Ogre::LogManager::getSingleton().logMessage(
"NormalDebug: No plaza or lot meshes found");
return;
}
// Begin drawing lines (2 vertices per normal)
m_manualObj->begin("Debug/NormalOverlay",
Ogre::RenderOperation::OT_LINE_LIST);
// Draw plaza normals
for (const auto &pair : plazaMeshes) {
if (!pair.second.entity)
continue;
// Find the scene node for this plaza entity
Ogre::SceneNode *parentNode = nullptr;
flecs::entity districtEntity = m_world.entity(pair.first);
if (districtEntity.is_alive() &&
districtEntity.has<TransformComponent>()) {
const auto &transform =
districtEntity.get<TransformComponent>();
parentNode = transform.node;
}
drawMeshNormals(pair.second.entity, parentNode, m_manualObj);
}
// Draw lot base normals
for (const auto &pair : lotBaseMeshes) {
if (!pair.second.entity)
continue;
// Find the scene node for this lot entity
Ogre::SceneNode *parentNode = nullptr;
flecs::entity lotEntity = m_world.entity(pair.first);
if (lotEntity.is_alive() &&
lotEntity.has<TransformComponent>()) {
const auto &transform =
lotEntity.get<TransformComponent>();
parentNode = transform.node;
}
drawMeshNormals(pair.second.entity, parentNode, m_manualObj);
}
m_manualObj->end();
Ogre::LogManager::getSingleton().logMessage(
"NormalDebug: Drew " +
Ogre::StringConverter::toString(totalNormals) +
" normal lines");
}
void NormalDebugSystem::drawMeshNormals(Ogre::Entity *entity,
Ogre::SceneNode *parentNode,
Ogre::ManualObject *manualObj)
{
if (!entity || !manualObj) {
return;
}
Ogre::MeshPtr mesh = entity->getMesh();
if (mesh.isNull()) {
return;
}
// Get world transform from parent node
Ogre::Vector3 worldPos(0, 0, 0);
Ogre::Quaternion worldRot = Ogre::Quaternion::IDENTITY;
Ogre::Vector3 worldScale(1, 1, 1);
if (parentNode) {
worldPos = parentNode->_getDerivedPosition();
worldRot = parentNode->_getDerivedOrientation();
worldScale = parentNode->_getDerivedScale();
}
for (unsigned int i = 0; i < mesh->getNumSubMeshes(); ++i) {
Ogre::SubMesh *subMesh = mesh->getSubMesh(i);
if (!subMesh || !subMesh->vertexData) {
continue;
}
Ogre::VertexData *vertexData = subMesh->vertexData;
const Ogre::VertexElement *posElem =
vertexData->vertexDeclaration->findElementBySemantic(
Ogre::VES_POSITION);
const Ogre::VertexElement *normElem =
vertexData->vertexDeclaration->findElementBySemantic(
Ogre::VES_NORMAL);
if (!posElem || !normElem) {
continue;
}
// Get vertex buffer
Ogre::HardwareVertexBufferSharedPtr vbuf =
vertexData->vertexBufferBinding->getBuffer(0);
unsigned char *vertexDataPtr = static_cast<unsigned char *>(
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
float *pFloat;
Ogre::Vector3 pos;
Ogre::Vector3 normal;
for (size_t v = 0; v < vertexData->vertexCount; ++v) {
// Read position
posElem->baseVertexPointerToElement(vertexDataPtr,
&pFloat);
pos.x = *pFloat++;
pos.y = *pFloat++;
pos.z = *pFloat;
// Read normal
normElem->baseVertexPointerToElement(vertexDataPtr,
&pFloat);
normal.x = *pFloat++;
normal.y = *pFloat++;
normal.z = *pFloat;
// Transform position and normal to world space
Ogre::Vector3 worldVertex =
worldPos + (worldRot * (pos * worldScale));
Ogre::Vector3 worldNormal = worldRot * normal;
worldNormal.normalise();
// Calculate end point of normal line
Ogre::Vector3 endPoint =
worldVertex + worldNormal * NORMAL_LINE_LENGTH;
// Colour based on normal direction for visual clarity
// Red = X, Green = Y, Blue = Z
Ogre::ColourValue colour(std::abs(worldNormal.x),
std::abs(worldNormal.y),
std::abs(worldNormal.z), 1.0f);
// Draw line from vertex along normal
manualObj->colour(colour);
manualObj->position(worldVertex);
manualObj->colour(colour);
manualObj->position(endPoint);
vertexDataPtr += vbuf->getVertexSize();
}
vbuf->unlock();
}
}

View File

@@ -0,0 +1,71 @@
#ifndef NORMAL_DEBUG_SYSTEM_HPP
#define NORMAL_DEBUG_SYSTEM_HPP
#include <flecs.h>
#include <Ogre.h>
#include <memory>
#include <unordered_map>
class CellGridSystem;
/**
* Debug visualization system for polygon normals.
*
* Draws normal lines for plaza (DistrictComponent) and lot base
* (LotComponent) geometry using ManualObject with a see-through
* overlay material (Debug/NormalOverlay).
*
* Disabled by default. Toggle via EditorUISystem Settings menu.
*/
class NormalDebugSystem {
public:
NormalDebugSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
CellGridSystem *cellGridSystem);
~NormalDebugSystem();
/**
* Update normal visualization each frame.
* Rebuilds ManualObject if mesh data has changed.
*/
void update();
/**
* Enable/disable normal visualization.
*/
void setEnabled(bool enabled);
bool isEnabled() const
{
return m_enabled;
}
private:
/**
* Rebuild all normal visualizations from scratch.
*/
void rebuildAll();
/**
* Draw normals for a single mesh entity.
* @param entity The Ogre::Entity whose mesh normals to visualize.
* @param parentNode The scene node the entity is attached to (for world-space transform).
* @param manualObj The ManualObject to draw into (must be in begin/end scope).
*/
void drawMeshNormals(Ogre::Entity *entity, Ogre::SceneNode *parentNode,
Ogre::ManualObject *manualObj);
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
CellGridSystem *m_cellGridSystem;
bool m_enabled = false;
// ManualObject for normal lines
Ogre::ManualObject *m_manualObj = nullptr;
Ogre::SceneNode *m_node = nullptr;
// Cached entity IDs to detect changes
std::vector<uint64_t> m_cachedPlazaEntities;
std::vector<uint64_t> m_cachedLotEntities;
};
#endif // NORMAL_DEBUG_SYSTEM_HPP