diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 0d55bedcab..54189f8a1a 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -996,6 +996,7 @@ void OMW::Engine::go() // Map extractor + std::unique_ptr mapExtractor; if (mExtractMaps) { Log(Debug::Info) << "Starting map extraction mode..."; @@ -1004,12 +1005,11 @@ void OMW::Engine::go() Log(Debug::Info) << "Starting map extraction..."; - MapExtractor extractor(*mWorld, mViewer.get(), mWindowManager.get(), mWorldMapOutput, mLocalMapOutput); - extractor.extractWorldMap(); - //extractor.extractLocalMaps(); + //mapExtractor = std::make_unique(*mWorld, mViewer.get(), mWindowManager.get(), mWorldMapOutput, mLocalMapOutput); + //mapExtractor->extractWorldMap(); + //mapExtractor->extractLocalMaps(false); - Log(Debug::Info) << "Map extraction complete. Exiting..."; - //return; + Log(Debug::Info) << "Local map extraction started, will complete during gameplay..."; } @@ -1062,6 +1062,18 @@ void OMW::Engine::go() std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } + + // Update map extraction if active + if (mapExtractor) + { + mapExtractor->update(); + if (mapExtractor->isExtractionComplete()) + { + Log(Debug::Info) << "Map extraction complete."; + mapExtractor.reset(); + } + } + timeManager.updateIsPaused(); if (!timeManager.isPaused()) { diff --git a/apps/openmw/mapextractor.cpp b/apps/openmw/mapextractor.cpp index e3261e9af4..31d8ba33fb 100644 --- a/apps/openmw/mapextractor.cpp +++ b/apps/openmw/mapextractor.cpp @@ -223,9 +223,26 @@ namespace OMW if (!mLocalMap) { Log(Debug::Error) << "Local map not initialized - cannot extract local maps"; + Log(Debug::Error) << "Make sure the game is fully loaded before calling extractLocalMaps"; return; } + Log(Debug::Info) << "LocalMap instance is available, starting extraction"; + + mForceOverwrite = forceOverwrite; + mFramesToWait = 10; // Wait 10 frames before checking (increased from 3) + + startExtraction(forceOverwrite); + } + + void MapExtractor::startExtraction(bool forceOverwrite) + { + // Enable extraction mode to prevent automatic camera cleanup + if (mLocalMap) + { + mLocalMap->setExtractionMode(true); + } + // Get currently active cells MWWorld::Scene* scene = &mWorld.getWorldScene(); if (!scene) @@ -237,34 +254,219 @@ namespace OMW const auto& activeCells = scene->getActiveCells(); Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells..."; - int exteriorCount = 0; - int interiorCount = 0; - int skipped = 0; - + mPendingExtractions.clear(); + for (const MWWorld::CellStore* cellStore : activeCells) { if (cellStore->getCell()->isExterior()) { - if (extractExteriorCell(cellStore, forceOverwrite)) - exteriorCount++; - else - skipped++; + int x = cellStore->getCell()->getGridX(); + int y = cellStore->getCell()->getGridY(); + + std::ostringstream filename; + filename << "(" << x << "," << y << ").png"; + std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); + + if (!forceOverwrite && std::filesystem::exists(outputPath)) + { + Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; + continue; + } + + PendingExtraction extraction; + extraction.cellStore = cellStore; + extraction.isExterior = true; + extraction.outputPath = outputPath; + extraction.framesWaited = 0; + mPendingExtractions.push_back(extraction); } else { - if (extractInteriorCell(cellStore, forceOverwrite)) - interiorCount++; - else - skipped++; + ESM::RefId cellId = cellStore->getCell()->getId(); + std::string cellName(cellStore->getCell()->getNameId()); + + std::string lowerCaseId = cellId.toDebugString(); + std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); + + const std::string invalidChars = "/\\:*?\"<>|"; + lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), + [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), + lowerCaseId.end() + ); + + if (lowerCaseId.empty()) + { + lowerCaseId = "_unnamed_cell_"; + } + + std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); + std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); + + if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) + { + Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; + continue; + } + + PendingExtraction extraction; + extraction.cellStore = cellStore; + extraction.isExterior = false; + extraction.outputPath = texturePath; + extraction.framesWaited = 0; + mPendingExtractions.push_back(extraction); } } - - Log(Debug::Info) << "Saved " << exteriorCount << " exterior local map textures"; - Log(Debug::Info) << "Saved " << interiorCount << " interior local map textures"; - if (skipped > 0) - Log(Debug::Info) << "Skipped " << skipped << " cells (files already exist or without valid textures)"; - - Log(Debug::Info) << "Extraction of active local maps complete"; + + if (!mPendingExtractions.empty()) + { + Log(Debug::Info) << "Queued " << mPendingExtractions.size() << " cells for extraction"; + processNextCell(); + } + else + { + Log(Debug::Info) << "No cells to extract"; + } + } + + void MapExtractor::update() + { + if (mPendingExtractions.empty()) + return; + + PendingExtraction& current = mPendingExtractions.front(); + current.framesWaited++; + + // Wait for the required number of frames before checking + if (current.framesWaited >= mFramesToWait) + { + // Check if the texture is ready before trying to save + bool textureReady = false; + + if (current.isExterior) + { + int x = current.cellStore->getCell()->getGridX(); + int y = current.cellStore->getCell()->getGridY(); + + // Check if the rendered image is ready + osg::ref_ptr image = mLocalMap->getMapImage(x, y); + + if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr) + { + textureReady = true; + } + else if (current.framesWaited <= 120) + { + return; + } + } + else + { + // For interior cells, check if at least one segment has a valid rendered image + MyGUI::IntRect grid = mLocalMap->getInteriorGrid(); + for (int x = grid.left; x < grid.right && !textureReady; ++x) + { + for (int y = grid.top; y < grid.bottom && !textureReady; ++y) + { + osg::ref_ptr image = mLocalMap->getMapImage(x, y); + if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr) + { + textureReady = true; + } + } + } + + + if (!textureReady && current.framesWaited <= 120) + { + return; + } + } + + if (textureReady) + { + savePendingExtraction(current); + + mPendingExtractions.erase(mPendingExtractions.begin()); + + if (!mPendingExtractions.empty()) + { + processNextCell(); + } + else + { + // Clean up cameras only after ALL extractions are complete + mLocalMap->cleanupCameras(); + // Disable extraction mode + mLocalMap->setExtractionMode(false); + Log(Debug::Info) << "Extraction of active local maps complete"; + } + } + else if (current.framesWaited > 120) + { + // If we've waited too long (120 frames = ~2 seconds at 60 fps), skip this cell + if (current.isExterior) + { + int x = current.cellStore->getCell()->getGridX(); + int y = current.cellStore->getCell()->getGridY(); + Log(Debug::Warning) << "Timeout waiting for texture for cell (" << x << "," << y << "), skipping"; + } + else + { + std::string cellName(current.cellStore->getCell()->getNameId()); + Log(Debug::Warning) << "Timeout waiting for texture for interior cell: " << cellName << ", skipping"; + } + + mPendingExtractions.erase(mPendingExtractions.begin()); + + if (!mPendingExtractions.empty()) + { + processNextCell(); + } + else + { + // Clean up cameras even if we timed out + mLocalMap->cleanupCameras(); + // Disable extraction mode + mLocalMap->setExtractionMode(false); + Log(Debug::Info) << "Extraction of active local maps complete"; + } + } + // Otherwise keep waiting for the texture to be ready + } + } + + bool MapExtractor::isExtractionComplete() const + { + return mPendingExtractions.empty(); + } + + void MapExtractor::processNextCell() + { + if (mPendingExtractions.empty()) + return; + + const PendingExtraction& extraction = mPendingExtractions.front(); + + if (extraction.isExterior) + { + int x = extraction.cellStore->getCell()->getGridX(); + int y = extraction.cellStore->getCell()->getGridY(); + mLocalMap->clearCellCache(x, y); + } + + mLocalMap->requestMap(const_cast(extraction.cellStore)); + } + + bool MapExtractor::savePendingExtraction(const PendingExtraction& extraction) + { + if (extraction.isExterior) + { + return extractExteriorCell(extraction.cellStore, mForceOverwrite); + } + else + { + return extractInteriorCell(extraction.cellStore, mForceOverwrite); + } } bool MapExtractor::extractExteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite) @@ -272,36 +474,11 @@ namespace OMW int x = cellStore->getCell()->getGridX(); int y = cellStore->getCell()->getGridY(); - // Check if file already exists std::ostringstream filename; filename << "(" << x << "," << y << ").png"; std::filesystem::path outputPath = mLocalMapOutputDir / filename.str(); - if (!forceOverwrite && std::filesystem::exists(outputPath)) - { - Log(Debug::Info) << "Skipping cell (" << x << "," << y << ") - file already exists"; - return false; - } - - 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 << ")"; - - // 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 << ")"; - return false; - } - - // Get the image from the texture (should be set by LocalMapRenderToTexture) - osg::Image* image = texture->getImage(); + osg::ref_ptr image = mLocalMap->getMapImage(x, y); if (!image) { @@ -311,13 +488,11 @@ namespace OMW if (image->s() == 0 || image->t() == 0) { - Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ")"; + Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ") - size: " + << image->s() << "x" << image->t(); return false; } - 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) @@ -327,15 +502,14 @@ namespace OMW outputImage->scaleImage(256, 256, 1); outputImage = resized; } - if (osgDB::writeImageFile(*outputImage, outputPath.string())) { - Log(Debug::Info) << "Saved local map texture for cell (" << x << "," << y << ")"; + Log(Debug::Info) << "Successfully saved local map for cell (" << x << "," << y << ") to " << outputPath; return true; } else { - Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ")"; + Log(Debug::Warning) << "Failed to write texture for cell (" << x << "," << y << ") to " << outputPath; return false; } } @@ -345,32 +519,7 @@ namespace OMW ESM::RefId cellId = cellStore->getCell()->getId(); std::string cellName(cellStore->getCell()->getNameId()); - // Prepare lowercase ID for file naming - std::string lowerCaseId = cellId.toDebugString(); - std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower); - - const std::string invalidChars = "/\\:*?\"<>|"; - lowerCaseId.erase(std::remove_if(lowerCaseId.begin(), lowerCaseId.end(), - [&invalidChars](char c) { return invalidChars.find(c) != std::string::npos; }), - lowerCaseId.end() - ); - - if (lowerCaseId.empty()) - { - lowerCaseId = "_unnamed_cell_"; - } - - // Check if both files exist - std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); - std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); - - if (!forceOverwrite && std::filesystem::exists(texturePath) && std::filesystem::exists(yamlPath)) - { - Log(Debug::Info) << "Skipping interior cell: " << cellName << " - files already exist"; - return false; - } - - Log(Debug::Info) << "Processing active interior cell: " << cellName; + Log(Debug::Info) << "Saving interior cell: " << cellName; saveInteriorCellTextures(cellId, cellName); return true; @@ -394,23 +543,23 @@ namespace OMW lowerCaseId = "_unnamed_cell_"; } - int minX = grid.right; int maxX = grid.left; int minY = grid.bottom; int maxY = grid.top; - // Cache textures and find bounds - std::map, osg::ref_ptr> textureCache; + // Cache images and find bounds + std::map, osg::ref_ptr> imageCache; 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()) + // Get the rendered image directly from camera attachment + osg::ref_ptr image = mLocalMap->getMapImage(x, y); + if (image && image->s() > 0 && image->t() > 0 && image->data() != nullptr) { - textureCache[{x, y}] = texture; + imageCache[{x, y}] = image; minX = std::min(minX, x); maxX = std::max(maxX, x); minY = std::min(minY, y); @@ -420,13 +569,19 @@ namespace OMW } if (minX > maxX || minY > maxY) + { + Log(Debug::Warning) << "No valid image segments found for interior cell: " << cellName; return; + } int segmentsX = maxX - minX + 1; int segmentsY = maxY - minY + 1; if (segmentsX <= 0 || segmentsY <= 0) + { + Log(Debug::Warning) << "Invalid segment dimensions for interior cell: " << cellName; return; + } int totalWidth = segmentsX * 256; int totalHeight = segmentsY * 256; @@ -441,12 +596,11 @@ namespace OMW { for (int y = minY; y <= maxY; ++y) { - auto it = textureCache.find({x, y}); - if (it == textureCache.end()) + auto it = imageCache.find({x, y}); + if (it == imageCache.end()) continue; - osg::ref_ptr texture = it->second; - osg::Image* segmentImage = texture->getImage(); + osg::ref_ptr segmentImage = it->second; int segWidth = segmentImage->s(); int segHeight = segmentImage->t(); @@ -474,7 +628,16 @@ namespace OMW } std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png"); - osgDB::writeImageFile(*combinedImage, texturePath.string()); + + if (osgDB::writeImageFile(*combinedImage, texturePath.string())) + { + Log(Debug::Info) << "Successfully saved interior map to " << texturePath; + } + else + { + Log(Debug::Error) << "Failed to write interior map to " << texturePath; + return; + } saveInteriorMapInfo(cellId, lowerCaseId, segmentsX, segmentsY); } @@ -486,37 +649,27 @@ namespace OMW 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); + // Get the bounds, center and angle that LocalMap actually used for rendering + const osg::BoundingBox& bounds = mLocalMap->getInteriorBounds(); + const osg::Vec2f& center = mLocalMap->getInteriorCenter(); + const float nA = mLocalMap->getInteriorAngle(); - 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; + // Calculate position of world origin (0,0) on the rotated map + osg::Vec2f toOrigin(0.0f - center.x(), 0.0f - center.y()); + float rotatedX = toOrigin.x() * std::cos(nA) - toOrigin.y() * std::sin(nA) + center.x(); + float rotatedY = toOrigin.x() * std::sin(nA) + toOrigin.y() * std::cos(nA) + center.y(); + + // Convert to texture coordinates (pixels from bottom-left corner) + float oX = (rotatedX - min.x()) / mapWorldSize * 256.0f; + float oY = (rotatedY - min.y()) / mapWorldSize * 256.0f; + + float totalHeight = segmentsY * 256.0f; + float mX = oX * 2.0f; + float mY = (oY - totalHeight) * 2.0f; std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml"); std::ofstream file(yamlPath); @@ -530,6 +683,8 @@ namespace OMW file << "nA: " << nA << "\n"; file << "mX: " << mX << "\n"; file << "mY: " << mY << "\n"; + file << "oX: " << oX << "\n"; + file << "oY: " << oY << "\n"; file << "width: " << segmentsX << "\n"; file << "height: " << segmentsY << "\n"; diff --git a/apps/openmw/mapextractor.hpp b/apps/openmw/mapextractor.hpp index 800a08c16a..2989aced90 100644 --- a/apps/openmw/mapextractor.hpp +++ b/apps/openmw/mapextractor.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include @@ -50,8 +52,23 @@ namespace OMW void extractWorldMap(); void extractLocalMaps(bool forceOverwrite = false); + + // Called every frame to process pending extractions + void update(); + + // Check if extraction is complete + bool isExtractionComplete() const; private: + struct PendingExtraction + { + const MWWorld::CellStore* cellStore; + bool isExterior; + std::filesystem::path outputPath; + int framesWaited; + std::function completionCallback; + }; + MWWorld::World& mWorld; osgViewer::Viewer* mViewer; MWBase::WindowManager* mWindowManager; @@ -60,10 +77,18 @@ namespace OMW std::unique_ptr mGlobalMap; MWRender::LocalMap* mLocalMap; + + std::vector mPendingExtractions; + int mFramesToWait; + bool mForceOverwrite; void saveWorldMapTexture(); void saveWorldMapInfo(); + void startExtraction(bool forceOverwrite); + void processNextCell(); + bool savePendingExtraction(const PendingExtraction& extraction); + bool extractExteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite); bool extractInteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite); void saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName); diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 969be697a4..9a814fb47a 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -618,6 +618,9 @@ namespace MWBase virtual void extractLocalMaps(const std::string& localMapOutput) = 0; ///< Extract local maps to the specified directory + + virtual bool isMapExtractionActive() const = 0; + ///< Check if map extraction is currently in progress }; } diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 637cb7e343..0f5c83be2b 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -1159,7 +1159,9 @@ namespace MWGui void MapWindow::cellExplored(int x, int y) { - mGlobalMapRender->cleanupCameras(); + // Note: Don't cleanup cameras here! This is called frequently during gameplay + // and would interfere with map extraction which batches multiple camera renders + // mGlobalMapRender->cleanupCameras(); mGlobalMapRender->exploreCell(x, y, mLocalMapRender->getMapTexture(x, y)); } @@ -1167,6 +1169,10 @@ namespace MWGui { LocalMapBase::onFrame(dt); NoDrop::onFrame(dt); + + // Note: Don't cleanup cameras here during normal gameplay + // Cameras are cleaned up only when explicitly requested (e.g., after map extraction) + // For global map overlay updates, cleanup happens in cellExplored() when needed } void MapWindow::setGlobalMapMarkerTooltip(MyGUI::Widget* markerWidget, int x, int y) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index e53c0a5b39..a4e9b26fa5 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -1041,7 +1041,7 @@ namespace MWGui mToolTips->onFrame(frameDuration); if (mLocalMapRender) - mLocalMapRender->cleanupCameras(); + //mLocalMapRender->cleanupCameras(); mDebugWindow->onFrame(frameDuration); diff --git a/apps/openmw/mwlua/worldbindings.cpp b/apps/openmw/mwlua/worldbindings.cpp index 234c0cac76..dbd842b6f7 100644 --- a/apps/openmw/mwlua/worldbindings.cpp +++ b/apps/openmw/mwlua/worldbindings.cpp @@ -298,6 +298,11 @@ namespace MWLua "disableExtractionModeAction"); }; + api["isMapExtractionActive"] = [lua = context.mLua]() -> bool { + checkGameInitialized(lua); + return MWBase::Environment::get().getWorld()->isMapExtractionActive(); + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 3546c29852..28abe98c4d 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -169,14 +169,15 @@ namespace MWRender void LocalMap::setupRenderToTexture( int segmentX, int segmentY, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax) { - mLocalMapRTTs.emplace_back( - new LocalMapRenderToTexture(mSceneRoot, mMapResolution, mMapWorldSize, left, top, upVector, zmin, zmax)); + auto rttNode = new LocalMapRenderToTexture(mSceneRoot, mMapResolution, mMapWorldSize, left, top, upVector, zmin, zmax); + mLocalMapRTTs.emplace_back(rttNode); mRoot->addChild(mLocalMapRTTs.back()); MapSegment& segment = mInterior ? mInteriorSegments[std::make_pair(segmentX, segmentY)] : mExteriorSegments[std::make_pair(segmentX, segmentY)]; segment.mMapTexture = static_cast(mLocalMapRTTs.back()->getColorTexture(nullptr)); + segment.mRTT = rttNode; // Store reference to RTT node } void LocalMap::requestMap(const MWWorld::CellStore* cell) @@ -239,6 +240,36 @@ namespace MWRender return found->second.mFogOfWarTexture; } + osg::ref_ptr LocalMap::getMapImage(int x, int y) + { + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) + return osg::ref_ptr(); + + MapSegment& segment = found->second; + + if (!segment.mRTT) + return osg::ref_ptr(); + + osg::Camera* camera = segment.mRTT->getCamera(nullptr); + if (!camera) + return osg::ref_ptr(); + + const osg::Camera::BufferAttachmentMap& attachments = camera->getBufferAttachmentMap(); + auto it = attachments.find(osg::Camera::COLOR_BUFFER); + if (it != attachments.end()) + { + osg::Image* img = it->second._image.get(); + if (img && img->s() > 0 && img->t() > 0 && img->data() != nullptr) + { + return img; + } + } + + return osg::ref_ptr(); + } + void LocalMap::cleanupCameras() { auto it = mLocalMapRTTs.begin(); @@ -475,6 +506,21 @@ namespace MWRender return mRoot; } + void LocalMap::setExtractionMode(bool enabled) + { + mExtractionMode = enabled; + } + + void LocalMap::clearCellCache(int x, int y) + { + auto it = mExteriorSegments.find(std::make_pair(x, y)); + if (it != mExteriorSegments.end()) + { + // Reset the render flags to force re-rendering + it->second.mLastRenderNeighbourFlags = 0; + } + } + void LocalMap::updatePlayer(const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, int& y, osg::Vec3f& direction) { @@ -775,28 +821,20 @@ 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()); + // CRITICAL: Attach an Image to COLOR_BUFFER to enable pixel readback from FBO + // This is required for map extraction functionality + // The image MUST be pre-allocated before attaching to the camera + osg::ref_ptr image = new osg::Image; - // 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()); + // Get the texture size from the camera's viewport + const osg::Viewport* vp = camera->getViewport(); + int width = vp ? vp->width() : 512; + int height = vp ? vp->height() : 512; - // Also set the Image on the texture so mapextractor can retrieve it via texture->getImage() - texture->setImage(image.get()); + // Allocate the image with the same format as the color buffer + image->allocateImage(width, height, 1, GL_RGB, GL_UNSIGNED_BYTE); + + camera->attach(osg::Camera::COLOR_BUFFER, image); } void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv) diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index c5b63766b7..f7201bf6df 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -62,6 +62,12 @@ namespace MWRender osg::ref_ptr getMapTexture(int x, int y); osg::ref_ptr getFogOfWarTexture(int x, int y); + + /** + * Get the rendered map image for a cell (for extraction purposes) + * Returns the osg::Image that contains the rendered pixel data + */ + osg::ref_ptr getMapImage(int x, int y); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that @@ -97,9 +103,36 @@ namespace MWRender */ bool isPositionExplored(float nX, float nY, int x, int y); + /** + * Clear the render cache for a specific exterior cell, forcing it to be re-rendered on next request + */ + void clearCellCache(int x, int y); + osg::Group* getRoot(); MyGUI::IntRect getInteriorGrid() const; + + /** + * Enable/disable extraction mode. When enabled, cameras won't be automatically cleaned up + * after rendering, allowing batch extraction of multiple maps. + */ + void setExtractionMode(bool enabled); + bool isExtractionMode() const { return mExtractionMode; } + + /** + * Get interior map bounds (with padding applied) - for map extraction + */ + const osg::BoundingBox& getInteriorBounds() const { return mBounds; } + + /** + * Get interior map center after rotation - for map extraction + */ + const osg::Vec2f& getInteriorCenter() const { return mCenter; } + + /** + * Get interior map rotation angle - for map extraction + */ + float getInteriorAngle() const { return mAngle; } private: osg::ref_ptr mRoot; @@ -132,6 +165,7 @@ namespace MWRender osg::ref_ptr mMapTexture; osg::ref_ptr mFogOfWarTexture; osg::ref_ptr mFogOfWarImage; + osg::ref_ptr mRTT; // Reference to the RTT node for this segment }; typedef std::map, MapSegment> SegmentMap; @@ -160,6 +194,7 @@ namespace MWRender osg::BoundingBox mBounds; osg::Vec2f mCenter; bool mInterior; + bool mExtractionMode = false; std::uint8_t getExteriorNeighbourFlags(int cellX, int cellY) const; }; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index d83e3f9fa2..16a8067d90 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1660,6 +1660,17 @@ namespace MWWorld if (mGoToJail && !paused) goToJail(); + // Update map extraction if active + if (mMapExtractor) + { + mMapExtractor->update(); + if (mMapExtractor->isExtractionComplete()) + { + Log(Debug::Info) << "Map extraction complete."; + mMapExtractor.reset(); + } + } + // Reset "traveling" flag - there was a frame to detect traveling. mPlayerTraveling = false; @@ -3931,14 +3942,20 @@ namespace MWWorld throw std::runtime_error("Viewer is not initialized"); } + // If extraction is already in progress, ignore the request + if (mMapExtractor) + { + Log(Debug::Warning) << "Map extraction is already in progress"; + return; + } + std::string outputPath = worldMapOutput.empty() ? getWorldMapOutputPath() : worldMapOutput; MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - OMW::MapExtractor extractor(*this, viewer, windowManager, outputPath, ""); + mMapExtractor = std::make_unique(*this, viewer, windowManager, outputPath, ""); Log(Debug::Info) << "Starting world map extraction to: " << outputPath; - extractor.extractWorldMap(); - Log(Debug::Info) << "World map extraction complete"; + mMapExtractor->extractWorldMap(); } void World::extractLocalMaps(const std::string& localMapOutput) @@ -3954,13 +3971,25 @@ namespace MWWorld throw std::runtime_error("Viewer is not initialized"); } + // If extraction is already in progress, ignore the request + if (mMapExtractor) + { + Log(Debug::Warning) << "Map extraction is already in progress"; + return; + } + std::string outputPath = localMapOutput.empty() ? getLocalMapOutputPath() : localMapOutput; MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); - OMW::MapExtractor extractor(*this, viewer, windowManager, "", outputPath); + mMapExtractor = std::make_unique(*this, viewer, windowManager, "", outputPath); Log(Debug::Info) << "Starting local maps extraction to: " << outputPath; - extractor.extractLocalMaps(); - Log(Debug::Info) << "Local maps extraction complete"; + mMapExtractor->extractLocalMaps(false); + Log(Debug::Info) << "Local maps extraction started, will complete during gameplay..."; + } + + bool World::isMapExtractionActive() const + { + return mMapExtractor != nullptr; } } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 71c1ee6203..44989f08bc 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -12,6 +12,8 @@ #include "../mwbase/world.hpp" +#include "../mapextractor.hpp" + #include "contentloader.hpp" #include "esmstore.hpp" #include "globals.hpp" @@ -102,10 +104,11 @@ namespace MWWorld std::unique_ptr mPhysics; std::unique_ptr mNavigator; std::unique_ptr mRendering; - std::unique_ptr mWorldScene; - std::unique_ptr mWeatherManager; - std::unique_ptr mTimeManager; - std::unique_ptr mProjectileManager; + std::unique_ptr mWorldScene; + std::unique_ptr mWeatherManager; + std::unique_ptr mTimeManager; + std::unique_ptr mProjectileManager; + std::unique_ptr mMapExtractor; bool mSky; bool mGodMode; @@ -686,6 +689,7 @@ namespace MWWorld void extractWorldMap(const std::string& worldMapOutput) override; void extractLocalMaps(const std::string& localMapOutput) override; + bool isMapExtractionActive() const override; }; } diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index c89eb3b0d6..bbf27a7ce4 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -253,4 +253,18 @@ -- @function [parent=#world] disableExtractionMode -- @usage world.disableExtractionMode() +--- +-- Check if map extraction is currently in progress. +-- Returns true if world map or local map extraction is active, false otherwise. +-- Use this to avoid starting multiple extractions simultaneously. +-- @function [parent=#world] isMapExtractionActive +-- @return #boolean true if map extraction is active, false otherwise +-- @usage +-- if not world.isMapExtractionActive() then +-- world.extractWorldMap("path/to/output") +-- else +-- print("Map extraction already in progress") +-- end + return nil +