From b8192138afbbad58f01288a7e7a30c75ea7441e2 Mon Sep 17 00:00:00 2001
From: "florent.teppe" <teppe.florent@hotmail.fr>
Date: Mon, 12 Aug 2024 20:32:27 +0200
Subject: [PATCH 1/6] initial commit applying changes from cc9cii

---
 apps/openmw/mwrender/groundcover.cpp         |   2 +-
 apps/openmw/mwrender/groundcover.hpp         |   2 +-
 apps/openmw/mwrender/landmanager.cpp         |   7 +
 apps/openmw/mwrender/landmanager.hpp         |   8 +
 apps/openmw/mwrender/objectpaging.cpp        |   2 +-
 apps/openmw/mwrender/objectpaging.hpp        |   2 +-
 apps/openmw/mwrender/renderingmanager.cpp    |  21 +
 apps/openmw/mwrender/terrainstorage.cpp      |  28 ++
 apps/openmw/mwrender/terrainstorage.hpp      |  16 +
 apps/openmw/mwworld/esmstore.hpp             |   3 +-
 apps/openmw/mwworld/scene.cpp                |   4 +
 apps/openmw/mwworld/store.cpp                |   3 +-
 components/esm/records.hpp                   |   1 +
 components/esm4/loadland.cpp                 | 117 ++---
 components/esmterrain/storage.cpp            | 493 ++++++++++++++++++-
 components/esmterrain/storage.hpp            |  19 +
 components/terrain/chunkmanager.cpp          |  80 ++-
 components/terrain/chunkmanager.hpp          |   9 +-
 components/terrain/defs.hpp                  |   1 +
 components/terrain/material.cpp              |  20 +-
 components/terrain/material.hpp              |   2 +-
 components/terrain/quadtreeworld.cpp         |  39 +-
 components/terrain/quadtreeworld.hpp         |   2 +-
 components/terrain/terraingrid.cpp           |  39 +-
 components/terrain/terraingrid.hpp           |   3 +-
 files/shaders/compatibility/esm4terrain.frag | 110 +++++
 files/shaders/compatibility/esm4terrain.vert |  73 +++
 27 files changed, 1011 insertions(+), 95 deletions(-)
 create mode 100644 files/shaders/compatibility/esm4terrain.frag
 create mode 100644 files/shaders/compatibility/esm4terrain.vert

diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp
index 7a29f1bb07..97d2025a31 100644
--- a/apps/openmw/mwrender/groundcover.cpp
+++ b/apps/openmw/mwrender/groundcover.cpp
@@ -322,7 +322,7 @@ namespace MWRender
     }
 
     osg::ref_ptr<osg::Node> Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod,
-        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
+        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/)
     {
         if (lod > getMaxLodLevel())
             return nullptr;
diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp
index df40d9d529..51c2cb4628 100644
--- a/apps/openmw/mwrender/groundcover.hpp
+++ b/apps/openmw/mwrender/groundcover.hpp
@@ -27,7 +27,7 @@ namespace MWRender
         ~Groundcover();
 
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags,
-            bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override;
+            bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/) override;
 
         unsigned int getNodeMask() override;
 
diff --git a/apps/openmw/mwrender/landmanager.cpp b/apps/openmw/mwrender/landmanager.cpp
index d17933b2b7..ed7d0aa71e 100644
--- a/apps/openmw/mwrender/landmanager.cpp
+++ b/apps/openmw/mwrender/landmanager.cpp
@@ -49,6 +49,13 @@ namespace MWRender
         return landObj;
     }
 
+    const ESM4::Land *LandManager::getLandRecord(ESM::ExteriorCellLocation cellIndex) const
+    {
+        const MWBase::World& world = *MWBase::Environment::get().getWorld();
+        const ESM4::Land* land = world.getStore().get<ESM4::Land>().search(cellIndex);
+        return land;
+    }
+
     void LandManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const
     {
         Resource::reportStats("Land", frameNumber, mCache->getStats(), *stats);
diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp
index 1b82f32ce9..21bb80cf48 100644
--- a/apps/openmw/mwrender/landmanager.hpp
+++ b/apps/openmw/mwrender/landmanager.hpp
@@ -12,6 +12,11 @@ namespace ESM
     struct Land;
 }
 
+namespace ESM4
+{
+    struct Land;
+}
+
 namespace MWRender
 {
 
@@ -23,6 +28,9 @@ namespace MWRender
         /// @note Will return nullptr if not found.
         osg::ref_ptr<ESMTerrain::LandObject> getLand(ESM::ExteriorCellLocation cellIndex);
 
+        // FIXME: returning a pointer is probably not compatible with the rest of the codebase
+        const ESM4::Land *getLandRecord(ESM::ExteriorCellLocation cellIndex) const;
+
         void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
 
     private:
diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp
index 6799b2358b..ff7436e862 100644
--- a/apps/openmw/mwrender/objectpaging.cpp
+++ b/apps/openmw/mwrender/objectpaging.cpp
@@ -78,7 +78,7 @@ namespace MWRender
     }
 
     osg::ref_ptr<osg::Node> ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char /*lod*/,
-        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
+        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/)
     {
         if (activeGrid && !mActiveGrid)
             return nullptr;
diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp
index 11be6009ca..34214bc476 100644
--- a/apps/openmw/mwrender/objectpaging.hpp
+++ b/apps/openmw/mwrender/objectpaging.hpp
@@ -24,7 +24,7 @@ namespace MWRender
         ~ObjectPaging() = default;
 
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags,
-            bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override;
+            bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/) override;
 
         osg::ref_ptr<osg::Node> createChunk(float size, const osg::Vec2f& center, bool activeGrid,
             const osg::Vec3f& viewPoint, bool compile, unsigned char lod);
diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp
index 469d71fc4f..0516bb9700 100644
--- a/apps/openmw/mwrender/renderingmanager.cpp
+++ b/apps/openmw/mwrender/renderingmanager.cpp
@@ -49,6 +49,7 @@
 #include <components/terrain/quadtreeworld.hpp>
 #include <components/terrain/terraingrid.hpp>
 
+#include <components/esm/util.hpp>
 #include <components/esm3/loadcell.hpp>
 #include <components/esm4/loadcell.hpp>
 
@@ -490,6 +491,8 @@ namespace MWRender
         const bool useTerrainNormalMaps = Settings::shaders().mAutoUseTerrainNormalMaps;
         const bool useTerrainSpecularMaps = Settings::shaders().mAutoUseTerrainSpecularMaps;
 
+        // NOTE: Maybe we need to swap out this with a different storage type during
+        //       enableTerrain() if we are in a foreign (i.e. non-Morrowind) worldspace.
         mTerrainStorage = std::make_unique<TerrainStorage>(mResourceSystem, normalMapPattern, heightMapPattern,
             useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps);
 
@@ -798,12 +801,20 @@ namespace MWRender
         mWater->removeCell(store);
     }
 
+    // NOTE: For cc9cii's fork, this is where the terrain storage type is decided when we know
+    //       the worldspace.  But in this implementation the decision is made in the
+    //       constructor.
     void RenderingManager::enableTerrain(bool enable, ESM::RefId worldspace)
     {
         if (!enable)
             mWater->setCullCallback(nullptr);
         else
         {
+            // need to set our ESM4 state in mTerrainStorage here to change some behaviours in
+            // ESMTerrain::Storage
+            if (ESM::isEsm4Ext(worldspace))
+                mTerrainStorage->setIsEsm4Ext(true);
+
             WorldspaceChunkMgr& newChunks = getWorldspaceChunkMgr(worldspace);
             if (newChunks.mTerrain.get() != mTerrain)
             {
@@ -1445,6 +1456,16 @@ namespace MWRender
         mStateUpdater->setFogColor(color);
     }
 
+    // NOTE: mTerrainStorage is a unique_ptr to MWRender::TerrainStorage which is a child
+    //       class of ESMTerrain::Storage.  This makes switching out ESMTerrain::Storage with
+    //       another class impossible unless we decide to accept a man-in-the-middle type mess.
+    //
+    //       mTerrainStorage is initialised in the constructor.  It's probably not a good idea
+    //       to chnage that when we switch to another worldspace so we'll need a different
+    //       solution.
+    //
+    //       So maybe we have to accept the "least bad option" and have the complications
+    //       within MWRender::TerrainStorage e.g. set a state variable (see enableTerrain())
     RenderingManager::WorldspaceChunkMgr& RenderingManager::getWorldspaceChunkMgr(ESM::RefId worldspace)
     {
         auto existingChunkMgr = mWorldspaceChunks.find(worldspace);
diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp
index 9776d7e632..a4e0c52025 100644
--- a/apps/openmw/mwrender/terrainstorage.cpp
+++ b/apps/openmw/mwrender/terrainstorage.cpp
@@ -2,6 +2,8 @@
 
 #include <components/esm3/loadland.hpp>
 #include <components/esm4/loadwrld.hpp>
+#include <components/esm4/loadltex.hpp>
+#include <components/esm4/loadtxst.hpp>
 
 #include "../mwbase/environment.hpp"
 #include "../mwworld/esmstore.hpp"
@@ -105,10 +107,36 @@ namespace MWRender
         return mLandManager->getLand(cellLocation);
     }
 
+    // NOTE: We need to return different land texture if mIsEsm4Ext is set.
+    //       But there isn't just one texture for a given land, so the method name
+    //       is misleading and may cause confusion for future maintainers.
+    //
+    //       It's a pity we have to use pointers to string here.  FormId would have been
+    //       more than adequate.
+    //
+    // Update: Decided to add new methods instead.  See getEsm4Land(), getEsm4LandTexture()
+    //        and getEsm4TextureSet().
     const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin)
     {
         const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
         return esmStore.get<ESM::LandTexture>().search(index, plugin);
     }
 
+    const ESM4::Land *TerrainStorage::getEsm4Land(ESM::ExteriorCellLocation cellLocation) const
+    {
+        return mLandManager->getLandRecord(cellLocation);
+    }
+
+    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);
+    }
+
 }
diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp
index 731f396713..85968ded96 100644
--- a/apps/openmw/mwrender/terrainstorage.hpp
+++ b/apps/openmw/mwrender/terrainstorage.hpp
@@ -7,6 +7,13 @@
 
 #include <components/resource/resourcesystem.hpp>
 
+namespace ESM4
+{
+    struct Land;
+    struct LandTexture;
+    struct TextureSet;
+}
+
 namespace MWRender
 {
 
@@ -31,6 +38,15 @@ namespace MWRender
 
         LandManager* getLandManager() const;
 
+        const ESM4::Land *getEsm4Land(ESM::ExteriorCellLocation cellLocation) const override;
+        const ESM4::LandTexture *getEsm4LandTexture(ESM::RefId ltexId) const override;
+        const ESM4::TextureSet *getEsm4TextureSet(ESM::RefId txstId) const override;
+
+        // Intended to be set by RenderingManager.  Ideally this should be part of the
+        // construction but this class is initialised in the constructor and we man end up with
+        // a different terrain during RenderingManager::enableTerrain().
+        void setIsEsm4Ext(bool state) { mIsEsm4Ext = state; }
+
     private:
         std::unique_ptr<LandManager> mLandManager;
 
diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp
index d8cfd1dcdf..a1e3f1fc03 100644
--- a/apps/openmw/mwworld/esmstore.hpp
+++ b/apps/openmw/mwworld/esmstore.hpp
@@ -94,6 +94,7 @@ namespace ESM4
     struct ItemMod;
     struct Land;
     struct LandTexture;
+    struct TextureSet;
     struct LevelledCreature;
     struct LevelledItem;
     struct LevelledNpc;
@@ -147,7 +148,7 @@ namespace MWWorld
             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::Static>, Store<ESM4::StaticCollection>, Store<ESM4::Terminal>, Store<ESM4::Tree>,
-            Store<ESM4::Weapon>, Store<ESM4::World>>;
+            Store<ESM4::Weapon>, Store<ESM4::World>, Store<ESM4::TextureSet> >;
 
     private:
         template <typename T>
diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp
index 52917e92e3..694d076c33 100644
--- a/apps/openmw/mwworld/scene.cpp
+++ b/apps/openmw/mwworld/scene.cpp
@@ -438,6 +438,10 @@ namespace MWWorld
 
         if (cellVariant.isExterior())
         {
+            // NOTE: LandObject may be of type ESM4
+            // NOTE: It's probably a very bad idea to keep the pointer returned from
+            //       getLandManager() since it may change if the worldspace type changes from
+            //       TES3 to TES4 and vice-versa (depends on the final implementation).
             osg::ref_ptr<const ESMTerrain::LandObject> land = mRendering.getLandManager()->getLand(cellIndex);
             const ESM::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr;
             const int verts = ESM::getLandSize(worldspace);
diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp
index 1e92df85ec..6718db86c8 100644
--- a/apps/openmw/mwworld/store.cpp
+++ b/apps/openmw/mwworld/store.cpp
@@ -1332,7 +1332,7 @@ template class MWWorld::TypedDynamicStore<ESM4::HeadPart>;
 template class MWWorld::TypedDynamicStore<ESM4::Ingredient>;
 template class MWWorld::TypedDynamicStore<ESM4::ItemMod>;
 template class MWWorld::TypedDynamicStore<ESM4::Land>;
-template class MWWorld::TypedDynamicStore<ESM4::LandTexture>;
+template class MWWorld::TypedDynamicStore<ESM4::LandTexture>; // FIXME: maybe we need some special handling?
 template class MWWorld::TypedDynamicStore<ESM4::LevelledCreature>;
 template class MWWorld::TypedDynamicStore<ESM4::LevelledItem>;
 template class MWWorld::TypedDynamicStore<ESM4::LevelledNpc>;
@@ -1346,6 +1346,7 @@ template class MWWorld::TypedDynamicStore<ESM4::Race>;
 template class MWWorld::TypedDynamicStore<ESM4::Static>;
 template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>;
 template class MWWorld::TypedDynamicStore<ESM4::Terminal>;
+template class MWWorld::TypedDynamicStore<ESM4::TextureSet>;
 template class MWWorld::TypedDynamicStore<ESM4::Tree>;
 template class MWWorld::TypedDynamicStore<ESM4::Weapon>;
 template class MWWorld::TypedDynamicStore<ESM4::World>;
diff --git a/components/esm/records.hpp b/components/esm/records.hpp
index 0b60b44cf0..2f67629be5 100644
--- a/components/esm/records.hpp
+++ b/components/esm/records.hpp
@@ -77,6 +77,7 @@
 #include <components/esm4/loadstat.hpp>
 #include <components/esm4/loadterm.hpp>
 #include <components/esm4/loadtree.hpp>
+#include <components/esm4/loadtxst.hpp>
 #include <components/esm4/loadweap.hpp>
 #include <components/esm4/loadwrld.hpp>
 
diff --git a/components/esm4/loadland.cpp b/components/esm4/loadland.cpp
index 53fb1de083..e3e6bb65d5 100644
--- a/components/esm4/loadland.cpp
+++ b/components/esm4/loadland.cpp
@@ -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
   warranty.  In no event will the authors be held liable for any damages
@@ -17,7 +17,7 @@
      misrepresented as being the original software.
   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
   from Tes4Mod:Mod_File_Format and Tes5Mod:File_Formats but also refined by
@@ -27,13 +27,31 @@
 #include "loadland.hpp"
 
 #include <cstdint>
+#include <cassert>
 #include <stdexcept>
+#include <iostream>
 
 #include <components/debug/debuglog.hpp>
 
 #include "reader.hpp"
 // #include "writer.hpp"
 
+namespace
+{
+    std::uint32_t getDefaultTexture(bool isTES4, bool isFONV, bool isTES5)
+    {
+        // WARN: guessed for FO3/FONV (might be Dirt02)
+        if (isTES4)
+            return 0x000008C0; // TerrainHDDirt01.dds (LTEX)
+        else if (isFONV)
+            return 0x00000A0D; // Landscape\Dirt01.dds (TXST 0x00004453)
+        else if (isTES5)
+            return 0x00000C16; // Landscape\Dirt02.dds (TXST 0x00000C0F)
+        else // FO3
+            return 0x00000A0D; // Landscape\Dirt01.dds (prob. same as FONV)
+    }
+}
+
 //             overlap north
 //
 //         32
@@ -53,13 +71,20 @@ void ESM4::Land::load(ESM4::Reader& reader)
 {
     mId = reader.getFormIdFromHeader();
     mFlags = reader.hdr().record.flags;
+
+    std::uint32_t esmVer = reader.esmVersion();
+    bool isTES4 = (esmVer == ESM::VER_080 || esmVer == ESM::VER_100);
+    bool isFONV = (esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134);
+    bool isTES5 = (esmVer == ESM::VER_094 || esmVer == ESM::VER_170); // WARN: FO3 is also VER_094
+    // WARN: below workaround assumes the data directory path has "Fallout" somewhere
+    if (esmVer == ESM4::VER_094 && reader.getContext().filename.find("allout") != std::string::npos)
+        isTES5 = false; // FIXME: terrible hack
+
     mDataTypes = 0;
     mCell = reader.currCell();
     TxtLayer layer;
     std::int8_t currentAddQuad = -1; // for VTXT following ATXT
 
-    // std::map<FormId, int> uniqueTextures; // FIXME: for temp testing only
-
     while (reader.getSubRecordHeader())
     {
         const ESM4::SubRecordHeader& subHdr = reader.subRecordHeader();
@@ -78,12 +103,6 @@ void ESM4::Land::load(ESM4::Reader& reader)
             }
             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);
                 mDataTypes |= LAND_VHGT;
                 break;
@@ -104,11 +123,6 @@ void ESM4::Land::load(ESM4::Reader& reader)
 
                     reader.adjustFormId(base.formId);
                     mTextures[base.quadrant].base = std::move(base);
-#if 0
-                    std::cout << "Base Texture formid: 0x"
-                        << std::hex << mTextures[base.quadrant].base.formId
-                        << ", quad " << std::dec << (int)base.quadrant << std::endl;
-#endif
                 }
                 break;
             }
@@ -116,31 +130,27 @@ void ESM4::Land::load(ESM4::Reader& reader)
             {
                 if (currentAddQuad != -1)
                 {
-                    // FIXME: sometimes there are no VTXT following an ATXT?  Just add a dummy one for now
-                    Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex;
+                    // NOTE: sometimes there are no VTXT following an ATXT
+                    //Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex
+                        //<< " FormId " << ESM::FormId::toString(mFormId) << std::endl;
+                    if (!layer.texture.formId)
+                      layer.texture.formId = getDefaultTexture(isTES4, isFONV, isTES5);
+
+                    layer.data.resize(1);            // just one spot
+                    layer.data.back().position = 0;  // this corner
+                    layer.data.back().opacity = 0.f; // transparent
+
+                    assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()
+                            && "additional texture skipping layer");
+
                     mTextures[currentAddQuad].layers.push_back(layer);
                 }
+
                 reader.get(layer.texture);
                 reader.adjustFormId(layer.texture.formId);
                 if (layer.texture.quadrant >= 4)
                     throw std::runtime_error("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;
                 break;
             }
@@ -158,23 +168,16 @@ void ESM4::Land::load(ESM4::Reader& reader)
                     layer.data.resize(count);
                     std::vector<ESM4::Land::VTXT>::iterator it = layer.data.begin();
                     for (; it != layer.data.end(); ++it)
-                    {
                         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
-                // FIXME: Knights.esp doesn't seem to observe this - investigate more
-                // assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()-1
-                //&& "additional texture layer index error");
+                assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()
+                        && "additional texture skipping layer");
+
+                mTextures[currentAddQuad].layers.push_back(layer);
 
                 currentAddQuad = -1;
                 layer.data.clear();
-                // FIXME: debug only
-                // std::cout << "VTXT: count " << std::dec << count << std::endl;
                 break;
             }
             case ESM::fourCC("VTEX"): // only in Oblivion?
@@ -199,34 +202,22 @@ void ESM4::Land::load(ESM4::Reader& reader)
         }
     }
 
+    //if (mCell.toUint32() == 0x00005e1f)
+        //std::cout << "vilverin exterior" << std::endl;
+
     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 "
                             << static_cast<unsigned>(layer.texture.quadrant);
-        mTextures[currentAddQuad].layers.push_back(layer);
     }
 
-    bool missing = false;
     for (int i = 0; i < 4; ++i)
     {
+        // just use some defaults
         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;
-        // }
+            mTextures[i].base.formId = getDefaultTexture(isTES4, isFONV, isTES5);
     }
-    // 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
diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp
index 35ec814aa2..d4e6f935cf 100644
--- a/components/esmterrain/storage.cpp
+++ b/components/esmterrain/storage.cpp
@@ -12,6 +12,8 @@
 #include <components/esm/util.hpp>
 #include <components/esm3/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/strings/algorithm.hpp>
 #include <components/vfs/manager.hpp>
@@ -40,6 +42,26 @@ namespace ESMTerrain
 
             return { tex, land->getPlugin() };
         }
+#if 0
+        UniqueTextureId getQuadTextureIdAt(const ESM4::Land* land, std::size_t x, std::size_t y)
+        {
+            assert(x < 17);
+            assert(y < 17);
+
+            if (land == nullptr)
+                return { 0, 0 };
+
+            const ESM::LandData* data = land->getData(ESM::Land::DATA_VTEX);
+            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() };
+        }
+#endif
     }
 
     class LandCache
@@ -106,7 +128,8 @@ namespace ESMTerrain
     Storage::Storage(const VFS::Manager* vfs, std::string_view normalMapPattern,
         std::string_view normalHeightMapPattern, bool autoUseNormalMaps, std::string_view specularMapPattern,
         bool autoUseSpecularMaps)
-        : mVFS(vfs)
+        : mIsEsm4Ext(false)
+        , mVFS(vfs)
         , mNormalMapPattern(normalMapPattern)
         , mNormalHeightMapPattern(normalHeightMapPattern)
         , mAutoUseNormalMaps(autoUseNormalMaps)
@@ -366,6 +389,10 @@ namespace ESMTerrain
             std::fill(positions.begin(), positions.end(), osg::Vec3f());
     }
 
+    // NOTE: getLandTexture() is implemented by our child class.  Also note that ESM4 doesn't
+    //       want to call correctTexturePath().  We need a way of figuring out that we are in
+    //       ESM4 worldspace, either via a parameter or via a state kept as a member to Storage
+    //       or TerrainStorage (i.e. our child class).
     std::string Storage::getTextureName(UniqueTextureId id)
     {
         std::string_view texture = "_land_default.dds";
@@ -385,6 +412,37 @@ namespace ESMTerrain
         return Misc::ResourceHelpers::correctTexturePath(texture, mVFS);
     }
 
+    // FIXME: for FO3/FONV/TES5 this is rather inefficient since the TextureSet indicates
+    //        whether normal map exists, etc, saving us the need to do any searching
+    //        in getLayerInfo()
+    //
+    //        maybe if ltex->mTextureFile is empty simply return a null string and process
+    //        differently?
+    std::string Storage::getEsm4TextureName(ESM::RefId id)
+    {
+        //if (mIsEsm4Ext)
+        if (const ESM4::LandTexture *ltex = getEsm4LandTexture(id))
+        {
+            if (ltex->mTextureFile.empty()) // WARN: we assume FO3/FONV/TES5
+            {
+                if (const ESM4::TextureSet *txst = getEsm4TextureSet(ltex->mTexture))
+                {
+                    return "textures\\"+txst->mDiffuse;
+                }
+            }
+            else
+                return "textures\\landscape\\"+ltex->mTextureFile;
+        }
+
+        // FIXME: add a debug log here
+        return "";
+    }
+
+    // FIXME: May need some changes here to support ESM4 terrain.  Not sure how to deal with many
+    //        chunks (i.e. 4 ESM4 quads).  Maybe we just go with the flow here, but do
+    //        things 4 times as much?
+    //
+    //        For now decided to create another method instead (getQuadBlendmaps).
     void Storage::getBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
         std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace)
     {
@@ -577,6 +635,7 @@ namespace ESMTerrain
         Terrain::LayerInfo info;
         info.mParallax = false;
         info.mSpecular = false;
+        //info.mIsEsm4 = false; // hint for Terrain::createPasses()
         info.mDiffuseMap = texture;
 
         if (mAutoUseNormalMaps)
@@ -613,6 +672,46 @@ namespace ESMTerrain
         return info;
     }
 
+    Terrain::LayerInfo Storage::getLayerInfo(const ESM4::TextureSet *txst)
+    {
+        Terrain::LayerInfo info;
+        info.mDiffuseMap = "";
+        info.mNormalMap = "";
+        info.mParallax = false;
+        info.mSpecular = false;
+        //info.mIsEsm4 = true; // hint for Terrain::createPasses()
+
+        if (txst)
+        {
+            assert(!txst->mDiffuseMap.empty() && "getlayerInfo: empty diffuse map");
+
+            std::string diffuse = "textures\\landscape\\"+txst->mDiffuse;
+            std::map<std::string, Terrain::LayerInfo>::iterator found = mLayerInfoMap.find(diffuse);
+            if (found != mLayerInfoMap.end())
+                return found->second;
+
+            info.mDiffuseMap = diffuse;
+            if (!txst->mNormalMap.empty())
+                info.mNormalMap = "textures\\landscape\\"+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
+
+            mLayerInfoMap[diffuse] = info;
+        }
+
+        return info;
+    }
+
     float Storage::getCellWorldSize(ESM::RefId worldspace)
     {
         return static_cast<float>(ESM::getCellSize(worldspace));
@@ -623,9 +722,401 @@ namespace ESMTerrain
         return ESM::getLandSize(worldspace);
     }
 
+    // NOTE: For now we are only conident when chunkSize is 1.  Needs more testing to see
+    //       if this will work with different chunkSize values.
+    //
+    //       This is called by ChunkManager::createPasses() which then calls
+    //       Terrain::createPasses() which ultimately calls BlendmapTexMat::value().
+    //       I suspect that is where UV mapping is done (just a guess; LayerTexMat
+    //       may need to be looked at as well).
+    //
+    // WARN: the value sQuadTexturePerSide was determined empirically for TES4 only
+    //       FO3/FONV/TES5 may well have a different value - needs testing
     int Storage::getBlendmapScale(float chunkSize)
     {
+        if (mIsEsm4Ext)
+        {
+            //std::cout << "blendmap scale "
+                //<< std::to_string(ESM4::Land::sQuadTexturePerSide * chunkSize) << std::endl;
+            return ESM4::Land::sQuadTexturePerSide;// * chunkSize;
+        }
+
         return ESM::Land::LAND_TEXTURE_SIZE * chunkSize;
     }
 
+    void Storage::fillQuadVertexBuffers(float size, const osg::Vec2f& center, ESM::RefId worldspace,
+            osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours, int quad)
+    {
+        // sampleSize is not used but declared here in order to keep the code as close to
+        // fillVertexBuffers() as possible
+        const std::size_t sampleSize = 1;
+
+        // DEBUG NOTES: cellSize should be 33 for ESM4
+        //              numVerts should be 17 for ESM4
+        const std::size_t cellSize = static_cast<std::size_t>(ESM::getLandSize(worldspace));
+        const std::size_t numVerts = static_cast<std::size_t>(size * (cellSize - 1) / sampleSize) + 1;
+
+        positions.resize(numVerts*numVerts*3);
+        normals.resize(numVerts*numVerts*3);
+        colours.resize(numVerts*numVerts*4);
+
+        const bool alteration = useAlteration(); // Does nothing by default, override in OpenMW-CS
+        const int landSizeInUnits = ESM::getCellSize(worldspace);
+
+// I think the current code copied from fillVertexBuffers() works fine
+#if 0
+        // NOTE: here center is the center of the ESM4 cell (in terms of cell grid position) which
+        //       is subtly different to the way fillVertexBuffers() treats it because we don't
+        //       worry about chunk sizes or LOD
+        //
+        //       center is wrong here due to the way TerrainGrid::buildTerrain() calculates the
+        //       new center
+        osg::Vec2f realCenter;
+        switch (quad)
+        {
+            case 3: realCenter = center - osg::Vec2f( 0.25f,  0.25f); break;
+            case 1: realCenter = center - osg::Vec2f( 0.25f, -0.25f); break;
+            case 2: realCenter = center - osg::Vec2f(-0.25f,  0.25f); break;
+            case 0: realCenter = center - osg::Vec2f(-0.25f, -0.25f); break;
+            default: realCenter = center; break;
+        }
+        const osg::Vec2f origin2 = realCenter - osg::Vec2f(1.f, 1.f) * 0.5f; // assumed to be bottom left corner
+        //std::cout << origin2.x() << ", " << origin2.y() << std::endl;
+#endif
+        const osg::Vec2f origin = center - osg::Vec2f(size, size) * 0.5f;
+        //std::cout << origin.x() << ", " << origin.y() << std::endl;
+        const int startCellX = static_cast<int>(std::floor(origin.x()));
+        const int startCellY = static_cast<int>(std::floor(origin.y()));
+        LandCache cache(startCellX - 1, startCellY - 1, static_cast<std::size_t>(std::ceil(size)) + 2);
+        std::pair lastCell{ startCellX, startCellY };
+        const LandObject* land = getLand(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace), cache);
+        const ESM::LandData* heightData = nullptr;
+        const ESM::LandData* normalData = nullptr;
+        const ESM::LandData* colourData = nullptr;
+        bool validHeightDataExists = false;
+
+        if (land != nullptr)
+        {
+            heightData = land->getData(ESM::Land::DATA_VHGT);
+            normalData = land->getData(ESM::Land::DATA_VNML);
+            colourData = land->getData(ESM::Land::DATA_VCLR);
+            validHeightDataExists = true;
+        }
+
+        int rowStart = 0;
+        int colStart = 0;
+        int rowEnd, colEnd;
+
+        // FIXME: how to ignore the repeat of left/bottom quad?
+        switch (quad)
+        {
+            case 0: // bottom left
+            {
+                rowStart = 0;
+                colStart = 0;
+
+                rowEnd = int(cellSize / 2) + 1; // int(33 / 2) + 1 = 17
+                colEnd = int(cellSize / 2) + 1;
+
+                break;
+            }
+            case 2: // bottom right
+            {
+                rowStart = 0;
+                colStart = int(cellSize / 2); // 16, repeat the last of the left quad
+
+                rowEnd = int(cellSize / 2) + 1; // 17
+                colEnd = cellSize;
+
+                break;
+            }
+            case 1: // top left
+            {
+                rowStart = int(cellSize / 2); // 16, repeat the last of the bottom quad
+                colStart = 0;
+
+                rowEnd = cellSize;
+                colEnd = int(cellSize / 2) + 1; // 17
+
+                break;
+            }
+            case 3: // top right
+            {
+                rowStart = int(cellSize / 2); // 16
+                colStart = int(cellSize / 2); // 16
+
+                rowEnd = cellSize; // 33
+                colEnd = cellSize; // 33
+
+                break;
+            }
+            default:
+                std::fill(positions.begin(), positions.end(), osg::Vec3f());
+                return; // FIXME: throw instead?
+        }
+
+        osg::Vec3f normal(0, 0, 1);
+        osg::Vec4ub color(255, 255, 255, 255);
+
+        // ESM4::Land::mLandData.mHeights start at the bottom left hand corner
+        //
+        //               row
+        //                |
+        //                v
+        // 1056 ..1088   32
+        // 1023 ..1055   31
+        //      ..
+        //   99 .. 131    3
+        //   66 ..  98    2
+        //   33 ..  65    1
+        //    0 ..  32    0
+        //
+        //    0 ..  32  <- col
+        //
+        // row and col represent cell space (i.e. mHeights, mVertNorm and mVertColr)
+        // vertX and vertY represent quad space
+        float vertY = 0;
+        float vertX = 0;
+        for (int col = colStart; col < colEnd; col += 1)
+        {
+            vertX = 0;
+            for (int row = rowStart; row < rowEnd; row += 1)
+            {
+                float height = -2048;
+                if (land && heightData) // validHeightDataExists
+                    height = heightData->getHeights()[col*cellSize + row];
+
+                // FIXME: I suspect landSizeInUnits should be 2048
+                const std::size_t vertIndex = vertX * numVerts + vertY;
+                positions[vertIndex]
+                    = osg::Vec3f((vertX / static_cast<float>(numVerts - 1) - 0.5f) * size * landSizeInUnits,
+                                 (vertY / static_cast<float>(numVerts - 1) - 0.5f) * size * landSizeInUnits,
+                                 height);
+
+                if (land && normalData)
+                {
+                    normal.x() = normalData->getNormals()[col * cellSize * 3 + row * 3 + 0];
+                    normal.y() = normalData->getNormals()[col * cellSize * 3 + row * 3 + 1];
+                    normal.z() = normalData->getNormals()[col * cellSize * 3 + row * 3 + 2];
+                    normal.normalize();
+                }
+                else
+                    normal = osg::Vec3f(0, 0, 1);
+
+// FIXME: not sure if below normal fixes for Morrowind also applies to TES4
+// TODO: needs testing
+#if 0
+                // Normals apparently don't connect seamlessly between cells
+                if (col == cellSize - 1 || row == cellSize - 1)
+                    fixNormal(normal, cellLocation, col, row, cache);
+
+                // some corner normals appear to be complete garbage (z < 0)
+                if ((row == 0 || row == cellSize - 1) && (col == 0 || col == cellSize - 1))
+                    averageNormal(normal, cellLocation, col, row, cache);
+#endif
+                //assert(normal.z() > 0); // ToddLand triggers this
+                if (normal.z() < 0)
+                    normal.z() = 0;
+
+                normals[vertIndex] = normal;
+
+                if (land && colourData)
+                {
+                    color.r() = colourData->getColors()[col * cellSize * 3 + row * 3 + 0];
+                    color.g() = colourData->getColors()[col * cellSize * 3 + row * 3 + 1];
+                    color.b() = colourData->getColors()[col * cellSize * 3 + row * 3 + 2];
+                }
+                else
+                {
+                    color.r() = 1;
+                    color.g() = 1;
+                    color.b() = 1;
+                }
+
+// FIXME: not sure if below colour fixes for Morrowind also applies to TES4
+// TODO: needs testing
+#if 0
+                // Unlike normals, colors mostly connect seamlessly between cells, but not always...
+                if (col == cellSize - 1 || row == cellSize - 1)
+                    fixColour(color, cellLocation, col, row, cache);
+#endif
+//              color.a() = 1;
+                colours[vertIndex] = color;
+
+                ++vertX;
+            }
+            ++vertY;
+        }
+    }
+
+    void Storage::getQuadBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
+            std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace, int quad)
+    {
+        // VTXT info indicates texture size is 17x17 - but the cell grid is 33x33
+        // (cf. TES3 has 65x65 cell) do we discard one row and column or overlap?
+        //
+        // NOTE: each base texture does not completely "fill" a quadrant.  The observations in
+        // TES4 vanilla indicates that the texture repeats (or "wraps") 6 times each side
+        //
+        //     ///////////////// ////////////////   <-- discard texture row?
+        //    +-----------------+----------------+/
+        // 32 |\               \|                |/
+        // 31 |\               \|                |/
+        //    |\     17x16     \|      16x16     |/
+        //  . |\               \|                |/
+        //  . |\       2       \|        3       |/
+        //  . |\               \|                |/
+        //  . |\               \<---------------------- overlap column instead?
+        // 17 |\               \|                |/
+        //    +-----------------+----------------+
+        // 16 |\                |\\\\\\\\\\\\\\\\|<---- overlap row instead?
+        // 15 |\                |                |/
+        //  . |\     17x17      |      16x17     |/
+        //  . |\                |                |/
+        //  . |\       0        |        1       |/
+        //  . |\                |                |/
+        //  2 |\                |                |/
+        //  1 |\                |                |/
+        //  0 |\\\\\\\\\\\\\\\\\|\\\\\\\\\\\\\\\\|<---- this row of vertices is a copy of cell below
+        //    +-----------------+----------------+
+        //                   111 1             33 ^
+        //     0123  ......  456 7    .....    12 |
+        //     ^                                 discard texture column?
+        //     |
+        //    this column of vertices is a copy of the cell to the left
+        //
+        const osg::Vec2f origin = chunkCenter - osg::Vec2f(chunkSize, chunkSize) * 0.5f;
+        const int startCellX = static_cast<int>(std::floor(origin.x()));
+        const int startCellY = static_cast<int>(std::floor(origin.y()));
+        const int realTextureSize = 17; // FIXME: should be defined in Land record
+        //const std::size_t blendmapSize = getBlendmapSize(chunkSize, realTextureSize);
+        const std::size_t blendmapSize = realTextureSize;
+
+        // FIXME: temp testing
+        //if (startCellX == 12 && startCellY == 21)
+            //std::cout << "vilverin exterior" << std::endl;
+
+// FIXME: I don't think ESM4 needs this?
+#if 0
+        // We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla
+        constexpr std::size_t imageScaleFactor = 2;
+#else
+        constexpr std::size_t imageScaleFactor = 1;
+#endif
+        const std::size_t blendmapImageSize = blendmapSize * imageScaleFactor;
+        std::vector<UniqueTextureId> textureIds(blendmapSize * blendmapSize);
+
+// NOTE: we need all the texture data which are missing in LandObject
+#if 0
+        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);
+#endif
+        // FIXME: do we need to cache this data? (already in ESMStore, why cache again?)
+        //        alternatively modify LandObject with all the extra data rather than use getEsm4Land()?
+        const ESM4::Land* land = getEsm4Land(ESM::ExteriorCellLocation(startCellX, startCellY, worldspace));
+        if (!land)
+            return; // FIXME: throw instead?
+
+// I don't think we need this?
+#if 0
+        const auto handleSample = [&](const CellSample& sample) {
+            const std::pair cell{ sample.mCellX, sample.mCellY };
+            if (lastCell != cell)
+            {
+                land = getEsm4Land(ESM::ExteriorCellLocation(sample.mCellX, sample.mCellY, worldspace));
+                lastCell = cell;
+            }
+
+            textureIds[sample.mDstCol * blendmapSize + sample.mDstRow]
+                = getQuadTextureIdAt(land, sample.mSrcRow, sample.mSrcCol);
+        };
+
+        sampleBlendmaps(chunkSize, origin.x(), origin.y(), realTextureSize, handleSample);
+
+        std::map<UniqueTextureId, std::size_t> textureIndicesMap;
+#endif
+        // FIXME: debugging only
+        //std::cout << "quad " << quad << std::endl;
+
+        // base texture
+        Terrain::LayerInfo info;
+        ESM::FormId ltexId = ESM::FormId::fromUint32(land->mTextures[quad].base.formId);
+        std::string texture = getEsm4TextureName(ltexId);
+        if (texture == "")
+            info = getLayerInfo(getEsm4TextureSet(ltexId)); // FO3/FONV/TES5
+        else
+            info = getLayerInfo(texture); // TES4
+
+        // FIXME: debugging only
+        //std::cout << "base " << info.mDiffuseMap << std::endl;
+        osg::ref_ptr<osg::Image> image(new osg::Image);
+        image->allocateImage(static_cast<int>(blendmapImageSize), static_cast<int>(blendmapImageSize),
+            1, GL_ALPHA, GL_UNSIGNED_BYTE);
+        std::memset(image->data(), 255, image->getTotalDataSize()); // fully opaque for base texture
+        blendmaps.push_back(std::move(image));
+        layerList.push_back(std::move(info));
+
+        // additional textures
+
+        std::size_t numLayers = land->mTextures[quad].layers.size();
+        for (std::size_t i = 0; i < numLayers; ++i)
+        {
+            Terrain::LayerInfo layerInfo;
+            /*ESM::FormId*/ ltexId = ESM::FormId::fromUint32(land->mTextures[quad].layers[i].texture.formId);
+            std::string layerTexture = getEsm4TextureName(ltexId);
+            if (layerTexture == "")
+                layerInfo = getLayerInfo(getEsm4TextureSet(ltexId)); // FO3/FONV/TES5
+            else
+                layerInfo = getLayerInfo(layerTexture); // TES4
+
+            // FIXME: debugging only
+            //std::cout << "layer " << i << ", " << layerInfo.mDiffuseMap << std::endl;
+
+            osg::ref_ptr<osg::Image> layerImage(new osg::Image);
+            layerImage->allocateImage(static_cast<int>(blendmapImageSize), static_cast<int>(blendmapImageSize),
+                1, GL_ALPHA, GL_UNSIGNED_BYTE);
+            std::memset(layerImage->data(), 0, layerImage->getTotalDataSize());
+            blendmaps.push_back(std::move(layerImage));
+            layerList.push_back(std::move(layerInfo));
+
+            const std::size_t layerIndex = blendmaps.size() - 1;
+            unsigned char* const data = blendmaps[layerIndex]->data();
+
+            // osg::Image default origin is bottom left and VTXT data also starts at bottom left
+            // corner i.e. there should be no conversion required
+            //
+            // FIXME: but the observed behaviour is different - either VTXT starts at top left
+            //        corner or osg::Image is being interpreted differently by the shader
+            //
+            // Image      guessed VTXT
+            // index       position    y'
+            //
+            // 272 ..288     0 .. 16   0
+            //     ..          ..
+            //  51 .. 67   221 ..237  13
+            //  34 .. 50   238 ..254  14
+            //  17 .. 33   255 ..271  15
+            //   0 .. 16   272 ..288  16
+            //
+            // y  = floor(position / 17)
+            // y' = 17 - 1 - y
+            // x  = position % 17
+            //
+            // e.g. position = 275, y = 16, y' = 0,  x = 3
+            //      position = 50,  y = 2,  y' = 14, x = 16
+            const std::vector<ESM4::Land::VTXT>& opacityData = land->mTextures[quad].layers[i].data;
+            for (std::size_t j = 0; j < opacityData.size(); ++j)
+            {
+                // NOTE: blendmapImageSize, blendmapSize and realTextureSize are all the same (17)
+
+                int position = opacityData[j].position;
+
+                std::size_t y = realTextureSize - 1 - std::floor(position / realTextureSize);
+                std::size_t x = position % realTextureSize;
+                data[y*realTextureSize + x] = unsigned char(opacityData[j].opacity * 255);
+            }
+        }
+    }
+
 }
diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp
index 402f2147ab..27ec297613 100644
--- a/components/esmterrain/storage.hpp
+++ b/components/esmterrain/storage.hpp
@@ -13,6 +13,8 @@
 namespace ESM4
 {
     struct Land;
+    struct LandTexture;
+    struct TextureSet;
 }
 
 namespace ESM
@@ -130,6 +132,19 @@ namespace ESMTerrain
             return data->getHeights()[y * landSize + x];
         }
 
+        virtual const ESM4::Land *getEsm4Land(ESM::ExteriorCellLocation cellLocation) const = 0;
+        virtual const ESM4::LandTexture *getEsm4LandTexture(ESM::RefId ltexId) const = 0;
+        virtual const ESM4::TextureSet *getEsm4TextureSet(ESM::RefId txstId) const = 0;
+
+        void fillQuadVertexBuffers(float size, const osg::Vec2f& center, ESM::RefId worldspace,
+            osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours, int quad);
+
+        void getQuadBlendmaps(float size, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
+            std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace, int quad);
+
+    protected:
+        bool mIsEsm4Ext; // intended to be used by MWRender::TerrainStorage
+
     private:
         const VFS::Manager* mVFS;
 
@@ -148,6 +163,8 @@ namespace ESMTerrain
 
         std::string getTextureName(UniqueTextureId id);
 
+        std::string getEsm4TextureName(ESM::RefId id);
+
         std::map<std::string, Terrain::LayerInfo> mLayerInfoMap;
         std::mutex mLayerInfoMutex;
 
@@ -159,6 +176,8 @@ namespace ESMTerrain
         bool mAutoUseSpecularMaps;
 
         Terrain::LayerInfo getLayerInfo(const std::string& texture);
+
+        Terrain::LayerInfo getLayerInfo(const ESM4::TextureSet *txst);
     };
 
 }
diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp
index 7ccd89ac21..4cb4439ed3 100644
--- a/components/terrain/chunkmanager.cpp
+++ b/components/terrain/chunkmanager.cpp
@@ -10,6 +10,8 @@
 
 #include <components/sceneutil/lightmanager.hpp>
 
+#include <components/esmterrain/storage.hpp>
+
 #include "compositemaprenderer.hpp"
 #include "material.hpp"
 #include "storage.hpp"
@@ -39,6 +41,32 @@ namespace Terrain
         mMultiPassRoot->setAttributeAndModes(material, osg::StateAttribute::ON);
     }
 
+    // FIXME: don't know which is worse, adding duplicated code here or adding a parameter to
+    //        Terrain::QuadTreeWorld::getChunk().
+    osg::ref_ptr<osg::Node> ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod,
+        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile, int quad)
+    {
+        // Override lod with the vertexLodMod adjusted value.
+        // TODO: maybe we can refactor this code by moving all vertexLodMod code into this class.
+        lod = static_cast<unsigned char>(lodFlags >> (4 * 4));
+
+        const ChunkKey key{ .mCenter = center, .mLod = lod, .mLodFlags = lodFlags };
+        if (osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(key))
+            return static_cast<osg::Node*>(obj.get());
+
+        const TerrainDrawable* templateGeometry = nullptr;
+        const TemplateKey templateKey{ .mCenter = center, .mLod = lod };
+        const auto pair = mCache->lowerBound(templateKey);
+        if (pair.has_value() && templateKey == TemplateKey{ .mCenter = pair->first.mCenter, .mLod = pair->first.mLod })
+            templateGeometry = static_cast<const TerrainDrawable*>(pair->second.get());
+
+        osg::ref_ptr<osg::Node> node = createChunk(size, center, lod, lodFlags, compile, templateGeometry, quad);
+        mCache->addEntryToObjectCache(key, node.get());
+        return node;
+    }
+
+    // called from either TerrainGrid::buildTerrain() or QuadTreeWorld::loadRenderingNode()
+    // calls createChunk()
     osg::ref_ptr<osg::Node> ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod,
         unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
     {
@@ -137,12 +165,27 @@ namespace Terrain
         }
     }
 
+// >   openmw.exe!Terrain::ChunkManager::createPasses() Line 188
+//     openmw.exe!Terrain::ChunkManager::createCompositeMapGeometry() Line 123
+//     openmw.exe!Terrain::ChunkManager::createChunk() Line 275
+//     openmw.exe!Terrain::ChunkManager::getChunk() Line 59
+//     openmw.exe!Terrain::QuadTreeWorld::loadRenderingNode() Line 397
+//     openmw.exe!Terrain::QuadTreeWorld::preload() Line 558
+//     openmw.exe!MWWorld::TerrainPreloadItem::doWork() Line 184
+//     openmw.exe!SceneUtil::WorkThread::run() Line 135
     std::vector<osg::ref_ptr<osg::StateSet>> ChunkManager::createPasses(
-        float chunkSize, const osg::Vec2f& chunkCenter, bool forCompositeMap)
+        float chunkSize, const osg::Vec2f& chunkCenter, bool forCompositeMap, int quad)
     {
         std::vector<LayerInfo> layerList;
         std::vector<osg::ref_ptr<osg::Image>> blendmaps;
-        mStorage->getBlendmaps(chunkSize, chunkCenter, blendmaps, layerList, mWorldspace);
+
+        if (quad >= 0) // NOTE: quad == -1 has a special meaning of "no quads"
+        {
+            static_cast<ESMTerrain::Storage*>(mStorage)
+                ->getQuadBlendmaps(chunkSize, chunkCenter, blendmaps, layerList, mWorldspace, quad);
+        }
+        else
+            mStorage->getBlendmaps(chunkSize, chunkCenter, blendmaps, layerList, mWorldspace);
 
         bool useShaders = mSceneManager->getForceShaders();
         if (!mSceneManager->getClampLighting())
@@ -183,14 +226,26 @@ namespace Terrain
             blendmapTextures.push_back(texture);
         }
 
+        // NOTE: This needs to get different values for TES4.  That is, blendmapScale should
+        //       be 16 for TES3 and 6 for TES4 after calling getBlendmapScale() if we were
+        //       using TerrainGrid (i.e. chunksize of 1.f).  See the way Terrain::createPasses()
+        //       uses BlendmapTexMat::value(blendmapScale).  This scaling won't work if using
+        //       QuadTree with chunkSize other than 1.f (in which case need to do sampling).
+        //
+        //       We should remember that in TES4/ESM4 the land "chunk" size is not the same
+        //       size as the texture.  So we may have to do some maths here but it is unclear
+        //       whether it will work out properly until some testing is done.
+        //
+        // FIXME: TES5 and FO3/FONV may have different texture scaling - requires testing.
         float blendmapScale = mStorage->getBlendmapScale(chunkSize);
 
+        // TODO: not so sure about (i.e. don't understand) using blendmapScale for layerTileSize
         return ::Terrain::createPasses(
-            useShaders, mSceneManager, layers, blendmapTextures, blendmapScale, blendmapScale);
+            useShaders, mSceneManager, layers, blendmapTextures, blendmapScale, blendmapScale, quad);
     }
 
     osg::ref_ptr<osg::Node> ChunkManager::createChunk(float chunkSize, const osg::Vec2f& chunkCenter, unsigned char lod,
-        unsigned int lodFlags, bool compile, const TerrainDrawable* templateGeometry)
+        unsigned int lodFlags, bool compile, const TerrainDrawable* templateGeometry, int quad)
     {
         osg::ref_ptr<TerrainDrawable> geometry(new TerrainDrawable);
 
@@ -201,7 +256,18 @@ namespace Terrain
             osg::ref_ptr<osg::Vec4ubArray> colors(new osg::Vec4ubArray);
             colors->setNormalize(true);
 
-            mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors);
+// FIXME: I have a suspicion that existing fillVertexBuffers() probably already works even with
+//        the unwanted Morrowind specific "fixes".
+#if 1
+            // NOTE: decided on a new method rather than pass quad to fillVertexBuffers() just
+            //       in case the Morrowind specific "fixes" causes problems
+            // NOTE: LOD is not supported
+            if (quad >= 0) // NOTE: quad == -1 has a special meaning of "no quads"
+                static_cast<ESMTerrain::Storage*>(mStorage)
+                    ->fillQuadVertexBuffers(chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors, quad);
+            else
+#endif
+                mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors);
 
             osg::ref_ptr<osg::VertexBufferObject> vbo(new osg::VertexBufferObject);
             positions->setVertexBufferObject(vbo);
@@ -284,7 +350,9 @@ namespace Terrain
             }
             else
             {
-                geometry->setPasses(createPasses(chunkSize, chunkCenter, false));
+                // FIXME: maybe we need to pass quad here or call a new method
+                //        e.g. createQuadPasses()
+                geometry->setPasses(createPasses(chunkSize, chunkCenter, false, quad));
             }
         }
 
diff --git a/components/terrain/chunkmanager.hpp b/components/terrain/chunkmanager.hpp
index 20d6ba9327..e8833c2772 100644
--- a/components/terrain/chunkmanager.hpp
+++ b/components/terrain/chunkmanager.hpp
@@ -81,6 +81,10 @@ namespace Terrain
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags,
             bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override;
 
+        // NOTE: created to avoid adding another parameter to getChunk() in Terrain::QuadTreeWorld::ChunkManager
+        osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags,
+            bool activeGrid, const osg::Vec3f& viewPoint, bool compile, int quad);
+
         void setCompositeMapSize(unsigned int size) { mCompositeMapSize = size; }
         void setCompositeMapLevel(float level) { mCompositeMapLevel = level; }
         void setMaxCompositeGeometrySize(float maxCompGeometrySize) { mMaxCompGeometrySize = maxCompGeometrySize; }
@@ -95,8 +99,9 @@ namespace Terrain
         void releaseGLObjects(osg::State* state) override;
 
     private:
+        // NOTE: quad == -1 has a special meaning that we're not dealing with ESM4 quad structure
         osg::ref_ptr<osg::Node> createChunk(float size, const osg::Vec2f& center, unsigned char lod,
-            unsigned int lodFlags, bool compile, const TerrainDrawable* templateGeometry);
+            unsigned int lodFlags, bool compile, const TerrainDrawable* templateGeometry, int quad = -1);
 
         osg::ref_ptr<osg::Texture2D> createCompositeMapRTT();
 
@@ -104,7 +109,7 @@ namespace Terrain
             float chunkSize, const osg::Vec2f& chunkCenter, const osg::Vec4f& texCoords, CompositeMap& map);
 
         std::vector<osg::ref_ptr<osg::StateSet>> createPasses(
-            float chunkSize, const osg::Vec2f& chunkCenter, bool forCompositeMap);
+            float chunkSize, const osg::Vec2f& chunkCenter, bool forCompositeMap, int quad = -1);
 
         Terrain::Storage* mStorage;
         Resource::SceneManager* mSceneManager;
diff --git a/components/terrain/defs.hpp b/components/terrain/defs.hpp
index c2342c50d2..fdc74cf8c3 100644
--- a/components/terrain/defs.hpp
+++ b/components/terrain/defs.hpp
@@ -20,6 +20,7 @@ namespace Terrain
         std::string mNormalMap;
         bool mParallax; // Height info in normal map alpha channel?
         bool mSpecular; // Specular info in diffuse map alpha channel?
+        //bool mIsEsm4;   // intended to be used in Terrain::createPasses()
 
         bool requiresShaders() const { return !mNormalMap.empty() || mSpecular; }
     };
diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp
index 09d2680acd..f7536a08fa 100644
--- a/components/terrain/material.cpp
+++ b/components/terrain/material.cpp
@@ -27,6 +27,8 @@ namespace
             return instance.get(blendmapScale);
         }
 
+        // FIXME: Not sure if this pre-multiplication is needed for TES4
+        //        (needs A-B testing to confirm)
         const osg::ref_ptr<osg::TexMat>& get(const int blendmapScale)
         {
             const std::lock_guard<std::mutex> lock(mMutex);
@@ -223,7 +225,7 @@ namespace Terrain
 {
     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,
-        int blendmapScale, float layerTileSize)
+        int blendmapScale, float layerTileSize, int quad)
     {
         auto& shaderManager = sceneManager->getShaderManager();
         std::vector<osg::ref_ptr<osg::StateSet>> passes;
@@ -243,7 +245,12 @@ namespace Terrain
                 stateset->setRenderBinDetails(firstLayer ? 0 : 1, "RenderBin");
                 if (!firstLayer)
                 {
-                    stateset->setAttributeAndModes(BlendFunc::value(), osg::StateAttribute::ON);
+                    if (quad >= 0)
+                        stateset->setAttributeAndModes(
+                            new osg::BlendFunc(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE_MINUS_SRC_ALPHA),
+                            osg::StateAttribute::ON);
+                    else
+                        stateset->setAttributeAndModes(BlendFunc::value(), osg::StateAttribute::ON);
                     stateset->setAttributeAndModes(EqualDepth::value(), osg::StateAttribute::ON);
                 }
                 else
@@ -303,13 +310,20 @@ namespace Terrain
                 defineMap["parallax"] = parallax ? "1" : "0";
                 defineMap["writeNormals"] = (it == layers.end() - 1) ? "1" : "0";
                 defineMap["reconstructNormalZ"] = reconstructNormalZ ? "1" : "0";
+                if (quad >= 0)
+                    defineMap["baseLayer"] = (firstLayer) ? "1" : "0";
                 Stereo::shaderStereoDefines(defineMap);
 
-                stateset->setAttributeAndModes(shaderManager.getProgram("terrain", defineMap));
+                if (quad >= 0)
+                    stateset->setAttributeAndModes(shaderManager.getProgram("esm4terrain", defineMap));
+                else
+                    stateset->setAttributeAndModes(shaderManager.getProgram("terrain", defineMap));
                 stateset->addUniform(UniformCollection::value().mColorMode);
             }
             else
             {
+                // FIXME: needs some changes for ESM4
+
                 // Add the actual layer texture
                 osg::ref_ptr<osg::Texture2D> tex = it->mDiffuseMap;
                 stateset->setTextureAttributeAndModes(0, tex.get());
diff --git a/components/terrain/material.hpp b/components/terrain/material.hpp
index 1dbf6d8fc8..23d687b123 100644
--- a/components/terrain/material.hpp
+++ b/components/terrain/material.hpp
@@ -26,7 +26,7 @@ namespace Terrain
 
     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,
-        int blendmapScale, float layerTileSize);
+        int blendmapScale, float layerTileSize, int quad = -1);
 
 }
 
diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp
index 63b55abb21..70db339336 100644
--- a/components/terrain/quadtreeworld.cpp
+++ b/components/terrain/quadtreeworld.cpp
@@ -1,5 +1,7 @@
 #include "quadtreeworld.hpp"
 
+#include <cmath> // std::floor
+
 #include <osg/Material>
 #include <osg/PolygonMode>
 #include <osg/ShapeDrawable>
@@ -260,7 +262,7 @@ namespace Terrain
         {
         }
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& chunkCenter, unsigned char lod,
-            unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
+            unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/)
         {
             osg::Vec3f center = { chunkCenter.x(), chunkCenter.y(), 0 };
             auto chunkBorder = CellBorder::createBorderGeometry(center.x() - size / 2.f, center.y() - size / 2.f, size,
@@ -289,7 +291,7 @@ namespace Terrain
         , mLodFactor(lodFactor)
         , mVertexLodMod(vertexLodMod)
         , 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) // NOTE: increased min for ESM4
         , mDebugTerrainChunks(debugChunks)
     {
         mChunkManager->setCompositeMapSize(compMapResolution);
@@ -394,11 +396,32 @@ namespace Terrain
 
             for (QuadTreeWorld::ChunkManager* m : mChunkManagers)
             {
-                osg::ref_ptr<osg::Node> n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(),
-                    DefaultLodCallback::getNativeLodLevel(entry.mNode, mMinSize), entry.mLodFlags, activeGrid,
-                    vd->getViewPoint(), compile);
-                if (n)
-                    pat->addChild(n);
+                osg::ref_ptr<osg::Node> n;
+                if (ESM::isEsm4Ext(mWorldspace) && entry.mNode->getSize() == 0.5f && m == mChunkManager.get())
+                {
+                    osg::Vec2 chunkCenter = entry.mNode->getCenter();
+                    float originX = std::floor(chunkCenter.x());
+                    float originY = std::floor(chunkCenter.y());
+                    int quad = -1;
+                    if (chunkCenter.x() - originX == 0.25f)
+                        quad = (chunkCenter.y() - originY == 0.25f) ? 0 : 2;
+                    else
+                        quad = (chunkCenter.y() - originY == 0.25f) ? 1 : 3;
+
+                    n = static_cast<Terrain::ChunkManager*>(m)->getChunk(0.5f, entry.mNode->getCenter(),
+                        DefaultLodCallback::getNativeLodLevel(entry.mNode, mMinSize), entry.mLodFlags, activeGrid,
+                        vd->getViewPoint(), compile, quad);
+                    if (n)
+                        pat->addChild(n);
+                }
+                else
+                {
+                    n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(),
+                        DefaultLodCallback::getNativeLodLevel(entry.mNode, mMinSize), entry.mLodFlags, activeGrid,
+                        vd->getViewPoint(), compile);
+                    if (n)
+                        pat->addChild(n);
+                }
             }
             entry.mRenderingNode = pat;
         }
@@ -536,6 +559,8 @@ namespace Terrain
         return mViewDataMap->createIndependentView();
     }
 
+    // FIXME: I guess this is where it all starts?  We need to somehow deal with entry of chunk
+    //        size 1 to be split into 4 smaller "quads".
     void QuadTreeWorld::preload(View* view, const osg::Vec3f& viewPoint, const osg::Vec4i& grid,
         std::atomic<bool>& abort, Loading::Reporter& reporter)
     {
diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp
index fa800d2655..31db0d8878 100644
--- a/components/terrain/quadtreeworld.hpp
+++ b/components/terrain/quadtreeworld.hpp
@@ -67,7 +67,7 @@ namespace Terrain
                 mWorldspace = worldspace;
             }
             virtual osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod,
-                unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
+                unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad = -1*/)
                 = 0;
             virtual unsigned int getNodeMask() { return 0; }
 
diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp
index 2849c4d401..8f34599d06 100644
--- a/components/terrain/terraingrid.cpp
+++ b/components/terrain/terraingrid.cpp
@@ -10,6 +10,7 @@
 #include "storage.hpp"
 #include "view.hpp"
 #include <components/sceneutil/positionattitudetransform.hpp>
+#include <components/esm/util.hpp>
 
 namespace Terrain
 {
@@ -51,10 +52,36 @@ namespace Terrain
         static_cast<MyView*>(view)->mLoaded = buildTerrain(nullptr, 1.f, center);
     }
 
+    // I think this should be where we decide to split the land into 4 quads.
+    // Alternatively, we can do it in loadCell() and call a different kind of buildTerrain().
+    //
+    // Need to do some experiments to see if the existing code will produe the correct quads or
+    // special code needs to be added (depens on column/row start and ends).  But I think we
+    // still need to pass more info to getChunk() because we need to know which quadrant for
+    // the textures?
     osg::ref_ptr<osg::Node> TerrainGrid::buildTerrain(
-        osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter)
+        osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter, int quad)
     {
-        if (chunkSize * mNumSplits > 1.f)
+        if (ESM::isEsm4Ext(mWorldspace) && chunkSize == 1.f && mNumSplits == 4) // WARN: hard coded values for ESM4
+        {
+            osg::ref_ptr<osg::Group> group(new osg::Group);
+            if (parent)
+                parent->addChild(group); // should never happen
+
+            float newChunkSize = chunkSize / 2.f;
+            {
+                buildTerrain(group, // top right
+                        newChunkSize, chunkCenter + osg::Vec2f(newChunkSize / 2.f, newChunkSize / 2.f), 3);
+                buildTerrain(group, // top left
+                        newChunkSize, chunkCenter + osg::Vec2f(newChunkSize / 2.f, -newChunkSize / 2.f), 1);
+                buildTerrain(group, // bottom right
+                        newChunkSize, chunkCenter + osg::Vec2f(-newChunkSize / 2.f, newChunkSize / 2.f), 2);
+                buildTerrain(group, // bottom left
+                        newChunkSize, chunkCenter + osg::Vec2f(-newChunkSize / 2.f, -newChunkSize / 2.f), 0);
+            }
+            return group;
+        }
+        else if (!ESM::isEsm4Ext(mWorldspace) && chunkSize * mNumSplits > 1.f) // FIXME: needs better logic
         {
             // keep splitting
             osg::ref_ptr<osg::Group> group(new osg::Group);
@@ -70,8 +97,10 @@ namespace Terrain
         }
         else
         {
-            osg::ref_ptr<osg::Node> node
-                = mChunkManager->getChunk(chunkSize, chunkCenter, 0, 0, false, osg::Vec3f(), true);
+            // FIXME: not sure which is worse, this mess or adding a parameter to Terrain::QuadTreeWorld::getChunk()
+            osg::ref_ptr<osg::Node> node = ESM::isEsm4Ext(mWorldspace)
+                ? mChunkManager->getChunk(chunkSize, chunkCenter, 0, 0, false, osg::Vec3f(), true, quad)
+                : mChunkManager->getChunk(chunkSize, chunkCenter, 0, 0, false, osg::Vec3f(), true);
             if (!node)
                 return nullptr;
 
@@ -85,6 +114,8 @@ namespace Terrain
         }
     }
 
+    // Use ESM::isEsm4Ext(World::getWorldspace())
+    // or just ESM::isEsm4Ext(mWorldspace) since mWorldspace is declared as protected.
     void TerrainGrid::loadCell(int x, int y)
     {
         if (mGrid.find(std::make_pair(x, y)) != mGrid.end())
diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp
index 8483338f23..23a58d34e6 100644
--- a/components/terrain/terraingrid.hpp
+++ b/components/terrain/terraingrid.hpp
@@ -46,7 +46,8 @@ namespace Terrain
         bool isGridEmpty() const { return mGrid.empty(); }
 
     private:
-        osg::ref_ptr<osg::Node> buildTerrain(osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter);
+        // quad is meant to be used for ESM4 terrain only; if -1 it is ignored, should be [0..3]
+        osg::ref_ptr<osg::Node> buildTerrain(osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter, int quad = -1);
         void updateWaterCulling();
 
         // split each ESM::Cell into mNumSplits*mNumSplits terrain chunks
diff --git a/files/shaders/compatibility/esm4terrain.frag b/files/shaders/compatibility/esm4terrain.frag
new file mode 100644
index 0000000000..243e0e9ba1
--- /dev/null
+++ b/files/shaders/compatibility/esm4terrain.frag
@@ -0,0 +1,110 @@
+#version 120
+
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
+
+varying vec2 uv;
+
+uniform sampler2D diffuseMap;
+
+#if @normalMap
+uniform sampler2D normalMap;
+#endif
+
+#if @blendMap
+uniform sampler2D blendMap;
+#endif
+
+varying float euclideanDepth;
+varying float linearDepth;
+
+#define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL)
+
+#if !PER_PIXEL_LIGHTING
+centroid varying vec3 passLighting;
+centroid varying vec3 passSpecular;
+centroid varying vec3 shadowDiffuseLighting;
+centroid varying vec3 shadowSpecularLighting;
+#endif
+varying vec3 passViewPos;
+varying vec3 passNormal;
+
+uniform vec2 screenRes;
+uniform float far;
+
+#include "vertexcolors.glsl"
+#include "shadows_fragment.glsl"
+#include "lib/light/lighting.glsl"
+#include "lib/material/parallax.glsl"
+#include "fog.glsl"
+#include "compatibility/normals.glsl"
+
+void main()
+{
+    vec2 adjustedUV = (gl_TextureMatrix[0] * vec4(uv, 0.0, 1.0)).xy;
+
+#if @parallax
+    adjustedUV += getParallaxOffset(transpose(normalToViewMatrix) * normalize(-passViewPos), texture2D(normalMap, adjustedUV).a, 1.f);
+#endif
+    vec4 diffuseTex = texture2D(diffuseMap, adjustedUV);
+    gl_FragData[0] = vec4(diffuseTex.xyz, 1.0);
+
+#if @baseLayer
+    vec4 diffuseColor = getDiffuseColor();
+    gl_FragData[0].a *= diffuseColor.a;
+#endif
+
+#if @blendMap
+    vec2 blendMapUV = (gl_TextureMatrix[1] * vec4(uv, 0.0, 1.0)).xy;
+#if @baseLayer
+#else
+    gl_FragData[0].a = texture2D(blendMap, blendMapUV).a;
+#endif
+#endif
+
+#if @normalMap
+    vec4 normalTex = texture2D(normalMap, adjustedUV);
+    vec3 normal = normalTex.xyz * 2.0 - 1.0;
+#if @reconstructNormalZ
+    normal.z = sqrt(1.0 - dot(normal.xy, normal.xy));
+#endif
+    vec3 viewNormal = normalToView(normal);
+#else
+    vec3 viewNormal = normalize(gl_NormalMatrix * passNormal);
+#endif
+
+    float shadowing = unshadowedLightRatio(linearDepth);
+    vec3 lighting, specular;
+#if !PER_PIXEL_LIGHTING
+    lighting = passLighting + shadowDiffuseLighting * shadowing;
+    specular = passSpecular + shadowSpecularLighting * shadowing;
+#else
+#if @specularMap
+    float shininess = 128.0; // TODO: make configurable
+    vec3 specularColor = vec3(diffuseTex.a);
+#else
+    float shininess = gl_FrontMaterial.shininess;
+    vec3 specularColor = getSpecularColor().xyz;
+#endif
+    vec3 diffuseLight, ambientLight, specularLight;
+    doLighting(passViewPos, viewNormal, shininess, shadowing, diffuseLight, ambientLight, specularLight);
+    lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz;
+    specular = specularColor * specularLight;
+#endif
+
+    clampLightingResult(lighting);
+    gl_FragData[0].xyz = gl_FragData[0].xyz * lighting + specular;
+
+    gl_FragData[0] = applyFogAtDist(gl_FragData[0], euclideanDepth, linearDepth, far);
+
+#if !@disableNormals && @writeNormals
+    gl_FragData[1].xyz = viewNormal * 0.5 + 0.5;
+#endif
+
+    applyShadowDebugOverlay();
+}
diff --git a/files/shaders/compatibility/esm4terrain.vert b/files/shaders/compatibility/esm4terrain.vert
new file mode 100644
index 0000000000..cbfb7769ba
--- /dev/null
+++ b/files/shaders/compatibility/esm4terrain.vert
@@ -0,0 +1,73 @@
+#version 120
+
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
+
+#include "lib/core/vertex.h.glsl"
+varying vec2 uv;
+varying float euclideanDepth;
+varying float linearDepth;
+
+#define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL)
+
+#if !PER_PIXEL_LIGHTING
+centroid varying vec3 passLighting;
+centroid varying vec3 passSpecular;
+centroid varying vec3 shadowDiffuseLighting;
+centroid varying vec3 shadowSpecularLighting;
+#endif
+varying vec3 passViewPos;
+varying vec3 passNormal;
+
+#include "vertexcolors.glsl"
+#include "shadows_vertex.glsl"
+#include "compatibility/normals.glsl"
+
+#include "lib/light/lighting.glsl"
+#include "lib/view/depth.glsl"
+
+void main(void)
+{
+    gl_Position = modelToClip(gl_Vertex);
+
+    vec4 viewPos = modelToView(gl_Vertex);
+    gl_ClipVertex = viewPos;
+    euclideanDepth = length(viewPos.xyz);
+    linearDepth = getLinearDepth(gl_Position.z, viewPos.z);
+
+    passColor = gl_Color;
+    passNormal = gl_Normal.xyz;
+    passViewPos = viewPos.xyz;
+    normalToViewMatrix = gl_NormalMatrix;
+
+#if @normalMap
+    mat3 tbnMatrix = generateTangentSpace(vec4(1.0, 0.0, 0.0, 1.0), passNormal);
+    tbnMatrix[0] = normalize(cross(tbnMatrix[2], tbnMatrix[1])); // note, now we need to re-cross to derive tangent again because it wasn't orthonormal
+    normalToViewMatrix *= tbnMatrix;
+#endif
+
+#if !PER_PIXEL_LIGHTING || @shadows_enabled
+    vec3 viewNormal = normalize(gl_NormalMatrix * passNormal);
+#endif
+
+#if !PER_PIXEL_LIGHTING
+    vec3 diffuseLight, ambientLight, specularLight;
+    doLighting(viewPos.xyz, viewNormal, gl_FrontMaterial.shininess, diffuseLight, ambientLight, specularLight, shadowDiffuseLighting, shadowSpecularLighting);
+    passLighting = getDiffuseColor().xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz;
+    passSpecular = getSpecularColor().xyz * specularLight;
+    clampLightingResult(passLighting);
+    shadowDiffuseLighting *= getDiffuseColor().xyz;
+    shadowSpecularLighting *= getSpecularColor().xyz;
+#endif
+
+    uv = gl_MultiTexCoord0.xy;
+
+#if (@shadows_enabled)
+    setupShadowCoords(viewPos, viewNormal);
+#endif
+}

From 735b5fbf897e2f7c94f8afb2d80b16372074a574 Mon Sep 17 00:00:00 2001
From: "florent.teppe" <teppe.florent@hotmail.fr>
Date: Mon, 12 Aug 2024 20:38:44 +0200
Subject: [PATCH 2/6] Small fix so it compiles

---
 components/esm4/loadland.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/components/esm4/loadland.cpp b/components/esm4/loadland.cpp
index e3e6bb65d5..41bd8ff30e 100644
--- a/components/esm4/loadland.cpp
+++ b/components/esm4/loadland.cpp
@@ -77,8 +77,8 @@ void ESM4::Land::load(ESM4::Reader& reader)
     bool isFONV = (esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134);
     bool isTES5 = (esmVer == ESM::VER_094 || esmVer == ESM::VER_170); // WARN: FO3 is also VER_094
     // WARN: below workaround assumes the data directory path has "Fallout" somewhere
-    if (esmVer == ESM4::VER_094 && reader.getContext().filename.find("allout") != std::string::npos)
-        isTES5 = false; // FIXME: terrible hack
+    //if (esmVer == ESM::VER_094 && reader.getContext().filename.find("allout") != std::string::npos)
+    //    isTES5 = false; // FIXME: terrible hack
 
     mDataTypes = 0;
     mCell = reader.currCell();

From 5630bf8f6b93654a2e34779fd63daf38d237f526 Mon Sep 17 00:00:00 2001
From: "florent.teppe" <teppe.florent@hotmail.fr>
Date: Mon, 12 Aug 2024 21:39:04 +0200
Subject: [PATCH 3/6] clang format

clang format
---
 apps/openmw/mwrender/groundcover.cpp    |  2 +-
 apps/openmw/mwrender/groundcover.hpp    |  2 +-
 apps/openmw/mwrender/landmanager.cpp    |  2 +-
 apps/openmw/mwrender/landmanager.hpp    |  2 +-
 apps/openmw/mwrender/objectpaging.cpp   |  2 +-
 apps/openmw/mwrender/objectpaging.hpp   |  2 +-
 apps/openmw/mwrender/terrainstorage.cpp |  8 +--
 apps/openmw/mwrender/terrainstorage.hpp |  6 +--
 apps/openmw/mwworld/esmstore.hpp        |  2 +-
 components/esm4/loadland.cpp            | 24 ++++-----
 components/esmterrain/storage.cpp       | 69 ++++++++++++-------------
 components/esmterrain/storage.hpp       |  8 +--
 components/terrain/chunkmanager.cpp     | 24 ++++-----
 components/terrain/defs.hpp             |  2 +-
 components/terrain/quadtreeworld.cpp    |  2 +-
 components/terrain/quadtreeworld.hpp    |  2 +-
 components/terrain/terraingrid.cpp      | 10 ++--
 components/terrain/terraingrid.hpp      |  3 +-
 18 files changed, 86 insertions(+), 86 deletions(-)

diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp
index 97d2025a31..115f27149b 100644
--- a/apps/openmw/mwrender/groundcover.cpp
+++ b/apps/openmw/mwrender/groundcover.cpp
@@ -322,7 +322,7 @@ namespace MWRender
     }
 
     osg::ref_ptr<osg::Node> Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod,
-        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/)
+        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile /*, int quad*/)
     {
         if (lod > getMaxLodLevel())
             return nullptr;
diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp
index 51c2cb4628..f51abe48d0 100644
--- a/apps/openmw/mwrender/groundcover.hpp
+++ b/apps/openmw/mwrender/groundcover.hpp
@@ -27,7 +27,7 @@ namespace MWRender
         ~Groundcover();
 
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags,
-            bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/) override;
+            bool activeGrid, const osg::Vec3f& viewPoint, bool compile /*, int quad*/) override;
 
         unsigned int getNodeMask() override;
 
diff --git a/apps/openmw/mwrender/landmanager.cpp b/apps/openmw/mwrender/landmanager.cpp
index ed7d0aa71e..3c927601e3 100644
--- a/apps/openmw/mwrender/landmanager.cpp
+++ b/apps/openmw/mwrender/landmanager.cpp
@@ -49,7 +49,7 @@ namespace MWRender
         return landObj;
     }
 
-    const ESM4::Land *LandManager::getLandRecord(ESM::ExteriorCellLocation cellIndex) const
+    const ESM4::Land* LandManager::getLandRecord(ESM::ExteriorCellLocation cellIndex) const
     {
         const MWBase::World& world = *MWBase::Environment::get().getWorld();
         const ESM4::Land* land = world.getStore().get<ESM4::Land>().search(cellIndex);
diff --git a/apps/openmw/mwrender/landmanager.hpp b/apps/openmw/mwrender/landmanager.hpp
index 21bb80cf48..1d70f11e08 100644
--- a/apps/openmw/mwrender/landmanager.hpp
+++ b/apps/openmw/mwrender/landmanager.hpp
@@ -29,7 +29,7 @@ namespace MWRender
         osg::ref_ptr<ESMTerrain::LandObject> getLand(ESM::ExteriorCellLocation cellIndex);
 
         // FIXME: returning a pointer is probably not compatible with the rest of the codebase
-        const ESM4::Land *getLandRecord(ESM::ExteriorCellLocation cellIndex) const;
+        const ESM4::Land* getLandRecord(ESM::ExteriorCellLocation cellIndex) const;
 
         void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
 
diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp
index ff7436e862..7263a95ad9 100644
--- a/apps/openmw/mwrender/objectpaging.cpp
+++ b/apps/openmw/mwrender/objectpaging.cpp
@@ -78,7 +78,7 @@ namespace MWRender
     }
 
     osg::ref_ptr<osg::Node> ObjectPaging::getChunk(float size, const osg::Vec2f& center, unsigned char /*lod*/,
-        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/)
+        unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile /*, int quad*/)
     {
         if (activeGrid && !mActiveGrid)
             return nullptr;
diff --git a/apps/openmw/mwrender/objectpaging.hpp b/apps/openmw/mwrender/objectpaging.hpp
index 34214bc476..b93de48e47 100644
--- a/apps/openmw/mwrender/objectpaging.hpp
+++ b/apps/openmw/mwrender/objectpaging.hpp
@@ -24,7 +24,7 @@ namespace MWRender
         ~ObjectPaging() = default;
 
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags,
-            bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/) override;
+            bool activeGrid, const osg::Vec3f& viewPoint, bool compile /*, int quad*/) override;
 
         osg::ref_ptr<osg::Node> createChunk(float size, const osg::Vec2f& center, bool activeGrid,
             const osg::Vec3f& viewPoint, bool compile, unsigned char lod);
diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp
index a4e0c52025..5c9570b2ad 100644
--- a/apps/openmw/mwrender/terrainstorage.cpp
+++ b/apps/openmw/mwrender/terrainstorage.cpp
@@ -1,9 +1,9 @@
 #include "terrainstorage.hpp"
 
 #include <components/esm3/loadland.hpp>
-#include <components/esm4/loadwrld.hpp>
 #include <components/esm4/loadltex.hpp>
 #include <components/esm4/loadtxst.hpp>
+#include <components/esm4/loadwrld.hpp>
 
 #include "../mwbase/environment.hpp"
 #include "../mwworld/esmstore.hpp"
@@ -122,18 +122,18 @@ namespace MWRender
         return esmStore.get<ESM::LandTexture>().search(index, plugin);
     }
 
-    const ESM4::Land *TerrainStorage::getEsm4Land(ESM::ExteriorCellLocation cellLocation) const
+    const ESM4::Land* TerrainStorage::getEsm4Land(ESM::ExteriorCellLocation cellLocation) const
     {
         return mLandManager->getLandRecord(cellLocation);
     }
 
-    const ESM4::LandTexture *TerrainStorage::getEsm4LandTexture(ESM::RefId ltexId) const
+    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 ESM4::TextureSet* TerrainStorage::getEsm4TextureSet(ESM::RefId txstId) const
     {
         const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
         return esmStore.get<ESM4::TextureSet>().search(txstId);
diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp
index 85968ded96..621948afcc 100644
--- a/apps/openmw/mwrender/terrainstorage.hpp
+++ b/apps/openmw/mwrender/terrainstorage.hpp
@@ -38,9 +38,9 @@ namespace MWRender
 
         LandManager* getLandManager() const;
 
-        const ESM4::Land *getEsm4Land(ESM::ExteriorCellLocation cellLocation) const override;
-        const ESM4::LandTexture *getEsm4LandTexture(ESM::RefId ltexId) const override;
-        const ESM4::TextureSet *getEsm4TextureSet(ESM::RefId txstId) const override;
+        const ESM4::Land* getEsm4Land(ESM::ExteriorCellLocation cellLocation) const override;
+        const ESM4::LandTexture* getEsm4LandTexture(ESM::RefId ltexId) const override;
+        const ESM4::TextureSet* getEsm4TextureSet(ESM::RefId txstId) const override;
 
         // Intended to be set by RenderingManager.  Ideally this should be part of the
         // construction but this class is initialised in the constructor and we man end up with
diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp
index a1e3f1fc03..8f08ba3a03 100644
--- a/apps/openmw/mwworld/esmstore.hpp
+++ b/apps/openmw/mwworld/esmstore.hpp
@@ -148,7 +148,7 @@ namespace MWWorld
             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::Static>, Store<ESM4::StaticCollection>, Store<ESM4::Terminal>, Store<ESM4::Tree>,
-            Store<ESM4::Weapon>, Store<ESM4::World>, Store<ESM4::TextureSet> >;
+            Store<ESM4::Weapon>, Store<ESM4::World>, Store<ESM4::TextureSet>>;
 
     private:
         template <typename T>
diff --git a/components/esm4/loadland.cpp b/components/esm4/loadland.cpp
index 41bd8ff30e..d4fb72b107 100644
--- a/components/esm4/loadland.cpp
+++ b/components/esm4/loadland.cpp
@@ -26,10 +26,10 @@
 */
 #include "loadland.hpp"
 
-#include <cstdint>
 #include <cassert>
-#include <stdexcept>
+#include <cstdint>
 #include <iostream>
+#include <stdexcept>
 
 #include <components/debug/debuglog.hpp>
 
@@ -77,7 +77,7 @@ void ESM4::Land::load(ESM4::Reader& reader)
     bool isFONV = (esmVer == ESM::VER_132 || esmVer == ESM::VER_133 || esmVer == ESM::VER_134);
     bool isTES5 = (esmVer == ESM::VER_094 || esmVer == ESM::VER_170); // WARN: FO3 is also VER_094
     // WARN: below workaround assumes the data directory path has "Fallout" somewhere
-    //if (esmVer == ESM::VER_094 && reader.getContext().filename.find("allout") != std::string::npos)
+    // if (esmVer == ESM::VER_094 && reader.getContext().filename.find("allout") != std::string::npos)
     //    isTES5 = false; // FIXME: terrible hack
 
     mDataTypes = 0;
@@ -131,17 +131,17 @@ void ESM4::Land::load(ESM4::Reader& reader)
                 if (currentAddQuad != -1)
                 {
                     // NOTE: sometimes there are no VTXT following an ATXT
-                    //Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex
-                        //<< " FormId " << ESM::FormId::toString(mFormId) << std::endl;
+                    // Log(Debug::Verbose) << "ESM4::Land VTXT empty layer " << layer.texture.layerIndex
+                    //<< " FormId " << ESM::FormId::toString(mFormId) << std::endl;
                     if (!layer.texture.formId)
-                      layer.texture.formId = getDefaultTexture(isTES4, isFONV, isTES5);
+                        layer.texture.formId = getDefaultTexture(isTES4, isFONV, isTES5);
 
-                    layer.data.resize(1);            // just one spot
-                    layer.data.back().position = 0;  // this corner
+                    layer.data.resize(1); // just one spot
+                    layer.data.back().position = 0; // this corner
                     layer.data.back().opacity = 0.f; // transparent
 
                     assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()
-                            && "additional texture skipping layer");
+                        && "additional texture skipping layer");
 
                     mTextures[currentAddQuad].layers.push_back(layer);
                 }
@@ -172,7 +172,7 @@ void ESM4::Land::load(ESM4::Reader& reader)
                 }
 
                 assert(layer.texture.layerIndex == mTextures[currentAddQuad].layers.size()
-                        && "additional texture skipping layer");
+                    && "additional texture skipping layer");
 
                 mTextures[currentAddQuad].layers.push_back(layer);
 
@@ -202,8 +202,8 @@ void ESM4::Land::load(ESM4::Reader& reader)
         }
     }
 
-    //if (mCell.toUint32() == 0x00005e1f)
-        //std::cout << "vilverin exterior" << std::endl;
+    // if (mCell.toUint32() == 0x00005e1f)
+    // std::cout << "vilverin exterior" << std::endl;
 
     if (currentAddQuad != -1)
     {
diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp
index d4e6f935cf..ff2ccceb54 100644
--- a/components/esmterrain/storage.cpp
+++ b/components/esmterrain/storage.cpp
@@ -420,18 +420,18 @@ namespace ESMTerrain
     //        differently?
     std::string Storage::getEsm4TextureName(ESM::RefId id)
     {
-        //if (mIsEsm4Ext)
-        if (const ESM4::LandTexture *ltex = getEsm4LandTexture(id))
+        // if (mIsEsm4Ext)
+        if (const ESM4::LandTexture* ltex = getEsm4LandTexture(id))
         {
             if (ltex->mTextureFile.empty()) // WARN: we assume FO3/FONV/TES5
             {
-                if (const ESM4::TextureSet *txst = getEsm4TextureSet(ltex->mTexture))
+                if (const ESM4::TextureSet* txst = getEsm4TextureSet(ltex->mTexture))
                 {
-                    return "textures\\"+txst->mDiffuse;
+                    return "textures\\" + txst->mDiffuse;
                 }
             }
             else
-                return "textures\\landscape\\"+ltex->mTextureFile;
+                return "textures\\landscape\\" + ltex->mTextureFile;
         }
 
         // FIXME: add a debug log here
@@ -635,7 +635,7 @@ namespace ESMTerrain
         Terrain::LayerInfo info;
         info.mParallax = false;
         info.mSpecular = false;
-        //info.mIsEsm4 = false; // hint for Terrain::createPasses()
+        // info.mIsEsm4 = false; // hint for Terrain::createPasses()
         info.mDiffuseMap = texture;
 
         if (mAutoUseNormalMaps)
@@ -672,27 +672,27 @@ namespace ESMTerrain
         return info;
     }
 
-    Terrain::LayerInfo Storage::getLayerInfo(const ESM4::TextureSet *txst)
+    Terrain::LayerInfo Storage::getLayerInfo(const ESM4::TextureSet* txst)
     {
         Terrain::LayerInfo info;
         info.mDiffuseMap = "";
         info.mNormalMap = "";
         info.mParallax = false;
         info.mSpecular = false;
-        //info.mIsEsm4 = true; // hint for Terrain::createPasses()
+        // info.mIsEsm4 = true; // hint for Terrain::createPasses()
 
         if (txst)
         {
             assert(!txst->mDiffuseMap.empty() && "getlayerInfo: empty diffuse map");
 
-            std::string diffuse = "textures\\landscape\\"+txst->mDiffuse;
+            std::string diffuse = "textures\\landscape\\" + txst->mDiffuse;
             std::map<std::string, Terrain::LayerInfo>::iterator found = mLayerInfoMap.find(diffuse);
             if (found != mLayerInfoMap.end())
                 return found->second;
 
             info.mDiffuseMap = diffuse;
             if (!txst->mNormalMap.empty())
-                info.mNormalMap = "textures\\landscape\\"+txst->mNormalMap;
+                info.mNormalMap = "textures\\landscape\\" + txst->mNormalMap;
 
             // FIXME: this flag indicates height info in alpha channel of normal map
             //        but the normal map alpha channel has specular info instead
@@ -736,16 +736,16 @@ namespace ESMTerrain
     {
         if (mIsEsm4Ext)
         {
-            //std::cout << "blendmap scale "
-                //<< std::to_string(ESM4::Land::sQuadTexturePerSide * chunkSize) << std::endl;
-            return ESM4::Land::sQuadTexturePerSide;// * chunkSize;
+            // std::cout << "blendmap scale "
+            //<< std::to_string(ESM4::Land::sQuadTexturePerSide * chunkSize) << std::endl;
+            return ESM4::Land::sQuadTexturePerSide; // * chunkSize;
         }
 
         return ESM::Land::LAND_TEXTURE_SIZE * chunkSize;
     }
 
     void Storage::fillQuadVertexBuffers(float size, const osg::Vec2f& center, ESM::RefId worldspace,
-            osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours, int quad)
+        osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours, int quad)
     {
         // sampleSize is not used but declared here in order to keep the code as close to
         // fillVertexBuffers() as possible
@@ -756,9 +756,9 @@ namespace ESMTerrain
         const std::size_t cellSize = static_cast<std::size_t>(ESM::getLandSize(worldspace));
         const std::size_t numVerts = static_cast<std::size_t>(size * (cellSize - 1) / sampleSize) + 1;
 
-        positions.resize(numVerts*numVerts*3);
-        normals.resize(numVerts*numVerts*3);
-        colours.resize(numVerts*numVerts*4);
+        positions.resize(numVerts * numVerts * 3);
+        normals.resize(numVerts * numVerts * 3);
+        colours.resize(numVerts * numVerts * 4);
 
         const bool alteration = useAlteration(); // Does nothing by default, override in OpenMW-CS
         const int landSizeInUnits = ESM::getCellSize(worldspace);
@@ -784,7 +784,7 @@ namespace ESMTerrain
         //std::cout << origin2.x() << ", " << origin2.y() << std::endl;
 #endif
         const osg::Vec2f origin = center - osg::Vec2f(size, size) * 0.5f;
-        //std::cout << origin.x() << ", " << origin.y() << std::endl;
+        // std::cout << origin.x() << ", " << origin.y() << std::endl;
         const int startCellX = static_cast<int>(std::floor(origin.x()));
         const int startCellY = static_cast<int>(std::floor(origin.y()));
         LandCache cache(startCellX - 1, startCellY - 1, static_cast<std::size_t>(std::ceil(size)) + 2);
@@ -884,14 +884,13 @@ namespace ESMTerrain
             {
                 float height = -2048;
                 if (land && heightData) // validHeightDataExists
-                    height = heightData->getHeights()[col*cellSize + row];
+                    height = heightData->getHeights()[col * cellSize + row];
 
                 // FIXME: I suspect landSizeInUnits should be 2048
                 const std::size_t vertIndex = vertX * numVerts + vertY;
                 positions[vertIndex]
                     = osg::Vec3f((vertX / static_cast<float>(numVerts - 1) - 0.5f) * size * landSizeInUnits,
-                                 (vertY / static_cast<float>(numVerts - 1) - 0.5f) * size * landSizeInUnits,
-                                 height);
+                        (vertY / static_cast<float>(numVerts - 1) - 0.5f) * size * landSizeInUnits, height);
 
                 if (land && normalData)
                 {
@@ -914,7 +913,7 @@ namespace ESMTerrain
                 if ((row == 0 || row == cellSize - 1) && (col == 0 || col == cellSize - 1))
                     averageNormal(normal, cellLocation, col, row, cache);
 #endif
-                //assert(normal.z() > 0); // ToddLand triggers this
+                // assert(normal.z() > 0); // ToddLand triggers this
                 if (normal.z() < 0)
                     normal.z() = 0;
 
@@ -940,7 +939,7 @@ namespace ESMTerrain
                 if (col == cellSize - 1 || row == cellSize - 1)
                     fixColour(color, cellLocation, col, row, cache);
 #endif
-//              color.a() = 1;
+                //              color.a() = 1;
                 colours[vertIndex] = color;
 
                 ++vertX;
@@ -950,7 +949,7 @@ namespace ESMTerrain
     }
 
     void Storage::getQuadBlendmaps(float chunkSize, const osg::Vec2f& chunkCenter, ImageVector& blendmaps,
-            std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace, int quad)
+        std::vector<Terrain::LayerInfo>& layerList, ESM::RefId worldspace, int quad)
     {
         // VTXT info indicates texture size is 17x17 - but the cell grid is 33x33
         // (cf. TES3 has 65x65 cell) do we discard one row and column or overlap?
@@ -989,12 +988,12 @@ namespace ESMTerrain
         const int startCellX = static_cast<int>(std::floor(origin.x()));
         const int startCellY = static_cast<int>(std::floor(origin.y()));
         const int realTextureSize = 17; // FIXME: should be defined in Land record
-        //const std::size_t blendmapSize = getBlendmapSize(chunkSize, realTextureSize);
+        // const std::size_t blendmapSize = getBlendmapSize(chunkSize, realTextureSize);
         const std::size_t blendmapSize = realTextureSize;
 
         // FIXME: temp testing
-        //if (startCellX == 12 && startCellY == 21)
-            //std::cout << "vilverin exterior" << std::endl;
+        // if (startCellX == 12 && startCellY == 21)
+        // std::cout << "vilverin exterior" << std::endl;
 
 // FIXME: I don't think ESM4 needs this?
 #if 0
@@ -1037,7 +1036,7 @@ namespace ESMTerrain
         std::map<UniqueTextureId, std::size_t> textureIndicesMap;
 #endif
         // FIXME: debugging only
-        //std::cout << "quad " << quad << std::endl;
+        // std::cout << "quad " << quad << std::endl;
 
         // base texture
         Terrain::LayerInfo info;
@@ -1049,10 +1048,10 @@ namespace ESMTerrain
             info = getLayerInfo(texture); // TES4
 
         // FIXME: debugging only
-        //std::cout << "base " << info.mDiffuseMap << std::endl;
+        // std::cout << "base " << info.mDiffuseMap << std::endl;
         osg::ref_ptr<osg::Image> image(new osg::Image);
-        image->allocateImage(static_cast<int>(blendmapImageSize), static_cast<int>(blendmapImageSize),
-            1, GL_ALPHA, GL_UNSIGNED_BYTE);
+        image->allocateImage(
+            static_cast<int>(blendmapImageSize), static_cast<int>(blendmapImageSize), 1, GL_ALPHA, GL_UNSIGNED_BYTE);
         std::memset(image->data(), 255, image->getTotalDataSize()); // fully opaque for base texture
         blendmaps.push_back(std::move(image));
         layerList.push_back(std::move(info));
@@ -1071,11 +1070,11 @@ namespace ESMTerrain
                 layerInfo = getLayerInfo(layerTexture); // TES4
 
             // FIXME: debugging only
-            //std::cout << "layer " << i << ", " << layerInfo.mDiffuseMap << std::endl;
+            // std::cout << "layer " << i << ", " << layerInfo.mDiffuseMap << std::endl;
 
             osg::ref_ptr<osg::Image> layerImage(new osg::Image);
-            layerImage->allocateImage(static_cast<int>(blendmapImageSize), static_cast<int>(blendmapImageSize),
-                1, GL_ALPHA, GL_UNSIGNED_BYTE);
+            layerImage->allocateImage(static_cast<int>(blendmapImageSize), static_cast<int>(blendmapImageSize), 1,
+                GL_ALPHA, GL_UNSIGNED_BYTE);
             std::memset(layerImage->data(), 0, layerImage->getTotalDataSize());
             blendmaps.push_back(std::move(layerImage));
             layerList.push_back(std::move(layerInfo));
@@ -1114,7 +1113,7 @@ namespace ESMTerrain
 
                 std::size_t y = realTextureSize - 1 - std::floor(position / realTextureSize);
                 std::size_t x = position % realTextureSize;
-                data[y*realTextureSize + x] = unsigned char(opacityData[j].opacity * 255);
+                data[y * realTextureSize + x] = unsigned char(opacityData[j].opacity * 255);
             }
         }
     }
diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp
index 27ec297613..6872e1a039 100644
--- a/components/esmterrain/storage.hpp
+++ b/components/esmterrain/storage.hpp
@@ -132,9 +132,9 @@ namespace ESMTerrain
             return data->getHeights()[y * landSize + x];
         }
 
-        virtual const ESM4::Land *getEsm4Land(ESM::ExteriorCellLocation cellLocation) const = 0;
-        virtual const ESM4::LandTexture *getEsm4LandTexture(ESM::RefId ltexId) const = 0;
-        virtual const ESM4::TextureSet *getEsm4TextureSet(ESM::RefId txstId) const = 0;
+        virtual const ESM4::Land* getEsm4Land(ESM::ExteriorCellLocation cellLocation) const = 0;
+        virtual const ESM4::LandTexture* getEsm4LandTexture(ESM::RefId ltexId) const = 0;
+        virtual const ESM4::TextureSet* getEsm4TextureSet(ESM::RefId txstId) const = 0;
 
         void fillQuadVertexBuffers(float size, const osg::Vec2f& center, ESM::RefId worldspace,
             osg::Vec3Array& positions, osg::Vec3Array& normals, osg::Vec4ubArray& colours, int quad);
@@ -177,7 +177,7 @@ namespace ESMTerrain
 
         Terrain::LayerInfo getLayerInfo(const std::string& texture);
 
-        Terrain::LayerInfo getLayerInfo(const ESM4::TextureSet *txst);
+        Terrain::LayerInfo getLayerInfo(const ESM4::TextureSet* txst);
     };
 
 }
diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp
index 4cb4439ed3..9e057adc13 100644
--- a/components/terrain/chunkmanager.cpp
+++ b/components/terrain/chunkmanager.cpp
@@ -165,14 +165,14 @@ namespace Terrain
         }
     }
 
-// >   openmw.exe!Terrain::ChunkManager::createPasses() Line 188
-//     openmw.exe!Terrain::ChunkManager::createCompositeMapGeometry() Line 123
-//     openmw.exe!Terrain::ChunkManager::createChunk() Line 275
-//     openmw.exe!Terrain::ChunkManager::getChunk() Line 59
-//     openmw.exe!Terrain::QuadTreeWorld::loadRenderingNode() Line 397
-//     openmw.exe!Terrain::QuadTreeWorld::preload() Line 558
-//     openmw.exe!MWWorld::TerrainPreloadItem::doWork() Line 184
-//     openmw.exe!SceneUtil::WorkThread::run() Line 135
+    // >   openmw.exe!Terrain::ChunkManager::createPasses() Line 188
+    //     openmw.exe!Terrain::ChunkManager::createCompositeMapGeometry() Line 123
+    //     openmw.exe!Terrain::ChunkManager::createChunk() Line 275
+    //     openmw.exe!Terrain::ChunkManager::getChunk() Line 59
+    //     openmw.exe!Terrain::QuadTreeWorld::loadRenderingNode() Line 397
+    //     openmw.exe!Terrain::QuadTreeWorld::preload() Line 558
+    //     openmw.exe!MWWorld::TerrainPreloadItem::doWork() Line 184
+    //     openmw.exe!SceneUtil::WorkThread::run() Line 135
     std::vector<osg::ref_ptr<osg::StateSet>> ChunkManager::createPasses(
         float chunkSize, const osg::Vec2f& chunkCenter, bool forCompositeMap, int quad)
     {
@@ -181,8 +181,8 @@ namespace Terrain
 
         if (quad >= 0) // NOTE: quad == -1 has a special meaning of "no quads"
         {
-            static_cast<ESMTerrain::Storage*>(mStorage)
-                ->getQuadBlendmaps(chunkSize, chunkCenter, blendmaps, layerList, mWorldspace, quad);
+            static_cast<ESMTerrain::Storage*>(mStorage)->getQuadBlendmaps(
+                chunkSize, chunkCenter, blendmaps, layerList, mWorldspace, quad);
         }
         else
             mStorage->getBlendmaps(chunkSize, chunkCenter, blendmaps, layerList, mWorldspace);
@@ -263,8 +263,8 @@ namespace Terrain
             //       in case the Morrowind specific "fixes" causes problems
             // NOTE: LOD is not supported
             if (quad >= 0) // NOTE: quad == -1 has a special meaning of "no quads"
-                static_cast<ESMTerrain::Storage*>(mStorage)
-                    ->fillQuadVertexBuffers(chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors, quad);
+                static_cast<ESMTerrain::Storage*>(mStorage)->fillQuadVertexBuffers(
+                    chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors, quad);
             else
 #endif
                 mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, mWorldspace, *positions, *normals, *colors);
diff --git a/components/terrain/defs.hpp b/components/terrain/defs.hpp
index fdc74cf8c3..69c35da02f 100644
--- a/components/terrain/defs.hpp
+++ b/components/terrain/defs.hpp
@@ -20,7 +20,7 @@ namespace Terrain
         std::string mNormalMap;
         bool mParallax; // Height info in normal map alpha channel?
         bool mSpecular; // Specular info in diffuse map alpha channel?
-        //bool mIsEsm4;   // intended to be used in Terrain::createPasses()
+        // bool mIsEsm4;   // intended to be used in Terrain::createPasses()
 
         bool requiresShaders() const { return !mNormalMap.empty() || mSpecular; }
     };
diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp
index 70db339336..f9861fc4a0 100644
--- a/components/terrain/quadtreeworld.cpp
+++ b/components/terrain/quadtreeworld.cpp
@@ -262,7 +262,7 @@ namespace Terrain
         {
         }
         osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& chunkCenter, unsigned char lod,
-            unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad*/)
+            unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile /*, int quad*/)
         {
             osg::Vec3f center = { chunkCenter.x(), chunkCenter.y(), 0 };
             auto chunkBorder = CellBorder::createBorderGeometry(center.x() - size / 2.f, center.y() - size / 2.f, size,
diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp
index 31db0d8878..7bdbae82e7 100644
--- a/components/terrain/quadtreeworld.hpp
+++ b/components/terrain/quadtreeworld.hpp
@@ -67,7 +67,7 @@ namespace Terrain
                 mWorldspace = worldspace;
             }
             virtual osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod,
-                unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile/*, int quad = -1*/)
+                unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile /*, int quad = -1*/)
                 = 0;
             virtual unsigned int getNodeMask() { return 0; }
 
diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp
index 8f34599d06..1938e6ed7e 100644
--- a/components/terrain/terraingrid.cpp
+++ b/components/terrain/terraingrid.cpp
@@ -9,8 +9,8 @@
 #include "heightcull.hpp"
 #include "storage.hpp"
 #include "view.hpp"
-#include <components/sceneutil/positionattitudetransform.hpp>
 #include <components/esm/util.hpp>
+#include <components/sceneutil/positionattitudetransform.hpp>
 
 namespace Terrain
 {
@@ -71,13 +71,13 @@ namespace Terrain
             float newChunkSize = chunkSize / 2.f;
             {
                 buildTerrain(group, // top right
-                        newChunkSize, chunkCenter + osg::Vec2f(newChunkSize / 2.f, newChunkSize / 2.f), 3);
+                    newChunkSize, chunkCenter + osg::Vec2f(newChunkSize / 2.f, newChunkSize / 2.f), 3);
                 buildTerrain(group, // top left
-                        newChunkSize, chunkCenter + osg::Vec2f(newChunkSize / 2.f, -newChunkSize / 2.f), 1);
+                    newChunkSize, chunkCenter + osg::Vec2f(newChunkSize / 2.f, -newChunkSize / 2.f), 1);
                 buildTerrain(group, // bottom right
-                        newChunkSize, chunkCenter + osg::Vec2f(-newChunkSize / 2.f, newChunkSize / 2.f), 2);
+                    newChunkSize, chunkCenter + osg::Vec2f(-newChunkSize / 2.f, newChunkSize / 2.f), 2);
                 buildTerrain(group, // bottom left
-                        newChunkSize, chunkCenter + osg::Vec2f(-newChunkSize / 2.f, -newChunkSize / 2.f), 0);
+                    newChunkSize, chunkCenter + osg::Vec2f(-newChunkSize / 2.f, -newChunkSize / 2.f), 0);
             }
             return group;
         }
diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp
index 23a58d34e6..7ca9ffa56f 100644
--- a/components/terrain/terraingrid.hpp
+++ b/components/terrain/terraingrid.hpp
@@ -47,7 +47,8 @@ namespace Terrain
 
     private:
         // quad is meant to be used for ESM4 terrain only; if -1 it is ignored, should be [0..3]
-        osg::ref_ptr<osg::Node> buildTerrain(osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter, int quad = -1);
+        osg::ref_ptr<osg::Node> buildTerrain(
+            osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter, int quad = -1);
         void updateWaterCulling();
 
         // split each ESM::Cell into mNumSplits*mNumSplits terrain chunks

From a1486fc975af9a002c4e003fea3df225051ec789 Mon Sep 17 00:00:00 2001
From: fteppe <teppe.florent@hotmail.fr>
Date: Mon, 12 Aug 2024 23:11:54 +0200
Subject: [PATCH 4/6] Fix linux compilation

---
 apps/opencs/view/render/terrainstorage.cpp | 18 ++++++++++++++++++
 apps/opencs/view/render/terrainstorage.hpp |  4 ++++
 components/esmterrain/storage.cpp          |  7 ++-----
 3 files changed, 24 insertions(+), 5 deletions(-)

diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp
index ff9a8e09b1..c1d6f1d2d7 100644
--- a/apps/opencs/view/render/terrainstorage.cpp
+++ b/apps/opencs/view/render/terrainstorage.cpp
@@ -170,4 +170,22 @@ namespace CSVRender
     {
         return mAlteredHeight[static_cast<unsigned int>(col * ESM::Land::LAND_SIZE + row)];
     }
+
+    const ESM4::Land* TerrainStorage::getEsm4Land(ESM::ExteriorCellLocation cellLocation) const
+    {
+        assert(false && "ESM4 land not supported by CS");
+        return nullptr;
+    }
+
+    const ESM4::LandTexture* TerrainStorage::getEsm4LandTexture(ESM::RefId ltexId) const
+    {
+        assert(false && "ESM4 land not supported by CS");
+        return nullptr;
+    }
+
+    const ESM4::TextureSet* TerrainStorage::getEsm4TextureSet(ESM::RefId txstId) const
+    {
+        assert(false && "ESM4 land not supported by CS");
+        return nullptr;
+    }
 }
diff --git a/apps/opencs/view/render/terrainstorage.hpp b/apps/opencs/view/render/terrainstorage.hpp
index f7a7f72201..f9df281e43 100644
--- a/apps/opencs/view/render/terrainstorage.hpp
+++ b/apps/opencs/view/render/terrainstorage.hpp
@@ -33,6 +33,10 @@ namespace CSVRender
         float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY);
         float* getAlteredHeight(int inCellX, int inCellY);
 
+        const ESM4::Land* getEsm4Land(ESM::ExteriorCellLocation cellLocation) const override;
+        const ESM4::LandTexture* getEsm4LandTexture(ESM::RefId ltexId) const override;
+        const ESM4::TextureSet* getEsm4TextureSet(ESM::RefId txstId) const override;
+
     private:
         const CSMWorld::Data& mData;
         std::array<float, ESM::Land::LAND_SIZE * ESM::Land::LAND_SIZE> mAlteredHeight;
diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp
index ff2ccceb54..7dd30cc86d 100644
--- a/components/esmterrain/storage.cpp
+++ b/components/esmterrain/storage.cpp
@@ -683,7 +683,7 @@ namespace ESMTerrain
 
         if (txst)
         {
-            assert(!txst->mDiffuseMap.empty() && "getlayerInfo: empty diffuse map");
+            assert(!txst->mDiffuse.empty() && "getlayerInfo: empty diffuse map");
 
             std::string diffuse = "textures\\landscape\\" + txst->mDiffuse;
             std::map<std::string, Terrain::LayerInfo>::iterator found = mLayerInfoMap.find(diffuse);
@@ -760,7 +760,6 @@ namespace ESMTerrain
         normals.resize(numVerts * numVerts * 3);
         colours.resize(numVerts * numVerts * 4);
 
-        const bool alteration = useAlteration(); // Does nothing by default, override in OpenMW-CS
         const int landSizeInUnits = ESM::getCellSize(worldspace);
 
 // I think the current code copied from fillVertexBuffers() works fine
@@ -793,14 +792,12 @@ namespace ESMTerrain
         const ESM::LandData* heightData = nullptr;
         const ESM::LandData* normalData = nullptr;
         const ESM::LandData* colourData = nullptr;
-        bool validHeightDataExists = false;
 
         if (land != nullptr)
         {
             heightData = land->getData(ESM::Land::DATA_VHGT);
             normalData = land->getData(ESM::Land::DATA_VNML);
             colourData = land->getData(ESM::Land::DATA_VCLR);
-            validHeightDataExists = true;
         }
 
         int rowStart = 0;
@@ -1113,7 +1110,7 @@ namespace ESMTerrain
 
                 std::size_t y = realTextureSize - 1 - std::floor(position / realTextureSize);
                 std::size_t x = position % realTextureSize;
-                data[y * realTextureSize + x] = unsigned char(opacityData[j].opacity * 255);
+                data[y * realTextureSize + x] = (unsigned char)(opacityData[j].opacity * 255);
             }
         }
     }

From d81160cbff30e746771919d0c841a488e1dda234 Mon Sep 17 00:00:00 2001
From: "florent.teppe" <teppe.florent@hotmail.fr>
Date: Tue, 13 Aug 2024 16:43:13 +0200
Subject: [PATCH 5/6] Fix prurple texture + shader compilation

Gives a default diffuse texture if none is given
Fixes a shader compilation bug that made terrain big single texture squares instead of multiple textures blended together.
---
 components/terrain/chunkmanager.cpp          | 3 ++-
 files/shaders/compatibility/esm4terrain.frag | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp
index 9e057adc13..79fca6752f 100644
--- a/components/terrain/chunkmanager.cpp
+++ b/components/terrain/chunkmanager.cpp
@@ -200,7 +200,8 @@ namespace Terrain
                 textureLayer.mParallax = it->mParallax;
                 textureLayer.mSpecular = it->mSpecular;
 
-                textureLayer.mDiffuseMap = mTextureManager->getTexture(it->mDiffuseMap);
+                textureLayer.mDiffuseMap = mTextureManager->getTexture(
+                    (it->mDiffuseMap.empty() ? "textures\\landscape\\dirt02.dds" : it->mDiffuseMap));
 
                 if (!forCompositeMap && !it->mNormalMap.empty())
                     textureLayer.mNormalMap = mTextureManager->getTexture(it->mNormalMap);
diff --git a/files/shaders/compatibility/esm4terrain.frag b/files/shaders/compatibility/esm4terrain.frag
index 243e0e9ba1..f40f6bf6a9 100644
--- a/files/shaders/compatibility/esm4terrain.frag
+++ b/files/shaders/compatibility/esm4terrain.frag
@@ -93,7 +93,7 @@ void main()
 #endif
     vec3 diffuseLight, ambientLight, specularLight;
     doLighting(passViewPos, viewNormal, shininess, shadowing, diffuseLight, ambientLight, specularLight);
-    lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz;
+    lighting = diffuseTex.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz;
     specular = specularColor * specularLight;
 #endif
 

From 5df675e8416b200816b0ebad80f671502027b89e Mon Sep 17 00:00:00 2001
From: "florent.teppe" <teppe.florent@hotmail.fr>
Date: Thu, 15 Aug 2024 13:39:30 +0200
Subject: [PATCH 6/6] TEST: print the layer index of the blending

---
 components/terrain/material.cpp              |  4 ++
 files/shaders/compatibility/esm4terrain.frag | 53 +++++++++++++++++---
 2 files changed, 51 insertions(+), 6 deletions(-)

diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp
index f7536a08fa..2e19b00cd3 100644
--- a/components/terrain/material.cpp
+++ b/components/terrain/material.cpp
@@ -315,7 +315,11 @@ namespace Terrain
                 Stereo::shaderStereoDefines(defineMap);
 
                 if (quad >= 0)
+                {
                     stateset->setAttributeAndModes(shaderManager.getProgram("esm4terrain", defineMap));
+                    int LayerIndex = it - layers.begin();
+                    stateset->addUniform(new osg::Uniform("layerIndex", LayerIndex));
+                }
                 else
                     stateset->setAttributeAndModes(shaderManager.getProgram("terrain", defineMap));
                 stateset->addUniform(UniformCollection::value().mColorMode);
diff --git a/files/shaders/compatibility/esm4terrain.frag b/files/shaders/compatibility/esm4terrain.frag
index f40f6bf6a9..108b3ffb4f 100644
--- a/files/shaders/compatibility/esm4terrain.frag
+++ b/files/shaders/compatibility/esm4terrain.frag
@@ -36,6 +36,7 @@ varying vec3 passNormal;
 
 uniform vec2 screenRes;
 uniform float far;
+uniform int layerIndex;
 
 #include "vertexcolors.glsl"
 #include "shadows_fragment.glsl"
@@ -53,17 +54,57 @@ void main()
 #endif
     vec4 diffuseTex = texture2D(diffuseMap, adjustedUV);
     gl_FragData[0] = vec4(diffuseTex.xyz, 1.0);
-
-#if @baseLayer
+    
     vec4 diffuseColor = getDiffuseColor();
-    gl_FragData[0].a *= diffuseColor.a;
-#endif
 
 #if @blendMap
     vec2 blendMapUV = (gl_TextureMatrix[1] * vec4(uv, 0.0, 1.0)).xy;
+    //blendMapUV.x = 1.f - blendMapUV.x;
 #if @baseLayer
+    gl_FragData[0].a *= diffuseColor.a;
 #else
-    gl_FragData[0].a = texture2D(blendMap, blendMapUV).a;
+    float blendingValue = texture2D(blendMap, blendMapUV).a;
+    bool debug  = false;
+    if(debug)
+    {
+        gl_FragData[0].rgb = vec3(0.f);
+        if(layerIndex == 1)
+        {
+            gl_FragData[0].r = blendingValue;
+        }
+        if(layerIndex == 2)
+        {
+            gl_FragData[0].g = blendingValue;
+        }
+        if(layerIndex == 3)
+        {
+            gl_FragData[0].b = blendingValue;
+        }
+        if(layerIndex == 4)
+        {
+            gl_FragData[0].rg = vec2(blendingValue);
+        }
+        if(layerIndex == 5)
+        {
+            gl_FragData[0].rb = vec2(blendingValue);
+        }
+        if(layerIndex == 6)
+        {
+            gl_FragData[0].gb = vec2(blendingValue);
+        }
+        if(layerIndex > 6)
+        {
+            gl_FragData[0].rgb = vec3(0.f);
+        }
+    }
+
+    gl_FragData[0].a = blendingValue;
+   
+
+    //gl_FragData[0].a = 1.f;
+    //gl_FragData[0].a = 0.f ; //texture2D(blendMap, blendMapUV).a;
+
+
 #endif
 #endif
 
@@ -93,7 +134,7 @@ void main()
 #endif
     vec3 diffuseLight, ambientLight, specularLight;
     doLighting(passViewPos, viewNormal, shininess, shadowing, diffuseLight, ambientLight, specularLight);
-    lighting = diffuseTex.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz;
+    lighting = diffuseColor.xyz * diffuseLight + getAmbientColor().xyz * ambientLight + getEmissionColor().xyz;
     specular = specularColor * specularLight;
 #endif