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:
parent
dfaea48d73
commit
39aafcdc2d
12 changed files with 480 additions and 154 deletions
|
|
@ -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())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1041,7 +1041,7 @@ namespace MWGui
|
|||
mToolTips->onFrame(frameDuration);
|
||||
|
||||
if (mLocalMapRender)
|
||||
mLocalMapRender->cleanupCameras();
|
||||
//mLocalMapRender->cleanupCameras();
|
||||
|
||||
mDebugWindow->onFrame(frameDuration);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue