1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2026-01-24 17:30:55 +00:00

Implement asynchronous local map extraction and status API

This commit is contained in:
Diject 2025-12-28 19:51:17 +03:00
parent dfaea48d73
commit 39aafcdc2d
12 changed files with 480 additions and 154 deletions

View file

@ -996,6 +996,7 @@ void OMW::Engine::go()
// Map extractor
std::unique_ptr<MapExtractor> 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<MapExtractor>(*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())
{

View file

@ -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<osg::Image> 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<osg::Image> 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<MWWorld::CellStore*>(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<MWWorld::CellStore*>(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<osg::Texture2D> 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<osg::Image> 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<osg::Image> 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<std::pair<int, int>, osg::ref_ptr<osg::Texture2D>> textureCache;
// Cache images and find bounds
std::map<std::pair<int, int>, osg::ref_ptr<osg::Image>> imageCache;
for (int x = grid.left; x < grid.right; ++x)
{
for (int y = grid.top; y < grid.bottom; ++y)
{
osg::ref_ptr<osg::Texture2D> texture = mLocalMap->getMapTexture(x, y);
if (texture && texture->getImage())
// Get the rendered image directly from camera attachment
osg::ref_ptr<osg::Image> 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<osg::Texture2D> texture = it->second;
osg::Image* segmentImage = texture->getImage();
osg::ref_ptr<osg::Image> 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";

View file

@ -4,6 +4,8 @@
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include <functional>
#include <components/esm/refid.hpp>
@ -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<void()> completionCallback;
};
MWWorld::World& mWorld;
osgViewer::Viewer* mViewer;
MWBase::WindowManager* mWindowManager;
@ -60,10 +77,18 @@ namespace OMW
std::unique_ptr<MWRender::GlobalMap> mGlobalMap;
MWRender::LocalMap* mLocalMap;
std::vector<PendingExtraction> 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);

View file

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

View file

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

View file

@ -1041,7 +1041,7 @@ namespace MWGui
mToolTips->onFrame(frameDuration);
if (mLocalMapRender)
mLocalMapRender->cleanupCameras();
//mLocalMapRender->cleanupCameras();
mDebugWindow->onFrame(frameDuration);

View file

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

View file

@ -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<osg::Texture2D*>(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<osg::Image> 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<osg::Image>();
MapSegment& segment = found->second;
if (!segment.mRTT)
return osg::ref_ptr<osg::Image>();
osg::Camera* camera = segment.mRTT->getCamera(nullptr);
if (!camera)
return osg::ref_ptr<osg::Image>();
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<osg::Image>();
}
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<osg::Texture2D> 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<osg::Image> image = new osg::Image;
// Then attach Image for CPU-side readback
// OSG will automatically copy rendered pixels to this Image
osg::ref_ptr<osg::Image> 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)

View file

@ -62,6 +62,12 @@ namespace MWRender
osg::ref_ptr<osg::Texture2D> getMapTexture(int x, int y);
osg::ref_ptr<osg::Texture2D> 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<osg::Image> 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<osg::Group> mRoot;
@ -132,6 +165,7 @@ namespace MWRender
osg::ref_ptr<osg::Texture2D> mMapTexture;
osg::ref_ptr<osg::Texture2D> mFogOfWarTexture;
osg::ref_ptr<osg::Image> mFogOfWarImage;
osg::ref_ptr<LocalMapRenderToTexture> mRTT; // Reference to the RTT node for this segment
};
typedef std::map<std::pair<int, int>, 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;
};

View file

@ -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<OMW::MapExtractor>(*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<OMW::MapExtractor>(*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;
}
}

View file

@ -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<MWPhysics::PhysicsSystem> mPhysics;
std::unique_ptr<DetourNavigator::Navigator> mNavigator;
std::unique_ptr<MWRender::RenderingManager> mRendering;
std::unique_ptr<MWWorld::Scene> mWorldScene;
std::unique_ptr<MWWorld::WeatherManager> mWeatherManager;
std::unique_ptr<MWWorld::DateTimeManager> mTimeManager;
std::unique_ptr<ProjectileManager> mProjectileManager;
std::unique_ptr<MWWorld::Scene> mWorldScene;
std::unique_ptr<MWWorld::WeatherManager> mWeatherManager;
std::unique_ptr<MWWorld::DateTimeManager> mTimeManager;
std::unique_ptr<ProjectileManager> mProjectileManager;
std::unique_ptr<OMW::MapExtractor> 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;
};
}

View file

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