1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2026-01-25 12:00:54 +00:00
openmw/apps/openmw/mapextractor.cpp

688 lines
25 KiB
C++

#include "mapextractor.hpp"
#include <algorithm>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <map>
#include <thread>
#include <osg/ComputeBoundsVisitor>
#include <osg/Group>
#include <osg/Image>
#include <osg/Texture2D>
#include <osgDB/WriteFile>
#include <osgViewer/Viewer>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadcell.hpp>
#include <components/misc/constants.hpp>
#include <components/sceneutil/lightmanager.hpp>
#include <components/sceneutil/workqueue.hpp>
#include <components/settings/values.hpp>
#include "mwbase/environment.hpp"
#include "mwbase/mechanicsmanager.hpp"
#include "mwbase/windowmanager.hpp"
#include "mwbase/world.hpp"
#include "mwrender/globalmap.hpp"
#include "mwrender/localmap.hpp"
#include "mwrender/renderingmanager.hpp"
#include "mwrender/vismask.hpp"
#include "mwworld/cellstore.hpp"
#include "mwworld/esmstore.hpp"
#include "mwworld/worldimp.hpp"
#include "mwworld/worldmodel.hpp"
namespace OMW
{
MapExtractor::MapExtractor(const std::string& worldMapOutput, const std::string& localMapOutput,
bool forceOverwrite, MWRender::RenderingManager* renderingManager, const MWWorld::ESMStore* store)
: mWorldMapOutputDir(worldMapOutput)
, mLocalMapOutputDir(localMapOutput)
, mRenderingManager(renderingManager)
, mStore(store)
, mLocalMap(nullptr)
, mForceOverwrite(forceOverwrite)
{
// Only create directories if paths are not empty
if (!mWorldMapOutputDir.empty())
std::filesystem::create_directories(mWorldMapOutputDir);
if (!mLocalMapOutputDir.empty())
std::filesystem::create_directories(mLocalMapOutputDir);
// Create GlobalMap instance
if (!mRenderingManager)
{
Log(Debug::Error) << "RenderingManager is null in MapExtractor constructor";
throw std::runtime_error("RenderingManager is null");
}
osg::Group* lightRoot = mRenderingManager->getLightRoot();
if (!lightRoot)
{
Log(Debug::Error) << "LightRoot is null in MapExtractor constructor";
throw std::runtime_error("LightRoot is null");
}
osg::Group* root = lightRoot->getParent(0) ? lightRoot->getParent(0)->asGroup() : nullptr;
if (!root)
{
Log(Debug::Error) << "Root node is null in MapExtractor constructor";
throw std::runtime_error("Root node is null");
}
SceneUtil::WorkQueue* workQueue = mRenderingManager->getWorkQueue();
if (!workQueue)
{
Log(Debug::Error) << "WorkQueue is null in MapExtractor constructor";
throw std::runtime_error("WorkQueue is null");
}
try
{
mGlobalMap = std::make_unique<MWRender::GlobalMap>(root, workQueue);
// LocalMap will be set later via setLocalMap() method
mLocalMap = nullptr;
}
catch (const std::exception& e)
{
Log(Debug::Error) << "Failed to create map objects: " << e.what();
throw;
}
}
MapExtractor::~MapExtractor() = default;
void MapExtractor::extractWorldMap()
{
Log(Debug::Info) << "Extracting world map...";
if (mWorldMapOutputDir.empty())
{
Log(Debug::Warning) << "World map output directory is not set, skipping world map extraction";
return;
}
// Create output directory if it doesn't exist
std::filesystem::create_directories(mWorldMapOutputDir);
if (!mGlobalMap)
{
Log(Debug::Error) << "Global map not initialized";
throw std::runtime_error("Global map not initialized");
}
// Temporarily set cell size to 32 pixels for extraction
const int originalCellSize = Settings::map().mGlobalMapCellSize;
Settings::map().mGlobalMapCellSize.set(32);
mGlobalMap->render();
mGlobalMap->ensureLoaded();
saveWorldMapTexture();
saveWorldMapInfo();
// Restore original cell size
Settings::map().mGlobalMapCellSize.set(originalCellSize);
Log(Debug::Info) << "World map extraction complete";
}
void MapExtractor::saveWorldMapTexture()
{
osg::ref_ptr<osg::Texture2D> baseTexture = mGlobalMap->getBaseTexture();
if (!baseTexture || !baseTexture->getImage())
{
Log(Debug::Error) << "Failed to get world map base texture";
return;
}
osg::Image* image = baseTexture->getImage();
std::filesystem::path outputPath = mWorldMapOutputDir / "map.png";
if (!osgDB::writeImageFile(*image, outputPath.string()))
{
Log(Debug::Error) << "Failed to write world map texture to " << outputPath;
return;
}
Log(Debug::Info) << "Saved world map texture: " << outputPath;
}
void MapExtractor::saveWorldMapInfo()
{
int width = mGlobalMap->getWidth();
int height = mGlobalMap->getHeight();
int minX = std::numeric_limits<int>::max();
int maxX = std::numeric_limits<int>::min();
int minY = std::numeric_limits<int>::max();
int maxY = std::numeric_limits<int>::min();
MWWorld::Store<ESM::Cell>::iterator it = mStore->get<ESM::Cell>().extBegin();
for (; it != mStore->get<ESM::Cell>().extEnd(); ++it)
{
if (it->getGridX() < minX)
minX = it->getGridX();
if (it->getGridX() > maxX)
maxX = it->getGridX();
if (it->getGridY() < minY)
minY = it->getGridY();
if (it->getGridY() > maxY)
maxY = it->getGridY();
}
std::filesystem::path infoPath = mWorldMapOutputDir / "mapInfo.yaml";
std::ofstream file(infoPath);
if (!file)
{
Log(Debug::Error) << "Failed to create world map info file: " << infoPath;
return;
}
osg::Vec3f bgColor = mGlobalMap->getBackgroundColor();
file << "width: " << width << "\n";
file << "height: " << height << "\n";
file << "pixelsPerCell: 32\n";
file << "gridX:\n";
file << " min: " << minX << "\n";
file << " max: " << maxX << "\n";
file << "gridY:\n";
file << " min: " << minY << "\n";
file << " max: " << maxY << "\n";
file << "file: \"map.png\"\n";
file << "bColor: [" << bgColor.x() << ", " << bgColor.y() << ", " << bgColor.z() << "]\n";
file.close();
Log(Debug::Info) << "Saved world map info: " << infoPath;
}
void MapExtractor::extractLocalMaps(const std::vector<const MWWorld::CellStore*>& activeCells)
{
Log(Debug::Info) << "Extracting active local maps...";
if (mLocalMapOutputDir.empty())
{
Log(Debug::Warning) << "Local map output directory is not set, skipping local map extraction";
return;
}
// Create output directory if it doesn't exist
std::filesystem::create_directories(mLocalMapOutputDir);
if (!mLocalMap)
{
Log(Debug::Error) << "Local map not initialized - cannot extract local maps";
return;
}
Log(Debug::Info) << "LocalMap instance is available, starting extraction";
mFramesToWait = 10; // Wait 10 frames before checking (increased from 3)
startExtraction(activeCells);
}
void MapExtractor::startExtraction(const std::vector<const MWWorld::CellStore*>& activeCells)
{
// Enable extraction mode to prevent automatic camera cleanup
if (mLocalMap)
{
mLocalMap->setExtractionMode(true);
}
Log(Debug::Info) << "Processing " << activeCells.size() << " currently active cells...";
mPendingExtractions.clear();
for (const MWWorld::CellStore* cellStore : activeCells)
{
if (cellStore->getCell()->isExterior())
{
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 (!mForceOverwrite && 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
{
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 (!mForceOverwrite && 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);
}
}
if (!mPendingExtractions.empty())
{
Log(Debug::Info) << "Queued " << mPendingExtractions.size() << " cells for extraction";
processNextCell();
}
else
{
Log(Debug::Info) << "No cells to extract";
// Clean up any cameras that may have been created
if (mLocalMap)
{
mLocalMap->cleanupCameras();
mLocalMap->setExtractionMode(false);
}
}
}
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());
// Clean up the camera for the current cell immediately after saving
// This prevents memory buildup when processing many cells
if (mLocalMap)
mLocalMap->cleanupCameras();
if (!mPendingExtractions.empty())
{
processNextCell();
}
else
{
// 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());
// Clean up cameras even after timeout
if (mLocalMap)
mLocalMap->cleanupCameras();
if (!mPendingExtractions.empty())
{
processNextCell();
}
else
{
// 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)
{
int x = cellStore->getCell()->getGridX();
int y = cellStore->getCell()->getGridY();
std::ostringstream filename;
filename << "(" << x << "," << y << ").png";
std::filesystem::path outputPath = mLocalMapOutputDir / filename.str();
osg::ref_ptr<osg::Image> image = mLocalMap->getMapImage(x, y);
if (!image)
{
Log(Debug::Warning) << "Texture for cell (" << x << "," << y << ") has no image data attached";
return false;
}
if (image->s() == 0 || image->t() == 0)
{
Log(Debug::Warning) << "Empty image for cell (" << x << "," << y << ") - size: "
<< image->s() << "x" << image->t();
return false;
}
osg::ref_ptr<osg::Image> outputImage = new osg::Image(*image, osg::CopyOp::DEEP_COPY_ALL);
if (outputImage->s() != 256 || outputImage->t() != 256)
{
osg::ref_ptr<osg::Image> resized = new osg::Image;
resized->allocateImage(256, 256, 1, outputImage->getPixelFormat(), outputImage->getDataType());
outputImage->scaleImage(256, 256, 1);
outputImage = resized;
}
if (osgDB::writeImageFile(*outputImage, outputPath.string()))
{
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 << ") to " << outputPath;
return false;
}
}
bool MapExtractor::extractInteriorCell(const MWWorld::CellStore* cellStore, bool forceOverwrite)
{
ESM::RefId cellId = cellStore->getCell()->getId();
std::string cellName(cellStore->getCell()->getNameId());
Log(Debug::Info) << "Saving interior cell: " << cellName;
saveInteriorCellTextures(cellId, cellName);
return true;
}
void MapExtractor::saveInteriorCellTextures(const ESM::RefId& cellId, const std::string& cellName)
{
MyGUI::IntRect grid = mLocalMap->getInteriorGrid();
std::string lowerCaseId = cellId.toDebugString();
std::transform(lowerCaseId.begin(), lowerCaseId.end(), lowerCaseId.begin(), ::tolower);
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_";
}
int minX = grid.right;
int maxX = grid.left;
int minY = grid.bottom;
int maxY = grid.top;
// 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)
{
// 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)
{
imageCache[{x, y}] = image;
minX = std::min(minX, x);
maxX = std::max(maxX, x);
minY = std::min(minY, y);
maxY = std::max(maxY, y);
}
}
}
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;
osg::ref_ptr<osg::Image> combinedImage = new osg::Image;
combinedImage->allocateImage(totalWidth, totalHeight, 1, GL_RGB, GL_UNSIGNED_BYTE);
unsigned char* data = combinedImage->data();
memset(data, 0, totalWidth * totalHeight * 3);
for (int x = minX; x <= maxX; ++x)
{
for (int y = minY; y <= maxY; ++y)
{
auto it = imageCache.find({x, y});
if (it == imageCache.end())
continue;
osg::ref_ptr<osg::Image> segmentImage = it->second;
int segWidth = segmentImage->s();
int segHeight = segmentImage->t();
int destX = (x - minX) * 256;
int destY = (y - minY) * 256;
for (int sy = 0; sy < std::min(segHeight, 256); ++sy)
{
for (int sx = 0; sx < std::min(segWidth, 256); ++sx)
{
unsigned char* srcPixel = segmentImage->data(sx, sy);
int dx = destX + sx;
int dy = destY + sy;
if (dx < totalWidth && dy < totalHeight)
{
unsigned char* destPixel = data + ((dy * totalWidth + dx) * 3);
destPixel[0] = srcPixel[0];
destPixel[1] = srcPixel[1];
destPixel[2] = srcPixel[2];
}
}
}
}
}
std::filesystem::path texturePath = mLocalMapOutputDir / (lowerCaseId + ".png");
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);
}
void MapExtractor::saveInteriorMapInfo(const ESM::RefId& cellId, const std::string& lowerCaseId,
int segmentsX, int segmentsY)
{
// 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();
osg::Vec2f min(bounds.xMin(), bounds.yMin());
const float mapWorldSize = Constants::CellSizeInUnits;
// 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;
std::filesystem::path yamlPath = mLocalMapOutputDir / (lowerCaseId + ".yaml");
std::ofstream file(yamlPath);
if (!file)
{
Log(Debug::Error) << "Failed to create interior map info file: " << yamlPath;
return;
}
// TODO: Replace hardcoded padding with a constant from LocalMap
// Padding value taken from the implementation in localmap.cpp.
// Used to shrink the bounds to match the one used during cell rendering.
const float padding = 500.0f;
file << "v: 2\n";
file << "nA: " << nA << "\n";
file << "oX: " << oX << "\n";
file << "oY: " << oY << "\n";
file << "wT: " << segmentsX << "\n";
file << "hT: " << segmentsY << "\n";
file << "mBnds:\n";
file << " min: [" << bounds.xMin() + padding << ", " << bounds.yMin() + padding << ", " << bounds.zMin() << "]\n";
file << " max: [" << bounds.xMax() - padding << ", " << bounds.yMax() - padding << ", " << bounds.zMax() << "]\n";
file.close();
}
}