From 3dd3d345431ebb1905c996963fb8bc4f763b352e Mon Sep 17 00:00:00 2001 From: Diject Date: Sat, 27 Dec 2025 20:03:00 +0300 Subject: [PATCH] Add local map texture extraction for starting cell --- apps/openmw/engine.cpp | 33 ++- apps/openmw/mapextractor.cpp | 411 ++++++++++++++++++++++++++++-- apps/openmw/mapextractor.hpp | 22 +- apps/openmw/mwrender/localmap.cpp | 23 ++ 4 files changed, 452 insertions(+), 37 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 8bd0aad8e1..65db4d668e 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -960,20 +960,6 @@ void OMW::Engine::go() prepareEngine(); - if (mExtractMaps) - { - Log(Debug::Info) << "Starting map extraction mode..."; - - mStateManager->newGame(true); - - MapExtractor extractor(*mWorld, mWorldMapOutput, mLocalMapOutput); - extractor.extractWorldMap(); - extractor.extractLocalMaps(); - - Log(Debug::Info) << "Map extraction complete. Exiting..."; - return; - } - #ifdef _WIN32 const auto* statsFile = _wgetenv(L"OPENMW_OSG_STATS_FILE"); #else @@ -1008,6 +994,25 @@ void OMW::Engine::go() if (stats.is_open()) Resource::collectStatistics(*mViewer); + + // Map extractor + if (mExtractMaps) + { + Log(Debug::Info) << "Starting map extraction mode..."; + + mStateManager->newGame(true); + + Log(Debug::Info) << "Starting map extraction..."; + + MapExtractor extractor(*mWorld, mViewer.get(), mWorldMapOutput, mLocalMapOutput); + extractor.extractWorldMap(); + extractor.extractLocalMaps(); + + Log(Debug::Info) << "Map extraction complete. Exiting..."; + return; + } + + // Start the game if (!mSaveGameFile.empty()) { diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index bfe36ee3ef..6b0bdd8753 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -1,12 +1,17 @@ #include "mapextractor.hpp" +#include +#include #include #include +#include +#include #include #include #include #include +#include #include #include @@ -16,19 +21,23 @@ #include #include "mwbase/environment.hpp" +#include "mwbase/mechanicsmanager.hpp" #include "mwbase/world.hpp" #include "mwrender/globalmap.hpp" #include "mwrender/localmap.hpp" #include "mwrender/renderingmanager.hpp" +#include "mwrender/vismask.hpp" #include "mwworld/cellstore.hpp" #include "mwworld/esmstore.hpp" #include "mwworld/worldimp.hpp" +#include "mwworld/worldmodel.hpp" namespace OMW { - MapExtractor::MapExtractor( - MWWorld::World& world, const std::string& worldMapOutput, const std::string& localMapOutput) + MapExtractor::MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, + const std::string& worldMapOutput, const std::string& localMapOutput) : mWorld(world) + , mViewer(viewer) , mWorldMapOutputDir(worldMapOutput) , mLocalMapOutputDir(localMapOutput) { @@ -37,14 +46,43 @@ namespace OMW // Create GlobalMap and LocalMap instances MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); - if (renderingManager) + if (!renderingManager) { - osg::Group* root = renderingManager->getLightRoot()->getParent(0)->asGroup(); - SceneUtil::WorkQueue* workQueue = renderingManager->getWorkQueue(); + Log(Debug::Error) << "RenderingManager is null in MapExtractor constructor"; + throw std::runtime_error("RenderingManager is null"); + } + osg::Group* lightRoot = renderingManager->getLightRoot(); + if (!lightRoot) + { + Log(Debug::Error) << "LightRoot is null in MapExtractor constructor"; + throw std::runtime_error("LightRoot is null"); + } + + osg::Group* root = lightRoot->getParent(0) ? lightRoot->getParent(0)->asGroup() : nullptr; + if (!root) + { + Log(Debug::Error) << "Root node is null in MapExtractor constructor"; + throw std::runtime_error("Root node is null"); + } + + SceneUtil::WorkQueue* workQueue = renderingManager->getWorkQueue(); + if (!workQueue) + { + Log(Debug::Error) << "WorkQueue is null in MapExtractor constructor"; + throw std::runtime_error("WorkQueue is null"); + } + + try + { mGlobalMap = std::make_unique(root, workQueue); mLocalMap = std::make_unique(root); } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to create map objects: " << e.what(); + throw; + } } MapExtractor::~MapExtractor() = default; @@ -56,7 +94,7 @@ namespace OMW if (!mGlobalMap) { Log(Debug::Error) << "Global map not initialized"; - return; + throw std::runtime_error("Global map not initialized"); } // Temporarily set cell size to 32 pixels for extraction @@ -149,44 +187,375 @@ namespace OMW { Log(Debug::Info) << "Extracting local maps..."; - saveLocalMapTextures(); + setupExtractionMode(); + extractExteriorLocalMaps(); + extractInteriorLocalMaps(); + restoreNormalMode(); Log(Debug::Info) << "Local map extraction complete"; } - void MapExtractor::saveLocalMapTextures() + void MapExtractor::setupExtractionMode() + { + mWorld.toggleCollisionMode(); + MWBase::Environment::get().getMechanicsManager()->toggleAI(); + mWorld.toggleScripts(); + mWorld.toggleGodMode(); + } + + void MapExtractor::restoreNormalMode() + { + if (!mWorld.getGodModeState()) + mWorld.toggleGodMode(); + if (!mWorld.getScriptsEnabled()) + mWorld.toggleScripts(); + if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) + MWBase::Environment::get().getMechanicsManager()->toggleAI(); + } + + void MapExtractor::extractExteriorLocalMaps() { if (!mLocalMap) { Log(Debug::Error) << "Local map not initialized"; - return; + throw std::runtime_error("Local map not initialized"); } - const MWWorld::ESMStore& store = mWorld.getStore(); - int count = 0; - - MWWorld::Store::iterator it = store.get().extBegin(); - for (; it != store.get().extEnd(); ++it) + // Get currently active cells + MWWorld::Scene* scene = &mWorld.getWorldScene(); + if (!scene) { - int x = it->getGridX(); - int y = it->getGridY(); + Log(Debug::Error) << "Scene not available"; + throw std::runtime_error("Scene not available"); + } - osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); - if (!texture || !texture->getImage()) + const auto& activeCells = scene->getActiveCells(); + Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells..."; + + int count = 0; + int skipped = 0; + + for (const MWWorld::CellStore* cellStore : activeCells) + { + if (!cellStore->getCell()->isExterior()) continue; + int x = cellStore->getCell()->getGridX(); + int y = cellStore->getCell()->getGridY(); + + Log(Debug::Info) << "Processing active cell (" << x << "," << y << ")"; + + // Request map generation for this cell + Log(Debug::Verbose) << "Requesting map for cell (" << x << "," << y << ")"; + mLocalMap->requestMap(const_cast(cellStore)); + Log(Debug::Verbose) << "Map requested for cell (" << x << "," << y << ")"; + + // CRITICAL: LocalMap::requestMap() creates RTT cameras that render asynchronously. + // We must run the render loop to actually execute the RTT rendering before we can + // access the resulting textures. + + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager && mViewer) + { + Log(Debug::Verbose) << "Starting render loop for cell (" << x << "," << y << ")"; + + // Phase 1: Setup (let RTT cameras initialize) + for (int i = 0; i < 5; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 2: Main rendering (RTT cameras execute) + for (int i = 0; i < 60; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 3: Finalization (ensure GPU->CPU transfer completes) + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + for (int i = 0; i < 10; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + Log(Debug::Verbose) << "Render loop completed for cell (" << x << "," << y << ")"; + } + + // Clean up RTT cameras before trying to access textures + mLocalMap->cleanupCameras(); + + // Now try to get the texture + Log(Debug::Verbose) << "Getting texture for cell (" << x << "," << y << ")"; + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + + if (!texture) + { + Log(Debug::Warning) << "No texture for cell (" << x << "," << y << ")"; + skipped++; + continue; + } + + // Get the image from the texture (should be set by LocalMapRenderToTexture) + osg::Image* image = texture->getImage(); + + if (!image) + { + Log(Debug::Warning) << "Texture for cell (" << x << "," << y << ") has no image data attached"; + skipped++; + continue; + } + + if (image->s() == 0 || image->t() == 0) + { + Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ")"; + skipped++; + continue; + } + + Log(Debug::Info) << "Got image size: " << image->s() << "x" << image->t() + << " for cell (" << x << "," << y << ")"; + + osg::ref_ptr outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL); + + if (outputImage->s() != 256 || outputImage->t() != 256) + { + osg::ref_ptr resized = new osg::Image; + resized->allocateImage(256, 256, 1, outputImage->getPixelFormat(), outputImage->getDataType()); + outputImage->scaleImage(256, 256, 1); + outputImage = resized; + } + std::ostringstream filename; filename << "(" << x << "," << y << ").png"; std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - if (osgDB::writeImageFile(*texture->getImage(), outputPath.string())) + if (osgDB::writeImageFile(*outputImage, outputPath.string())) { count++; - if (count % 100 == 0) - Log(Debug::Info) << "Saved " << count << " local map textures..."; + Log(Debug::Info) << "Saved local map texture for cell (" << x << "," << y << ")"; + } + else + { + Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ")"; + skipped++; } } Log(Debug::Info) << "Saved " << count << " exterior local map textures"; + if (skipped > 0) + Log(Debug::Warning) << "Skipped " << skipped << " cells without valid textures"; + } + + void MapExtractor::extractInteriorLocalMaps() + { + if (!mLocalMap) + { + Log(Debug::Error) << "Local map not initialized"; + throw std::runtime_error("Local map not initialized"); + } + + // Get currently active cells + MWWorld::Scene* scene = &mWorld.getWorldScene(); + if (!scene) + { + Log(Debug::Error) << "Scene not available"; + throw std::runtime_error("Scene not available"); + } + + const auto& activeCells = scene->getActiveCells(); + Log(Debug::Info) << "Processing active interior cells..."; + + int count = 0; + + for (const MWWorld::CellStore* cellStore : activeCells) + { + if (cellStore->getCell()->isExterior()) + continue; + + ESM::RefId cellId = cellStore->getCell()->getId(); + std::string cellName(cellStore->getCell()->getNameId()); + + Log(Debug::Info) << "Processing active interior cell: " << cellName; + + // Request map generation for this cell + mLocalMap->requestMap(const_cast(cellStore)); + + // CRITICAL: LocalMap::requestMap() creates RTT cameras that render asynchronously. + // We must run the render loop to actually execute the RTT rendering before we can + // access the resulting textures. + + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager && mViewer) + { + Log(Debug::Verbose) << "Starting render loop for interior: " << cellName; + + // Phase 1: Setup (let RTT cameras initialize) + for (int i = 0; i < 5; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 2: Main rendering (RTT cameras execute) + for (int i = 0; i < 60; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + // Phase 3: Finalization (ensure GPU->CPU transfer completes) + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + for (int i = 0; i < 10; ++i) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + renderingManager->update(0.016f, false); + mViewer->renderingTraversals(); + } + + Log(Debug::Verbose) << "Render loop completed for interior: " << cellName; + } + + // Clean up RTT cameras before trying to access textures + mLocalMap->cleanupCameras(); + + saveInteriorCellTextures(cellId, cellName); + count++; + } + + Log(Debug::Info) << "Saved " << count << " interior local map textures"; + } + + void MapExtractor::saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName) + { + MyGUI::IntRect grid = mLocalMap->getInteriorGrid(); + + std::string lowerCaseId = cellId.toDebugString(); + std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); + + int segmentsX = grid.width() + 1; + int segmentsY = grid.height() + 1; + + if (segmentsX <= 0 || segmentsY <= 0) + return; + + int totalWidth = segmentsX * 256; + int totalHeight = segmentsY * 256; + + osg::ref_ptr combinedImage = new osg::Image; + combinedImage->allocateImage(totalWidth, totalHeight, 1, GL_RGB, GL_UNSIGNED_BYTE); + + unsigned char* data = combinedImage->data(); + memset(data, 0, totalWidth * totalHeight * 3); + + for (int x = grid.left; x < grid.right; ++x) + { + for (int y = grid.top; y < grid.bottom; ++y) + { + osg::ref_ptr texture = mLocalMap->getMapTexture(x, y); + if (!texture || !texture->getImage()) + continue; + + osg::Image* segmentImage = texture->getImage(); + int segWidth = segmentImage->s(); + int segHeight = segmentImage->t(); + + int destX = (x - grid.left) * 256; + int destY = (y - grid.top) * 256; + + for (int sy = 0; sy < std::min(segHeight, 256); ++sy) + { + for (int sx = 0; sx < std::min(segWidth, 256); ++sx) + { + unsigned char* srcPixel = segmentImage->data(sx, sy); + int dx = destX + sx; + int dy = destY + sy; + + if (dx < totalWidth && dy < totalHeight) + { + unsigned char* destPixel = data + ((dy * totalWidth + dx) * 3); + destPixel[0] = srcPixel[0]; + destPixel[1] = srcPixel[1]; + destPixel[2] = srcPixel[2]; + } + } + } + } + } + + std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); + osgDB::writeImageFile(*combinedImage, texturePath.string()); + + saveInteriorMapInfo(cellId, lowerCaseId, segmentsX, segmentsY); + } + + void MapExtractor::saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, + int segmentsX, int segmentsY) + { + MWWorld::CellStore* cell = mWorld.getWorldModel().findCell(cellId); + if (!cell) + return; + + float nA = 0.0f; + float mX = 0.0f; + float mY = 0.0f; + + MWWorld::ConstPtr northmarker = cell->searchConst(ESM::RefId::stringRefId("northmarker")); + if (!northmarker.isEmpty()) + { + osg::Quat orient(-northmarker.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, 1)); + osg::Vec3f dir = orient * osg::Vec3f(0, 1, 0); + nA = std::atan2(dir.x(), dir.y()); + } + + osg::BoundingBox bounds; + osg::ComputeBoundsVisitor computeBoundsVisitor; + computeBoundsVisitor.setTraversalMask(MWRender::Mask_Scene | MWRender::Mask_Terrain | + MWRender::Mask_Object | MWRender::Mask_Static); + + MWRender::RenderingManager* renderingManager = mWorld.getRenderingManager(); + if (renderingManager && renderingManager->getLightRoot()) + { + renderingManager->getLightRoot()->accept(computeBoundsVisitor); + bounds = computeBoundsVisitor.getBoundingBox(); + } + + osg::Vec2f center(bounds.center().x(), bounds.center().y()); + osg::Vec2f min(bounds.xMin(), bounds.yMin()); + + const float mapWorldSize = Constants::CellSizeInUnits; + + mX = ((0 - center.x()) * std::cos(nA) - (0 - center.y()) * std::sin(nA) + center.x() - min.x()) / mapWorldSize * 256.0f * 2.0f; + mY = ((0 - center.x()) * std::sin(nA) + (0 - center.y()) * std::cos(nA) + center.y() - min.y()) / mapWorldSize * 256.0f * 2.0f; + + std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); + std::ofstream file(yamlPath); + + if (!file) + { + Log(Debug::Error) << "Failed to create interior map info file: " << yamlPath; + return; + } + + file << "nA: " << nA << "\n"; + file << "mX: " << mX << "\n"; + file << "mY: " << mY << "\n"; + + file.close(); } } diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 59013f4227..9410c67742 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -5,11 +5,18 @@ #include #include +#include + namespace osg { class Group; } +namespace osgViewer +{ + class Viewer; +} + namespace SceneUtil { class WorkQueue; @@ -31,7 +38,8 @@ namespace OMW class MapExtractor { public: - MapExtractor(MWWorld::World& world, const std::string& worldMapOutput, const std::string& localMapOutput); + MapExtractor(MWWorld::World& world, osgViewer::Viewer* viewer, const std::string& worldMapOutput, + const std::string& localMapOutput); ~MapExtractor(); void extractWorldMap(); @@ -39,6 +47,7 @@ namespace OMW private: MWWorld::World& mWorld; + osgViewer::Viewer* mViewer; std::filesystem::path mWorldMapOutputDir; std::filesystem::path mLocalMapOutputDir; @@ -47,7 +56,16 @@ namespace OMW void saveWorldMapTexture(); void saveWorldMapInfo(); - void saveLocalMapTextures(); + + void setupExtractionMode(); + void restoreNormalMode(); + void extractExteriorLocalMaps(); + void extractInteriorLocalMaps(); + void loadCellAndWait(int x, int y); + void loadInteriorCellAndWait(const std::string& cellName); + void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); + void saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId, + int segmentsX, int segmentsY); }; } diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 39d088084f..3546c29852 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -774,6 +774,29 @@ namespace MWRender camera->addChild(lightSource); camera->addChild(mSceneRoot); + + // CRITICAL FIX: Setup both texture and image for CPU-side access (needed by mapextractor) + // First attach texture for normal rendering (GPU-side) + osg::ref_ptr texture = new osg::Texture2D(); + texture->setTextureSize(camera->getViewport()->width(), camera->getViewport()->height()); + texture->setInternalFormat(GL_RGB); + texture->setSourceFormat(GL_RGB); + texture->setSourceType(GL_UNSIGNED_BYTE); + texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + camera->attach(osg::Camera::COLOR_BUFFER, texture.get()); + + // Then attach Image for CPU-side readback + // OSG will automatically copy rendered pixels to this Image + osg::ref_ptr image = new osg::Image(); + image->setPixelFormat(GL_RGB); + image->setDataType(GL_UNSIGNED_BYTE); + camera->attach(osg::Camera::COLOR_BUFFER, image.get()); + + // Also set the Image on the texture so mapextractor can retrieve it via texture->getImage() + texture->setImage(image.get()); } void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv)