From 1d2c33048182f21ae053ada5059ef227ca3dae31 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 24 Apr 2026 20:29:54 +0300 Subject: [PATCH] Material fixes, playing with navmesh --- .../editScene/recast/TileCacheNavMesh.cpp | 333 ++++++----- .../editScene/systems/NavMeshSystem.cpp | 535 +++++++++--------- .../systems/ProceduralMaterialSystem.cpp | 254 +++++---- .../editScene/systems/SceneSerializer.cpp | 455 +++++++++++++-- .../editScene/systems/SceneSerializer.hpp | 26 +- 5 files changed, 996 insertions(+), 607 deletions(-) diff --git a/src/features/editScene/recast/TileCacheNavMesh.cpp b/src/features/editScene/recast/TileCacheNavMesh.cpp index 0ba3d09..9e69df4 100644 --- a/src/features/editScene/recast/TileCacheNavMesh.cpp +++ b/src/features/editScene/recast/TileCacheNavMesh.cpp @@ -18,10 +18,9 @@ static bool isNavMeshGeometry(const Ogre::String &meshName) Ogre::String lower = meshName; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); }); - static const char *exclude[] = { - "wall", "ceiling", "roof", "door", - "window", "frame", "furniture" - }; + static const char *exclude[] = { "wall", "ceiling", "roof", + "door", "window", "frame", + "furniture" }; for (const char *pat : exclude) { if (lower.find(pat) != Ogre::String::npos) return false; @@ -80,24 +79,21 @@ struct FastLZCompressor : public dtTileCacheCompressor { { return (int)(bufferSize * 1.05f); } - dtStatus compress(const unsigned char *buffer, - const int bufferSize, + dtStatus compress(const unsigned char *buffer, const int bufferSize, unsigned char *compressed, const int maxCompressedSize, int *compressedSize) override { - *compressedSize = fastlz_compress( - (const void *)buffer, bufferSize, compressed); + *compressedSize = fastlz_compress((const void *)buffer, + bufferSize, compressed); return DT_SUCCESS; } dtStatus decompress(const unsigned char *compressed, - const int compressedSize, - unsigned char *buffer, - const int maxBufferSize, - int *bufferSize) override + const int compressedSize, unsigned char *buffer, + const int maxBufferSize, int *bufferSize) override { - *bufferSize = fastlz_decompress( - compressed, compressedSize, buffer, maxBufferSize); + *bufferSize = fastlz_decompress(compressed, compressedSize, + buffer, maxBufferSize); return *bufferSize < 0 ? DT_FAILURE : DT_SUCCESS; } }; @@ -155,14 +151,13 @@ struct RasterizationContext { // Ogre mesh extraction // ------------------------------------------------------------------ -static void extractEntityMesh(Ogre::Entity *ent, - std::vector &verts, +static void extractEntityMesh(Ogre::Entity *ent, std::vector &verts, std::vector &tris) { Ogre::MeshPtr mesh = ent->getMesh(); Ogre::SceneNode *node = ent->getParentSceneNode(); - Ogre::Matrix4 transform = node ? node->_getFullTransform() - : Ogre::Matrix4::IDENTITY; + Ogre::Matrix4 transform = node ? node->_getFullTransform() : + Ogre::Matrix4::IDENTITY; bool addedShared = false; size_t sharedOffset = 0; @@ -171,9 +166,9 @@ static void extractEntityMesh(Ogre::Entity *ent, for (unsigned short i = 0; i < mesh->getNumSubMeshes(); ++i) { Ogre::SubMesh *sub = mesh->getSubMesh(i); - Ogre::VertexData *vd = sub->useSharedVertices - ? mesh->sharedVertexData - : sub->vertexData; + Ogre::VertexData *vd = sub->useSharedVertices ? + mesh->sharedVertexData : + sub->vertexData; if ((!sub->useSharedVertices) || (sub->useSharedVertices && !addedShared)) { @@ -183,26 +178,25 @@ static void extractEntityMesh(Ogre::Entity *ent, } const Ogre::VertexElement *posElem = - vd->vertexDeclaration - ->findElementBySemantic( - Ogre::VES_POSITION); + vd->vertexDeclaration->findElementBySemantic( + Ogre::VES_POSITION); if (!posElem) continue; Ogre::HardwareVertexBufferSharedPtr vbuf = - vd->vertexBufferBinding->getBuffer( - posElem->getSource()); + vd->vertexBufferBinding->getBuffer( + posElem->getSource()); - unsigned char *vptr = static_cast( - vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY)); + unsigned char *vptr = + static_cast(vbuf->lock( + Ogre::HardwareBuffer::HBL_READ_ONLY)); for (size_t v = 0; v < vd->vertexCount; ++v, vptr += vbuf->getVertexSize()) { float *pReal; - posElem->baseVertexPointerToElement( - vptr, &pReal); - Ogre::Vector3 pt(pReal[0], pReal[1], - pReal[2]); + posElem->baseVertexPointerToElement(vptr, + &pReal); + Ogre::Vector3 pt(pReal[0], pReal[1], pReal[2]); pt = transform * pt; verts.push_back(pt.x); verts.push_back(pt.y); @@ -217,8 +211,7 @@ static void extractEntityMesh(Ogre::Entity *ent, currentOffset = nextOffset; continue; } - Ogre::HardwareIndexBufferSharedPtr ibuf = - id->indexBuffer; + Ogre::HardwareIndexBufferSharedPtr ibuf = id->indexBuffer; if (!ibuf) { currentOffset = nextOffset; continue; @@ -226,25 +219,24 @@ static void extractEntityMesh(Ogre::Entity *ent, bool use32 = (ibuf->getType() == Ogre::HardwareIndexBuffer::IT_32BIT); - void *idata = ibuf->lock( - Ogre::HardwareBuffer::HBL_READ_ONLY); - size_t offset = sub->useSharedVertices ? sharedOffset - : currentOffset; + void *idata = ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY); + size_t offset = sub->useSharedVertices ? sharedOffset : + currentOffset; if (use32) { - unsigned int *p = static_cast( - idata); + unsigned int *p = static_cast(idata); for (size_t k = 0; k < id->indexCount; ++k) tris.push_back(static_cast(p[k]) + static_cast(offset)); } else { - unsigned short *p = static_cast( - idata); + unsigned short *p = + static_cast(idata); for (size_t k = 0; k < id->indexCount; ++k) tris.push_back(static_cast(p[k]) + static_cast(offset)); } ibuf->unlock(); + currentOffset = nextOffset; } } @@ -305,8 +297,8 @@ bool TileCacheNavMesh::extractMeshData( continue; if (!isNavMeshGeometry(ent->getMesh()->getName())) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] Skipping " + ent->getName() + - " mesh=" + ent->getMesh()->getName()); + "[NavMesh] Skipping " + ent->getName() + + " mesh=" + ent->getMesh()->getName()); continue; } int vbefore = static_cast(m_verts.size() / 3); @@ -315,22 +307,22 @@ bool TileCacheNavMesh::extractMeshData( int vafter = static_cast(m_verts.size() / 3); int tafter = static_cast(m_tris.size() / 3); Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] Entity " + ent->getName() + " verts=" + - Ogre::StringConverter::toString(vafter - vbefore) + - " tris=" + - Ogre::StringConverter::toString(tafter - tbefore)); + "[NavMesh] Entity " + ent->getName() + " verts=" + + Ogre::StringConverter::toString(vafter - vbefore) + + " tris=" + + Ogre::StringConverter::toString(tafter - tbefore)); processed++; } Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] Processed " + - Ogre::StringConverter::toString(processed) + - " entities, total verts=" + - Ogre::StringConverter::toString( - static_cast(m_verts.size() / 3)) + - " tris=" + - Ogre::StringConverter::toString( - static_cast(m_tris.size() / 3))); + "[NavMesh] Processed " + + Ogre::StringConverter::toString(processed) + + " entities, total verts=" + + Ogre::StringConverter::toString( + static_cast(m_verts.size() / 3)) + + " tris=" + + Ogre::StringConverter::toString( + static_cast(m_tris.size() / 3))); if (m_verts.empty() || m_tris.empty()) return false; @@ -361,8 +353,8 @@ bool TileCacheNavMesh::extractMeshData( // Build spatial partitioning m_partitionedMesh = std::make_unique(); - m_partitionedMesh->PartitionMesh(m_verts.data(), - m_tris.data(), ntris, 256); + m_partitionedMesh->PartitionMesh(m_verts.data(), m_tris.data(), ntris, + 256); return true; } @@ -384,14 +376,14 @@ bool TileCacheNavMesh::initTileCache() m_th = 1; Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] Grid: " + Ogre::StringConverter::toString(m_tw) + - "x" + Ogre::StringConverter::toString(m_th) + - " bounds=[" + Ogre::StringConverter::toString(m_bmin[0]) + - "," + Ogre::StringConverter::toString(m_bmin[1]) + - "," + Ogre::StringConverter::toString(m_bmin[2]) + - "]-[" + Ogre::StringConverter::toString(m_bmax[0]) + - "," + Ogre::StringConverter::toString(m_bmax[1]) + - "," + Ogre::StringConverter::toString(m_bmax[2]) + "]"); + "[NavMesh] Grid: " + Ogre::StringConverter::toString(m_tw) + + "x" + Ogre::StringConverter::toString(m_th) + " bounds=[" + + Ogre::StringConverter::toString(m_bmin[0]) + "," + + Ogre::StringConverter::toString(m_bmin[1]) + "," + + Ogre::StringConverter::toString(m_bmin[2]) + "]-[" + + Ogre::StringConverter::toString(m_bmax[0]) + "," + + Ogre::StringConverter::toString(m_bmax[1]) + "," + + Ogre::StringConverter::toString(m_bmax[2]) + "]"); // Tile cache params dtTileCacheParams tcparams; @@ -411,16 +403,15 @@ bool TileCacheNavMesh::initTileCache() m_tileCache = dtAllocTileCache(); if (!m_tileCache) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] initTileCache failed: dtAllocTileCache"); + "[NavMesh] initTileCache failed: dtAllocTileCache"); return false; } dtStatus status = - m_tileCache->init(&tcparams, m_talloc, m_tcomp, - m_tmproc); + m_tileCache->init(&tcparams, m_talloc, m_tcomp, m_tmproc); if (dtStatusFailed(status)) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] initTileCache failed: tileCache init"); + "[NavMesh] initTileCache failed: tileCache init"); return false; } @@ -429,7 +420,7 @@ bool TileCacheNavMesh::initTileCache() // tileBits + polyBits <= 22, leaving >= 10 salt bits. // This matches the upstream RecastDemo calculation. int tileBits = (int)dtIlog2( - dtNextPow2((unsigned int)(m_tw * m_th * MAX_LAYERS))); + dtNextPow2((unsigned int)(m_tw * m_th * MAX_LAYERS))); if (tileBits > 14) tileBits = 14; int polyBits = 22 - tileBits; @@ -443,25 +434,24 @@ bool TileCacheNavMesh::initTileCache() nmparams.maxPolys = 1 << polyBits; Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] tileBits=" + - Ogre::StringConverter::toString(tileBits) + " polyBits=" + - Ogre::StringConverter::toString(polyBits) + - " maxTiles=" + - Ogre::StringConverter::toString(nmparams.maxTiles) + - " maxPolys=" + - Ogre::StringConverter::toString(nmparams.maxPolys)); + "[NavMesh] tileBits=" + + Ogre::StringConverter::toString(tileBits) + " polyBits=" + + Ogre::StringConverter::toString(polyBits) + " maxTiles=" + + Ogre::StringConverter::toString(nmparams.maxTiles) + + " maxPolys=" + + Ogre::StringConverter::toString(nmparams.maxPolys)); m_navMesh = dtAllocNavMesh(); if (!m_navMesh) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] initTileCache failed: dtAllocNavMesh"); + "[NavMesh] initTileCache failed: dtAllocNavMesh"); return false; } status = m_navMesh->init(&nmparams); if (dtStatusFailed(status)) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] initTileCache failed: navMesh init"); + "[NavMesh] initTileCache failed: navMesh init"); return false; } @@ -469,17 +459,16 @@ bool TileCacheNavMesh::initTileCache() status = m_navQuery->init(m_navMesh, 2048); if (dtStatusFailed(status)) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] initTileCache failed: navQuery init"); + "[NavMesh] initTileCache failed: navQuery init"); return false; } Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] initTileCache success"); + "[NavMesh] initTileCache success"); return true; } -bool TileCacheNavMesh::build( - const std::vector &entities) +bool TileCacheNavMesh::build(const std::vector &entities) { if (!extractMeshData(entities)) return false; @@ -508,20 +497,17 @@ bool TileCacheNavMesh::build( } Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] Built " + - Ogre::StringConverter::toString(builtTiles) + "/" + - Ogre::StringConverter::toString(m_tw * m_th) + - " tiles, " + - Ogre::StringConverter::toString(tilesWithLayers) + - " with layers, " + - Ogre::StringConverter::toString(totalPolys) + - " polygons"); + "[NavMesh] Built " + + Ogre::StringConverter::toString(builtTiles) + "/" + + Ogre::StringConverter::toString(m_tw * m_th) + " tiles, " + + Ogre::StringConverter::toString(tilesWithLayers) + + " with layers, " + Ogre::StringConverter::toString(totalPolys) + + " polygons"); return true; } -int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, - TileCacheData *tiles, +int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, TileCacheData *tiles, int maxTiles) { if (!m_partitionedMesh) @@ -569,9 +555,8 @@ int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, // Allocate heightfield rc.solid = rcAllocHeightfield(); if (!rc.solid || - !rcCreateHeightfield(m_ctx, *rc.solid, tcfg.width, - tcfg.height, tcfg.bmin, tcfg.bmax, - tcfg.cs, tcfg.ch)) + !rcCreateHeightfield(m_ctx, *rc.solid, tcfg.width, tcfg.height, + tcfg.bmin, tcfg.bmax, tcfg.cs, tcfg.ch)) return 0; rc.triareas = new unsigned char[m_partitionedMesh->maxTrisPerChunk]; @@ -592,34 +577,51 @@ int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, for (int nodeIndex : overlappingNodes) { const PartitionedMesh::Node &node = - m_partitionedMesh->nodes[nodeIndex]; - const int *tris = - &m_partitionedMesh->tris[node.triIndex * 3]; + m_partitionedMesh->nodes[nodeIndex]; + const int *tris = &m_partitionedMesh->tris[node.triIndex * 3]; const int ntris = node.numTris; - memset(rc.triareas, 0, - ntris * sizeof(unsigned char)); - rcMarkWalkableTriangles( - m_ctx, tcfg.walkableSlopeAngle, verts, nverts, - tris, ntris, rc.triareas); - rcRasterizeTriangles(m_ctx, verts, nverts, tris, - rc.triareas, ntris, - *rc.solid, - tcfg.walkableClimb); + memset(rc.triareas, 0, ntris * sizeof(unsigned char)); + rcMarkWalkableTriangles(m_ctx, tcfg.walkableSlopeAngle, verts, + nverts, tris, ntris, rc.triareas); + + // After rcMarkWalkableTriangles has set area based on slope, + // additionally clear triangles whose normal faces downward + // (negative Y in world space). Some meshes (e.g. Lot base + // boxes) have bottom faces wound the same way as top faces, + // causing rcMarkWalkableTriangles to compute an upward-pointing + // normal and mark them as walkable. We detect this by + // computing the world-space normal and forcing RC_NULL_AREA. + for (int i = 0; i < ntris; ++i) { + const int *tri = &tris[i * 3]; + const float *v0 = &verts[tri[0] * 3]; + const float *v1 = &verts[tri[1] * 3]; + const float *v2 = &verts[tri[2] * 3]; + float ux = v1[0] - v0[0]; + float uy = v1[1] - v0[1]; + float uz = v1[2] - v0[2]; + float vx = v2[0] - v0[0]; + float vy = v2[1] - v0[1]; + float vz = v2[2] - v0[2]; + float ny = uz * vx - ux * vz; + if (ny < -1e-6f) + rc.triareas[i] = RC_NULL_AREA; + } + + rcRasterizeTriangles(m_ctx, verts, nverts, tris, rc.triareas, + ntris, *rc.solid, tcfg.walkableClimb); } rcFilterLowHangingWalkableObstacles(m_ctx, tcfg.walkableClimb, *rc.solid); - rcFilterLedgeSpans(m_ctx, tcfg.walkableHeight, - tcfg.walkableClimb, *rc.solid); - rcFilterWalkableLowHeightSpans(m_ctx, tcfg.walkableHeight, - *rc.solid); + rcFilterLedgeSpans(m_ctx, tcfg.walkableHeight, tcfg.walkableClimb, + *rc.solid); + rcFilterWalkableLowHeightSpans(m_ctx, tcfg.walkableHeight, *rc.solid); rc.chf = rcAllocCompactHeightfield(); if (!rc.chf || !rcBuildCompactHeightfield(m_ctx, tcfg.walkableHeight, - tcfg.walkableClimb, *rc.solid, - *rc.chf)) + tcfg.walkableClimb, *rc.solid, *rc.chf)) return 0; rcFreeHeightField(rc.solid); @@ -631,21 +633,19 @@ int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, if (!rcBuildDistanceField(m_ctx, *rc.chf)) return 0; - if (!rcBuildRegions(m_ctx, *rc.chf, tcfg.borderSize, - tcfg.minRegionArea, tcfg.mergeRegionArea)) + if (!rcBuildRegions(m_ctx, *rc.chf, tcfg.borderSize, tcfg.minRegionArea, + tcfg.mergeRegionArea)) return 0; rc.lset = rcAllocHeightfieldLayerSet(); if (!rc.lset || - !rcBuildHeightfieldLayers(m_ctx, *rc.chf, - tcfg.borderSize, + !rcBuildHeightfieldLayers(m_ctx, *rc.chf, tcfg.borderSize, tcfg.walkableHeight, *rc.lset)) return 0; for (int i = 0; i < rcMin(rc.lset->nlayers, maxTiles); ++i) { TileCacheData *tile = &rc.tiles[rc.ntiles++]; - const rcHeightfieldLayer *layer = - &rc.lset->layers[i]; + const rcHeightfieldLayer *layer = &rc.lset->layers[i]; dtTileCacheLayerHeader header; header.magic = DT_TILECACHE_MAGIC; @@ -665,8 +665,8 @@ int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, header.hmax = (unsigned short)layer->hmax; dtStatus status = dtBuildTileCacheLayer( - &comp, &header, layer->heights, layer->areas, - layer->cons, &tile->data, &tile->dataSize); + &comp, &header, layer->heights, layer->areas, + layer->cons, &tile->data, &tile->dataSize); if (dtStatusFailed(status)) return 0; } @@ -692,9 +692,9 @@ bool TileCacheNavMesh::buildTile(int tx, int ty) int ntiles = rasterizeTileLayers(tx, ty, tiles, MAX_LAYERS); for (int i = 0; i < ntiles; ++i) { - dtStatus status = m_tileCache->addTile( - tiles[i].data, tiles[i].dataSize, - DT_COMPRESSEDTILE_FREE_DATA, 0); + dtStatus status = + m_tileCache->addTile(tiles[i].data, tiles[i].dataSize, + DT_COMPRESSEDTILE_FREE_DATA, 0); if (dtStatusFailed(status)) { dtFree(tiles[i].data); tiles[i].data = nullptr; @@ -725,8 +725,7 @@ bool TileCacheNavMesh::removeTile(int tx, int ty) return true; } -void TileCacheNavMesh::rebuildTilesInArea( - const Ogre::AxisAlignedBox &area) +void TileCacheNavMesh::rebuildTilesInArea(const Ogre::AxisAlignedBox &area) { if (!m_tileCache) return; @@ -754,8 +753,7 @@ void TileCacheNavMesh::rebuildTilesInArea( } } -void TileCacheNavMesh::getTileCoords(const float *pos, int &tx, - int &ty) +void TileCacheNavMesh::getTileCoords(const float *pos, int &tx, int &ty) { float ts = m_params.tileSize * m_cellSize; tx = (int)((pos[0] - m_bmin[0]) / ts); @@ -788,21 +786,21 @@ bool TileCacheNavMesh::findPath(const Ogre::Vector3 &start, dtPolyRef startPoly, endPoly; float startNearest[3], endNearest[3]; - dtStatus status = m_navQuery->findNearestPoly( - s, m_extents, m_filter, &startPoly, startNearest); + dtStatus status = m_navQuery->findNearestPoly(s, m_extents, m_filter, + &startPoly, startNearest); if (dtStatusFailed(status)) return false; - status = m_navQuery->findNearestPoly(e, m_extents, m_filter, - &endPoly, endNearest); + status = m_navQuery->findNearestPoly(e, m_extents, m_filter, &endPoly, + endNearest); if (dtStatusFailed(status)) return false; dtPolyRef polyPath[256]; int npoly = 0; status = m_navQuery->findPath(startPoly, endPoly, startNearest, - endNearest, m_filter, - polyPath, &npoly, 256); + endNearest, m_filter, polyPath, &npoly, + 256); if (dtStatusFailed(status) || npoly == 0) return false; @@ -810,10 +808,10 @@ bool TileCacheNavMesh::findPath(const Ogre::Vector3 &start, unsigned char straightPathFlags[256]; dtPolyRef straightPathPolys[256]; int nvert = 0; - status = m_navQuery->findStraightPath( - startNearest, endNearest, polyPath, npoly, - straightPath, straightPathFlags, straightPathPolys, - &nvert, 256); + status = m_navQuery->findStraightPath(startNearest, endNearest, + polyPath, npoly, straightPath, + straightPathFlags, + straightPathPolys, &nvert, 256); if (dtStatusFailed(status) || nvert == 0) return false; @@ -836,8 +834,8 @@ bool TileCacheNavMesh::findNearestPoint(const Ogre::Vector3 &pos, dtPolyRef poly; float nearest[3]; - dtStatus status = m_navQuery->findNearestPoly( - p, m_extents, m_filter, &poly, nearest); + dtStatus status = m_navQuery->findNearestPoly(p, m_extents, m_filter, + &poly, nearest); if (dtStatusFailed(status)) return false; @@ -858,11 +856,9 @@ Ogre::Vector3 TileCacheNavMesh::getRandomPoint() dtPolyRef poly; float pt[3]; dtStatus status = m_navQuery->findRandomPoint( - m_filter, - []() -> float { - return (float)rand() / (float)RAND_MAX; - }, - &poly, pt); + m_filter, + []() -> float { return (float)rand() / (float)RAND_MAX; }, + &poly, pt); if (dtStatusFailed(status)) return Ogre::Vector3::ZERO; @@ -878,17 +874,15 @@ void TileCacheNavMesh::drawNavMesh() clearDebugDraw(); if (!m_navMesh) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] drawNavMesh: no navmesh"); + "[NavMesh] drawNavMesh: no navmesh"); return; } static int debugId = 0; - Ogre::String moName = "NavMeshDebug_" + - Ogre::StringConverter::toString( - debugId++); + Ogre::String moName = + "NavMeshDebug_" + Ogre::StringConverter::toString(debugId++); Ogre::String nodeName = "NavMeshDebugNode_" + - Ogre::StringConverter::toString( - debugId++); + Ogre::StringConverter::toString(debugId++); m_debugMO = m_sceneMgr->createManualObject(moName); m_debugMO->setDynamic(false); @@ -897,19 +891,18 @@ void TileCacheNavMesh::drawNavMesh() Ogre::ColourValue groundCol(0.0f, 0.7f, 0.0f, 1.0f); Ogre::ColourValue edgeCol(1.0f, 0.0f, 0.0f, 1.0f); - Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton() - .getByName("BaseWhiteNoLighting"); + Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName( + "BaseWhiteNoLighting"); if (!mat) { Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] BaseWhiteNoLighting not found, creating fallback"); + "[NavMesh] BaseWhiteNoLighting not found, creating fallback"); mat = Ogre::MaterialManager::getSingleton().create( - "NavMeshDebugMat", - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + "NavMeshDebugMat", + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); mat->getTechnique(0)->getPass(0)->setLightingEnabled(false); - mat->getTechnique(0)->getPass(0)->setDepthWriteEnabled( - false); + mat->getTechnique(0)->getPass(0)->setDepthWriteEnabled(false); mat->getTechnique(0)->getPass(0)->setSceneBlending( - Ogre::SBT_TRANSPARENT_ALPHA); + Ogre::SBT_TRANSPARENT_ALPHA); } m_debugMO->begin("BaseWhiteNoLighting", @@ -962,8 +955,8 @@ void TileCacheNavMesh::drawNavMesh() if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) continue; - for (int k = 0, k2 = p->vertCount - 1; - k < p->vertCount; k2 = k++) { + for (int k = 0, k2 = p->vertCount - 1; k < p->vertCount; + k2 = k++) { unsigned short v0i = p->verts[k2]; unsigned short v1i = p->verts[k]; if (p->neis[k2] != 0) @@ -982,8 +975,8 @@ void TileCacheNavMesh::drawNavMesh() } m_debugMO->end(); - m_debugNode = m_sceneMgr->getRootSceneNode() - ->createChildSceneNode(nodeName); + m_debugNode = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(nodeName); m_debugNode->attachObject(m_debugMO); // Count total polygons for diagnostics @@ -994,12 +987,10 @@ void TileCacheNavMesh::drawNavMesh() polyCount += tile->header->polyCount; } Ogre::LogManager::getSingleton().logMessage( - "[NavMesh] drawNavMesh: created " + moName + - " with " + - Ogre::StringConverter::toString(m_debugMO->getNumSections()) + - " sections, " + - Ogre::StringConverter::toString(polyCount) + - " polygons"); + "[NavMesh] drawNavMesh: created " + moName + " with " + + Ogre::StringConverter::toString(m_debugMO->getNumSections()) + + " sections, " + Ogre::StringConverter::toString(polyCount) + + " polygons"); } void TileCacheNavMesh::clearDebugDraw() diff --git a/src/features/editScene/systems/NavMeshSystem.cpp b/src/features/editScene/systems/NavMeshSystem.cpp index af1a53c..f765737 100644 --- a/src/features/editScene/systems/NavMeshSystem.cpp +++ b/src/features/editScene/systems/NavMeshSystem.cpp @@ -24,10 +24,9 @@ static bool isNavMeshGeometry(const Ogre::String &meshName) // span the walkable surface is pushed to the top of the wall; // if a roof is close above, rcFilterWalkableLowHeightSpans // removes the whole column. - static const char *exclude[] = { - "wall", "ceiling", "roof", "door", - "window", "frame", "furniture" - }; + static const char *exclude[] = { "wall", "ceiling", "roof", + "door", "window", "frame", + "furniture" }; for (const char *pat : exclude) { if (lower.find(pat) != Ogre::String::npos) return false; @@ -42,31 +41,29 @@ static void collectEntitiesFromNode(Ogre::SceneNode *node, if (!node) return; try { - for (unsigned short i = 0; - i < node->numAttachedObjects(); ++i) { - Ogre::MovableObject *obj = - node->getAttachedObject(i); + for (unsigned short i = 0; i < node->numAttachedObjects(); + ++i) { + Ogre::MovableObject *obj = node->getAttachedObject(i); if (!obj) continue; if (obj->getMovableType() == "Entity") { Ogre::Entity *ent = - static_cast(obj); + static_cast(obj); if (!isNavMeshGeometry( - ent->getMesh()->getName())) + ent->getMesh()->getName())) continue; // Avoid duplicates - if (std::find(out.begin(), out.end(), - ent) == out.end()) + if (std::find(out.begin(), out.end(), ent) == + out.end()) out.push_back(ent); } } - for (unsigned short i = 0; - i < node->numChildren(); ++i) { + for (unsigned short i = 0; i < node->numChildren(); ++i) { Ogre::Node *child = node->getChild(i); if (!child) continue; - Ogre::SceneNode *childSN = dynamic_cast< - Ogre::SceneNode *>(child); + Ogre::SceneNode *childSN = + dynamic_cast(child); if (childSN) collectEntitiesFromNode(childSN, out); } @@ -79,8 +76,8 @@ static void collectEntitiesFromNode(Ogre::SceneNode *node, // and all its descendants. This is needed because flecs parent/child // hierarchy does NOT map to Ogre SceneNode hierarchy; each entity has // its own independent scene node. -static void collectEntitiesFromFlecsEntity( - flecs::entity e, std::vector &out) +static void collectEntitiesFromFlecsEntity(flecs::entity e, + std::vector &out) { if (e.has()) { auto &trans = e.get(); @@ -94,8 +91,7 @@ static void collectEntitiesFromFlecsEntity( NavMeshSystem *NavMeshSystem::s_instance = nullptr; -NavMeshSystem::NavMeshSystem(flecs::world &world, - Ogre::SceneManager *sceneMgr) +NavMeshSystem::NavMeshSystem(flecs::world &world, Ogre::SceneManager *sceneMgr) : m_world(world) , m_sceneMgr(sceneMgr) { @@ -117,93 +113,89 @@ void NavMeshSystem::update(float deltaTime) { (void)deltaTime; - m_world.query().each( - [&](flecs::entity e, NavMeshComponent &comp) { - if (!comp.enabled) - return; + m_world.query().each([&](flecs::entity e, + NavMeshComponent &comp) { + if (!comp.enabled) + return; - auto &state = m_states[e.id()]; + auto &state = m_states[e.id()]; - // Handle full rebuild request - if (comp.dirty) { - buildOrUpdate(e.id(), comp, state); - comp.dirty = false; - comp.needsPartialRebuild = false; - state.lastTransformVersions.clear(); - return; - } + // Handle full rebuild request + if (comp.dirty) { + buildOrUpdate(e.id(), comp, state); + comp.dirty = false; + comp.needsPartialRebuild = false; + state.lastTransformVersions.clear(); + return; + } - // Check for geometry changes that need partial rebuild - bool changed = false; - Ogre::AxisAlignedBox dirtyArea; - m_world.query().each( - [&](flecs::entity ge, - TransformComponent &trans) { - bool contributes = false; - if (ge.has()) { - auto &rb = ge.get(); - if (rb.bodyType == - RigidBodyComponent::BodyType::Static) - contributes = true; - } - if (ge.has()) { - auto &sgm = ge.get(); - if (!sgm.meshName.empty()) - contributes = true; - } - if (ge.has()) { - auto &nmg = ge.get(); - if (nmg.include) - contributes = true; - } - if (ge.has()) - contributes = true; - if (ge.has()) - contributes = true; - if (ge.has()) - contributes = true; - if (!contributes) - return; + // Check for geometry changes that need partial rebuild + bool changed = false; + Ogre::AxisAlignedBox dirtyArea; + m_world.query().each([&](flecs::entity ge, + TransformComponent + &trans) { + bool contributes = false; + if (ge.has()) { + auto &rb = ge.get(); + if (rb.bodyType == + RigidBodyComponent::BodyType::Static) + contributes = true; + } + if (ge.has()) { + auto &sgm = + ge.get(); + if (!sgm.meshName.empty()) + contributes = true; + } + if (ge.has()) { + auto &nmg = ge.get(); + if (nmg.include) + contributes = true; + } + if (ge.has()) + contributes = true; + if (ge.has()) + contributes = true; + if (ge.has()) + contributes = true; + if (!contributes) + return; - auto it = state.lastTransformVersions.find( - ge.id()); - if (it == state.lastTransformVersions.end() || - it->second != trans.version) { - changed = true; - Ogre::AxisAlignedBox bb = - getEntityBounds(ge); - if (!bb.isNull()) { - if (dirtyArea.isNull()) - dirtyArea = bb; - else - dirtyArea.merge(bb); - } - state.lastTransformVersions[ge.id()] = - trans.version; - } - }); + auto it = state.lastTransformVersions.find(ge.id()); + if (it == state.lastTransformVersions.end() || + it->second != trans.version) { + changed = true; + Ogre::AxisAlignedBox bb = getEntityBounds(ge); + if (!bb.isNull()) { + if (dirtyArea.isNull()) + dirtyArea = bb; + else + dirtyArea.merge(bb); + } + state.lastTransformVersions[ge.id()] = + trans.version; + } + }); - if (changed && state.navmesh) { - if (!dirtyArea.isNull()) { - Ogre::Vector3 padding( - comp.agentRadius * 2, - comp.agentHeight * 2, - comp.agentRadius * 2); - dirtyArea.setMinimum( - dirtyArea.getMinimum() - padding); - dirtyArea.setMaximum( - dirtyArea.getMaximum() + padding); - state.navmesh->rebuildTilesInArea( - dirtyArea); - } - if (state.debugDraw) - state.navmesh->drawNavMesh(); - } - }); + if (changed && state.navmesh) { + if (!dirtyArea.isNull()) { + Ogre::Vector3 padding(comp.agentRadius * 2, + comp.agentHeight * 2, + comp.agentRadius * 2); + dirtyArea.setMinimum(dirtyArea.getMinimum() - + padding); + dirtyArea.setMaximum(dirtyArea.getMaximum() + + padding); + state.navmesh->rebuildTilesInArea(dirtyArea); + } + if (state.debugDraw) + state.navmesh->drawNavMesh(); + } + }); } -void NavMeshSystem::buildOrUpdate(flecs::entity_t e, - NavMeshComponent &comp, +void NavMeshSystem::buildOrUpdate(flecs::entity_t e, NavMeshComponent &comp, NavMeshState &state) { state.navmesh.reset(); @@ -222,8 +214,7 @@ void NavMeshSystem::buildOrUpdate(flecs::entity_t e, params.regionMergeSize = comp.regionMergeSize; params.tileSize = comp.tileSize; - state.navmesh = std::make_unique( - m_sceneMgr, params); + state.navmesh = std::make_unique(m_sceneMgr, params); std::vector entities; std::vector tempEntities; @@ -231,7 +222,7 @@ void NavMeshSystem::buildOrUpdate(flecs::entity_t e, if (entities.empty()) { Ogre::LogManager::getSingleton().logMessage( - "[NavMeshSystem] No static geometry found for navmesh"); + "[NavMeshSystem] No static geometry found for navmesh"); return; } @@ -240,53 +231,49 @@ void NavMeshSystem::buildOrUpdate(flecs::entity_t e, if (!ok) { Ogre::LogManager::getSingleton().logMessage( - "[NavMeshSystem] Navmesh build failed"); + "[NavMeshSystem] Navmesh build failed"); return; } Ogre::LogManager::getSingleton().logMessage( - "[NavMeshSystem] Navmesh built successfully"); + "[NavMeshSystem] Navmesh built successfully"); if (state.debugDraw) state.navmesh->drawNavMesh(); state.firstBuild = false; - m_world.query().each( - [&](flecs::entity ge, TransformComponent &trans) { - bool contributes = false; - if (ge.has()) { - auto &rb = ge.get(); - if (rb.bodyType == - RigidBodyComponent::BodyType::Static) - contributes = true; - } - if (ge.has()) { - auto &sgm = - ge.get(); - if (!sgm.meshName.empty()) - contributes = true; - } - if (ge.has()) { - auto &nmg = ge.get(); - if (nmg.include) - contributes = true; - } - if (ge.has()) - contributes = true; - if (ge.has()) - contributes = true; - if (ge.has()) - contributes = true; - if (contributes) - state.lastTransformVersions[ge.id()] = - trans.version; - }); + m_world.query().each([&](flecs::entity ge, + TransformComponent &trans) { + bool contributes = false; + if (ge.has()) { + auto &rb = ge.get(); + if (rb.bodyType == RigidBodyComponent::BodyType::Static) + contributes = true; + } + if (ge.has()) { + auto &sgm = ge.get(); + if (!sgm.meshName.empty()) + contributes = true; + } + if (ge.has()) { + auto &nmg = ge.get(); + if (nmg.include) + contributes = true; + } + if (ge.has()) + contributes = true; + if (ge.has()) + contributes = true; + if (ge.has()) + contributes = true; + if (contributes) + state.lastTransformVersions[ge.id()] = trans.version; + }); } -void NavMeshSystem::collectStaticEntities( - std::vector &out, - std::vector &outTemp) +void NavMeshSystem::collectStaticEntities(std::vector &out, + std::vector &outTemp) { // Helper: add existing scene entity (do NOT destroy later) auto addExisting = [&](Ogre::Entity *ent) { @@ -302,121 +289,112 @@ void NavMeshSystem::collectStaticEntities( }; Ogre::LogManager::getSingleton().logMessage( - "[NavMeshSystem] collectStaticEntities started"); + "[NavMeshSystem] collectStaticEntities started"); // 1. Static rigid bodies with renderables - m_world.query() - .each([&](flecs::entity e, TransformComponent &trans, - RigidBodyComponent &rb) { - if (rb.bodyType != - RigidBodyComponent::BodyType::Static) - return; - if (!e.has()) - return; - auto &rend = e.get(); - if (rend.entity) { - addExisting(rend.entity); - } else if (!rend.meshName.empty() && - isNavMeshGeometry(rend.meshName)) { - addTemp(createTempEntity( - rend.meshName, trans.position, - trans.rotation, trans.scale)); - } - }); + m_world.query().each( + [&](flecs::entity e, TransformComponent &trans, + RigidBodyComponent &rb) { + if (rb.bodyType != RigidBodyComponent::BodyType::Static) + return; + if (!e.has()) + return; + auto &rend = e.get(); + if (rend.entity) { + addExisting(rend.entity); + } else if (!rend.meshName.empty() && + isNavMeshGeometry(rend.meshName)) { + addTemp(createTempEntity( + rend.meshName, trans.position, + trans.rotation, trans.scale)); + } + }); // 2. StaticGeometry members - m_world - .query() - .each([&](flecs::entity e, TransformComponent &trans, - StaticGeometryMemberComponent &sgm) { - if (sgm.meshName.empty() || - !isNavMeshGeometry(sgm.meshName)) - return; - addTemp(createTempEntity( - sgm.meshName, trans.position, trans.rotation, - trans.scale)); - }); + m_world.query().each( + [&](flecs::entity e, TransformComponent &trans, + StaticGeometryMemberComponent &sgm) { + if (sgm.meshName.empty() || + !isNavMeshGeometry(sgm.meshName)) + return; + addTemp(createTempEntity(sgm.meshName, trans.position, + trans.rotation, trans.scale)); + }); // 3. Forced geometry sources - m_world.query() - .each([&](flecs::entity e, TransformComponent &trans, - NavMeshGeometrySource &nmg) { - if (!nmg.include) - return; - Ogre::String meshName; - if (e.has()) { - auto &rend = e.get(); - if (rend.entity) { - addExisting(rend.entity); - return; - } - meshName = rend.meshName; - } - if (e.has()) { - auto &sgm = - e.get(); - if (!sgm.meshName.empty()) - meshName = sgm.meshName; - } - if (!meshName.empty() && - isNavMeshGeometry(meshName)) { - addTemp(createTempEntity( - meshName, trans.position, trans.rotation, - trans.scale)); - } - }); + m_world.query().each( + [&](flecs::entity e, TransformComponent &trans, + NavMeshGeometrySource &nmg) { + if (!nmg.include) + return; + Ogre::String meshName; + if (e.has()) { + auto &rend = e.get(); + if (rend.entity) { + addExisting(rend.entity); + return; + } + meshName = rend.meshName; + } + if (e.has()) { + auto &sgm = + e.get(); + if (!sgm.meshName.empty()) + meshName = sgm.meshName; + } + if (!meshName.empty() && isNavMeshGeometry(meshName)) { + addTemp(createTempEntity( + meshName, trans.position, + trans.rotation, trans.scale)); + } + }); // 4. Cell Grid, District, and Lot geometry (always included) // We recursively traverse the flecs hierarchy because flecs // parent/child relationships do NOT map to Ogre SceneNode // hierarchy; each entity has its own independent scene node. - m_world - .query() - .each([&](flecs::entity e, TransformComponent &trans, - CellGridComponent &grid) { - (void)trans; - (void)grid; - collectEntitiesFromFlecsEntity(e, out); - }); - m_world - .query() - .each([&](flecs::entity e, TransformComponent &trans, - DistrictComponent &district) { - (void)trans; - (void)district; - collectEntitiesFromFlecsEntity(e, out); - }); - m_world - .query() - .each([&](flecs::entity e, TransformComponent &trans, - LotComponent &lot) { - (void)trans; - (void)lot; - collectEntitiesFromFlecsEntity(e, out); - }); + m_world.query().each( + [&](flecs::entity e, TransformComponent &trans, + CellGridComponent &grid) { + (void)trans; + (void)grid; + collectEntitiesFromFlecsEntity(e, out); + }); + m_world.query().each( + [&](flecs::entity e, TransformComponent &trans, + DistrictComponent &district) { + (void)trans; + (void)district; + collectEntitiesFromFlecsEntity(e, out); + }); + m_world.query().each( + [&](flecs::entity e, TransformComponent &trans, + LotComponent &lot) { + (void)trans; + (void)lot; + collectEntitiesFromFlecsEntity(e, out); + }); // 5. Frame and furniture placements from CellGridSystem if (m_cellGridSystem) { - m_world.query() - .each([&](flecs::entity e, CellGridComponent &grid) { - (void)grid; - // Frames and furniture are excluded from navmesh - // geometry because their vertical spans merge with - // floor spans in Recast and break walkability. - (void)m_cellGridSystem; - }); + m_world.query().each( + [&](flecs::entity e, CellGridComponent &grid) { + (void)grid; + // Frames and furniture are excluded from navmesh + // geometry because their vertical spans merge with + // floor spans in Recast and break walkability. + (void)m_cellGridSystem; + }); } // Detailed logging Ogre::LogManager::getSingleton().logMessage( - "[NavMeshSystem] collectStaticEntities done: existing=" + - Ogre::StringConverter::toString( - static_cast(out.size() - outTemp.size())) + - " temp=" + - Ogre::StringConverter::toString( - static_cast(outTemp.size()))); + "[NavMeshSystem] collectStaticEntities done: existing=" + + Ogre::StringConverter::toString( + static_cast(out.size() - outTemp.size())) + + " temp=" + + Ogre::StringConverter::toString( + static_cast(outTemp.size()))); for (size_t i = 0; i < out.size(); ++i) { Ogre::Entity *ent = out[i]; if (!ent) @@ -424,35 +402,33 @@ void NavMeshSystem::collectStaticEntities( Ogre::SceneNode *node = ent->getParentSceneNode(); Ogre::String nodeName = node ? node->getName() : "(none)"; Ogre::String meshName = ent->getMesh()->getName(); - bool isTemp = std::find(outTemp.begin(), outTemp.end(), - ent) != outTemp.end(); + bool isTemp = std::find(outTemp.begin(), outTemp.end(), ent) != + outTemp.end(); Ogre::LogManager::getSingleton().logMessage( - "[NavMeshSystem] entity[" + - Ogre::StringConverter::toString( - static_cast(i)) + - "] mesh=" + meshName + " node=" + nodeName + - " temp=" + (isTemp ? "yes" : "no")); + "[NavMeshSystem] entity[" + + Ogre::StringConverter::toString(static_cast(i)) + + "] mesh=" + meshName + " node=" + nodeName + + " temp=" + (isTemp ? "yes" : "no")); } } -Ogre::Entity *NavMeshSystem::createTempEntity( - const Ogre::String &meshName, const Ogre::Vector3 &pos, - const Ogre::Quaternion &rot, const Ogre::Vector3 &scale) +Ogre::Entity *NavMeshSystem::createTempEntity(const Ogre::String &meshName, + const Ogre::Vector3 &pos, + const Ogre::Quaternion &rot, + const Ogre::Vector3 &scale) { static int tempId = 0; - Ogre::String uniqueName = "_NavTempEnt_" + - Ogre::StringConverter::toString( - tempId++); - Ogre::String nodeName = "_NavTempNode_" + - Ogre::StringConverter::toString( - tempId++); + Ogre::String uniqueName = + "_NavTempEnt_" + Ogre::StringConverter::toString(tempId++); + Ogre::String nodeName = + "_NavTempNode_" + Ogre::StringConverter::toString(tempId++); try { - Ogre::Entity *ent = m_sceneMgr->createEntity( - uniqueName, meshName); + Ogre::Entity *ent = + m_sceneMgr->createEntity(uniqueName, meshName); Ogre::SceneNode *node = - m_sceneMgr->getRootSceneNode()->createChildSceneNode( - nodeName); + m_sceneMgr->getRootSceneNode()->createChildSceneNode( + nodeName); node->setPosition(pos); node->setOrientation(rot); node->setScale(scale); @@ -463,8 +439,7 @@ Ogre::Entity *NavMeshSystem::createTempEntity( } } -void NavMeshSystem::destroyTempEntities( - std::vector &entities) +void NavMeshSystem::destroyTempEntities(std::vector &entities) { for (auto *ent : entities) { if (!ent) @@ -500,8 +475,8 @@ Ogre::AxisAlignedBox NavMeshSystem::getEntityBounds(flecs::entity e) if (!sgm.meshName.empty()) { try { Ogre::MeshPtr mesh = - Ogre::MeshManager::getSingleton() - .getByName(sgm.meshName); + Ogre::MeshManager::getSingleton() + .getByName(sgm.meshName); if (mesh) { localBB = mesh->getBounds(); hasBounds = true; @@ -512,8 +487,7 @@ Ogre::AxisAlignedBox NavMeshSystem::getEntityBounds(flecs::entity e) } if (!hasBounds && e.has()) { auto &col = e.get(); - localBB = Ogre::AxisAlignedBox(-col.parameters, - col.parameters); + localBB = Ogre::AxisAlignedBox(-col.parameters, col.parameters); hasBounds = true; } @@ -523,27 +497,27 @@ Ogre::AxisAlignedBox NavMeshSystem::getEntityBounds(flecs::entity e) Ogre::Matrix4 mat; mat.makeTransform(trans.position, trans.scale, trans.rotation); Ogre::AxisAlignedBox worldBB; + worldBB.merge(mat * + localBB.getCorner(Ogre::AxisAlignedBox::FAR_LEFT_BOTTOM)); + worldBB.merge(mat * + localBB.getCorner(Ogre::AxisAlignedBox::FAR_LEFT_TOP)); worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::FAR_LEFT_BOTTOM)); + Ogre::AxisAlignedBox::FAR_RIGHT_BOTTOM)); + worldBB.merge(mat * + localBB.getCorner(Ogre::AxisAlignedBox::FAR_RIGHT_TOP)); worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::FAR_LEFT_TOP)); + Ogre::AxisAlignedBox::NEAR_LEFT_BOTTOM)); + worldBB.merge(mat * + localBB.getCorner(Ogre::AxisAlignedBox::NEAR_LEFT_TOP)); worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::FAR_RIGHT_BOTTOM)); - worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::FAR_RIGHT_TOP)); - worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::NEAR_LEFT_BOTTOM)); - worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::NEAR_LEFT_TOP)); - worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::NEAR_RIGHT_BOTTOM)); - worldBB.merge(mat * localBB.getCorner( - Ogre::AxisAlignedBox::NEAR_RIGHT_TOP)); + Ogre::AxisAlignedBox::NEAR_RIGHT_BOTTOM)); + worldBB.merge(mat * + localBB.getCorner(Ogre::AxisAlignedBox::NEAR_RIGHT_TOP)); return worldBB; } -bool NavMeshSystem::findPath(flecs::entity navmeshEntity, - Ogre::Vector3 start, Ogre::Vector3 end, +bool NavMeshSystem::findPath(flecs::entity navmeshEntity, Ogre::Vector3 start, + Ogre::Vector3 end, std::vector &path) { auto it = m_states.find(navmeshEntity.id()); @@ -554,8 +528,7 @@ bool NavMeshSystem::findPath(flecs::entity navmeshEntity, } bool NavMeshSystem::findNearestPoint(flecs::entity navmeshEntity, - Ogre::Vector3 pos, - Ogre::Vector3 &out) + Ogre::Vector3 pos, Ogre::Vector3 &out) { auto it = m_states.find(navmeshEntity.id()); if (it == m_states.end() || !it->second.navmesh) @@ -564,8 +537,7 @@ bool NavMeshSystem::findNearestPoint(flecs::entity navmeshEntity, return it->second.navmesh->findNearestPoint(pos, out); } -Ogre::Vector3 NavMeshSystem::getRandomPoint( - flecs::entity navmeshEntity) +Ogre::Vector3 NavMeshSystem::getRandomPoint(flecs::entity navmeshEntity) { auto it = m_states.find(navmeshEntity.id()); if (it == m_states.end() || !it->second.navmesh) @@ -584,8 +556,7 @@ void NavMeshSystem::rebuild(flecs::entity navmeshEntity) comp.dirty = false; } -void NavMeshSystem::setDebugDraw(flecs::entity navmeshEntity, - bool enable) +void NavMeshSystem::setDebugDraw(flecs::entity navmeshEntity, bool enable) { auto it = m_states.find(navmeshEntity.id()); if (it == m_states.end()) diff --git a/src/features/editScene/systems/ProceduralMaterialSystem.cpp b/src/features/editScene/systems/ProceduralMaterialSystem.cpp index 2c6f426..8a2e32d 100644 --- a/src/features/editScene/systems/ProceduralMaterialSystem.cpp +++ b/src/features/editScene/systems/ProceduralMaterialSystem.cpp @@ -8,10 +8,11 @@ #include #include -ProceduralMaterialSystem::ProceduralMaterialSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) - : m_world(world) - , m_sceneMgr(sceneMgr) - , m_query(world.query()) +ProceduralMaterialSystem::ProceduralMaterialSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_query(world.query()) { } @@ -19,134 +20,165 @@ ProceduralMaterialSystem::~ProceduralMaterialSystem() = default; void ProceduralMaterialSystem::initialize() { - if (m_initialized) return; - m_initialized = true; - - Ogre::LogManager::getSingleton().logMessage("ProceduralMaterialSystem initialized"); + if (m_initialized) + return; + m_initialized = true; + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMaterialSystem initialized"); } void ProceduralMaterialSystem::update() { - if (!m_initialized) return; + if (!m_initialized) + return; - m_query.each([&](flecs::entity entity, ProceduralMaterialComponent& component) { - bool needsRecreate = component.dirty || !component.created; - - // Check if texture has changed (new version) - if (!needsRecreate && component.diffuseTextureEntity.is_alive() && - component.diffuseTextureEntity.has()) { - const auto& tex = component.diffuseTextureEntity.get(); - if (tex.version != component.textureVersion) { - needsRecreate = true; - } - } - - if (needsRecreate) { - createMaterial(entity, component); - } - }); + m_query.each([&](flecs::entity entity, + ProceduralMaterialComponent &component) { + bool needsRecreate = component.dirty || !component.created; + + // Check if texture has changed (new version) + if (!needsRecreate && + component.diffuseTextureEntity.is_alive() && + component.diffuseTextureEntity + .has()) { + const auto &tex = + component.diffuseTextureEntity + .get(); + if (tex.version != component.textureVersion) { + needsRecreate = true; + } + } + + if (needsRecreate) { + createMaterial(entity, component); + } + }); } -void ProceduralMaterialSystem::createMaterial(flecs::entity entity, ProceduralMaterialComponent& component) +void ProceduralMaterialSystem::createMaterial( + flecs::entity entity, ProceduralMaterialComponent &component) { - try { - // Generate unique material name if not set - if (component.materialName.empty()) { - component.materialName = "ProceduralMat_" + std::to_string(entity.id()) + "_" + std::to_string(m_createdCount++); - } - - // Destroy old material if exists - if (component.ogreMaterial) { - destroyMaterial(component); - } - - // Create new material - component.ogreMaterial = Ogre::MaterialManager::getSingleton().create( - component.materialName, - Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); - - // Get the default technique and pass - Ogre::Technique* technique = component.ogreMaterial->getTechnique(0); - Ogre::Pass* pass = technique->getPass(0); - - // Set material colors - pass->setAmbient(component.ambient[0], component.ambient[1], component.ambient[2]); - pass->setDiffuse(component.diffuse[0], component.diffuse[1], component.diffuse[2], 1.0f); - pass->setSpecular(component.specular[0], component.specular[1], component.specular[2], 1.0f); - pass->setShininess(component.shininess); - - // Add diffuse texture if we have a reference to a ProceduralTexture entity - unsigned int currentTexVersion = 0; - if (component.diffuseTextureEntity.is_alive() && - component.diffuseTextureEntity.has()) { - - const auto& textureComp = component.diffuseTextureEntity.get(); - currentTexVersion = textureComp.version; - if (textureComp.generated && !textureComp.textureName.empty()) { - Ogre::TextureUnitState* texUnit = pass->createTextureUnitState(); - texUnit->setTextureName(textureComp.textureName); - texUnit->setTextureAddressingMode(Ogre::TAM_CLAMP); - } - } - - // Register with RTSS to generate shaders (fixes "RenderSystem does not support FixedFunction") - Ogre::RTShader::ShaderGenerator *shaderGen = - Ogre::RTShader::ShaderGenerator::getSingletonPtr(); - if (shaderGen && component.ogreMaterial) { - shaderGen->createShaderBasedTechnique( - *component.ogreMaterial, - Ogre::MaterialManager::DEFAULT_SCHEME_NAME, - Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME); - shaderGen->validateMaterial( - Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME, - component.materialName); - } + try { + // Generate unique material name if not set + if (component.materialName.empty()) { + component.materialName = + "ProceduralMat_" + std::to_string(entity.id()) + + "_" + std::to_string(m_createdCount++); + } - component.created = true; - component.dirty = false; - component.textureVersion = currentTexVersion; - - Ogre::LogManager::getSingleton().logMessage( - "ProceduralMaterial: Created '" + component.materialName + "'"); - - } catch (const Ogre::Exception& e) { - Ogre::LogManager::getSingleton().logMessage( - "ProceduralMaterial ERROR: " + e.getDescription()); - component.dirty = true; // Will retry - } + // Destroy old material if exists + if (component.ogreMaterial) { + destroyMaterial(component); + } + + // Create new material + component.ogreMaterial = + Ogre::MaterialManager::getSingleton().create( + component.materialName, + Ogre::ResourceGroupManager:: + DEFAULT_RESOURCE_GROUP_NAME); + + // Get the default technique and pass + Ogre::Technique *technique = + component.ogreMaterial->getTechnique(0); + Ogre::Pass *pass = technique->getPass(0); + + // Set material colors + pass->setAmbient(component.ambient[0], component.ambient[1], + component.ambient[2]); + pass->setDiffuse(component.diffuse[0], component.diffuse[1], + component.diffuse[2], 1.0f); + pass->setSpecular(component.specular[0], component.specular[1], + component.specular[2], 1.0f); + pass->setShininess(component.shininess); + + // Cull back-facing geometry + pass->setCullingMode(Ogre::CULL_CLOCKWISE); + + // Add diffuse texture if we have a reference to a ProceduralTexture entity + unsigned int currentTexVersion = 0; + if (component.diffuseTextureEntity.is_alive() && + component.diffuseTextureEntity + .has()) { + const auto &textureComp = + component.diffuseTextureEntity + .get(); + currentTexVersion = textureComp.version; + if (textureComp.generated && + !textureComp.textureName.empty()) { + Ogre::TextureUnitState *texUnit = + pass->createTextureUnitState(); + texUnit->setTextureName( + textureComp.textureName); + texUnit->setTextureAddressingMode( + Ogre::TAM_CLAMP); + } + } + + // Register with RTSS to generate shaders (fixes "RenderSystem does not support FixedFunction") + Ogre::RTShader::ShaderGenerator *shaderGen = + Ogre::RTShader::ShaderGenerator::getSingletonPtr(); + if (shaderGen && component.ogreMaterial) { + shaderGen->createShaderBasedTechnique( + *component.ogreMaterial, + Ogre::MaterialManager::DEFAULT_SCHEME_NAME, + Ogre::RTShader::ShaderGenerator:: + DEFAULT_SCHEME_NAME); + shaderGen->validateMaterial( + Ogre::RTShader::ShaderGenerator:: + DEFAULT_SCHEME_NAME, + component.materialName); + } + + component.created = true; + component.dirty = false; + component.textureVersion = currentTexVersion; + + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMaterial: Created '" + + component.materialName + "'"); + + } catch (const Ogre::Exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "ProceduralMaterial ERROR: " + e.getDescription()); + component.dirty = true; // Will retry + } } -void ProceduralMaterialSystem::destroyMaterial(ProceduralMaterialComponent& component) +void ProceduralMaterialSystem::destroyMaterial( + ProceduralMaterialComponent &component) { - if (component.ogreMaterial) { - try { - Ogre::MaterialManager::getSingleton().remove(component.ogreMaterial); - } catch (...) { - // Ignore errors during cleanup - } - component.ogreMaterial.reset(); - } + if (component.ogreMaterial) { + try { + Ogre::MaterialManager::getSingleton().remove( + component.ogreMaterial); + } catch (...) { + // Ignore errors during cleanup + } + component.ogreMaterial.reset(); + } } void ProceduralMaterialSystem::recreateMaterial(flecs::entity entity) { - if (!entity.is_alive() || !entity.has()) { - return; - } - - entity.get_mut().markDirty(); + if (!entity.is_alive() || !entity.has()) { + return; + } + + entity.get_mut().markDirty(); } std::string ProceduralMaterialSystem::getMaterialName(flecs::entity entity) { - if (!entity.is_alive() || !entity.has()) { - return ""; - } - - return entity.get().materialName; + if (!entity.is_alive() || !entity.has()) { + return ""; + } + + return entity.get().materialName; } -bool ProceduralMaterialSystem::materialExists(const std::string& name) +bool ProceduralMaterialSystem::materialExists(const std::string &name) { - return Ogre::MaterialManager::getSingleton().resourceExists(name); + return Ogre::MaterialManager::getSingleton().resourceExists(name); } diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 9bfe637..b808f5d 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -115,21 +115,26 @@ bool SceneSerializer::loadFromFile(const std::string &filepath, // Clear entity map for new load m_entityMap.clear(); - // Load entities + // First pass: create all entities and populate the entity map if (scene.contains("entities") && scene["entities"].is_array()) { Ogre::LogManager::getSingleton().logMessage( "SceneSerializer: Loading " + Ogre::StringConverter::toString( scene["entities"].size()) + - " entities"); + " entities (first pass)"); for (const auto &entityJson : scene["entities"]) { - deserializeEntity(entityJson, - flecs::entity::null(), - uiSystem); + deserializeEntityFirstPass( + entityJson, flecs::entity::null(), + uiSystem); } } + // Second pass: resolve material entity references + Ogre::LogManager::getSingleton().logMessage( + "SceneSerializer: Resolving material references (second pass)"); + resolveMaterialReferences(); + m_entityMap.clear(); return true; } catch (const std::exception &e) { @@ -221,7 +226,8 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) } if (entity.has()) { - json["animationTreeTemplate"] = serializeAnimationTreeTemplate(entity); + json["animationTreeTemplate"] = + serializeAnimationTreeTemplate(entity); } if (entity.has()) { @@ -263,7 +269,8 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) } if (entity.has()) { - json["navMeshGeometrySource"] = serializeNavMeshGeometrySource(entity); + json["navMeshGeometrySource"] = + serializeNavMeshGeometrySource(entity); } // Buoyancy components @@ -408,7 +415,8 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, } if (json.contains("animationTreeTemplate")) { - deserializeAnimationTreeTemplate(entity, json["animationTreeTemplate"]); + deserializeAnimationTreeTemplate(entity, + json["animationTreeTemplate"]); } if (json.contains("startupMenu")) { @@ -454,7 +462,8 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, } if (json.contains("navMeshGeometrySource")) { - deserializeNavMeshGeometrySource(entity, json["navMeshGeometrySource"]); + deserializeNavMeshGeometrySource(entity, + json["navMeshGeometrySource"]); } // Buoyancy components @@ -501,6 +510,370 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, } } +void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, + flecs::entity parent, + EditorUISystem *uiSystem) +{ + // Create new entity + flecs::entity entity = m_world.entity(); + entity.add(); + + // Store in map for potential future reference + if (json.contains("id")) { + uint64_t id = json["id"]; + m_entityMap[id] = entity; + } + + // Set parent relationship + if (parent.is_valid() && parent != 0) { + entity.child_of(parent); + } + + // Deserialize components that don't depend on material references + if (json.contains("name")) { + deserializeEntityName(entity, json["name"]); + } else { + entity.set(EntityNameComponent("Entity")); + } + + if (json.contains("transform")) { + deserializeTransform(entity, json["transform"], parent); + } + + if (json.contains("renderable")) { + deserializeRenderable(entity, json["renderable"]); + } + + if (json.contains("rigidBody")) { + deserializeRigidBody(entity, json["rigidBody"]); + } + + if (json.contains("collider")) { + deserializeCollider(entity, json["collider"]); + } + + if (json.contains("light")) { + deserializeLight(entity, json["light"]); + } + + if (json.contains("camera")) { + deserializeCamera(entity, json["camera"]); + } + + if (json.contains("lodSettings")) { + deserializeLodSettings(entity, json["lodSettings"]); + } + + if (json.contains("lod")) { + deserializeLod(entity, json["lod"]); + } + + if (json.contains("staticGeometry")) { + deserializeStaticGeometry(entity, json["staticGeometry"]); + } + + if (json.contains("staticGeometryMember")) { + deserializeStaticGeometryMember(entity, + json["staticGeometryMember"]); + } + + if (json.contains("proceduralTexture")) { + deserializeProceduralTexture(entity, json["proceduralTexture"]); + } + + if (json.contains("proceduralMaterial")) { + deserializeProceduralMaterial(entity, + json["proceduralMaterial"]); + } + + if (json.contains("primitive")) { + deserializePrimitive(entity, json["primitive"]); + } + + if (json.contains("character")) { + deserializeCharacter(entity, json["character"]); + } + + if (json.contains("characterSlots")) { + deserializeCharacterSlots(entity, json["characterSlots"]); + } + + if (json.contains("animationTree")) { + deserializeAnimationTree(entity, json["animationTree"]); + } + + if (json.contains("animationTreeTemplate")) { + deserializeAnimationTreeTemplate(entity, + json["animationTreeTemplate"]); + } + + if (json.contains("startupMenu")) { + deserializeStartupMenu(entity, json["startupMenu"]); + } + + if (json.contains("playerController")) { + deserializePlayerController(entity, json["playerController"]); + } + + if (json.contains("triangleBuffer")) { + deserializeTriangleBuffer(entity, json["triangleBuffer"]); + } + + // CellGrid/Town components - deserialize WITHOUT resolving material references + // Material references will be resolved in the second pass + if (json.contains("cellGrid")) { + deserializeCellGrid(entity, json["cellGrid"]); + } + if (json.contains("town")) { + // Store the raw JSON for second-pass resolution + m_pendingTownResolutions.push_back({ entity, json["town"] }); + // Still deserialize basic town data (name, colorRects, etc.) + // but skip material entity resolution + TownComponent town; + town.townName = json["town"].value("townName", "New Town"); + town.materialName = json["town"].value("materialName", ""); + town.proceduralMaterialEntityId = + json["town"].value("proceduralMaterialEntityId", ""); + town.textureRectName = + json["town"].value("textureRectName", ""); + // Deserialize color rects + if (json["town"].contains("colorRects") && + json["town"]["colorRects"].is_object()) { + for (auto &[name, rectJson] : + json["town"]["colorRects"].items()) { + TownComponent::ColorRect rect; + rect.left = rectJson.value("left", 0.0f); + rect.top = rectJson.value("top", 0.0f); + rect.right = rectJson.value("right", 1.0f); + rect.bottom = rectJson.value("bottom", 1.0f); + if (rectJson.contains("color")) { + auto &c = rectJson["color"]; + rect.color.r = c.value("r", 1.0f); + rect.color.g = c.value("g", 1.0f); + rect.color.b = c.value("b", 1.0f); + rect.color.a = c.value("a", 1.0f); + } + town.colorRects[name] = rect; + } + } + town.dirty = true; + town.materialDirty = true; + entity.set(town); + } + if (json.contains("district")) { + // Store the raw JSON for second-pass resolution + m_pendingDistrictResolutions.push_back( + { entity, json["district"] }); + // Still deserialize basic district data + DistrictComponent district; + district.radius = json["district"].value("radius", 50.0f); + district.elevation = json["district"].value("elevation", 0.0f); + district.height = json["district"].value("height", 0.2f); + district.isPlaza = json["district"].value("isPlaza", false); + district.proceduralMaterialEntityId = json["district"].value( + "proceduralMaterialEntityId", ""); + district.textureRectName = + json["district"].value("textureRectName", ""); + district.friction = json["district"].value("friction", 0.5f); + if (json["district"].contains("lotTemplateNames") && + json["district"]["lotTemplateNames"].is_array()) { + for (const auto &name : + json["district"]["lotTemplateNames"]) { + if (name.is_string()) { + district.lotTemplateNames.push_back( + name); + } + } + } + district.dirty = true; + entity.set(district); + } + if (json.contains("lot")) { + // Store the raw JSON for second-pass resolution + m_pendingLotResolutions.push_back({ entity, json["lot"] }); + // Still deserialize basic lot data + LotComponent lot; + lot.width = json["lot"].value("width", 10); + lot.depth = json["lot"].value("depth", 10); + lot.elevation = json["lot"].value("elevation", 0.0f); + lot.angle = json["lot"].value("angle", 0.0f); + lot.offsetX = json["lot"].value("offsetX", 0.0f); + lot.offsetZ = json["lot"].value("offsetZ", 0.0f); + lot.templateName = json["lot"].value("templateName", ""); + lot.proceduralMaterialEntityId = + json["lot"].value("proceduralMaterialEntityId", ""); + lot.textureRectName = json["lot"].value("textureRectName", ""); + lot.friction = json["lot"].value("friction", 0.5f); + lot.dirty = true; + entity.set(lot); + } + if (json.contains("room")) { + deserializeRoom(entity, json["room"]); + } + if (json.contains("roof")) { + deserializeRoof(entity, json["roof"]); + } + if (json.contains("furnitureTemplate")) { + deserializeFurnitureTemplate(entity, json["furnitureTemplate"]); + } + if (json.contains("clearArea")) { + deserializeClearArea(entity, json["clearArea"]); + } + + if (json.contains("navMesh")) { + deserializeNavMesh(entity, json["navMesh"]); + } + + if (json.contains("navMeshGeometrySource")) { + deserializeNavMeshGeometrySource(entity, + json["navMeshGeometrySource"]); + } + + // Buoyancy components + if (json.contains("buoyancyInfo")) { + deserializeBuoyancyInfo(entity, json["buoyancyInfo"]); + } + + if (json.contains("waterPhysics")) { + deserializeWaterPhysics(entity, json["waterPhysics"]); + } + + if (json.contains("waterPlane")) { + deserializeWaterPlane(entity, json["waterPlane"]); + } + + if (json.contains("actionDatabase")) { + deserializeActionDatabase(entity, json["actionDatabase"]); + } + if (json.contains("actionDebug")) { + deserializeActionDebug(entity, json["actionDebug"]); + } + if (json.contains("behaviorTree")) { + deserializeBehaviorTree(entity, json["behaviorTree"]); + } + + if (json.contains("sun")) { + deserializeSun(entity, json["sun"]); + } + + if (json.contains("skybox")) { + deserializeSkybox(entity, json["skybox"]); + } + + // Add to UI system if provided + if (uiSystem) { + uiSystem->addEntity(entity); + } + + // Deserialize children recursively (first pass) + if (json.contains("children") && json["children"].is_array()) { + for (const auto &childJson : json["children"]) { + deserializeEntityFirstPass(childJson, entity, uiSystem); + } + } +} + +void SceneSerializer::resolveMaterialReferences() +{ + // Resolve Town material references + for (auto &pending : m_pendingTownResolutions) { + flecs::entity entity = pending.entity; + if (!entity.is_alive()) + continue; + auto &town = entity.get_mut(); + + if (!town.proceduralMaterialEntityId.empty()) { + try { + uint64_t matId = std::stoull( + town.proceduralMaterialEntityId); + auto matIt = m_entityMap.find(matId); + if (matIt != m_entityMap.end()) { + flecs::entity matEntity = matIt->second; + if (matEntity.is_alive() && + matEntity.has< + ProceduralMaterialComponent>()) { + town.proceduralMaterialEntity = + matEntity; + Ogre::LogManager::getSingleton().logMessage( + "SceneSerializer: Resolved Town material entity " + + Ogre::StringConverter:: + toString( + matId)); + } + } + } catch (...) { + } + } + } + + // Resolve District material references + for (auto &pending : m_pendingDistrictResolutions) { + flecs::entity entity = pending.entity; + if (!entity.is_alive()) + continue; + auto &district = entity.get_mut(); + + if (!district.proceduralMaterialEntityId.empty()) { + try { + uint64_t matId = std::stoull( + district.proceduralMaterialEntityId); + auto matIt = m_entityMap.find(matId); + if (matIt != m_entityMap.end()) { + flecs::entity matEntity = matIt->second; + if (matEntity.is_alive() && + matEntity.has< + ProceduralMaterialComponent>()) { + district.proceduralMaterialEntity = + matEntity; + Ogre::LogManager::getSingleton().logMessage( + "SceneSerializer: Resolved District material entity " + + Ogre::StringConverter:: + toString( + matId)); + } + } + } catch (...) { + } + } + } + + // Resolve Lot material references + for (auto &pending : m_pendingLotResolutions) { + flecs::entity entity = pending.entity; + if (!entity.is_alive()) + continue; + auto &lot = entity.get_mut(); + + if (!lot.proceduralMaterialEntityId.empty()) { + try { + uint64_t matId = std::stoull( + lot.proceduralMaterialEntityId); + auto matIt = m_entityMap.find(matId); + if (matIt != m_entityMap.end()) { + flecs::entity matEntity = matIt->second; + if (matEntity.is_alive() && + matEntity.has< + ProceduralMaterialComponent>()) { + lot.proceduralMaterialEntity = + matEntity; + Ogre::LogManager::getSingleton().logMessage( + "SceneSerializer: Resolved Lot material entity " + + Ogre::StringConverter:: + toString( + matId)); + } + } + } catch (...) { + } + } + } + + // Clear pending resolutions + m_pendingTownResolutions.clear(); + m_pendingDistrictResolutions.clear(); + m_pendingLotResolutions.clear(); +} + nlohmann::json SceneSerializer::serializeTransform(flecs::entity entity) { auto &transform = entity.get(); @@ -1645,7 +2018,8 @@ void SceneSerializer::deserializeAnimationTree(flecs::entity entity, entity.set(at); } -nlohmann::json SceneSerializer::serializeAnimationTreeTemplate(flecs::entity entity) +nlohmann::json +SceneSerializer::serializeAnimationTreeTemplate(flecs::entity entity) { auto &templ = entity.get(); nlohmann::json json; @@ -1654,8 +2028,8 @@ nlohmann::json SceneSerializer::serializeAnimationTreeTemplate(flecs::entity ent return json; } -void SceneSerializer::deserializeAnimationTreeTemplate(flecs::entity entity, - const nlohmann::json &json) +void SceneSerializer::deserializeAnimationTreeTemplate( + flecs::entity entity, const nlohmann::json &json) { AnimationTreeTemplate templ; templ.name = json.value("name", ""); @@ -2400,7 +2774,6 @@ void SceneSerializer::deserializeWaterPhysics(flecs::entity entity, entity.set(water); } - nlohmann::json SceneSerializer::serializeSun(flecs::entity entity) { const SunComponent &sun = entity.get(); @@ -2409,8 +2782,7 @@ nlohmann::json SceneSerializer::serializeSun(flecs::entity entity) json["enabled"] = sun.enabled; json["timeOfDay"] = sun.timeOfDay; json["timeSpeed"] = sun.timeSpeed; - json["sunColor"] = { sun.sunColor.r, sun.sunColor.g, - sun.sunColor.b }; + json["sunColor"] = { sun.sunColor.r, sun.sunColor.g, sun.sunColor.b }; json["moonColor"] = { sun.moonColor.r, sun.moonColor.g, sun.moonColor.b }; json["ambientDay"] = { sun.ambientDay.r, sun.ambientDay.g, @@ -2499,11 +2871,12 @@ void SceneSerializer::deserializeSun(flecs::entity entity, json["ambientSunrise"][0], json["ambientSunrise"][1], json["ambientSunrise"][2]); } - if (json.contains("ambientSunset") && json["ambientSunset"].is_array() && + if (json.contains("ambientSunset") && + json["ambientSunset"].is_array() && json["ambientSunset"].size() >= 3) { sun.ambientSunset = Ogre::ColourValue(json["ambientSunset"][0], - json["ambientSunset"][1], - json["ambientSunset"][2]); + json["ambientSunset"][1], + json["ambientSunset"][2]); } sun.showSunSphere = json.value("showSunSphere", true); sun.showMoonSphere = json.value("showMoonSphere", true); @@ -2526,8 +2899,8 @@ void SceneSerializer::deserializeSkybox(flecs::entity entity, if (json.contains("dayTopColor") && json["dayTopColor"].is_array() && json["dayTopColor"].size() >= 3) { sky.dayTopColor = Ogre::ColourValue(json["dayTopColor"][0], - json["dayTopColor"][1], - json["dayTopColor"][2]); + json["dayTopColor"][1], + json["dayTopColor"][2]); } if (json.contains("dayBottomColor") && json["dayBottomColor"].is_array() && @@ -2536,18 +2909,20 @@ void SceneSerializer::deserializeSkybox(flecs::entity entity, json["dayBottomColor"][0], json["dayBottomColor"][1], json["dayBottomColor"][2]); } - if (json.contains("nightTopColor") && json["nightTopColor"].is_array() && + if (json.contains("nightTopColor") && + json["nightTopColor"].is_array() && json["nightTopColor"].size() >= 3) { sky.nightTopColor = Ogre::ColourValue(json["nightTopColor"][0], - json["nightTopColor"][1], - json["nightTopColor"][2]); + json["nightTopColor"][1], + json["nightTopColor"][2]); } if (json.contains("nightBottomColor") && json["nightBottomColor"].is_array() && json["nightBottomColor"].size() >= 3) { - sky.nightBottomColor = Ogre::ColourValue( - json["nightBottomColor"][0], json["nightBottomColor"][1], - json["nightBottomColor"][2]); + sky.nightBottomColor = + Ogre::ColourValue(json["nightBottomColor"][0], + json["nightBottomColor"][1], + json["nightBottomColor"][2]); } if (json.contains("sunriseColor") && json["sunriseColor"].is_array() && json["sunriseColor"].size() >= 3) { @@ -2569,7 +2944,6 @@ void SceneSerializer::deserializeSkybox(flecs::entity entity, entity.set(sky); } - nlohmann::json SceneSerializer::serializeWaterPlane(flecs::entity entity) { const WaterPlane &wp = entity.get(); @@ -2602,9 +2976,10 @@ void SceneSerializer::deserializeWaterPlane(flecs::entity entity, json.value("autoUpdateFromWaterPhysics", true); if (json.contains("waterColor") && json["waterColor"].is_array() && json["waterColor"].size() >= 4) { - wp.waterColor = Ogre::ColourValue( - json["waterColor"][0], json["waterColor"][1], - json["waterColor"][2], json["waterColor"][3]); + wp.waterColor = Ogre::ColourValue(json["waterColor"][0], + json["waterColor"][1], + json["waterColor"][2], + json["waterColor"][3]); } wp.reflectivity = json.value("reflectivity", 0.5f); wp.waveSpeed = json.value("waveSpeed", 1.0f); @@ -2615,7 +2990,6 @@ void SceneSerializer::deserializeWaterPlane(flecs::entity entity, entity.set(wp); } - // ============================================================================ // AI/GOAP Component Serialization Helpers // ============================================================================ @@ -2667,9 +3041,10 @@ static void deserializeGoapBlackboard(GoapBlackboard &bb, if (json.contains("vec3Values") && json["vec3Values"].is_object()) { for (auto &[key, val] : json["vec3Values"].items()) { if (val.is_array() && val.size() >= 3) - bb.vec3Values[key] = Ogre::Vector3( - val[0].get(), val[1].get(), - val[2].get()); + bb.vec3Values[key] = + Ogre::Vector3(val[0].get(), + val[1].get(), + val[2].get()); } } } @@ -2812,8 +3187,8 @@ void SceneSerializer::deserializeActionDatabase(flecs::entity entity, for (const auto &entry : json["bitNames"]) { if (entry.contains("index") && entry.contains("name")) GoapBlackboard::setBitName( - entry["index"].get(), - entry["name"].get()); + entry["index"].get(), + entry["name"].get()); } } @@ -2866,7 +3241,6 @@ void SceneSerializer::deserializeBehaviorTree(flecs::entity entity, entity.set(bt); } - nlohmann::json SceneSerializer::serializeNavMesh(flecs::entity entity) { const NavMeshComponent &nm = entity.get(); @@ -2908,7 +3282,8 @@ void SceneSerializer::deserializeNavMesh(flecs::entity entity, entity.set(nm); } -nlohmann::json SceneSerializer::serializeNavMeshGeometrySource(flecs::entity entity) +nlohmann::json +SceneSerializer::serializeNavMeshGeometrySource(flecs::entity entity) { const NavMeshGeometrySource &src = entity.get(); nlohmann::json json; @@ -2916,8 +3291,8 @@ nlohmann::json SceneSerializer::serializeNavMeshGeometrySource(flecs::entity ent return json; } -void SceneSerializer::deserializeNavMeshGeometrySource(flecs::entity entity, - const nlohmann::json &json) +void SceneSerializer::deserializeNavMeshGeometrySource( + flecs::entity entity, const nlohmann::json &json) { NavMeshGeometrySource src; src.include = json.value("include", true); diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index be2fa1c..d75dcc7 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -43,6 +43,15 @@ private: void deserializeEntity(const nlohmann::json &json, flecs::entity parent, EditorUISystem *uiSystem); + // First-pass deserialization: creates entities and populates entity map + // without resolving material references (deferred to second pass) + void deserializeEntityFirstPass(const nlohmann::json &json, + flecs::entity parent, + EditorUISystem *uiSystem); + + // Second-pass: resolve material entity references for Town, District, Lot + void resolveMaterialReferences(); + // Component serialization nlohmann::json serializeTransform(flecs::entity entity); nlohmann::json serializeRenderable(flecs::entity entity); @@ -115,7 +124,7 @@ private: void deserializeAnimationTree(flecs::entity entity, const nlohmann::json &json); void deserializeAnimationTreeTemplate(flecs::entity entity, - const nlohmann::json &json); + const nlohmann::json &json); void deserializeStartupMenu(flecs::entity entity, const nlohmann::json &json); void deserializePlayerController(flecs::entity entity, @@ -137,7 +146,7 @@ private: void deserializeNavMesh(flecs::entity entity, const nlohmann::json &json); void deserializeNavMeshGeometrySource(flecs::entity entity, - const nlohmann::json &json); + const nlohmann::json &json); // Buoyancy component serialization nlohmann::json serializeBuoyancyInfo(flecs::entity entity); @@ -156,7 +165,8 @@ private: nlohmann::json serializeSun(flecs::entity entity); nlohmann::json serializeSkybox(flecs::entity entity); void deserializeSun(flecs::entity entity, const nlohmann::json &json); - void deserializeSkybox(flecs::entity entity, const nlohmann::json &json); + void deserializeSkybox(flecs::entity entity, + const nlohmann::json &json); // AI/GOAP serialization nlohmann::json serializeActionDatabase(flecs::entity entity); @@ -175,6 +185,16 @@ private: // Track entity ID mapping for parent/child relationships std::unordered_map m_entityMap; + + // Pending material entity resolutions (two-pass load) + // Store entity reference directly (not just ID) so we don't need to look up in m_entityMap + struct PendingResolution { + flecs::entity entity; + nlohmann::json data; + }; + std::vector m_pendingTownResolutions; + std::vector m_pendingDistrictResolutions; + std::vector m_pendingLotResolutions; }; #endif // EDITSCENE_SCENESERIALIZER_HPP