ESM4 landscape textures

This commit is contained in:
Petr Mikheev 2025-07-18 21:53:21 +02:00
parent b160cee0b7
commit 0b5c8271e0
18 changed files with 304 additions and 107 deletions

View File

@ -1,6 +1,8 @@
#include "terrainstorage.hpp" #include "terrainstorage.hpp"
#include <components/esm3/loadland.hpp> #include <components/esm3/loadland.hpp>
#include <components/esm4/loadltex.hpp>
#include <components/esm4/loadtxst.hpp>
#include <components/esm4/loadwrld.hpp> #include <components/esm4/loadwrld.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -111,4 +113,15 @@ namespace MWRender
return esmStore.get<ESM::LandTexture>().search(index, plugin); return esmStore.get<ESM::LandTexture>().search(index, plugin);
} }
const ESM4::LandTexture* TerrainStorage::getEsm4LandTexture(ESM::RefId ltexId) const
{
const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
return esmStore.get<ESM4::LandTexture>().search(ltexId);
}
const ESM4::TextureSet* TerrainStorage::getEsm4TextureSet(ESM::RefId txstId) const
{
const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
return esmStore.get<ESM4::TextureSet>().search(txstId);
}
} }

View File

@ -24,6 +24,9 @@ namespace MWRender
osg::ref_ptr<const ESMTerrain::LandObject> getLand(ESM::ExteriorCellLocation cellLocation) override; osg::ref_ptr<const ESMTerrain::LandObject> getLand(ESM::ExteriorCellLocation cellLocation) override;
const std::string* getLandTexture(std::uint16_t index, int plugin) override; const std::string* getLandTexture(std::uint16_t index, int plugin) override;
const ESM4::LandTexture* getEsm4LandTexture(ESM::RefId ltexId) const override;
const ESM4::TextureSet* getEsm4TextureSet(ESM::RefId txstId) const override;
bool hasData(ESM::ExteriorCellLocation cellLocation) override; bool hasData(ESM::ExteriorCellLocation cellLocation) override;
/// Get bounds of the whole terrain in cell units /// Get bounds of the whole terrain in cell units

View File

@ -110,6 +110,7 @@ namespace ESM4
struct Static; struct Static;
struct StaticCollection; struct StaticCollection;
struct Terminal; struct Terminal;
struct TextureSet;
struct Tree; struct Tree;
struct Weapon; struct Weapon;
struct World; struct World;
@ -149,7 +150,7 @@ namespace MWWorld
Store<ESM4::LevelledNpc>, Store<ESM4::Light>, Store<ESM4::MiscItem>, Store<ESM4::MovableStatic>, Store<ESM4::LevelledNpc>, Store<ESM4::Light>, Store<ESM4::MiscItem>, Store<ESM4::MovableStatic>,
Store<ESM4::Npc>, Store<ESM4::Outfit>, Store<ESM4::Potion>, Store<ESM4::Race>, Store<ESM4::Reference>, Store<ESM4::Npc>, Store<ESM4::Outfit>, Store<ESM4::Potion>, Store<ESM4::Race>, Store<ESM4::Reference>,
Store<ESM4::Sound>, Store<ESM4::SoundReference>, Store<ESM4::Static>, Store<ESM4::StaticCollection>, Store<ESM4::Sound>, Store<ESM4::SoundReference>, Store<ESM4::Static>, Store<ESM4::StaticCollection>,
Store<ESM4::Terminal>, Store<ESM4::Tree>, Store<ESM4::Weapon>, Store<ESM4::World>>; Store<ESM4::Terminal>, Store<ESM4::TextureSet>, Store<ESM4::Tree>, Store<ESM4::Weapon>, Store<ESM4::World>>;
private: private:
template <typename T> template <typename T>

View File

@ -1354,6 +1354,7 @@ template class MWWorld::TypedDynamicStore<ESM4::SoundReference>;
template class MWWorld::TypedDynamicStore<ESM4::Static>; template class MWWorld::TypedDynamicStore<ESM4::Static>;
template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>; template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>;
template class MWWorld::TypedDynamicStore<ESM4::Terminal>; template class MWWorld::TypedDynamicStore<ESM4::Terminal>;
template class MWWorld::TypedDynamicStore<ESM4::TextureSet>;
template class MWWorld::TypedDynamicStore<ESM4::Tree>; template class MWWorld::TypedDynamicStore<ESM4::Tree>;
template class MWWorld::TypedDynamicStore<ESM4::Weapon>; template class MWWorld::TypedDynamicStore<ESM4::Weapon>;
template class MWWorld::TypedDynamicStore<ESM4::World>; template class MWWorld::TypedDynamicStore<ESM4::World>;

View File

@ -31,6 +31,7 @@ namespace ESM
VER_134 = 0x3fab851f, // FONV, GunRunnersArsenal, LonesomeRoad, OldWorldBlues VER_134 = 0x3fab851f, // FONV, GunRunnersArsenal, LonesomeRoad, OldWorldBlues
VER_094 = 0x3f70a3d7, // TES5/FO3 VER_094 = 0x3f70a3d7, // TES5/FO3
VER_170 = 0x3fd9999a, // TES5 VER_170 = 0x3fd9999a, // TES5
VER_171 = 0x3fdae148, // TES5
VER_095 = 0x3f733333, // FO4 VER_095 = 0x3f733333, // FO4
}; };

View File

@ -33,6 +33,7 @@ ESM::LandData::LandData(const ESM::Land& land, int loadFlags)
, mNormals(mData->mNormals) , mNormals(mData->mNormals)
, mColors(mData->mColours) , mColors(mData->mColours)
, mTextures(mData->mTextures) , mTextures(mData->mTextures)
, mIsEsm4(false)
{ {
} }
@ -43,9 +44,11 @@ ESM::LandData::LandData(const ESM4::Land& land, int /*loadFlags*/)
, mMaxHeight(std::numeric_limits<float>::lowest()) , mMaxHeight(std::numeric_limits<float>::lowest())
, mSize(Constants::ESM4CellSizeInUnits) , mSize(Constants::ESM4CellSizeInUnits)
, mLandSize(ESM4::Land::sVertsPerSide) , mLandSize(ESM4::Land::sVertsPerSide)
, mPlugin(land.mId.mContentFile)
, mNormals(land.mVertNorm) , mNormals(land.mVertNorm)
, mColors(land.mVertColr) , mColors(land.mVertColr)
, mTextures(textures) , mTextures(textures)
, mIsEsm4(true)
{ {
float rowOffset = land.mHeightMap.heightOffset; float rowOffset = land.mHeightMap.heightOffset;
for (int y = 0; y < mLandSize; y++) for (int y = 0; y < mLandSize; y++)
@ -69,6 +72,9 @@ ESM::LandData::LandData(const ESM4::Land& land, int /*loadFlags*/)
} }
mHeights = mHeightsData; mHeights = mHeightsData;
for (int i = 0; i < 4; ++i)
mEsm4Textures[i] = land.mTextures[i];
} }
namespace ESM namespace ESM

View File

@ -1,15 +1,13 @@
#ifndef COMPONENTS_ESM_ESMTERRAIN #ifndef COMPONENTS_ESM_ESMTERRAIN
#define COMPONENTS_ESM_ESMTERRAIN #define COMPONENTS_ESM_ESMTERRAIN
#include <array>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <span> #include <span>
#include <vector> #include <vector>
namespace ESM4 #include <components/esm4/loadland.hpp>
{
struct Land;
}
namespace ESM namespace ESM
{ {
@ -28,7 +26,6 @@ namespace ESM
std::span<const float> getHeights() const { return mHeights; } std::span<const float> getHeights() const { return mHeights; }
std::span<const std::int8_t> getNormals() const { return mNormals; } std::span<const std::int8_t> getNormals() const { return mNormals; }
std::span<const std::uint8_t> getColors() const { return mColors; } std::span<const std::uint8_t> getColors() const { return mColors; }
std::span<const std::uint16_t> getTextures() const { return mTextures; }
float getSize() const { return mSize; } float getSize() const { return mSize; }
float getMinHeight() const { return mMinHeight; } float getMinHeight() const { return mMinHeight; }
float getMaxHeight() const { return mMaxHeight; } float getMaxHeight() const { return mMaxHeight; }
@ -36,6 +33,22 @@ namespace ESM
int getLoadFlags() const { return mLoadFlags; } int getLoadFlags() const { return mLoadFlags; }
int getPlugin() const { return mPlugin; } int getPlugin() const { return mPlugin; }
bool isEsm4() const { return mIsEsm4; }
std::span<const std::uint16_t> getTextures() const
{
if (mIsEsm4)
throw std::logic_error("ESM3 textures requested from ESM4 LandData");
return mTextures;
}
const ESM4::Land::Texture& getEsm4Texture(std::size_t quad) const
{
if (!mIsEsm4)
throw std::logic_error("ESM4 texture requested from ESM3 LandData");
return mEsm4Textures[quad];
}
private: private:
std::unique_ptr<const ESM::LandRecordData> mData; std::unique_ptr<const ESM::LandRecordData> mData;
int mLoadFlags = 0; int mLoadFlags = 0;
@ -49,6 +62,8 @@ namespace ESM
std::span<const std::int8_t> mNormals; std::span<const std::int8_t> mNormals;
std::span<const std::uint8_t> mColors; std::span<const std::uint8_t> mColors;
std::span<const std::uint16_t> mTextures; std::span<const std::uint16_t> mTextures;
std::array<ESM4::Land::Texture, 4> mEsm4Textures;
bool mIsEsm4;
}; };
} }

View File

@ -79,6 +79,7 @@
#include <components/esm4/loadstat.hpp> #include <components/esm4/loadstat.hpp>
#include <components/esm4/loadterm.hpp> #include <components/esm4/loadterm.hpp>
#include <components/esm4/loadtree.hpp> #include <components/esm4/loadtree.hpp>
#include <components/esm4/loadtxst.hpp>
#include <components/esm4/loadweap.hpp> #include <components/esm4/loadweap.hpp>
#include <components/esm4/loadwrld.hpp> #include <components/esm4/loadwrld.hpp>

View File

@ -1,5 +1,5 @@
/* /*
Copyright (C) 2015-2016, 2018, 2020-2021 cc9cii Copyright (C) 2015 - 2024 cc9cii
This software is provided 'as-is', without any express or implied This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages warranty. In no event will the authors be held liable for any damages
@ -17,7 +17,7 @@
misrepresented as being the original software. misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution. 3. This notice may not be removed or altered from any source distribution.
cc9cii cc9c@iinet.net.au cc9cii cc9cii@hotmail.com
Much of the information on the data structures are based on the information Much of the information on the data structures are based on the information
from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by
@ -26,13 +26,51 @@
*/ */
#include "loadland.hpp" #include "loadland.hpp"
#include <cassert>
#include <cstdint> #include <cstdint>
#include <stdexcept> #include <stdexcept>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include "reader.hpp" #include "reader.hpp"
// #include "writer.hpp"
namespace
{
void assignDefaultTextures(ESM4::Land& land, ESM4::Reader& reader)
{
std::uint32_t esmVer = reader.esmVersion();
// Note: in games after TES4 it can be configured in ini file (sDefaultLandDiffuseTexture)
if (!reader.hasFormVersion() && (esmVer == ESM::VER_080 || esmVer == ESM::VER_100)) // TES4
{
land.mDefaultDiffuseMap = VFS::Path::NormalizedView("textures/landscape/terrainhddirt01.dds");
land.mDefaultNormalMap = VFS::Path::NormalizedView("textures/landscape/terrainhddirt01_n.dds");
}
else if (reader.hasFormVersion() && reader.formVersion() >= 16
&& (esmVer == ESM::VER_094 || esmVer == ESM::VER_170 || esmVer == ESM::VER_171)) // TES5
{
land.mDefaultDiffuseMap = VFS::Path::NormalizedView("textures/landscape/dirt02.dds");
land.mDefaultNormalMap = VFS::Path::NormalizedView("textures/landscape/dirt02_n.dds");
}
else if (esmVer == ESM::VER_095 || esmVer == ESM::VER_100) // FO4
{
land.mDefaultDiffuseMap
= VFS::Path::NormalizedView("textures/landscape/ground/commonwealthdefault01_d.dds");
land.mDefaultNormalMap = VFS::Path::NormalizedView("textures/landscape/ground/commonwealthdefault01_n.dds");
}
else if (esmVer == ESM::VER_094 || esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134)
{ // FO3, FONV
land.mDefaultDiffuseMap = VFS::Path::NormalizedView("textures/landscape/dirtwasteland01.dds");
land.mDefaultNormalMap = VFS::Path::NormalizedView("textures/landscape/dirtwasteland01_n.dds");
}
else
{
// Nothing especially bad happens if default texture is not set (except of the missing texture of course),
// but we throw an error because this case is unexpected and detection logic needs to be updated.
throw std::runtime_error("ESM4::Land unknown ESM version");
}
}
}
// overlap north // overlap north
// //
@ -53,12 +91,16 @@ void ESM4::Land::load(ESM4::Reader& reader)
{ {
mId = reader.getFormIdFromHeader(); mId = reader.getFormIdFromHeader();
mFlags = reader.hdr().record.flags; mFlags = reader.hdr().record.flags;
mDataTypes = 0; mDataTypes = 0;
mCell = reader.currCell(); mCell = reader.currCell();
TxtLayer layer; TxtLayer layer;
std::int8_t currentAddQuad = -1; // for VTXT following ATXT std::int8_t currentAddQuad = -1; // for VTXT following ATXT
assignDefaultTextures(*this, reader);
// std::map<FormId, int> uniqueTextures; // FIXME: for temp testing only layer.texture.formId = 0;
for (int i = 0; i < 4; ++i)
mTextures[i].base.formId = 0;
while (reader.getSubRecordHeader()) while (reader.getSubRecordHeader())
{ {
@ -78,12 +120,6 @@ void ESM4::Land::load(ESM4::Reader& reader)
} }
case ESM::fourCC("VHGT"): // vertex height gradient, 4+33x33+3 = 4+1089+3 = 1096 case ESM::fourCC("VHGT"): // vertex height gradient, 4+33x33+3 = 4+1089+3 = 1096
{ {
#if 0
reader.get(mHeightMap.heightOffset);
reader.get(mHeightMap.gradientData);
reader.get(mHeightMap.unknown1);
reader.get(mHeightMap.unknown2);
#endif
reader.get(mHeightMap); reader.get(mHeightMap);
mDataTypes |= LAND_VHGT; mDataTypes |= LAND_VHGT;
break; break;
@ -102,13 +138,9 @@ void ESM4::Land::load(ESM4::Reader& reader)
if (base.quadrant >= 4) if (base.quadrant >= 4)
throw std::runtime_error("base texture quadrant index error"); throw std::runtime_error("base texture quadrant index error");
reader.adjustFormId(base.formId); if (base.formId != 0)
mTextures[base.quadrant].base = std::move(base); reader.adjustFormId(base.formId);
#if 0 mTextures[base.quadrant].base = base;
std::cout << "Base Texture formid: 0x"
<< std::hex << mTextures[base.quadrant].base.formId
<< ", quad " << std::dec << (int)base.quadrant << std::endl;
#endif
} }
break; break;
} }
@ -116,31 +148,23 @@ void ESM4::Land::load(ESM4::Reader& reader)
{ {
if (currentAddQuad != -1) if (currentAddQuad != -1)
{ {
// FIXME: sometimes there are no VTXT following an ATXT? Just add a dummy one for now // NOTE: sometimes there are no VTXT following an ATXT
Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex; layer.data.resize(1); // just one spot
layer.data.back().position = 0; // this corner
layer.data.back().opacity = 0.f; // transparent
if (layer.texture.layerIndex != mTextures[currentAddQuad].layers.size())
throw std::runtime_error("ESM4::LAND additional texture skipping layer");
mTextures[currentAddQuad].layers.push_back(layer); mTextures[currentAddQuad].layers.push_back(layer);
} }
reader.get(layer.texture); reader.get(layer.texture);
reader.adjustFormId(layer.texture.formId); if (layer.texture.formId != 0)
reader.adjustFormId(layer.texture.formId);
if (layer.texture.quadrant >= 4) if (layer.texture.quadrant >= 4)
throw std::runtime_error("additional texture quadrant index error"); throw std::runtime_error("ESM4::LAND additional texture quadrant index error");
#if 0
FormId txt = layer.texture.formId;
std::map<FormId, int>::iterator lb = uniqueTextures.lower_bound(txt);
if (lb != uniqueTextures.end() && !(uniqueTextures.key_comp()(txt, lb->first)))
{
lb->second += 1;
}
else
uniqueTextures.insert(lb, std::make_pair(txt, 1));
#endif
#if 0
std::cout << "Additional Texture formId: 0x"
<< std::hex << layer.texture.formId
<< ", quad " << std::dec << (int)layer.texture.quadrant << std::endl;
std::cout << "Additional Texture layer: "
<< std::dec << (int)layer.texture.layerIndex << std::endl;
#endif
currentAddQuad = layer.texture.quadrant; currentAddQuad = layer.texture.quadrant;
break; break;
} }
@ -156,25 +180,17 @@ void ESM4::Land::load(ESM4::Reader& reader)
if (count) if (count)
{ {
layer.data.resize(count); layer.data.resize(count);
std::vector<ESM4::Land::VTXT>::iterator it = layer.data.begin(); for (ESM4::Land::VTXT& vtxt : layer.data)
for (; it != layer.data.end(); ++it) reader.get(vtxt);
{
reader.get(*it);
// FIXME: debug only
// std::cout << "pos: " << std::dec << (int)(*it).position << std::endl;
}
} }
mTextures[currentAddQuad].layers.push_back(layer);
// Assumed that the layers are added in the correct sequence if (layer.texture.layerIndex != mTextures[currentAddQuad].layers.size())
// FIXME: Knights.esp doesn't seem to observe this - investigate more throw std::runtime_error("ESM4::LAND additional texture skipping layer");
// assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()-1
//&& "additional texture layer index error"); mTextures[currentAddQuad].layers.push_back(layer);
currentAddQuad = -1; currentAddQuad = -1;
layer.data.clear(); layer.data.clear();
// FIXME: debug only
// std::cout << "VTXT: count " << std::dec << count << std::endl;
break; break;
} }
case ESM::fourCC("VTEX"): // only in Oblivion? case ESM::fourCC("VTEX"): // only in Oblivion?
@ -195,44 +211,14 @@ void ESM4::Land::load(ESM4::Reader& reader)
reader.skipSubRecordData(); reader.skipSubRecordData();
break; break;
default: default:
throw std::runtime_error("ESM4::LAND::load - Unknown subrecord " + ESM::printName(subHdr.typeId)); throw std::runtime_error("ESM4::LAND - Unknown subrecord " + ESM::printName(subHdr.typeId));
} }
} }
if (currentAddQuad != -1) if (currentAddQuad != -1)
{ {
// FIXME: not sure if it happens here as well // not sure if it happens here as well, if so just ignore
Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex << " quad " Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex << " quad "
<< static_cast<unsigned>(layer.texture.quadrant); << static_cast<unsigned>(layer.texture.quadrant);
mTextures[currentAddQuad].layers.push_back(layer);
} }
bool missing = false;
for (int i = 0; i < 4; ++i)
{
if (mTextures[i].base.formId == 0)
{
// std::cout << "ESM4::LAND " << ESM4::formIdToString(mFormId) << " missing base, quad " << i << std::endl;
// std::cout << "layers " << mTextures[i].layers.size() << std::endl;
// NOTE: can't set the default here since FO3/FONV may have different defaults
// mTextures[i].base.formId = 0x000008C0; // TerrainHDDirt01.dds
missing = true;
}
// else
//{
// std::cout << "ESM4::LAND " << ESM4::formIdToString(mFormId) << " base, quad " << i << std::endl;
// std::cout << "layers " << mTextures[i].layers.size() << std::endl;
// }
}
// at least one of the quadrants do not have a base texture, return without setting the flag
if (!missing)
mDataTypes |= LAND_VTEX;
} }
// void ESM4::Land::save(ESM4::Writer& writer) const
//{
// }
// void ESM4::Land::blank()
//{
// }

View File

@ -32,6 +32,7 @@
#include <components/esm/defs.hpp> #include <components/esm/defs.hpp>
#include <components/esm/formid.hpp> #include <components/esm/formid.hpp>
#include <components/vfs/pathutil.hpp>
namespace ESM4 namespace ESM4
{ {
@ -124,6 +125,8 @@ namespace ESM4
Texture mTextures[4]; // 0 = bottom left, 1 = bottom right, 2 = top left, 3 = top right Texture mTextures[4]; // 0 = bottom left, 1 = bottom right, 2 = top left, 3 = top right
std::vector<ESM::FormId> mIds; // land texture (LTEX) formids std::vector<ESM::FormId> mIds; // land texture (LTEX) formids
ESM::FormId mCell; ESM::FormId mCell;
VFS::Path::NormalizedView mDefaultDiffuseMap;
VFS::Path::NormalizedView mDefaultNormalMap;
void load(Reader& reader); void load(Reader& reader);

View File

@ -12,6 +12,8 @@
#include <components/esm/util.hpp> #include <components/esm/util.hpp>
#include <components/esm3/loadland.hpp> #include <components/esm3/loadland.hpp>
#include <components/esm4/loadland.hpp> #include <components/esm4/loadland.hpp>
#include <components/esm4/loadltex.hpp>
#include <components/esm4/loadtxst.hpp>
#include <components/misc/resourcehelpers.hpp> #include <components/misc/resourcehelpers.hpp>
#include <components/misc/strings/algorithm.hpp> #include <components/misc/strings/algorithm.hpp>
#include <components/vfs/manager.hpp> #include <components/vfs/manager.hpp>
@ -89,6 +91,8 @@ namespace ESMTerrain
LandObject::LandObject(const ESM4::Land& land, int loadFlags) LandObject::LandObject(const ESM4::Land& land, int loadFlags)
: mData(land, loadFlags) : mData(land, loadFlags)
{ {
mEsm4DefaultLayerInfo.mDiffuseMap = land.mDefaultDiffuseMap;
mEsm4DefaultLayerInfo.mNormalMap = land.mDefaultNormalMap;
} }
LandObject::LandObject(const ESM::Land& land, int loadFlags) LandObject::LandObject(const ESM::Land& land, int loadFlags)
@ -385,9 +389,105 @@ namespace ESMTerrain
return Misc::ResourceHelpers::correctTexturePath(texture, mVFS); return Misc::ResourceHelpers::correctTexturePath(texture, mVFS);
} }
void Storage::getEsm4Blendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace)
{
const osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize - 1, chunkSize + 1) * 0.5f;
const int startCellX = static_cast<int>(std::floor(origin.x()));
const int startCellY = static_cast<int>(std::floor(origin.y()));
constexpr int quadsPerCell = 2;
constexpr int quadSize = ESM4::Land::sVertsPerSide / quadsPerCell;
const int quadCount = static_cast<int>(chunkSize * quadsPerCell);
assert(quadCount > 0);
const int blendmapSize = quadCount * quadSize + 1;
LandCache cache(startCellX - 1, startCellY - 1, static_cast<std::size_t>(std::ceil(chunkSize)) + 2);
std::pair lastCell{ startCellX, startCellY };
const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache);
std::map<ESM::FormId, std::size_t> textureIndicesMap;
auto getOrCreateBlendmap = [&](ESM::FormId texId) -> unsigned char* {
auto found = textureIndicesMap.find(texId);
if (found != textureIndicesMap.end())
return blendmaps[found->second]->data();
Terrain::LayerInfo info
= texId.isZeroOrUnset() ? land->getEsm4DefaultLayerInfo() : getLandTextureLayerInfo(texId);
osg::ref_ptr<osg::Image> image(new osg::Image);
image->allocateImage(blendmapSize, blendmapSize, 1, GL_ALPHA, GL_UNSIGNED_BYTE);
std::memset(image->data(), 0, image->getTotalDataSize());
textureIndicesMap.emplace(texId, blendmaps.size());
blendmaps.push_back(std::move(image));
layerList.push_back(std::move(info));
return blendmaps.back()->data();
};
const auto handleSample = [&](const CellSample& sample) {
const std::pair cell{ sample.mCellX, sample.mCellY };
if (lastCell != cell)
{
land = getLand(ESM::ExteriorCellLocation(sample.mCellX, sample.mCellY, worldspace), cache);
lastCell = cell;
}
if (!land)
return;
const ESM::LandData* ldata = land->getData(0);
if (!ldata)
return;
int quad;
if (sample.mSrcRow == 0)
quad = sample.mSrcCol == 0 ? 0 : 2;
else
quad = sample.mSrcCol == 0 ? 1 : 3;
const ESM4::Land::Texture& ltex = ldata->getEsm4Texture(quad);
unsigned char* const baseBlendmap = getOrCreateBlendmap(ESM::FormId::fromUint32(ltex.base.formId));
int starty = (static_cast<int>(sample.mDstCol) - 1) * quadSize;
int startx = sample.mDstRow * quadSize;
for (int y = std::max(0, starty + 1); y <= starty + quadSize && y < blendmapSize; ++y)
{
unsigned char* const row = baseBlendmap + (blendmapSize - y - 1) * blendmapSize;
for (int x = startx; x < startx + quadSize && x < blendmapSize; ++x)
row[x] = 255;
}
for (const auto& layer : ltex.layers)
{
unsigned char* const layerBlendmap = getOrCreateBlendmap(ESM::FormId::fromUint32(layer.texture.formId));
for (const ESM4::Land::VTXT& v : layer.data)
{
int y = v.position / (quadSize + 1);
int x = v.position % (quadSize + 1);
if (x == quadSize || startx + x >= blendmapSize || y == 0 || starty + y >= blendmapSize
|| starty + y < 0)
{
continue;
}
int index = (blendmapSize - starty - y - 1) * blendmapSize + startx + x;
int delta = std::clamp(static_cast<int>(v.opacity * 255), 0, 255);
baseBlendmap[index] = std::max(0, baseBlendmap[index] - delta);
layerBlendmap[index] = delta;
}
}
};
sampleBlendmaps(chunkSize, origin.x(), origin.y(), quadsPerCell, handleSample);
if (blendmaps.size() == 1)
blendmaps.clear(); // If a single texture fills the whole terrain, there is no need to blend
}
void Storage::getBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps, void Storage::getBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace) std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace)
{ {
if (ESM::isEsm4Ext(worldspace))
{
getEsm4Blendmaps(chunkSize, chunkCenter, blendmaps, layerList, worldspace);
return;
}
const osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize, chunkSize) * 0.5f; const osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize, chunkSize) * 0.5f;
const int startCellX = static_cast<int>(std::floor(origin.x())); const int startCellX = static_cast<int>(std::floor(origin.x()));
const int startCellY = static_cast<int>(std::floor(origin.y())); const int startCellY = static_cast<int>(std::floor(origin.y()));
@ -613,6 +713,47 @@ namespace ESMTerrain
return info; return info;
} }
Terrain::LayerInfo Storage::getLandTextureLayerInfo(ESM::FormId id)
{
if (const ESM4::LandTexture* ltex = getEsm4LandTexture(id))
{
if (!ltex->mTextureFile.empty())
return getLayerInfo("textures/landscape/" + ltex->mTextureFile); // TES4
if (const ESM4::TextureSet* txst = getEsm4TextureSet(ltex->mTexture))
return getTextureSetLayerInfo(*txst); // TES5
else
Log(Debug::Warning) << "TextureSet not found: " << ltex->mTexture.toString();
}
else
Log(Debug::Warning) << "LandTexture not found: " << id.toString();
return getLayerInfo("");
}
Terrain::LayerInfo Storage::getTextureSetLayerInfo(const ESM4::TextureSet& txst)
{
Terrain::LayerInfo info;
assert(!txst.mDiffuse.empty() && "getlayerInfo: empty diffuse map");
info.mDiffuseMap = "textures/" + txst.mDiffuse;
if (!txst.mNormalMap.empty())
info.mNormalMap = "textures/" + txst.mNormalMap;
// FIXME: this flag indicates height info in alpha channel of normal map
// but the normal map alpha channel has specular info instead
// (probably needs some flag in the terrain shader to fix)
info.mParallax = false;
// FIXME: this flag indicates specular info in alpha channel of diffuse
// but the diffuse alpha channel has transparency data instead
// (probably needs some flag in the terrain shader to fix)
info.mSpecular = false;
// FIXME: should support other features of ESM4::TextureSet
// probably need corresponding support in the terrain shader
return info;
}
float Storage::getCellWorldSize(ESM::RefId worldspace) float Storage::getCellWorldSize(ESM::RefId worldspace)
{ {
return static_cast<float>(ESM::getCellSize(worldspace)); return static_cast<float>(ESM::getCellSize(worldspace));
@ -623,9 +764,12 @@ namespace ESMTerrain
return ESM::getLandSize(worldspace); return ESM::getLandSize(worldspace);
} }
int Storage::getBlendmapScale(float chunkSize) int Storage::getTextureTileCount(float chunkSize, ESM::RefId worldspace)
{ {
return ESM::Land::LAND_TEXTURE_SIZE * chunkSize; if (ESM::isEsm4Ext(worldspace))
return static_cast<int>(2 * ESM4::Land::sQuadTexturePerSide * chunkSize);
else
return static_cast<int>(ESM::Land::LAND_TEXTURE_SIZE * chunkSize);
} }
} }

View File

@ -4,6 +4,7 @@
#include <cassert> #include <cassert>
#include <mutex> #include <mutex>
#include <components/terrain/defs.hpp>
#include <components/terrain/storage.hpp> #include <components/terrain/storage.hpp>
#include <components/esm/esmterrain.hpp> #include <components/esm/esmterrain.hpp>
@ -13,6 +14,8 @@
namespace ESM4 namespace ESM4
{ {
struct Land; struct Land;
struct LandTexture;
struct TextureSet;
} }
namespace ESM namespace ESM
@ -51,9 +54,13 @@ namespace ESMTerrain
int getPlugin() const { return mData.getPlugin(); } int getPlugin() const { return mData.getPlugin(); }
const Terrain::LayerInfo& getEsm4DefaultLayerInfo() const { return mEsm4DefaultLayerInfo; }
private: private:
ESM::LandData mData; ESM::LandData mData;
Terrain::LayerInfo mEsm4DefaultLayerInfo;
LandObject(const LandObject& copy, const osg::CopyOp& copyOp); LandObject(const LandObject& copy, const osg::CopyOp& copyOp);
}; };
@ -74,6 +81,11 @@ namespace ESMTerrain
// Not implemented in this class, because we need different Store implementations for game and editor // Not implemented in this class, because we need different Store implementations for game and editor
virtual osg::ref_ptr<const LandObject> getLand(ESM::ExteriorCellLocation cellLocation) = 0; virtual osg::ref_ptr<const LandObject> getLand(ESM::ExteriorCellLocation cellLocation) = 0;
virtual const std::string* getLandTexture(std::uint16_t index, int plugin) = 0; virtual const std::string* getLandTexture(std::uint16_t index, int plugin) = 0;
// Not implemented in this class because requires ESMStore
virtual const ESM4::LandTexture* getEsm4LandTexture(ESM::RefId ltexId) const { return nullptr; }
virtual const ESM4::TextureSet* getEsm4TextureSet(ESM::RefId txstId) const { return nullptr; }
/// Get bounds of the whole terrain in cell units /// Get bounds of the whole terrain in cell units
void getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) override = 0; void getBounds(float& minX, float& maxX, float& minY, float& maxY, ESM::RefId worldspace) override = 0;
@ -120,7 +132,7 @@ namespace ESMTerrain
/// Get the number of vertices on one side for each cell. Should be (power of two)+1 /// Get the number of vertices on one side for each cell. Should be (power of two)+1
int getCellVertices(ESM::RefId worldspace) override; int getCellVertices(ESM::RefId worldspace) override;
int getBlendmapScale(float chunkSize) override; int getTextureTileCount(float chunkSize, ESM::RefId worldspace) override;
float getVertexHeight(const ESM::LandData* data, int x, int y) float getVertexHeight(const ESM::LandData* data, int x, int y)
{ {
@ -159,6 +171,11 @@ namespace ESMTerrain
bool mAutoUseSpecularMaps; bool mAutoUseSpecularMaps;
Terrain::LayerInfo getLayerInfo(const std::string& texture); Terrain::LayerInfo getLayerInfo(const std::string& texture);
Terrain::LayerInfo getTextureSetLayerInfo(const ESM4::TextureSet& txst);
Terrain::LayerInfo getLandTextureLayerInfo(ESM::FormId id);
void getEsm4Blendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace);
}; };
} }

View File

@ -5,6 +5,7 @@
#include <osgUtil/IncrementalCompileOperation> #include <osgUtil/IncrementalCompileOperation>
#include <components/esm/util.hpp>
#include <components/resource/objectcache.hpp> #include <components/resource/objectcache.hpp>
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
@ -205,10 +206,10 @@ namespace Terrain
blendmapTextures.push_back(texture); blendmapTextures.push_back(texture);
} }
float blendmapScale = mStorage->getBlendmapScale(chunkSize); float tileCount = mStorage->getTextureTileCount(chunkSize, mWorldspace);
return ::Terrain::createPasses( return ::Terrain::createPasses(
useShaders, mSceneManager, layers, blendmapTextures, blendmapScale, blendmapScale); useShaders, mSceneManager, layers, blendmapTextures, tileCount, tileCount, ESM::isEsm4Ext(mWorldspace));
} }
osg::ref_ptr<osg::Node> ChunkManager::createChunk(float chunkSize, const osg::Vec2f& chunkCenter, unsigned char lod, osg::ref_ptr<osg::Node> ChunkManager::createChunk(float chunkSize, const osg::Vec2f& chunkCenter, unsigned char lod,

View File

@ -224,7 +224,7 @@ namespace Terrain
{ {
std::vector<osg::ref_ptr<osg::StateSet>> createPasses(bool useShaders, Resource::SceneManager* sceneManager, std::vector<osg::ref_ptr<osg::StateSet>> createPasses(bool useShaders, Resource::SceneManager* sceneManager,
const std::vector<TextureLayer>& layers, const std::vector<osg::ref_ptr<osg::Texture2D>>& blendmaps, const std::vector<TextureLayer>& layers, const std::vector<osg::ref_ptr<osg::Texture2D>>& blendmaps,
int blendmapScale, float layerTileSize) int blendmapScale, float layerTileSize, bool esm4terrain)
{ {
auto& shaderManager = sceneManager->getShaderManager(); auto& shaderManager = sceneManager->getShaderManager();
std::vector<osg::ref_ptr<osg::StateSet>> passes; std::vector<osg::ref_ptr<osg::StateSet>> passes;
@ -269,7 +269,8 @@ namespace Terrain
osg::ref_ptr<osg::Texture2D> blendmap = blendmaps.at(blendmapIndex++); osg::ref_ptr<osg::Texture2D> blendmap = blendmaps.at(blendmapIndex++);
stateset->setTextureAttributeAndModes(1, blendmap.get()); stateset->setTextureAttributeAndModes(1, blendmap.get());
stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); if (!esm4terrain)
stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale));
stateset->addUniform(UniformCollection::value().mBlendMap); stateset->addUniform(UniformCollection::value().mBlendMap);
} }
@ -329,7 +330,8 @@ namespace Terrain
stateset->setTextureAttributeAndModes(1, blendmap.get()); stateset->setTextureAttributeAndModes(1, blendmap.get());
// This is to map corner vertices directly to the center of a blendmap texel. // This is to map corner vertices directly to the center of a blendmap texel.
stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); if (!esm4terrain)
stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale));
stateset->setTextureAttributeAndModes(1, TexEnvCombine::value(), osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(1, TexEnvCombine::value(), osg::StateAttribute::ON);
} }
} }

View File

@ -20,14 +20,13 @@ namespace Terrain
{ {
osg::ref_ptr<osg::Texture2D> mDiffuseMap; osg::ref_ptr<osg::Texture2D> mDiffuseMap;
osg::ref_ptr<osg::Texture2D> mNormalMap; // optional osg::ref_ptr<osg::Texture2D> mNormalMap; // optional
bool mParallax; bool mParallax = false;
bool mSpecular; bool mSpecular = false;
}; };
std::vector<osg::ref_ptr<osg::StateSet>> createPasses(bool useShaders, Resource::SceneManager* sceneManager, std::vector<osg::ref_ptr<osg::StateSet>> createPasses(bool useShaders, Resource::SceneManager* sceneManager,
const std::vector<TextureLayer>& layers, const std::vector<osg::ref_ptr<osg::Texture2D>>& blendmaps, const std::vector<TextureLayer>& layers, const std::vector<osg::ref_ptr<osg::Texture2D>>& blendmaps,
int blendmapScale, float layerTileSize); int blendmapScale, float layerTileSize, bool esm4terrain = false);
} }
#endif #endif

View File

@ -289,11 +289,12 @@ namespace Terrain
, mLodFactor(lodFactor) , mLodFactor(lodFactor)
, mVertexLodMod(vertexLodMod) , mVertexLodMod(vertexLodMod)
, mViewDistance(std::numeric_limits<float>::max()) , mViewDistance(std::numeric_limits<float>::max())
, mMinSize(ESM::isEsm4Ext(worldspace) ? 1 / 4.f : 1 / 8.f) , mMinSize(ESM::isEsm4Ext(worldspace) ? 1 / 2.f : 1 / 8.f)
, mDebugTerrainChunks(debugChunks) , mDebugTerrainChunks(debugChunks)
{ {
mChunkManager->setCompositeMapSize(compMapResolution); mChunkManager->setCompositeMapSize(compMapResolution);
mChunkManager->setCompositeMapLevel(compMapLevel); mChunkManager->setCompositeMapLevel(
ESM::isEsm4Ext(worldspace) ? compMapLevel * 2 /*because cells are twice smaller*/ : compMapLevel);
mChunkManager->setMaxCompositeGeometrySize(maxCompGeometrySize); mChunkManager->setMaxCompositeGeometrySize(maxCompGeometrySize);
mChunkManagers.push_back(mChunkManager.get()); mChunkManagers.push_back(mChunkManager.get());

View File

@ -88,7 +88,8 @@ namespace Terrain
/// Get the number of vertices on one side for each cell. Should be (power of two)+1 /// Get the number of vertices on one side for each cell. Should be (power of two)+1
virtual int getCellVertices(ESM::RefId worldspace) = 0; virtual int getCellVertices(ESM::RefId worldspace) = 0;
virtual int getBlendmapScale(float chunkSize) = 0; /// Get the number of texture tiles on one side per chunk (chunkSize 1.0 = 1 cell).
virtual int getTextureTileCount(float chunkSize, ESM::RefId worldspace) = 0;
}; };
} }

View File

@ -9,6 +9,8 @@
#include "heightcull.hpp" #include "heightcull.hpp"
#include "storage.hpp" #include "storage.hpp"
#include "view.hpp" #include "view.hpp"
#include <components/esm/util.hpp>
#include <components/sceneutil/positionattitudetransform.hpp> #include <components/sceneutil/positionattitudetransform.hpp>
namespace Terrain namespace Terrain
@ -27,13 +29,13 @@ namespace Terrain
unsigned int borderMask) unsigned int borderMask)
: Terrain::World( : Terrain::World(
parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask, worldspace, expiryDelay) parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask, worldspace, expiryDelay)
, mNumSplits(4) , mNumSplits(ESM::isEsm4Ext(worldspace) ? 2 : 4)
{ {
} }
TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, ESM::RefId worldspace, unsigned int nodeMask) TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, ESM::RefId worldspace, unsigned int nodeMask)
: Terrain::World(parent, storage, nodeMask, worldspace) : Terrain::World(parent, storage, nodeMask, worldspace)
, mNumSplits(4) , mNumSplits(ESM::isEsm4Ext(worldspace) ? 2 : 4)
{ {
} }