From 6d3528af7099e300e819f6311a0b299130d7e834 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 29 May 2015 01:49:52 +0200 Subject: [PATCH] Port global map exploration --- apps/openmw/mwgui/mapwindow.cpp | 9 +- apps/openmw/mwrender/globalmap.cpp | 241 +++++++++++++++++++++++------ apps/openmw/mwrender/globalmap.hpp | 53 ++++++- apps/openmw/mwrender/localmap.cpp | 5 + apps/openmw/mwrender/localmap.hpp | 2 + 5 files changed, 256 insertions(+), 54 deletions(-) diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 46e2f29e0..d748bef6c 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -587,7 +587,7 @@ namespace MWGui , mGlobal(false) , mEventBoxGlobal(NULL) , mEventBoxLocal(NULL) - , mGlobalMapRender(0) + , mGlobalMapRender(new MWRender::GlobalMap(localMapRender->getRoot())) , mEditNoteDialog() { static bool registered = false; @@ -730,7 +730,6 @@ namespace MWGui void MapWindow::renderGlobalMap(Loading::Listener* loadingListener) { - mGlobalMapRender = new MWRender::GlobalMap(); mGlobalMapRender->render(loadingListener); mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); @@ -794,9 +793,11 @@ namespace MWGui { LocalMapBase::onFrame(dt); + mGlobalMapRender->cleanupCameras(); + for (std::vector::iterator it = mQueuedToExplore.begin(); it != mQueuedToExplore.end(); ++it) { - mGlobalMapRender->exploreCell(it->first, it->second); + mGlobalMapRender->exploreCell(it->first, it->second, mLocalMapRender->getMapTexture(it->first, it->second)); } mQueuedToExplore.clear(); @@ -866,6 +867,8 @@ namespace MWGui void MapWindow::notifyPlayerUpdate () { globalMapUpdatePlayer (); + + setGlobalMapPlayerDir(mLastDirectionX, mLastDirectionY); } void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 8ca21cbfb..1e30eb0fc 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -4,6 +4,10 @@ #include #include +#include +#include +#include +#include #include @@ -18,11 +22,66 @@ #include "../mwworld/esmstore.hpp" +#include "vismask.hpp" + +namespace +{ + + // Create a screen-aligned quad with given texture coordinates. + // Assumes a top-left origin of the sampled image. + osg::ref_ptr createTexturedQuad(float leftTexCoord, float topTexCoord, float rightTexCoord, float bottomTexCoord) + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-1, -1, 0)); + verts->push_back(osg::Vec3f(-1, 1, 0)); + verts->push_back(osg::Vec3f(1, 1, 0)); + verts->push_back(osg::Vec3f(1, -1, 0)); + + geom->setVertexArray(verts); + + osg::ref_ptr texcoords = new osg::Vec2Array; + texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f-bottomTexCoord)); + texcoords->push_back(osg::Vec2f(leftTexCoord, 1.f-topTexCoord)); + texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f-topTexCoord)); + texcoords->push_back(osg::Vec2f(rightTexCoord, 1.f-bottomTexCoord)); + + geom->setTexCoordArray(0, texcoords, osg::Array::BIND_PER_VERTEX); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); + + return geom; + } + + + class CameraUpdateCallback : public osg::NodeCallback + { + public: + CameraUpdateCallback(osg::Camera* cam, MWRender::GlobalMap* parent) + : mCamera(cam), mParent(parent) + { + } + + virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + mParent->markForRemoval(mCamera); + traverse(node, nv); + } + + private: + osg::ref_ptr mCamera; + MWRender::GlobalMap* mParent; + }; + +} + namespace MWRender { - GlobalMap::GlobalMap() - : mWidth(0) + GlobalMap::GlobalMap(osg::Group* root) + : mRoot(root) + , mWidth(0) , mHeight(0) , mMinX(0), mMaxX(0) , mMinY(0), mMaxY(0) @@ -164,46 +223,78 @@ namespace MWRender imageY = 1.f-float(y - mMinY + 1) / (mMaxY - mMinY + 1); } - void GlobalMap::exploreCell(int cellX, int cellY) + void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, + float srcLeft, float srcTop, float srcRight, float srcBottom) { - //float originX = static_cast((cellX - mMinX) * mCellSize); - // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is - //float originY = static_cast(mHeight - (cellY + 1 - mMinY) * mCellSize); + osg::ref_ptr camera (new osg::Camera); + camera->setNodeMask(Mask_RenderToTexture); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + camera->setViewMatrix(osg::Matrix::identity()); + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setProjectionResizePolicy(osg::Camera::FIXED); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + camera->setViewport(x, y, width, height); + + if (clear) + { + camera->setClearMask(GL_COLOR_BUFFER_BIT); + camera->setClearColor(osg::Vec4(0,0,0,0)); + } + else + camera->setClearMask(GL_NONE); + + camera->setUpdateCallback(new CameraUpdateCallback(camera, this)); + + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); + camera->attach(osg::Camera::COLOR_BUFFER, mOverlayTexture); + + if (cpuCopy) + { + // Attach an image to copy the render back to the CPU when finished + osg::ref_ptr image (new osg::Image); + image->setPixelFormat(mOverlayImage->getPixelFormat()); + image->setDataType(mOverlayImage->getDataType()); + camera->attach(osg::Camera::COLOR_BUFFER, image); + + // FIXME: why does the image get slightly darker by the read back? + ImageDest imageDest; + imageDest.mImage = image; + imageDest.mX = x; + imageDest.mY = y; + mPendingImageDest.push_back(imageDest); + } + + // Create a quad rendering the updated texture + if (texture) + { + osg::ref_ptr geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom); + osg::ref_ptr depth = new osg::Depth; + depth->setFunction(osg::Depth::ALWAYS); + geom->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); + geom->getOrCreateStateSet()->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); + geom->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + osg::ref_ptr geode = new osg::Geode; + geode->addDrawable(geom); + camera->addChild(geode); + } + + mRoot->addChild(camera); + + mActiveCameras.push_back(camera); + } + + void GlobalMap::exploreCell(int cellX, int cellY, osg::ref_ptr localMapTexture) + { + if (!localMapTexture) + return; + + int originX = (cellX - mMinX) * mCellSize; + int originY = (cellY - mMinY) * mCellSize; if (cellX > mMaxX || cellX < mMinX || cellY > mMaxY || cellY < mMinY) return; - /* - Ogre::TexturePtr localMapTexture = Ogre::TextureManager::getSingleton().getByName("Cell_" - + boost::lexical_cast(cellX) + "_" + boost::lexical_cast(cellY)); - - if (!localMapTexture.isNull()) - { - int mapWidth = localMapTexture->getWidth(); - int mapHeight = localMapTexture->getHeight(); - mOverlayTexture->load(); - mOverlayTexture->getBuffer()->blit(localMapTexture->getBuffer(), Ogre::Image::Box(0,0,mapWidth,mapHeight), - Ogre::Image::Box(static_cast(originX), static_cast(originY), - static_cast(originX + mCellSize), static_cast(originY + mCellSize))); - - Ogre::Image backup; - std::vector data; - data.resize(mCellSize*mCellSize*4, 0); - backup.loadDynamicImage(&data[0], mCellSize, mCellSize, Ogre::PF_A8B8G8R8); - - localMapTexture->getBuffer()->blitToMemory(Ogre::Image::Box(0,0,mapWidth,mapHeight), backup.getPixelBox()); - - for (int x=0; x(originX + x), static_cast(originY + y), 0); - } - } - */ + requestOverlayTextureUpdate(originX, originY, mCellSize, mCellSize, localMapTexture, false, true); } void GlobalMap::clear() @@ -215,7 +306,6 @@ namespace MWRender assert(mOverlayImage->isDataContiguous()); } memset(mOverlayImage->data(), 0, mOverlayImage->getTotalSizeInBytes()); - mOverlayImage->dirty(); if (!mOverlayTexture) { @@ -224,9 +314,16 @@ namespace MWRender mOverlayTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mOverlayTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mOverlayTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mOverlayTexture->setImage(mOverlayImage); mOverlayTexture->setResizeNonPowerOfTwoHint(false); + mOverlayTexture->setInternalFormat(GL_RGBA); + mOverlayTexture->setTextureSize(mWidth, mHeight); } + + mPendingImageDest.clear(); + + // just push a Camera to clear the FBO, instead of setImage()/dirty() + // easier, since we don't need to worry about synchronizing access :) + requestOverlayTextureUpdate(0, 0, mWidth, mHeight, osg::ref_ptr(), true, false); } void GlobalMap::write(ESM::GlobalMap& map) @@ -257,12 +354,16 @@ namespace MWRender struct Box { - int mLeft, mRight, mTop, mBottom; + int mLeft, mTop, mRight, mBottom; - Box(int left, int right, int top, int bottom) - : mLeft(left), mRight(right), mTop(top), mBottom(bottom) + Box(int left, int top, int right, int bottom) + : mLeft(left), mTop(top), mRight(right), mBottom(bottom) { } + bool operator == (const Box& other) + { + return mLeft == other.mLeft && mTop == other.mTop && mRight == other.mRight && mBottom == other.mBottom; + } }; void GlobalMap::read(ESM::GlobalMap& map) @@ -336,20 +437,29 @@ namespace MWRender std::min(mWidth, mWidth + rightDiff * cellImageSizeDst), std::min(mHeight, mHeight + bottomDiff * cellImageSizeDst)); - if (srcBox.mLeft == destBox.mLeft && srcBox.mRight == destBox.mRight - && srcBox.mTop == destBox.mTop && srcBox.mBottom == destBox.mBottom - && imageWidth == mWidth && imageHeight == mHeight) + osg::ref_ptr texture (new osg::Texture2D); + texture->setImage(image); + texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + texture->setResizeNonPowerOfTwoHint(false); + + if (srcBox == destBox && imageWidth == mWidth && imageHeight == mHeight) { mOverlayImage->copySubImage(0, 0, 0, image); + + requestOverlayTextureUpdate(0, 0, mWidth, mHeight, texture, true, false); } else { - // TODO: // Dimensions don't match. This could mean a changed map region, or a changed map resolution. - // In the latter case, we'll want to use filtering. - // Create a RTT Camera and draw the image onto mOverlayImage in the next frame? + // In the latter case, we'll want filtering. + // Create a RTT Camera and draw the image onto mOverlayImage in the next frame. + requestOverlayTextureUpdate(destBox.mLeft, destBox.mTop, destBox.mRight-destBox.mLeft, destBox.mBottom-destBox.mTop, texture, true, true, + srcBox.mLeft/float(imageWidth), srcBox.mTop/float(imageHeight), + srcBox.mRight/float(imageWidth), srcBox.mBottom/float(imageHeight)); } - mOverlayImage->dirty(); } osg::ref_ptr GlobalMap::getBaseTexture() @@ -361,4 +471,37 @@ namespace MWRender { return mOverlayTexture; } + + void GlobalMap::markForRemoval(osg::Camera *camera) + { + CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), camera); + if (found == mActiveCameras.end()) + { + std::cerr << "GlobalMap trying to remove an inactive camera" << std::endl; + return; + } + mActiveCameras.erase(found); + mCamerasPendingRemoval.push_back(camera); + } + + void GlobalMap::cleanupCameras() + { + for (CameraVector::iterator it = mCamerasPendingRemoval.begin(); it != mCamerasPendingRemoval.end(); ++it) + mRoot->removeChild(*it); + mCamerasPendingRemoval.clear(); + + for (ImageDestVector::iterator it = mPendingImageDest.begin(); it != mPendingImageDest.end();) + { + ImageDest& imageDest = *it; + if (--imageDest.mFramesUntilDone > 0) + { + ++it; + continue; + } + + mOverlayImage->copySubImage(imageDest.mX, imageDest.mY, 0, imageDest.mImage); + + it = mPendingImageDest.erase(it); + } + } } diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index 9ca7ed5b4..7adb8218a 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -10,6 +10,8 @@ namespace osg { class Texture2D; class Image; + class Group; + class Camera; } namespace Loading @@ -28,7 +30,7 @@ namespace MWRender class GlobalMap { public: - GlobalMap(); + GlobalMap(osg::Group* root); ~GlobalMap(); void render(Loading::Listener* loadingListener); @@ -42,11 +44,23 @@ namespace MWRender void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); - void exploreCell (int cellX, int cellY); + void exploreCell (int cellX, int cellY, osg::ref_ptr localMapTexture); /// Clears the overlay void clear(); + /** + * Removes cameras that have already been rendered. Should be called every frame to ensure that + * we do not render the same map more than once. Note, this cleanup is difficult to implement in an + * automated fashion, since we can't alter the scene graph structure from within an update callback. + */ + void cleanupCameras(); + + /** + * Mark a camera for cleanup in the next update. For internal use only. + */ + void markForRemoval(osg::Camera* camera); + void write (ESM::GlobalMap& map); void read (ESM::GlobalMap& map); @@ -54,13 +68,48 @@ namespace MWRender osg::ref_ptr getOverlayTexture(); private: + /** + * Request rendering a 2d quad onto mOverlayTexture. + * x, y, width and height are the destination coordinates. + * @param cpuCopy copy the resulting render onto mOverlayImage as well? + */ + void requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, + float srcLeft = 0.f, float srcTop = 0.f, float srcRight = 1.f, float srcBottom = 1.f); + int mCellSize; + osg::ref_ptr mRoot; + + typedef std::vector > CameraVector; + CameraVector mActiveCameras; + + CameraVector mCamerasPendingRemoval; + + struct ImageDest + { + ImageDest() + : mFramesUntilDone(3) // wait an extra frame to ensure the draw thread has completed its frame. + { + } + + osg::ref_ptr mImage; + int mX, mY; + int mFramesUntilDone; + }; + + typedef std::vector ImageDestVector; + + ImageDestVector mPendingImageDest; + std::vector< std::pair > mExploredCells; osg::ref_ptr mBaseTexture; + + // GPU copy of overlay + // Note, uploads are pushed through a Camera, instead of through mOverlayImage osg::ref_ptr mOverlayTexture; + // CPU copy of overlay osg::ref_ptr mOverlayImage; int mWidth; diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 495ea2d1d..edc88ac55 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -480,6 +480,11 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y, bool interi return alpha < 200; } +osg::Group* LocalMap::getRoot() +{ + return mRoot; +} + void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orientation, float& u, float& v, int& x, int& y, osg::Vec3f& direction) { diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index c319b7ce7..72ee0354e 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -101,6 +101,8 @@ namespace MWRender */ bool isPositionExplored (float nX, float nY, int x, int y, bool interior); + osg::Group* getRoot(); + private: osg::ref_ptr mViewer;