diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index 9574564ada..865e670011 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -31,6 +33,7 @@ #include #include #include +#include namespace MWLua { @@ -182,6 +185,7 @@ namespace MWLua { // Ensure data is loaded if necessary land->loadData(ESM::Land::DATA_VHGT); + landData = land->getLandData(ESM::Land::DATA_VHGT); } } if (landData == nullptr) @@ -192,6 +196,91 @@ namespace MWLua return ESMTerrain::Storage::getHeightAt(landData->mHeights, landData->sLandSize, pos, cellSize); }; + api["getLandTextureAt"] = [lua = context.mLua](const osg::Vec3f& pos, sol::object cellOrName) { + sol::variadic_results values; + ESM::RefId worldspace; + if (cellOrName.is()) + worldspace = cellOrName.as().mStore->getCell()->getWorldSpace(); + else if (cellOrName.is() && !cellOrName.as().empty()) + worldspace = MWBase::Environment::get() + .getWorldModel() + ->getCell(cellOrName.as()) + .getCell() + ->getWorldSpace(); + else + worldspace = ESM::Cell::sDefaultWorldspaceId; + + const float cellSize = ESM::getCellSize(worldspace); + + int cellX = static_cast(std::floor(pos.x() / cellSize)); + int cellY = static_cast(std::floor(pos.y() / cellSize)); + + auto store = MWBase::Environment::get().getESMStore(); + // We need to read land twice. Once to get the amount of texture samples per cell edge, and the second time + // to get the actual data + auto landStore = store->get(); + auto land = landStore.search(cellX, cellY); + const ESM::Land::LandData* landData = nullptr; + if (land != nullptr) + { + landData = land->getLandData(ESM::Land::DATA_VTEX); + if (landData != nullptr) + { + // Ensure data is loaded if necessary + land->loadData(ESM::Land::DATA_VTEX); + landData = land->getLandData(ESM::Land::DATA_VTEX); + } + } + if (landData == nullptr) + { + // If we fail to preload land data, return, we need to be able to get *any* land to know how to correct + // the position used to sample terrain + return values; + } + + const osg::Vec3f correctedPos + = ESMTerrain::Storage::getTextureCorrectedWorldPos(pos, landData->sLandTextureSize, cellSize); + int correctedCellX = static_cast(std::floor(correctedPos.x() / cellSize)); + int correctedCellY = static_cast(std::floor(correctedPos.y() / cellSize)); + auto correctedLand = landStore.search(correctedCellX, correctedCellY); + const ESM::Land::LandData* correctedLandData = nullptr; + if (correctedLand != nullptr) + { + correctedLandData = correctedLand->getLandData(ESM::Land::DATA_VTEX); + if (correctedLandData != nullptr) + { + // Ensure data is loaded if necessary + land->loadData(ESM::Land::DATA_VTEX); + correctedLandData = correctedLand->getLandData(ESM::Land::DATA_VTEX); + } + } + if (correctedLandData == nullptr) + { + return values; + } + + // We're passing in sLandTextureSize, NOT sLandSize like with getHeightAt + const ESMTerrain::UniqueTextureId textureId + = ESMTerrain::Storage::getLandTextureAt(correctedLandData->mTextures, correctedLand->getPlugin(), + correctedLandData->sLandTextureSize, correctedPos, cellSize); + + // Need to check for 0, 0 so that we can safely subtract 1 later, as per documentation on UniqueTextureId + if (textureId.first != 0) + { + values.push_back(sol::make_object(lua->sol(), textureId.first - 1)); + values.push_back(sol::make_object(lua->sol(), textureId.second)); + + auto textureStore = store->get(); + const std::string* textureString = textureStore.search(textureId.first - 1, textureId.second); + if (textureString) + { + values.push_back(sol::make_object(lua->sol(), *textureString)); + } + } + + return values; + }; + sol::table readOnlyApi = LuaUtil::makeReadOnly(api); return context.setTypePackage(readOnlyApi, "openmw_core"); } diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index 330fa0440b..903ec11c29 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -22,6 +23,19 @@ namespace ESMTerrain { namespace { + UniqueTextureId getTextureIdAt( + const std::span data, const int plugin, const std::size_t x, const std::size_t y) + { + assert(x < ESM::Land::LAND_TEXTURE_SIZE); + assert(y < ESM::Land::LAND_TEXTURE_SIZE); + + const std::uint16_t tex = data[y * ESM::Land::LAND_TEXTURE_SIZE + x]; + if (tex == 0) + return { 0, 0 }; // vtex 0 is always the base texture, regardless of plugin + + return { tex, plugin }; + } + UniqueTextureId getTextureIdAt(const LandObject* land, std::size_t x, std::size_t y) { assert(x < ESM::Land::LAND_TEXTURE_SIZE); @@ -34,11 +48,7 @@ namespace ESMTerrain if (data == nullptr) return { 0, 0 }; - const std::uint16_t tex = data->getTextures()[y * ESM::Land::LAND_TEXTURE_SIZE + x]; - if (tex == 0) - return { 0, 0 }; // vtex 0 is always the base texture, regardless of plugin - - return { tex, land->getPlugin() }; + return getTextureIdAt(data->getTextures(), land->getPlugin(), x, y); } } @@ -465,6 +475,73 @@ namespace ESMTerrain blendmaps.clear(); // If a single texture fills the whole terrain, there is no need to blend } + osg::Vec3f Storage::getTextureCorrectedWorldPos( + const osg::Vec3f& uncorrectedWorldPos, const int textureSize, const float cellSize) + { + // the offset is [-0.25, +0.25] of a single texture's size + // TODO: verify whether or not this works in TES4 and beyond + float offset = (cellSize / textureSize) * 0.25; + return uncorrectedWorldPos + osg::Vec3f{ -offset, +offset, 0.0f }; + } + + // Takes in a corrected world pos to match the visuals. + UniqueTextureId Storage::getLandTextureAt(const std::span landData, const int plugin, + const int textureSize, const osg::Vec3f& correctedWorldPos, const float cellSize) + { + int cellX = static_cast(std::floor(correctedWorldPos.x() / cellSize)); + int cellY = static_cast(std::floor(correctedWorldPos.y() / cellSize)); + + // Normalized position in the cell + float nX = (correctedWorldPos.x() - (cellX * cellSize)) / cellSize; + float nY = (correctedWorldPos.y() - (cellY * cellSize)) / cellSize; + + int startX = static_cast(nX * textureSize); + int startY = static_cast(nY * textureSize); + + int endX = startX + 1; + int endY = startY + 1; + endX = std::min(endX, textureSize - 1); + endY = std::min(endY, textureSize - 1); + + float fractionX = std::clamp(nX * textureSize - startX, 0.0f, 1.0f); + float fractionY = std::clamp(nY * textureSize - startY, 0.0f, 1.0f); + + /* For even / odd tri strip rows, triangles are this shape: + even odd + 3---2 3---2 + | / | | \ | + 0---1 0---1 + */ + return getTextureIdAt(landData, plugin, startX, startY); + + if (fractionX <= 0.5f) + { + if (fractionY <= 0.5) + { + // 0 + return getTextureIdAt(landData, plugin, startX, startY); + } + else + { + // 3 + return getTextureIdAt(landData, plugin, startX, endY); + } + } + else + { + if (fractionY <= 0.5) + { + // 1 + return getTextureIdAt(landData, plugin, endX, startY); + } + else + { + // 2 + return getTextureIdAt(landData, plugin, endX, endY); + } + } + } + float Storage::getHeightAt( const std::span data, const int landSize, const osg::Vec3f& worldPos, const float cellSize) { diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index c88a15fa30..7b4e9f8694 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -117,6 +117,12 @@ namespace ESMTerrain static float getHeightAt( const std::span data, const int landSize, const osg::Vec3f& worldPos, const float cellSize); + static osg::Vec3f getTextureCorrectedWorldPos( + const osg::Vec3f& uncorrectedWorldPos, const int textureSize, const float cellSize); + + static UniqueTextureId getLandTextureAt(const std::span landData, const int plugin, + const int textureSize, const osg::Vec3f& worldPos, const float cellSize); + /// Get the transformation factor for mapping cell units to world units. float getCellWorldSize(ESM::RefId worldspace) override; diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 5813b32b27..1ffc60f6d5 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -69,6 +69,15 @@ -- @param #any cellOrName (optional) cell or cell name in their exterior world space to query -- @return #number +--- +-- Get the terrain texture at a given location. +-- @function [parent=#core] getLandTextureAt +-- @param openmw.util#Vector3 position +-- @param #any cellOrName (optional) cell or cell name in their exterior world space to query +-- @return #nil, #number Land texture index or nil if failed to retrieve the texture +-- @return #nil, #number Plugin id or nil if failed to retrieve the texture +-- @return #nil, #string Texture path or nil if one isn't defined + --- -- Return l10n formatting function for the given context. -- Localisation files (containing the message names and translations) should be stored in