diff --git a/CHANGELOG.md b/CHANGELOG.md
index a46f47f53..face13d19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,6 @@
 0.47.0
 ------
 
-    Bug #832: OpenMW-CS: Handle deleted references
     Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path
     Bug #1901: Actors colliding behaviour is different from vanilla
     Bug #1952: Incorrect particle lighting
@@ -9,7 +8,7 @@
     Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs
     Bug #2473: Unable to overstock merchants
     Bug #2798: Mutable ESM records
-    Bug #2976 [reopened]: Issues combining settings from the command line and both config files
+    Bug #2976: [reopened]: Issues combining settings from the command line and both config files
     Bug #3137: Walking into a wall prevents jumping
     Bug #3372: Projectiles and magic bolts go through moving targets
     Bug #3676: NiParticleColorModifier isn't applied properly
@@ -24,7 +23,7 @@
     Bug #4201: Projectile-projectile collision
     Bug #4247: Cannot walk up stairs in Ebonheart docks
     Bug #4357: OpenMW-CS: TopicInfos index sorting and rearranging isn't fully functional
-    Bug #4363: Editor: Defect in Clone Function for Dialogue Info records
+    Bug #4363: OpenMW-CS: Defect in Clone Function for Dialogue Info records
     Bug #4447: Actor collision capsule shape allows looking through some walls
     Bug #4465: Collision shape overlapping causes twitching
     Bug #4476: Abot Gondoliers: player hangs in air during scenic travel
@@ -34,6 +33,7 @@
     Bug #4764: Data race in osg ParticleSystem
     Bug #4765: Data race in ChunkManager -> Array::setBinding
     Bug #4774: Guards are ignorant of an invisible player that tries to attack them
+    Bug #5026: Data races with rain intensity uniform set by sky and used by water
     Bug #5101: Hostile followers travel with the player
     Bug #5108: Savegame bloating due to inefficient fog textures format
     Bug #5165: Active spells should use real time intead of timestamps
@@ -44,11 +44,11 @@
     Bug #5367: Selecting a spell on an enchanted item per hotkey always plays the equip sound
     Bug #5369: Spawnpoint in the Grazelands doesn't produce oversized creatures
     Bug #5370: Opening an unlocked but trapped door uses the key
-    Bug #5384: openmw-cs: deleting an instance requires reload of scene window to show in editor
+    Bug #5384: OpenMW-CS: Deleting an instance requires reload of scene window to show in editor
     Bug #5387: Move/MoveWorld don't update the object's cell properly
     Bug #5391: Races Redone 1.2 bodies don't show on the inventory
     Bug #5397: NPC greeting does not reset if you leave + reenter area
-    Bug #5400: Editor: Verifier checks race of non-skin bodyparts
+    Bug #5400: OpenMW-CS: Verifier checks race of non-skin bodyparts
     Bug #5403: Enchantment effect doesn't show on an enemy during death animation
     Bug #5415: Environment maps in ebony cuirass and HiRez Armors Indoril cuirass don't work
     Bug #5416: Junk non-node records before the root node are not handled gracefully
@@ -70,6 +70,7 @@
     Bug #5499: Faction advance is available when requirements not met
     Bug #5502: Dead zone for analogue stick movement is too small
     Bug #5507: Sound volume is not clamped on ingame settings update
+    Bug #5525: Case-insensitive search in the inventory window does not work with non-ASCII characters
     Bug #5531: Actors flee using current rotation by axis x
     Bug #5539: Window resize breaks when going from a lower resolution to full screen resolution
     Bug #5548: Certain exhausted topics can be highlighted again even though there's no new dialogue
@@ -93,27 +94,33 @@
     Bug #5703: OpenMW-CS menu system crashing on XFCE
     Bug #5706: AI sequences stop looping after the saved game is reloaded
     Bug #5713: OpenMW-CS: Collada models are corrupted in Qt-based scene view
-    Bug #5731: Editor: skirts are invisible on characters
+    Bug #5731: OpenMW-CS: skirts are invisible on characters
     Bug #5739: Saving and loading the save a second or two before hitting the ground doesn't count fall damage
     Bug #5758: Paralyzed actors behavior is inconsistent with vanilla
     Bug #5762: Movement solver is insufficiently robust
+    Bug #5807: Video decoding crash on ARM
     Bug #5821: NPCs from mods getting removed if mod order was changed
     Bug #5835: OpenMW doesn't accept negative values for NPC's hello, alarm, fight, and flee
     Bug #5836: OpenMW dialogue/greeting/voice filter doesn't accept negative Ai values for NPC's hello, alarm, fight, and flee
     Bug #5838: Local map and other menus become blank in some locations while playing Wizards' Islands mod.
     Bug #5840: GetSoundPlaying "Health Damage" doesn't play when NPC hits target with shield effect ( vanilla engine behavior )
     Bug #5841: Can't Cast Zero Cost Spells When Magicka is < 0
+    Bug #5869: Guards can initiate arrest dialogue behind locked doors
     Bug #5871: The console appears if you type the Russian letter "Ё" in the name of the enchantment
+    Bug #5877: Effects appearing with empty icon
+    Bug #5899: Visible modal windows and dropdowns crashing game on exit
     Feature #390: 3rd person look "over the shoulder"
+    Feature #832: OpenMW-CS: Handle deleted references
     Feature #1536: Show more information about level on menu
     Feature #2386: Distant Statics in the form of Object Paging
     Feature #2404: Levelled List can not be placed into a container
     Feature #2686: Timestamps in openmw.log
     Feature #3171: OpenMW-CS: Instance drag selection
     Feature #4894: Consider actors as obstacles for pathfinding
+    Feature #4899: Alpha-To-Coverage Anti-Aliasing for alpha testing
     Feature #4977: Use the "default icon.tga" when an item's icon is not found
     Feature #5043: Head Bobbing
-    Feature #5199: Improve Scene Colors
+    Feature #5199: OpenMW-CS: Improve scene view colors
     Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher
     Feature #5362: Show the soul gems' trapped soul in count dialog
     Feature #5445: Handle NiLines
@@ -122,7 +129,6 @@
     Feature #5486: Fixes trainers to choose their training skills based on their base skill points
     Feature #5519: Code Patch tab in launcher
     Feature #5524: Resume failed script execution after reload
-    Feature #5525: Search fields tweaks (utf-8)
     Feature #5545: Option to allow stealing from an unconscious NPC during combat
     Feature #5563: Run physics update in background thread
     Feature #5579: MCP SetAngle enhancement
diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh
index ba0ff00ca..6dad0abc1 100755
--- a/CI/before_install.osx.sh
+++ b/CI/before_install.osx.sh
@@ -1,13 +1,19 @@
-#!/bin/sh -e
+#!/bin/sh -ex
 
 # workaround python issue on travis
 HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true
 HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true
+HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true
 
 # Some of these tools can come from places other than brew, so check before installing
 command -v ccache >/dev/null 2>&1 || brew install ccache
 command -v cmake >/dev/null 2>&1 || brew install cmake
-command -v qmake >/dev/null 2>&1 || brew install qt
+command -v qmake >/dev/null 2>&1 || brew install qt@5
+export PATH="/usr/local/opt/qt@5/bin:$PATH"  # needed to use qmake in none default path as qt now points to qt6
+
+ccache --version
+cmake --version
+qmake --version
 
 curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-f8918dd.zip -o ~/openmw-deps.zip
 unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null
diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh
index 8f9be16e1..36fa79299 100755
--- a/CI/before_script.osx.sh
+++ b/CI/before_script.osx.sh
@@ -4,7 +4,7 @@ export CXX=clang++
 export CC=clang
 
 DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps"
-QT_PATH=$(brew --prefix qt)
+QT_PATH=$(brew --prefix qt@5)
 CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache
 mkdir build
 cd build
diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh
index 82d2ff681..3e7ab7fca 100755
--- a/CI/install_debian_deps.sh
+++ b/CI/install_debian_deps.sh
@@ -23,6 +23,7 @@ declare -rA GROUPED_DEPS=(
     libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-dev
     libbullet-dev liblz4-dev libpng-dev libjpeg-dev
   "
+  # TODO: add librecastnavigation-dev when debian is ready
 
   # These dependencies can alternatively be built and linked statically.
   [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fdac00d1e..fa1194205 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,6 +16,11 @@ endif()
 # Detect OS
 include(cmake/OSIdentity.cmake)
 
+option(OPENMW_GL4ES_MANUAL_INIT "Manually initialize gl4es. This is more reliable on platforms without a windowing system. Requires gl4es to be configured with -DNOEGL=ON -DNO_LOADER=ON -DNO_INIT_CONSTRUCTOR=ON." OFF)
+if(OPENMW_GL4ES_MANUAL_INIT)
+    add_definitions(-DOPENMW_GL4ES_MANUAL_INIT)
+endif()
+
 # Apps and tools
 option(BUILD_OPENMW             "Build OpenMW" ON)
 option(BUILD_LAUNCHER           "Build Launcher" ON)
@@ -130,6 +135,7 @@ option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_stat
 option(OPENMW_USE_SYSTEM_RECASTNAVIGATION "Use system provided recastnavigation library" OFF)
 if(OPENMW_USE_SYSTEM_RECASTNAVIGATION)
     set(_recastnavigation_static_default OFF)
+    find_package(RecastNavigation REQUIRED)
 else()
     set(_recastnavigation_static_default ON)
 endif()
diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt
index 8be1fa4d3..65575580a 100644
--- a/apps/opencs/CMakeLists.txt
+++ b/apps/opencs/CMakeLists.txt
@@ -215,12 +215,16 @@ if(APPLE)
 endif(APPLE)
 
 target_link_libraries(openmw-cs
-    ${OSG_LIBRARIES}
-    ${OSGTEXT_LIBRARIES}
-    ${OSGUTIL_LIBRARIES}
+    # CMake's built-in OSG finder does not use pkgconfig, so we have to
+    # manually ensure the order is correct for inter-library dependencies.
+    # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`.
+    # https://gitlab.kitware.com/cmake/cmake/-/issues/21701
     ${OSGVIEWER_LIBRARIES}
-    ${OSGGA_LIBRARIES}
     ${OSGFX_LIBRARIES}
+    ${OSGGA_LIBRARIES}
+    ${OSGUTIL_LIBRARIES}
+    ${OSGTEXT_LIBRARIES}
+    ${OSG_LIBRARIES}
     ${EXTERN_OSGQT_LIBRARY}
     ${Boost_SYSTEM_LIBRARY}
     ${Boost_FILESYSTEM_LIBRARY}
@@ -233,17 +237,24 @@ if(OSG_STATIC)
     add_library(openmw_cs_osg_plugins INTERFACE)
     foreach(_plugin ${USED_OSG_PLUGINS})
         string(TOUPPER ${_plugin} _plugin_uc)
-        if (${_plugin_uc}_LIBRARY MATCHES "[/.]")
+        if(OPENMW_USE_SYSTEM_OSG)
             list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY})
         else()
             list(APPEND _osg_plugins_static_files $<TARGET_FILE:${${_plugin_uc}_LIBRARY}>)
+            target_link_libraries(openmw_cs_osg_plugins INTERFACE $<TARGET_PROPERTY:${${_plugin_uc}_LIBRARY},LINK_LIBRARIES>)
+            add_dependencies(openmw_cs_osg_plugins ${${_plugin_uc}_LIBRARY})
         endif()
-        target_link_libraries(openmw_cs_osg_plugins INTERFACE ${${_plugin_uc}_LIBRARY})
     endforeach()
     # We use --whole-archive because OSG plugins use registration.
     get_whole_archive_options(_opts ${_osg_plugins_static_files})
     target_link_options(openmw_cs_osg_plugins INTERFACE ${_opts})
     target_link_libraries(openmw-cs openmw_cs_osg_plugins)
+
+    if(OPENMW_USE_SYSTEM_OSG)
+        # OSG plugin pkgconfig files are missing these dependencies.
+        # https://github.com/openscenegraph/OpenSceneGraph/issues/1052
+        target_link_libraries(openmw freetype jpeg png)
+    endif()
 endif(OSG_STATIC)
 
 target_link_libraries(openmw-cs Qt5::Widgets Qt5::Core Qt5::Network Qt5::OpenGL)
diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt
index dda517c44..5e5bb8a9f 100644
--- a/apps/openmw/CMakeLists.txt
+++ b/apps/openmw/CMakeLists.txt
@@ -122,13 +122,18 @@ include_directories(
 )
 
 target_link_libraries(openmw
-    ${OSG_LIBRARIES}
+    # CMake's built-in OSG finder does not use pkgconfig, so we have to
+    # manually ensure the order is correct for inter-library dependencies.
+    # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`.
+    # https://gitlab.kitware.com/cmake/cmake/-/issues/21701
     ${OSGPARTICLE_LIBRARIES}
-    ${OSGUTIL_LIBRARIES}
-    ${OSGDB_LIBRARIES}
     ${OSGVIEWER_LIBRARIES}
     ${OSGGA_LIBRARIES}
     ${OSGSHADOW_LIBRARIES}
+    ${OSGDB_LIBRARIES}
+    ${OSGUTIL_LIBRARIES}
+    ${OSG_LIBRARIES}
+
     ${Boost_SYSTEM_LIBRARY}
     ${Boost_THREAD_LIBRARY}
     ${Boost_FILESYSTEM_LIBRARY}
@@ -148,17 +153,24 @@ if(OSG_STATIC)
     add_library(openmw_osg_plugins INTERFACE)
     foreach(_plugin ${USED_OSG_PLUGINS})
         string(TOUPPER ${_plugin} _plugin_uc)
-        if (${_plugin_uc}_LIBRARY MATCHES "[/.]")
+        if(OPENMW_USE_SYSTEM_OSG)
             list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY})
         else()
             list(APPEND _osg_plugins_static_files $<TARGET_FILE:${${_plugin_uc}_LIBRARY}>)
+            target_link_libraries(openmw_osg_plugins INTERFACE $<TARGET_PROPERTY:${${_plugin_uc}_LIBRARY},LINK_LIBRARIES>)
+            add_dependencies(openmw_osg_plugins ${${_plugin_uc}_LIBRARY})
         endif()
-        target_link_libraries(openmw_osg_plugins INTERFACE ${${_plugin_uc}_LIBRARY})
     endforeach()
     # We use --whole-archive because OSG plugins use registration.
     get_whole_archive_options(_opts ${_osg_plugins_static_files})
     target_link_options(openmw_osg_plugins INTERFACE ${_opts})
     target_link_libraries(openmw openmw_osg_plugins)
+
+    if(OPENMW_USE_SYSTEM_OSG)
+        # OSG plugin pkgconfig files are missing these dependencies.
+        # https://github.com/openscenegraph/OpenSceneGraph/issues/1052
+        target_link_libraries(openmw freetype jpeg png)
+    endif()
 endif(OSG_STATIC)
 
 if (ANDROID)
diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp
index 194e8f695..27980f070 100644
--- a/apps/openmw/mwbase/world.hpp
+++ b/apps/openmw/mwbase/world.hpp
@@ -539,6 +539,7 @@ namespace MWBase
             virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0;
             virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile,
                                            const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0;
+            virtual void updateProjectilesCasters() = 0;
 
             virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) = 0;
 
diff --git a/apps/openmw/mwgui/bookpage.hpp b/apps/openmw/mwgui/bookpage.hpp
index 4e49b8f67..f9fd93089 100644
--- a/apps/openmw/mwgui/bookpage.hpp
+++ b/apps/openmw/mwgui/bookpage.hpp
@@ -53,7 +53,7 @@ namespace MWGui
         {
             static const int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight();
 
-            MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch);
+            const MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch);
             if (gi)
             {
                 const float scale = font->getDefaultHeight() / (float) fontHeight;
diff --git a/apps/openmw/mwgui/layout.cpp b/apps/openmw/mwgui/layout.cpp
index ae1c09659..9b9b9537f 100644
--- a/apps/openmw/mwgui/layout.cpp
+++ b/apps/openmw/mwgui/layout.cpp
@@ -35,6 +35,7 @@ namespace MWGui
 
     void Layout::shutdown()
     {
+        setVisible(false);
         MyGUI::Gui::getInstance().destroyWidget(mMainWidget);
         mListWindowRoot.clear();
     }
diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp
index e6a10ee32..405abfbae 100644
--- a/apps/openmw/mwgui/spellicons.cpp
+++ b/apps/openmw/mwgui/spellicons.cpp
@@ -181,7 +181,9 @@ namespace MWGui
             }
             else if (mWidgetMap.find(effectId) != mWidgetMap.end())
             {
-                mWidgetMap[effectId]->setVisible(false);
+                MyGUI::ImageBox* image = mWidgetMap[effectId];
+                image->setVisible(false);
+                image->setAlpha(1.f);
             }
         }
 
diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp
index 7da8cb7bd..62b3d32d0 100644
--- a/apps/openmw/mwgui/windowmanagerimp.cpp
+++ b/apps/openmw/mwgui/windowmanagerimp.cpp
@@ -172,7 +172,7 @@ namespace MWGui
       , mWerewolfOverlayEnabled(Settings::Manager::getBool ("werewolf overlay", "GUI"))
       , mHudEnabled(true)
       , mCursorVisible(true)
-      , mCursorActive(false)
+      , mCursorActive(true)
       , mPlayerBounty(-1)
       , mGui(nullptr)
       , mGuiModes()
@@ -503,8 +503,6 @@ namespace MWGui
         {
             mStatsWatcher.reset();
 
-            mKeyboardNavigation.reset();
-
             MyGUI::LanguageManager::getInstance().eventRequestTag.clear();
             MyGUI::PointerManager::getInstance().eventChangeMousePointer.clear();
             MyGUI::InputManager::getInstance().eventChangeKeyFocus.clear();
@@ -523,6 +521,8 @@ namespace MWGui
             delete mCursorManager;
             delete mToolTips;
 
+            mKeyboardNavigation.reset();
+
             cleanupGarbage();
 
             mFontLoader.reset();
diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp
index bfe860d6d..5af73887c 100644
--- a/apps/openmw/mwmechanics/aipursue.cpp
+++ b/apps/openmw/mwmechanics/aipursue.cpp
@@ -53,9 +53,14 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
 
     const float pathTolerance = 100.f;
 
-    if (pathTo(actor, dest, duration, pathTolerance) &&
-        std::abs(dest.z() - actorPos.z()) < pathTolerance) // check the true distance in case the target is far away in Z-direction
+    // check the true distance in case the target is far away in Z-direction
+    bool reached = pathTo(actor, dest, duration, pathTolerance) &&
+                   std::abs(dest.z() - actorPos.z()) < pathTolerance;
+
+    if (reached)
     {
+        if (!MWBase::Environment::get().getWorld()->getLOS(target, actor))
+            return false;
         MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, actor); //Arrest player when reached
         return true;
     }
diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp
index 172ddb25c..99f8632f3 100644
--- a/apps/openmw/mwmechanics/character.cpp
+++ b/apps/openmw/mwmechanics/character.cpp
@@ -2920,8 +2920,8 @@ void CharacterController::updateHeadTracking(float duration)
     if (!head)
         return;
 
-    float zAngleRadians = 0.f;
-    float xAngleRadians = 0.f;
+    double zAngleRadians = 0.f;
+    double xAngleRadians = 0.f;
 
     if (!mHeadTrackTarget.isEmpty())
     {
@@ -2954,15 +2954,16 @@ void CharacterController::updateHeadTracking(float duration)
         const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0);
 
         zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y());
+        zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw();
+        zAngleRadians *= (1 - direction.z() * direction.z());
         xAngleRadians = std::asin(direction.z());
     }
 
     const double xLimit = osg::DegreesToRadians(40.0);
     const double zLimit = osg::DegreesToRadians(30.0);
     double zLimitOffset = mAnimation->getUpperBodyYawRadians();
-    xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -xLimit, xLimit);
-    zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians),
-                                      -zLimit + zLimitOffset, zLimit + zLimitOffset);
+    xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit);
+    zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset);
 
     float factor = duration*5;
     factor = std::min(factor, 1.f);
diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp
index fd3f31811..9e5774ea9 100644
--- a/apps/openmw/mwmechanics/combat.cpp
+++ b/apps/openmw/mwmechanics/combat.cpp
@@ -305,6 +305,10 @@ namespace MWMechanics
 
     void applyElementalShields(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim)
     {
+        // Don't let elemental shields harm the player in god mode.
+        bool godmode = attacker == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
+        if (godmode)
+            return;
         for (int i=0; i<3; ++i)
         {
             float magnitude = victim.getClass().getCreatureStats(victim).getMagicEffects().get(ESM::MagicEffect::FireShield+i).getMagnitude();
diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp
index 3b52ee934..06abe7240 100644
--- a/apps/openmw/mwphysics/actor.cpp
+++ b/apps/openmw/mwphysics/actor.cpp
@@ -174,6 +174,9 @@ osg::Vec3f Actor::getCollisionObjectPosition() const
 bool Actor::setPosition(const osg::Vec3f& position)
 {
     std::scoped_lock lock(mPositionMutex);
+    // position is being forced, ignore simulation results until we sync up
+    if (mSkipSimulation)
+        return false;
     bool hasChanged = mPosition != position || mPositionOffset.length() != 0 || mWorldPositionChanged;
     mPreviousPosition = mPosition + mPositionOffset;
     mPosition = position + mPositionOffset;
@@ -187,6 +190,17 @@ void Actor::adjustPosition(const osg::Vec3f& offset)
     mPositionOffset += offset;
 }
 
+void Actor::applyOffsetChange()
+{
+    if (mPositionOffset.length() == 0)
+        return;
+    mWorldPosition += mPositionOffset;
+    mPosition += mPositionOffset;
+    mPreviousPosition += mPositionOffset;
+    mPositionOffset = osg::Vec3f();
+    mWorldPositionChanged = true;
+}
+
 osg::Vec3f Actor::getPosition() const
 {
     return mPosition;
diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp
index 9d129f2ba..031125f40 100644
--- a/apps/openmw/mwphysics/actor.hpp
+++ b/apps/openmw/mwphysics/actor.hpp
@@ -96,9 +96,16 @@ namespace MWPhysics
           * Returns true if the new position is different.
           */
         bool setPosition(const osg::Vec3f& position);
+
+        // force set actor position to be as in Ptr::RefData
         void updatePosition();
+
+        // register a position offset that will be applied during simulation.
         void adjustPosition(const osg::Vec3f& offset);
 
+        // apply position offset. Can't be called during simulation
+        void applyOffsetChange();
+
         osg::Vec3f getPosition() const;
 
         osg::Vec3f getPreviousPosition() const;
diff --git a/apps/openmw/mwphysics/heightfield.cpp b/apps/openmw/mwphysics/heightfield.cpp
index 34127fe3a..e210bc390 100644
--- a/apps/openmw/mwphysics/heightfield.cpp
+++ b/apps/openmw/mwphysics/heightfield.cpp
@@ -10,6 +10,12 @@
 
 #include <type_traits>
 
+#if BT_BULLET_VERSION < 310
+// Older Bullet versions only support `btScalar` heightfields.
+// Our heightfield data is `float`.
+//
+// These functions handle conversion from `float` to `double` when
+// `btScalar` is `double` (`BT_USE_DOUBLE_PRECISION`).
 namespace
 {
     template <class T>
@@ -40,14 +46,18 @@ namespace
         return btScalarHeights.data();
     }
 }
+#endif
 
 namespace MWPhysics
 {
     HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler)
         : mHoldObject(holdObject)
+#if BT_BULLET_VERSION < 310
         , mHeights(makeHeights(heights, sqrtVerts))
+#endif
         , mTaskScheduler(scheduler)
     {
+#if BT_BULLET_VERSION < 310
         mShape = std::make_unique<btHeightfieldTerrainShape>(
             sqrtVerts, sqrtVerts,
             getHeights(heights, mHeights),
@@ -55,6 +65,10 @@ namespace MWPhysics
             minH, maxH, 2,
             PHY_FLOAT, false
         );
+#else
+        mShape = std::make_unique<btHeightfieldTerrainShape>(
+            sqrtVerts, sqrtVerts, heights, minH, maxH, 2, false);
+#endif
         mShape->setUseDiamondSubdivision(true);
         mShape->setLocalScaling(btVector3(triSize, triSize, 1));
 
diff --git a/apps/openmw/mwphysics/heightfield.hpp b/apps/openmw/mwphysics/heightfield.hpp
index c76f8b943..93b2733f3 100644
--- a/apps/openmw/mwphysics/heightfield.hpp
+++ b/apps/openmw/mwphysics/heightfield.hpp
@@ -34,7 +34,9 @@ namespace MWPhysics
         std::unique_ptr<btHeightfieldTerrainShape> mShape;
         std::unique_ptr<btCollisionObject> mCollisionObject;
         osg::ref_ptr<const osg::Object> mHoldObject;
+#if BT_BULLET_VERSION < 310
         std::vector<btScalar> mHeights;
+#endif
 
         PhysicsTaskScheduler* mTaskScheduler;
 
diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp
index 754bb60af..2b0db5b82 100644
--- a/apps/openmw/mwphysics/mtphysics.cpp
+++ b/apps/openmw/mwphysics/mtphysics.cpp
@@ -492,6 +492,7 @@ namespace MWPhysics
                 if (actor->setPosition(actorData.mPosition))
                 {
                     std::scoped_lock lock(mCollisionWorldMutex);
+                    actorData.mPosition = actor->getPosition(); // account for potential position change made by script
                     actor->updateCollisionObjectPosition();
                     mCollisionWorld->updateSingleAabb(actor->getCollisionObject());
                 }
diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp
index 382b348b9..dc9ab629a 100644
--- a/apps/openmw/mwphysics/physicssystem.cpp
+++ b/apps/openmw/mwphysics/physicssystem.cpp
@@ -604,7 +604,9 @@ namespace MWPhysics
                 return object->getCollisionObject();
             return nullptr;
         }();
-        assert(caster);
+
+        if (caster == nullptr)
+            Log(Debug::Warning) << "No caster for projectile " << projectileId;
 
         ProjectileConvexCallback resultCallback(caster, btFrom, btTo, projectile);
         resultCallback.m_collisionFilterMask = 0xff;
@@ -695,6 +697,15 @@ namespace MWPhysics
         return mProjectileId;
     }
 
+    void PhysicsSystem::setCaster(int projectileId, const MWWorld::Ptr& caster)
+    {
+        const auto foundProjectile = mProjectiles.find(projectileId);
+        assert(foundProjectile != mProjectiles.end());
+        auto* projectile = foundProjectile->second.get();
+
+        projectile->setCaster(caster);
+    }
+
     bool PhysicsSystem::toggleCollisionMode()
     {
         ActorMap::iterator found = mActors.find(MWMechanics::getPlayer());
@@ -950,6 +961,10 @@ namespace MWPhysics
     void ActorFrameData::updatePosition()
     {
         mActorRaw->updateWorldPosition();
+        // If physics runs "fast enough", position are interpolated without simulation
+        // By calling this here, we are sure that offsets are applied at least once per frame,
+        // regardless of simulation speed.
+        mActorRaw->applyOffsetChange();
         mPosition = mActorRaw->getPosition();
         if (mMoveToWaterSurface)
         {
diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp
index 57ebbadbb..80b2d98bc 100644
--- a/apps/openmw/mwphysics/physicssystem.hpp
+++ b/apps/openmw/mwphysics/physicssystem.hpp
@@ -125,6 +125,7 @@ namespace MWPhysics
             void addActor (const MWWorld::Ptr& ptr, const std::string& mesh);
 
             int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater);
+            void setCaster(int projectileId, const MWWorld::Ptr& caster);
             void updateProjectile(const int projectileId, const osg::Vec3f &position) const;
             void removeProjectile(const int projectileId);
 
diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp
index 7ccf653ea..d8c759935 100644
--- a/apps/openmw/mwrender/animation.cpp
+++ b/apps/openmw/mwrender/animation.cpp
@@ -1613,7 +1613,7 @@ namespace MWRender
     {
         bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior();
 
-        SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior, mResourceSystem->getSceneManager()->getFFPLighting());
+        SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior);
     }
 
     void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture)
diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp
index 5527dd98b..63dd5ff70 100644
--- a/apps/openmw/mwrender/characterpreview.cpp
+++ b/apps/openmw/mwrender/characterpreview.cpp
@@ -11,6 +11,7 @@
 #include <osg/PositionAttitudeTransform>
 #include <osg/LightModel>
 #include <osg/LightSource>
+#include <osg/ValueObject>
 #include <osgUtil/IntersectionVisitor>
 #include <osgUtil/LineSegmentIntersector>
 
@@ -20,6 +21,7 @@
 #include <components/resource/resourcesystem.hpp>
 #include <components/sceneutil/lightmanager.hpp>
 #include <components/sceneutil/shadow.hpp>
+#include <components/settings/settings.hpp>
 
 #include "../mwbase/environment.hpp"
 #include "../mwbase/world.hpp"
@@ -84,7 +86,8 @@ namespace MWRender
     };
 
 
-    // Set up alpha blending to Additive mode to avoid issues caused by transparent objects writing onto the alpha value of the FBO
+    // Set up alpha blending mode to avoid issues caused by transparent objects writing onto the alpha value of the FBO
+    // This makes the RTT have premultiplied alpha, though, so the source blend factor must be GL_ONE when it's applied
     class SetUpBlendVisitor : public osg::NodeVisitor
     {
     public:
@@ -94,22 +97,40 @@ namespace MWRender
 
         void apply(osg::Node& node) override
         {
-            if (osg::StateSet* stateset = node.getStateSet())
+            if (osg::ref_ptr<osg::StateSet> stateset = node.getStateSet())
             {
+                osg::ref_ptr<osg::StateSet> newStateSet;
                 if (stateset->getAttribute(osg::StateAttribute::BLENDFUNC) || stateset->getBinNumber() == osg::StateSet::TRANSPARENT_BIN)
                 {
-                    osg::ref_ptr<osg::StateSet> newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY);
                     osg::BlendFunc* blendFunc = static_cast<osg::BlendFunc*>(stateset->getAttribute(osg::StateAttribute::BLENDFUNC));
-                    osg::ref_ptr<osg::BlendFunc> newBlendFunc = blendFunc ? new osg::BlendFunc(*blendFunc) : new osg::BlendFunc;
-                    newBlendFunc->setDestinationAlpha(osg::BlendFunc::ONE);
-                    newStateSet->setAttribute(newBlendFunc, osg::StateAttribute::ON);
-                    node.setStateSet(newStateSet);
+
+                    if (blendFunc)
+                    {
+                        newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY);
+                        node.setStateSet(newStateSet);
+                        osg::ref_ptr<osg::BlendFunc> newBlendFunc = new osg::BlendFunc(*blendFunc);
+                        newStateSet->setAttribute(newBlendFunc, osg::StateAttribute::ON);
+                        // I *think* (based on some by-hand maths) that the RGB and dest alpha factors are unchanged, and only dest determines source alpha factor
+                        // This has the benefit of being idempotent if we assume nothing used glBlendFuncSeparate before we touched it
+                        if (blendFunc->getDestination() == osg::BlendFunc::ONE_MINUS_SRC_ALPHA)
+                            newBlendFunc->setSourceAlpha(osg::BlendFunc::ONE);
+                        else if (blendFunc->getDestination() == osg::BlendFunc::ONE)
+                            newBlendFunc->setSourceAlpha(osg::BlendFunc::ZERO);
+                        // Other setups barely exist in the wild and aren't worth supporting as they're not equippable gear
+                        else
+                            Log(Debug::Info) << "Unable to adjust blend mode for character preview. Source factor 0x" << std::hex << blendFunc->getSource() << ", destination factor 0x" << blendFunc->getDestination() << std::dec;
+                    }
                 }
                 if (stateset->getMode(GL_BLEND) & osg::StateAttribute::ON)
                 {
+                    if (!newStateSet)
+                    {
+                        newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY);
+                        node.setStateSet(newStateSet);
+                    }
                     // Disable noBlendAlphaEnv
-                    stateset->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF);
-                    stateset->addUniform(mNoAlphaUniform);
+                    newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF);
+                    newStateSet->addUniform(mNoAlphaUniform);
                 }
             }
             traverse(node);
@@ -134,6 +155,7 @@ namespace MWRender
         mTexture->setInternalFormat(GL_RGBA);
         mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
         mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
+        mTexture->setUserValue("premultiplied alpha", true);
 
         mCamera = new osg::Camera;
         // hints that the camera is not relative to the master camera
@@ -145,14 +167,16 @@ namespace MWRender
         mCamera->setProjectionMatrixAsPerspective(fovYDegrees, sizeX/static_cast<float>(sizeY), 0.1f, 10000.f); // zNear and zFar are autocomputed
         mCamera->setViewport(0, 0, sizeX, sizeY);
         mCamera->setRenderOrder(osg::Camera::PRE_RENDER);
-        mCamera->attach(osg::Camera::COLOR_BUFFER, mTexture);
+        mCamera->attach(osg::Camera::COLOR_BUFFER, mTexture, 0, 0, false, Settings::Manager::getInt("antialiasing", "Video"));
         mCamera->setName("CharacterPreview");
         mCamera->setComputeNearFarMode(osg::Camera::COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES);
         mCamera->setCullMask(~(Mask_UpdateVisitor));
 
         mCamera->setNodeMask(Mask_RenderToTexture);
 
-        osg::ref_ptr<SceneUtil::LightManager> lightManager = new SceneUtil::LightManager(mResourceSystem->getSceneManager()->getFFPLighting());
+        bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP;
+
+        osg::ref_ptr<SceneUtil::LightManager> lightManager = new SceneUtil::LightManager(ffp);
         lightManager->setStartLight(1);
         osg::ref_ptr<osg::StateSet> stateset = lightManager->getOrCreateStateSet();
         stateset->setMode(GL_LIGHTING, osg::StateAttribute::ON);
diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp
index 049118c90..0baa85c52 100644
--- a/apps/openmw/mwrender/groundcover.cpp
+++ b/apps/openmw/mwrender/groundcover.cpp
@@ -1,5 +1,6 @@
 #include "groundcover.hpp"
 
+#include <osg/AlphaFunc>
 #include <osg/Geometry>
 #include <osg/VertexAttribDivisor>
 
@@ -258,11 +259,16 @@ namespace MWRender
             // Keep link to original mesh to keep it in cache
             group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp));
 
+            mSceneManager->reinstateRemovedState(node);
+
             InstancingVisitor visitor(pair.second, worldCenter);
             node->accept(visitor);
             group->addChild(node);
         }
 
+        // Force a unified alpha handling instead of data from meshes
+        osg::ref_ptr<osg::AlphaFunc> alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f);
+        group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON);
         group->getBound();
         group->setNodeMask(Mask_Groundcover);
         mSceneManager->recreateShaders(group, "groundcover", false, true);
diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp
index 8b1dda841..d572fe2d0 100644
--- a/apps/openmw/mwrender/localmap.cpp
+++ b/apps/openmw/mwrender/localmap.cpp
@@ -18,6 +18,7 @@
 #include <components/settings/settings.hpp>
 #include <components/sceneutil/visitor.hpp>
 #include <components/sceneutil/shadow.hpp>
+#include <components/sceneutil/util.hpp>
 #include <components/sceneutil/lightmanager.hpp>
 #include <components/files/memorystream.hpp>
 #include <components/resource/scenemanager.hpp>
@@ -223,12 +224,8 @@ osg::ref_ptr<osg::Camera> LocalMap::createOrthographicCamera(float x, float y, f
     SceneUtil::ShadowManager::disableShadowsForStateSet(stateset);
 
     // override sun for local map 
-    if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->getFFPLighting())
-    {
-        osg::ref_ptr<SceneUtil::SunlightStateAttribute> sun = new SceneUtil::SunlightStateAttribute;
-        sun->setFromLight(light);
-        sun->setStateSet(stateset, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
-    }
+    auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod();    
+    SceneUtil::configureStateSetSunOverride(lightingMethod, light, stateset);
 
     camera->addChild(lightSource);
     camera->setStateSet(stateset);
@@ -248,7 +245,7 @@ void LocalMap::setupRenderToTexture(osg::ref_ptr<osg::Camera> camera, int x, int
     texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE);
     texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);
 
-    camera->attach(osg::Camera::COLOR_BUFFER, texture);
+    SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, texture);
 
     camera->addChild(mSceneRoot);
     mRoot->addChild(camera);
diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp
index bbd7d0692..79f34c42e 100644
--- a/apps/openmw/mwrender/renderingmanager.cpp
+++ b/apps/openmw/mwrender/renderingmanager.cpp
@@ -3,7 +3,6 @@
 #include <limits>
 #include <cstdlib>
 
-#include <osg/AlphaFunc>
 #include <osg/Light>
 #include <osg/LightModel>
 #include <osg/Fog>
@@ -26,6 +25,7 @@
 #include <components/resource/scenemanager.hpp>
 #include <components/resource/keyframemanager.hpp>
 
+#include <components/shader/removedalphafunc.hpp>
 #include <components/shader/shadermanager.hpp>
 
 #include <components/settings/settings.hpp>
@@ -199,10 +199,13 @@ namespace MWRender
         , mFieldOfViewOverridden(false)
         , mFieldOfViewOverride(0.f)
     {
+        auto lightingModelString = Settings::Manager::getString("lighting method", "Shaders");
+        bool usingFFPLighting = lightingModelString == "legacy" && SceneUtil::LightManager::isValidLightingModelString(lightingModelString);
+
         resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem);
         resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders");
         // Shadows and radial fog have problems with fixed-function mode
-        bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows");
+        bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows") || usingFFPLighting;
         bool clampLighting = Settings::Manager::getBool("clamp lighting", "Shaders");
         resourceSystem->getSceneManager()->setForceShaders(forceShaders);
         // FIXME: calling dummy method because terrain needs to know whether lighting is clamped
@@ -213,11 +216,13 @@ namespace MWRender
         resourceSystem->getSceneManager()->setAutoUseSpecularMaps(Settings::Manager::getBool("auto use object specular maps", "Shaders"));
         resourceSystem->getSceneManager()->setSpecularMapPattern(Settings::Manager::getString("specular map pattern", "Shaders"));
         resourceSystem->getSceneManager()->setApplyLightingToEnvMaps(Settings::Manager::getBool("apply lighting to environment maps", "Shaders"));
-        bool useFFP = clampLighting || !forceShaders || !SceneUtil::LightManager::queryNonFFPLightingSupport();
-        resourceSystem->getSceneManager()->setFFPLighting(useFFP);
-        resourceSystem->getSceneManager()->getShaderManager().setFFPLighting(useFFP);
+        resourceSystem->getSceneManager()->setConvertAlphaTestToAlphaToCoverage(Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1);
         
-        osg::ref_ptr<SceneUtil::LightManager> sceneRoot = new SceneUtil::LightManager(useFFP);
+        osg::ref_ptr<SceneUtil::LightManager> sceneRoot = new SceneUtil::LightManager(!forceShaders || usingFFPLighting);
+        // Let LightManager choose which backend to use based, mostly depends on support for UBOs 
+        resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod());
+        resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod());
+
         sceneRoot->setLightingMask(Mask_Lighting);
         mSceneRoot = sceneRoot;
         sceneRoot->setStartLight(1);
@@ -242,9 +247,6 @@ namespace MWRender
         Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines();
         Shader::ShaderManager::DefineMap globalDefines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines();
 
-        for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++)
-            globalDefines[itr->first] = itr->second;
-
         for (auto itr = shadowDefines.begin(); itr != shadowDefines.end(); itr++)
             globalDefines[itr->first] = itr->second;
 
@@ -252,6 +254,10 @@ namespace MWRender
         globalDefines["clamp"] = Settings::Manager::getBool("clamp lighting", "Shaders") ? "1" : "0";
         globalDefines["preLightEnv"] = Settings::Manager::getBool("apply lighting to environment maps", "Shaders") ? "1" : "0";
         globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0";
+        globalDefines["useGPUShader4"] = "0";
+
+        for (auto itr = lightDefines.begin(); itr != lightDefines.end(); itr++)
+            globalDefines[itr->first] = itr->second;
 
         float groundcoverDistance = (Constants::CellSizeInUnits * std::max(1, Settings::Manager::getInt("distance", "Groundcover")) - 1024) * 0.93;
         globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * 0.9f);
@@ -318,10 +324,6 @@ namespace MWRender
             groundcoverRoot->setName("Groundcover Root");
             sceneRoot->addChild(groundcoverRoot);
 
-            // Force a unified alpha handling instead of data from meshes
-            osg::ref_ptr<osg::AlphaFunc> alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f/255.f);
-            groundcoverRoot->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON);
-
             mGroundcoverUpdater = new GroundcoverUpdater;
             groundcoverRoot->addUpdateCallback(mGroundcoverUpdater);
 
@@ -379,7 +381,6 @@ namespace MWRender
 
         mSky.reset(new SkyManager(sceneRoot, resourceSystem->getSceneManager()));
         mSky->setCamera(mViewer->getCamera());
-        mSky->setRainIntensityUniform(mWater->getRainIntensityUniform());
 
         source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON);
 
@@ -418,6 +419,11 @@ namespace MWRender
         mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance));
         mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false));
 
+        // Hopefully, anything genuinely requiring the default alpha func of GL_ALWAYS explicitly sets it
+        mRootNode->getOrCreateStateSet()->setAttribute(Shader::RemovedAlphaFunc::getInstance(GL_ALWAYS));
+        // The transparent renderbin sets alpha testing on because that was faster on old GPUs. It's now slower and breaks things.
+        mRootNode->getOrCreateStateSet()->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF);
+
         mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near");
         mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far");
         updateProjectionMatrix();
@@ -667,6 +673,9 @@ namespace MWRender
 
         mUnrefQueue->flush(mWorkQueue.get());
 
+        float rainIntensity = mSky->getPrecipitationAlpha();
+        mWater->setRainIntensity(rainIntensity);
+
         if (!paused)
         {
             mEffectManager->update(dt);
diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp
index 389944000..f82641676 100644
--- a/apps/openmw/mwrender/sky.cpp
+++ b/apps/openmw/mwrender/sky.cpp
@@ -1113,7 +1113,6 @@ private:
 SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager)
     : mSceneManager(sceneManager)
     , mCamera(nullptr)
-    , mRainIntensityUniform(nullptr)
     , mAtmosphereNightRoll(0.f)
     , mCreated(false)
     , mIsStorm(false)
@@ -1139,7 +1138,7 @@ SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneMana
     , mBaseWindSpeed(0.f)
     , mEnabled(true)
     , mSunEnabled(true)
-    , mEffectFade(0.f)
+    , mPrecipitationAlpha(0.f)
 {
     osg::ref_ptr<CameraRelativeTransform> skyroot (new CameraRelativeTransform);
     skyroot->setName("Sky Root");
@@ -1163,11 +1162,6 @@ SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneMana
     mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot);
 }
 
-void SkyManager::setRainIntensityUniform(osg::Uniform *uniform)
-{
-    mRainIntensityUniform = uniform;
-}
-
 void SkyManager::create()
 {
     assert(!mCreated);
@@ -1522,7 +1516,7 @@ void SkyManager::createRain()
 
     osg::ref_ptr<osgParticle::ModularProgram> program (new osgParticle::ModularProgram);
     program->addOperator(new WrapAroundOperator(mCamera,rainRange));
-    program->addOperator(new WeatherAlphaOperator(mEffectFade, true));
+    program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true));
     program->setParticleSystem(mRainParticleSystem);
     mRainNode->addChild(program);
 
@@ -1576,29 +1570,23 @@ bool SkyManager::isEnabled()
     return mEnabled;
 }
 
-bool SkyManager::hasRain()
+bool SkyManager::hasRain() const
 {
     return mRainNode != nullptr;
 }
 
+float SkyManager::getPrecipitationAlpha() const
+{
+    if (mEnabled && !mIsStorm && (hasRain() || mParticleNode))
+        return mPrecipitationAlpha;
+
+    return 0.f;
+}
+
 void SkyManager::update(float duration)
 {
     if (!mEnabled)
-    {
-        if (mRainIntensityUniform)
-            mRainIntensityUniform->set(0.f);
-
         return;
-    }
-
-    if (mRainIntensityUniform)
-    {
-        float rainIntensity = 0.f;
-        if (!mIsStorm && (hasRain() || mParticleNode))
-            rainIntensity = mEffectFade;
-
-        mRainIntensityUniform->set(rainIntensity);
-    }
 
     switchUnderwaterRain();
 
@@ -1737,7 +1725,7 @@ void SkyManager::setWeather(const WeatherResult& weather)
             SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr<SceneUtil::ControllerSource>(new SceneUtil::FrameTimeSource));
             mParticleEffect->accept(assignVisitor);
 
-            AlphaFader::SetupVisitor alphaFaderSetupVisitor(mEffectFade);
+            AlphaFader::SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha);
 
             mParticleEffect->accept(alphaFaderSetupVisitor);
 
@@ -1754,7 +1742,7 @@ void SkyManager::setWeather(const WeatherResult& weather)
                 osg::ref_ptr<osgParticle::ModularProgram> program (new osgParticle::ModularProgram);
                 if (!mIsStorm)
                     program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800)));
-                program->addOperator(new WeatherAlphaOperator(mEffectFade, false));
+                program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false));
                 program->setParticleSystem(ps);
                 mParticleNode->addChild(program);
             }
@@ -1843,7 +1831,7 @@ void SkyManager::setWeather(const WeatherResult& weather)
 
     mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0 : 0);
 
-    mEffectFade = weather.mEffectFade;
+    mPrecipitationAlpha = weather.mPrecipitationAlpha;
 }
 
 float SkyManager::getBaseWindSpeed() const
diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp
index f32c40192..f8c501dda 100644
--- a/apps/openmw/mwrender/sky.hpp
+++ b/apps/openmw/mwrender/sky.hpp
@@ -7,7 +7,6 @@
 
 #include <osg/ref_ptr>
 #include <osg/Vec4f>
-#include <osg/Uniform>
 
 namespace osg
 {
@@ -88,7 +87,7 @@ namespace MWRender
 
         std::string mParticleEffect;
         std::string mRainEffect;
-        float mEffectFade;
+        float mPrecipitationAlpha;
 
         float mRainDiameter;
         float mRainMinHeight;
@@ -157,7 +156,9 @@ namespace MWRender
 
         bool isEnabled();
 
-        bool hasRain();
+        bool hasRain() const;
+
+        float getPrecipitationAlpha() const;
 
         void setRainSpeed(float speed);
 
@@ -180,8 +181,6 @@ namespace MWRender
 
         void setCamera(osg::Camera *camera);
 
-        void setRainIntensityUniform(osg::Uniform *uniform);
-
         float getBaseWindSpeed() const;
 
     private:
@@ -196,7 +195,6 @@ namespace MWRender
         Resource::SceneManager* mSceneManager;
 
         osg::Camera *mCamera;
-        osg::Uniform *mRainIntensityUniform;
 
         osg::ref_ptr<osg::Group> mRootNode;
         osg::ref_ptr<osg::Group> mEarlyRenderBinRoot;
@@ -271,7 +269,7 @@ namespace MWRender
         bool mEnabled;
         bool mSunEnabled;
 
-        float mEffectFade;
+        float mPrecipitationAlpha;
 
         osg::Vec4f mMoonScriptColor;
     };
diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp
index 51792d1ba..a938db9bc 100644
--- a/apps/openmw/mwrender/water.cpp
+++ b/apps/openmw/mwrender/water.cpp
@@ -26,6 +26,7 @@
 #include <components/resource/scenemanager.hpp>
 
 #include <components/sceneutil/shadow.hpp>
+#include <components/sceneutil/util.hpp>
 #include <components/sceneutil/waterutil.hpp>
 #include <components/sceneutil/lightmanager.hpp>
 
@@ -209,6 +210,37 @@ public:
     }
 };
 
+class RainIntensityUpdater : public SceneUtil::StateSetUpdater
+{
+public:
+    RainIntensityUpdater()
+        : mRainIntensity(0.f)
+    {
+    }
+
+    void setRainIntensity(float rainIntensity)
+    {
+        mRainIntensity = rainIntensity;
+    }
+
+protected:
+    void setDefaults(osg::StateSet* stateset) override
+    {
+        osg::ref_ptr<osg::Uniform> rainIntensityUniform = new osg::Uniform("rainIntensity", 0.0f);
+        stateset->addUniform(rainIntensityUniform.get());
+    }
+
+    void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override
+    {
+        osg::ref_ptr<osg::Uniform> rainIntensityUniform = stateset->getUniform("rainIntensity");
+        if (rainIntensityUniform != nullptr)
+            rainIntensityUniform->set(mRainIntensity);
+    }
+
+private:
+    float mRainIntensity;
+};
+
 osg::ref_ptr<osg::Image> readPngImage (const std::string& file)
 {
     // use boost in favor of osgDB::readImage, to handle utf-8 path issues on Windows
@@ -272,7 +304,7 @@ public:
         mRefractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR);
         mRefractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR);
 
-        attach(osg::Camera::COLOR_BUFFER, mRefractionTexture);
+        SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mRefractionTexture);
 
         mRefractionDepthTexture = new osg::Texture2D;
         mRefractionDepthTexture->setTextureSize(rttSize, rttSize);
@@ -357,7 +389,7 @@ public:
         mReflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE);
         mReflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);
 
-        attach(osg::Camera::COLOR_BUFFER, mReflectionTexture);
+        SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mReflectionTexture);
 
         // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise.
         osg::ref_ptr<osg::FrontFace> frontFace (new osg::FrontFace);
@@ -432,7 +464,8 @@ public:
 
 Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem *resourceSystem,
              osgUtil::IncrementalCompileOperation *ico, const std::string& resourcePath)
-    : mParent(parent)
+    : mRainIntensityUpdater(nullptr)
+    , mParent(parent)
     , mSceneRoot(sceneRoot)
     , mResourceSystem(resourceSystem)
     , mResourcePath(resourcePath)
@@ -464,8 +497,6 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem
 
     setHeight(mTop);
 
-    mRainIntensityUniform = new osg::Uniform("rainIntensity",(float) 0.0);
-
     updateWaterMaterial();
 
     if (ico)
@@ -495,11 +526,6 @@ void Water::setCullCallback(osg::Callback* callback)
     }
 }
 
-osg::Uniform *Water::getRainIntensityUniform()
-{
-    return mRainIntensityUniform.get();
-}
-
 void Water::updateWaterMaterial()
 {
     if (mReflection)
@@ -557,6 +583,8 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha)
     osg::ref_ptr<osg::StateSet> stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water);
 
     node->setStateSet(stateset);
+    node->setUpdateCallback(nullptr);
+    mRainIntensityUpdater = nullptr;
 
     // Add animated textures
     std::vector<osg::ref_ptr<osg::Texture2D> > textures;
@@ -640,17 +668,18 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R
 
     shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
 
-    shaderStateset->addUniform(mRainIntensityUniform.get());
-
     osg::ref_ptr<osg::Program> program (new osg::Program);
     program->addShader(vertexShader);
     program->addShader(fragmentShader);
-    if (!mResourceSystem->getSceneManager()->getFFPLighting())
-        program->addBindUniformBlock("SunlightBuffer", 0);
+    auto method = mResourceSystem->getSceneManager()->getLightingMethod();
+    if (method == SceneUtil::LightingMethod::SingleUBO)
+        program->addBindUniformBlock("LightBufferBinding", static_cast<int>(Shader::UBOBinding::LightBuffer));
     shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON);
 
     node->setStateSet(shaderStateset);
-    node->setUpdateCallback(nullptr);
+
+    mRainIntensityUpdater = new RainIntensityUpdater();
+    node->setUpdateCallback(mRainIntensityUpdater);
 }
 
 void Water::processChangedSettings(const Settings::CategorySettingVector& settings)
@@ -733,6 +762,12 @@ void Water::setHeight(const float height)
         mRefraction->setWaterLevel(mTop);
 }
 
+void Water::setRainIntensity(float rainIntensity)
+{
+    if (mRainIntensityUpdater)
+        mRainIntensityUpdater->setRainIntensity(rainIntensity);
+}
+
 void Water::update(float dt)
 {
     mSimulation->update(dt);
diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp
index 3787ef426..ec7dc95db 100644
--- a/apps/openmw/mwrender/water.hpp
+++ b/apps/openmw/mwrender/water.hpp
@@ -6,7 +6,6 @@
 
 #include <osg/ref_ptr>
 #include <osg/Vec3f>
-#include <osg/Uniform>
 #include <osg/Camera>
 
 #include <components/settings/settings.hpp>
@@ -46,11 +45,12 @@ namespace MWRender
     class Refraction;
     class Reflection;
     class RippleSimulation;
+    class RainIntensityUpdater;
 
     /// Water rendering
     class Water
     {
-        osg::ref_ptr<osg::Uniform> mRainIntensityUniform;
+        osg::ref_ptr<RainIntensityUpdater> mRainIntensityUpdater;
 
         osg::ref_ptr<osg::Group> mParent;
         osg::ref_ptr<osg::Group> mSceneRoot;
@@ -112,6 +112,7 @@ namespace MWRender
 
         void changeCell(const MWWorld::CellStore* store);
         void setHeight(const float height);
+        void setRainIntensity(const float rainIntensity);
 
         void update(float dt);
 
@@ -119,8 +120,6 @@ namespace MWRender
         osg::Camera *getRefractionCamera();
 
         void processChangedSettings(const Settings::CategorySettingVector& settings);
-
-        osg::Uniform *getRainIntensityUniform();
     };
 
 }
diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp
index 103c6629d..e8b406977 100644
--- a/apps/openmw/mwscript/transformationextensions.cpp
+++ b/apps/openmw/mwscript/transformationextensions.cpp
@@ -270,20 +270,17 @@ namespace MWScript
                     Interpreter::Type_Float pos = runtime[0].mFloat;
                     runtime.pop();
 
-                    float ax = ptr.getRefData().getPosition().pos[0];
-                    float ay = ptr.getRefData().getPosition().pos[1];
-                    float az = ptr.getRefData().getPosition().pos[2];
-
                     // Note: SetPos does not skip weather transitions in vanilla engine, so we do not call setTeleported(true) here.
 
-                    MWWorld::Ptr updated = ptr;
+                    const auto curPos = ptr.getRefData().getPosition().asVec3();
+                    auto newPos = curPos;
                     if(axis == "x")
                     {
-                        updated = MWBase::Environment::get().getWorld()->moveObject(ptr,pos,ay,az,true);
+                        newPos[0] = pos;
                     }
                     else if(axis == "y")
                     {
-                        updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,pos,az,true);
+                        newPos[1] = pos;
                     }
                     else if(axis == "z")
                     {
@@ -292,20 +289,21 @@ namespace MWScript
                         {
                             float terrainHeight = -std::numeric_limits<float>::max();
                             if (ptr.getCell()->isExterior())
-                                terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(osg::Vec3f(ax, ay, az));
+                                terrainHeight = MWBase::Environment::get().getWorld()->getTerrainHeightAt(curPos);
 
                             if (pos < terrainHeight)
                                 pos = terrainHeight;
                         }
 
-                        updated = MWBase::Environment::get().getWorld()->moveObject(ptr,ax,ay,pos,true);
+                        newPos[2] = pos;
                     }
                     else
                     {
                         return;
                     }
 
-                    dynamic_cast<MWScript::InterpreterContext&>(runtime.getContext()).updatePtr(ptr,updated);
+                    dynamic_cast<MWScript::InterpreterContext&>(runtime.getContext()).updatePtr(ptr,
+                        MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos));
                 }
         };
 
diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp
index 95ed9eeed..ec36a5cdf 100644
--- a/apps/openmw/mwsound/ffmpeg_decoder.cpp
+++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp
@@ -287,9 +287,9 @@ void FFmpeg_Decoder::close()
     mStream = nullptr;
 
     av_packet_unref(&mPacket);
-    av_freep(&mFrame);
-    swr_free(&mSwr);
     av_freep(&mDataBuf);
+    av_frame_free(&mFrame);
+    swr_free(&mSwr);
 
     if(mFormatCtx)
     {
@@ -302,11 +302,13 @@ void FFmpeg_Decoder::close()
             //
             if (mFormatCtx->pb->buffer != nullptr)
             {
-                av_free(mFormatCtx->pb->buffer);
-                mFormatCtx->pb->buffer = nullptr;
+                av_freep(&mFormatCtx->pb->buffer);
             }
-            av_free(mFormatCtx->pb);
-            mFormatCtx->pb = nullptr;
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100)
+            avio_context_free(&mFormatCtx->pb);
+#else
+            av_freep(&mFormatCtx->pb);
+#endif
         }
         avformat_close_input(&mFormatCtx);
     }
diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp
index 72e4b1ae0..f605344bf 100644
--- a/apps/openmw/mwstate/statemanagerimp.cpp
+++ b/apps/openmw/mwstate/statemanagerimp.cpp
@@ -539,6 +539,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str
             MWBase::Environment::get().getWorld()->changeToCell(cell->getCell()->getCellId(), pos, true, false);
         }
 
+        MWBase::Environment::get().getWorld()->updateProjectilesCasters();
+
         // Vanilla MW will restart startup scripts when a save game is loaded. This is unintuitive,
         // but some mods may be using it as a reload detector.
         MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup();
diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp
index cc614133a..73f69e220 100644
--- a/apps/openmw/mwworld/cellstore.cpp
+++ b/apps/openmw/mwworld/cellstore.cpp
@@ -104,6 +104,45 @@ namespace
         }
     }
 
+    template<class RecordType, class T>
+    void fixRestockingImpl(const T* base, RecordType& state)
+    {
+        // Workaround for old saves not containing negative quantities
+        for(const auto& baseItem : base->mInventory.mList)
+        {
+            if(baseItem.mCount < 0)
+            {
+                for(auto& item : state.mInventory.mItems)
+                {
+                    if(item.mCount > 0 && Misc::StringUtils::ciEqual(baseItem.mItem, item.mRef.mRefID))
+                        item.mCount = -item.mCount;
+                }
+            }
+        }
+    }
+
+    template<class RecordType, class T>
+    void fixRestocking(const T* base, RecordType& state)
+    {}
+
+    template<>
+    void fixRestocking<>(const ESM::Creature* base, ESM::CreatureState& state)
+    {
+        fixRestockingImpl(base, state);
+    }
+
+    template<>
+    void fixRestocking<>(const ESM::NPC* base, ESM::NpcState& state)
+    {
+        fixRestockingImpl(base, state);
+    }
+
+    template<>
+    void fixRestocking<>(const ESM::Container* base, ESM::ContainerState& state)
+    {
+        fixRestockingImpl(base, state);
+    }
+
     template<typename RecordType, typename T>
     void readReferenceCollection (ESM::ESMReader& reader,
         MWWorld::CellRefList<T>& collection, const ESM::CellRef& cref, const std::map<int, int>& contentFileMap, MWWorld::CellStore* cellstore)
@@ -134,6 +173,9 @@ namespace
         if (!record)
             return;
 
+        if (state.mVersion < 15)
+            fixRestocking(record, state);
+
         if (state.mRef.mRefNum.hasContentFile())
         {
             for (typename MWWorld::CellRefList<T>::List::iterator iter (collection.mList.begin());
diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp
index 3ffc7bb95..f483905dd 100644
--- a/apps/openmw/mwworld/projectilemanager.cpp
+++ b/apps/openmw/mwworld/projectilemanager.cpp
@@ -352,6 +352,29 @@ namespace MWWorld
         mProjectiles.push_back(state);
     }
 
+    void ProjectileManager::updateCasters()
+    {
+        for (auto& state : mProjectiles)
+            mPhysics->setCaster(state.mProjectileId, state.getCaster());
+
+        for (auto& state : mMagicBolts)
+        {
+            // casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified back.
+            // TODO: should object-type caster be restored from savegame?
+            if (state.mActorId == -1)
+                continue;
+
+            auto caster = state.getCaster();
+            if (caster.isEmpty())
+            {
+                Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId;
+                cleanupMagicBolt(state);
+                continue;
+            }
+            mPhysics->setCaster(state.mProjectileId, caster);
+        }
+    }
+
     void ProjectileManager::update(float dt)
     {
         periodicCleanup(dt);
diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp
index c047d90dd..e4bcae1ae 100644
--- a/apps/openmw/mwworld/projectilemanager.hpp
+++ b/apps/openmw/mwworld/projectilemanager.hpp
@@ -54,6 +54,8 @@ namespace MWWorld
         void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile,
                                        const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength);
 
+        void updateCasters();
+
         void update(float dt);
 
         void processHits();
diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp
index 427ee3aa8..3f050ba36 100644
--- a/apps/openmw/mwworld/scene.cpp
+++ b/apps/openmw/mwworld/scene.cpp
@@ -447,11 +447,17 @@ namespace MWWorld
                     mPhysics->disableWater();
 
                 const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
+
+                // By default the player is grounded, with the scene fully loaded, we validate and correct this.
+                if (player.mCell == cell) // Only run once, during initial cell load.
+                {
+                    mPhysics->traceDown(player, player.getRefData().getPosition().asVec3(), 10.f);
+                }
+
                 navigator->update(player.getRefData().getPosition().asVec3());
 
                 if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
                 {
-
                     mRendering.configureAmbient(cell->getCell());
                 }
             }
diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp
index 415e69d20..4bdd784db 100644
--- a/apps/openmw/mwworld/weather.cpp
+++ b/apps/openmw/mwworld/weather.cpp
@@ -1129,7 +1129,7 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam
     mResult.mGlareView = current.mGlareView;
     mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID;
     mResult.mAmbientSoundVolume = 1.f;
-    mResult.mEffectFade = 1.f;
+    mResult.mPrecipitationAlpha = 1.f;
 
     mResult.mIsStorm = current.mIsStorm;
 
@@ -1236,7 +1236,7 @@ inline void WeatherManager::calculateTransitionResult(const float factor, const
         mResult.mRainSpeed = current.mRainSpeed;
         mResult.mRainEntranceSpeed = current.mRainEntranceSpeed;
         mResult.mAmbientSoundVolume = 1 - factor / threshold;
-        mResult.mEffectFade = mResult.mAmbientSoundVolume;
+        mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume;
         mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID;
         mResult.mRainDiameter = current.mRainDiameter;
         mResult.mRainMinHeight = current.mRainMinHeight;
@@ -1251,7 +1251,7 @@ inline void WeatherManager::calculateTransitionResult(const float factor, const
         mResult.mRainSpeed = other.mRainSpeed;
         mResult.mRainEntranceSpeed = other.mRainEntranceSpeed;
         mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold);
-        mResult.mEffectFade = mResult.mAmbientSoundVolume;
+        mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume;
         mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID;
 
         mResult.mRainDiameter = other.mRainDiameter;
diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp
index b609f325c..01b90e907 100644
--- a/apps/openmw/mwworld/worldimp.cpp
+++ b/apps/openmw/mwworld/worldimp.cpp
@@ -3186,6 +3186,11 @@ namespace MWWorld
         mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection);
     }
 
+    void World::updateProjectilesCasters()
+    {
+        mProjectileManager->updateCasters();
+    }
+
     class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor
     {
     private:
diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp
index bc2baafd8..23153a31c 100644
--- a/apps/openmw/mwworld/worldimp.hpp
+++ b/apps/openmw/mwworld/worldimp.hpp
@@ -644,6 +644,7 @@ namespace MWWorld
             void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override;
             void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile,
                                            const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override;
+            void updateProjectilesCasters() override;
 
             void applyLoopingParticles(const MWWorld::Ptr& ptr) override;
 
diff --git a/cmake/FindRecastNavigation.cmake b/cmake/FindRecastNavigation.cmake
new file mode 100644
index 000000000..8728a5e94
--- /dev/null
+++ b/cmake/FindRecastNavigation.cmake
@@ -0,0 +1,211 @@
+# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+# Copyright 2021 Bret Curtis for OpenMW
+#[=======================================================================[.rst:
+FindRecastNavigation
+-------
+
+Find the RecastNavigation include directory and library.
+
+Use this module by invoking find_package with the form::
+
+.. code-block:: cmake
+
+  find_package(RecastNavigation
+    [version]              # Minimum version e.g. 1.8.0
+    [REQUIRED]             # Fail with error if RECAST is not found
+  )
+
+Imported targets
+^^^^^^^^^^^^^^^^
+
+This module defines the following :prop_tgt:`IMPORTED` targets:
+
+.. variable:: RecastNavigation::Recast
+
+  Imported target for using the RECAST library, if found.
+
+Result variables
+^^^^^^^^^^^^^^^^
+
+.. variable:: RECAST_FOUND
+
+  Set to true if RECAST library found, otherwise false or undefined.
+
+.. variable:: RECAST_INCLUDE_DIRS
+
+  Paths to include directories listed in one variable for use by RECAST client.
+
+.. variable:: RECAST_LIBRARIES
+
+  Paths to libraries to linked against to use RECAST.
+
+.. variable:: RECAST_VERSION
+
+  The version string of RECAST found.
+
+Cache variables
+^^^^^^^^^^^^^^^
+
+For users who wish to edit and control the module behavior, this module
+reads hints about search locations from the following variables::
+
+.. variable:: RECAST_INCLUDE_DIR
+
+  Path to RECAST include directory with ``Recast.h`` header.
+
+.. variable:: RECAST_LIBRARY
+
+  Path to RECAST library to be linked.
+
+NOTE: The variables above should not usually be used in CMakeLists.txt files!
+
+#]=======================================================================]
+
+### Find libraries ##############################################################
+
+if(NOT RECAST_LIBRARY)
+    find_library(RECAST_LIBRARY_RELEASE NAMES Recast HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib)
+    find_library(RECAST_LIBRARY_DEBUG NAMES Recast-d HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib)
+    include(SelectLibraryConfigurations)
+    select_library_configurations(RECAST)
+    mark_as_advanced(RECAST_LIBRARY_RELEASE RECAST_LIBRARY_DEBUG)
+else()
+    file(TO_CMAKE_PATH "${RECAST_LIBRARY}" RECAST_LIBRARY)
+endif()
+
+if(NOT DETOUR_LIBRARY)
+    find_library(DETOUR_LIBRARY_RELEASE NAMES Detour HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib)
+    find_library(DETOUR_LIBRARY_DEBUG NAMES Detour-d HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib)
+    include(SelectLibraryConfigurations)
+    select_library_configurations(DETOUR)
+    mark_as_advanced(DETOUR_LIBRARY_RELEASE DETOUR_LIBRARY_DEBUG)
+else()
+    file(TO_CMAKE_PATH "${DETOUR_LIBRARY}" DETOUR_LIBRARY)
+endif()
+
+if(NOT DEBUGUTILS_LIBRARY)
+    find_library(DEBUGUTILS_LIBRARY_RELEASE NAMES DebugUtils HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib)
+    find_library(DEBUGUTILS_LIBRARY_DEBUG NAMES DebugUtils-d HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES lib)
+    include(SelectLibraryConfigurations)
+    select_library_configurations(DEBUGUTILS)
+    mark_as_advanced(DEBUGUTILS_LIBRARY_RELEASE DEBUGUTILS_LIBRARY_DEBUG)
+else()
+    file(TO_CMAKE_PATH "${DEBUGUTILS_LIBRARY}" DEBUGUTILS_LIBRARY)
+endif()
+
+### Find include directory ####################################################
+find_path(RECAST_INCLUDE_DIR NAMES Recast.h HINTS ${RECASTNAVIGATION_ROOT} PATH_SUFFIXES include RECAST include/recastnavigation)
+mark_as_advanced(RECAST_INCLUDE_DIR)
+
+if(RECAST_INCLUDE_DIR AND EXISTS "${RECAST_INCLUDE_DIR}/Recast.h")
+    file(STRINGS "${RECAST_INCLUDE_DIR}/Recast.h" _Recast_h_contents
+            REGEX "#define RECAST_VERSION_[A-Z]+[ ]+[0-9]+")
+    string(REGEX REPLACE "#define RECAST_VERSION_MAJOR[ ]+([0-9]+).+" "\\1"
+            RECAST_VERSION_MAJOR "${_Recast_h_contents}")
+    string(REGEX REPLACE ".+#define RECAST_VERSION_MINOR[ ]+([0-9]+).+" "\\1"
+            RECAST_VERSION_MINOR "${_Recast_h_contents}")
+    string(REGEX REPLACE ".+#define RECAST_VERSION_RELEASE[ ]+([0-9]+).*" "\\1"
+            RECAST_VERSION_RELEASE "${_Recast_h_contents}")
+    set(RECAST_VERSION "${RECAST_VERSION_MAJOR}.${RECAST_VERSION_MINOR}.${RECAST_VERSION_RELEASE}")
+    unset(_Recast_h_contents)
+endif()
+
+#TODO: they don't include a version yet
+set(RECAST_VERSION "1.5.1")
+
+### Set result variables ######################################################
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(RecastNavigation DEFAULT_MSG
+        RECAST_LIBRARY RECAST_INCLUDE_DIR RECAST_VERSION)
+
+set(RECAST_LIBRARIES ${RECAST_LIBRARY})
+set(RECAST_INCLUDE_DIRS ${RECAST_INCLUDE_DIR})
+
+set(DETOUR_LIBRARIES ${DETOUR_LIBRARY})
+set(DETOUR_INCLUDE_DIRS ${RECAST_INCLUDE_DIR})
+
+set(DEBUGUTILS_LIBRARIES ${DEBUGUTILS_LIBRARY})
+set(DEBUGUTILS_INCLUDE_DIRS ${RECAST_INCLUDE_DIR})
+
+### Import targets ############################################################
+if(RecastNavigation_FOUND)
+    if(NOT TARGET RecastNavigation::Recast)
+        add_library(RecastNavigation::Recast UNKNOWN IMPORTED)
+        set_target_properties(RecastNavigation::Recast PROPERTIES
+                IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+                INTERFACE_INCLUDE_DIRECTORIES "${RECAST_INCLUDE_DIR}")
+
+        if(RECAST_LIBRARY_RELEASE)
+            set_property(TARGET RecastNavigation::Recast APPEND PROPERTY
+                    IMPORTED_CONFIGURATIONS RELEASE)
+            set_target_properties(RecastNavigation::Recast PROPERTIES
+                    IMPORTED_LOCATION_RELEASE "${RECAST_LIBRARY_RELEASE}")
+        endif()
+
+        if(RECAST_LIBRARY_DEBUG)
+            set_property(TARGET RecastNavigation::Recast APPEND PROPERTY
+                    IMPORTED_CONFIGURATIONS DEBUG)
+            set_target_properties(RecastNavigation::Recast PROPERTIES
+                    IMPORTED_LOCATION_DEBUG "${RECAST_LIBRARY_DEBUG}")
+        endif()
+
+        if(NOT RECAST_LIBRARY_RELEASE AND NOT RECAST_LIBRARY_DEBUG)
+            set_property(TARGET RecastNavigation::Recast APPEND PROPERTY
+                    IMPORTED_LOCATION "${RECAST_LIBRARY}")
+        endif()
+    endif()
+
+    if(NOT TARGET RecastNavigation::Detour)
+        add_library(RecastNavigation::Detour UNKNOWN IMPORTED)
+        set_target_properties(RecastNavigation::Detour PROPERTIES
+                IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+                INTERFACE_INCLUDE_DIRECTORIES "${DETOUR_INCLUDE_DIR}")
+
+        if(DETOUR_LIBRARY_RELEASE)
+            set_property(TARGET RecastNavigation::Detour APPEND PROPERTY
+                    IMPORTED_CONFIGURATIONS RELEASE)
+            set_target_properties(RecastNavigation::Detour PROPERTIES
+                    IMPORTED_LOCATION_RELEASE "${DETOUR_LIBRARY_RELEASE}")
+        endif()
+
+        if(DETOUR_LIBRARY_DEBUG)
+            set_property(TARGET RecastNavigation::Detour APPEND PROPERTY
+                    IMPORTED_CONFIGURATIONS DEBUG)
+            set_target_properties(RecastNavigation::Detour PROPERTIES
+                    IMPORTED_LOCATION_DEBUG "${DETOUR_LIBRARY_DEBUG}")
+        endif()
+
+        if(NOT DETOUR_LIBRARY_RELEASE AND NOT DETOUR_LIBRARY_DEBUG)
+            set_property(TARGET RecastNavigation::Detour APPEND PROPERTY
+                    IMPORTED_LOCATION "${DETOUR_LIBRARY}")
+        endif()
+    endif()
+
+    if(NOT TARGET RecastNavigation::DebugUtils)
+        add_library(RecastNavigation::DebugUtils UNKNOWN IMPORTED)
+        set_target_properties(RecastNavigation::DebugUtils PROPERTIES
+                IMPORTED_LINK_INTERFACE_LANGUAGES "C"
+                INTERFACE_INCLUDE_DIRECTORIES "${DEBUGUTILS_INCLUDE_DIR}")
+
+        if(DEBUGUTILS_LIBRARY_RELEASE)
+            set_property(TARGET RecastNavigation::DebugUtils APPEND PROPERTY
+                    IMPORTED_CONFIGURATIONS RELEASE)
+            set_target_properties(RecastNavigation::DebugUtils PROPERTIES
+                    IMPORTED_LOCATION_RELEASE "${DEBUGUTILS_LIBRARY_RELEASE}")
+        endif()
+
+        if(DEBUGUTILS_LIBRARY_DEBUG)
+            set_property(TARGET RecastNavigation::DebugUtils APPEND PROPERTY
+                    IMPORTED_CONFIGURATIONS DEBUG)
+            set_target_properties(RecastNavigation::DebugUtils PROPERTIES
+                    IMPORTED_LOCATION_DEBUG "${DEBUGUTILS_LIBRARY_DEBUG}")
+        endif()
+
+        if(NOT DEBUGUTILS_LIBRARY_RELEASE AND NOT DEBUGUTILS_LIBRARY_DEBUG)
+            set_property(TARGET RecastNavigation::DebugUtils APPEND PROPERTY
+                    IMPORTED_LOCATION "${DEBUGUTILS_LIBRARY}")
+        endif()
+    endif()
+
+endif()
diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt
index 2120afc1a..822858c0b 100644
--- a/components/CMakeLists.txt
+++ b/components/CMakeLists.txt
@@ -46,7 +46,7 @@ add_component_dir (resource
     )
 
 add_component_dir (shader
-    shadermanager shadervisitor
+    shadermanager shadervisitor removedalphafunc
     )
 
 add_component_dir (sceneutil
@@ -140,7 +140,7 @@ add_component_dir (fontloader
     )
 
 add_component_dir (sdlutil
-    sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager
+    gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager
     )
 
 add_component_dir (version
@@ -223,20 +223,26 @@ include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
 add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR})
 
 target_link_libraries(components
+    # CMake's built-in OSG finder does not use pkgconfig, so we have to
+    # manually ensure the order is correct for inter-library dependencies.
+    # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`.
+    # https://gitlab.kitware.com/cmake/cmake/-/issues/21701
+    ${OSGPARTICLE_LIBRARIES}
+    ${OSGVIEWER_LIBRARIES}
+    ${OSGSHADOW_LIBRARIES}
+    ${OSGANIMATION_LIBRARIES}
+    ${OSGGA_LIBRARIES}
+    ${OSGTEXT_LIBRARIES}
+    ${OSGDB_LIBRARIES}
+    ${OSGUTIL_LIBRARIES}
+    ${OSG_LIBRARIES}
+    ${OPENTHREADS_LIBRARIES}
+
     ${Boost_SYSTEM_LIBRARY}
     ${Boost_FILESYSTEM_LIBRARY}
     ${Boost_PROGRAM_OPTIONS_LIBRARY}
     ${Boost_IOSTREAMS_LIBRARY}
-    ${OSG_LIBRARIES}
-    ${OPENTHREADS_LIBRARIES}
-    ${OSGPARTICLE_LIBRARIES}
-    ${OSGUTIL_LIBRARIES}
-    ${OSGDB_LIBRARIES}
-    ${OSGVIEWER_LIBRARIES}
-    ${OSGTEXT_LIBRARIES}
-    ${OSGGA_LIBRARIES}
-    ${OSGSHADOW_LIBRARIES}
-    ${OSGANIMATION_LIBRARIES}
+
     ${SDL2_LIBRARIES}
     ${OPENGL_gl_LIBRARY}
     ${MyGUI_LIBRARIES}
diff --git a/components/debug/gldebug.cpp b/components/debug/gldebug.cpp
index 3c5ec728a..37829a8c1 100644
--- a/components/debug/gldebug.cpp
+++ b/components/debug/gldebug.cpp
@@ -32,133 +32,246 @@ either expressed or implied, of the FreeBSD Project.
 #include "gldebug.hpp"
 
 #include <cstdlib>
+#include <memory>
 
 #include <components/debug/debuglog.hpp>
 
 // OpenGL constants not provided by OSG:
 #include <SDL_opengl_glext.h>
 
-void debugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam)
+namespace Debug
 {
+
+    void debugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam)
+    {
 #ifdef GL_DEBUG_OUTPUT
-    std::string srcStr;
-    switch (source)
-    {
-    case GL_DEBUG_SOURCE_API:
-        srcStr = "API";
-        break;
-    case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
-        srcStr = "WINDOW_SYSTEM";
-        break;
-    case GL_DEBUG_SOURCE_SHADER_COMPILER:
-        srcStr = "SHADER_COMPILER";
-        break;
-    case GL_DEBUG_SOURCE_THIRD_PARTY:
-        srcStr = "THIRD_PARTY";
-        break;
-    case GL_DEBUG_SOURCE_APPLICATION:
-        srcStr = "APPLICATION";
-        break;
-    case GL_DEBUG_SOURCE_OTHER:
-        srcStr = "OTHER";
-        break;
-    default:
-        srcStr = "UNDEFINED";
-        break;
-    }
+        std::string srcStr;
+        switch (source)
+        {
+        case GL_DEBUG_SOURCE_API:
+            srcStr = "API";
+            break;
+        case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
+            srcStr = "WINDOW_SYSTEM";
+            break;
+        case GL_DEBUG_SOURCE_SHADER_COMPILER:
+            srcStr = "SHADER_COMPILER";
+            break;
+        case GL_DEBUG_SOURCE_THIRD_PARTY:
+            srcStr = "THIRD_PARTY";
+            break;
+        case GL_DEBUG_SOURCE_APPLICATION:
+            srcStr = "APPLICATION";
+            break;
+        case GL_DEBUG_SOURCE_OTHER:
+            srcStr = "OTHER";
+            break;
+        default:
+            srcStr = "UNDEFINED";
+            break;
+        }
 
-    std::string typeStr;
+        std::string typeStr;
 
-    Debug::Level logSeverity = Debug::Warning;
-    switch (type)
-    {
-    case GL_DEBUG_TYPE_ERROR:
-        typeStr = "ERROR";
-        logSeverity = Debug::Error;
-        break;
-    case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
-        typeStr = "DEPRECATED_BEHAVIOR";
-        break;
-    case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
-        typeStr = "UNDEFINED_BEHAVIOR";
-        break;
-    case GL_DEBUG_TYPE_PORTABILITY:
-        typeStr = "PORTABILITY";
-        break;
-    case GL_DEBUG_TYPE_PERFORMANCE:
-        typeStr = "PERFORMANCE";
-        break;
-    case GL_DEBUG_TYPE_OTHER:
-        typeStr = "OTHER";
-        break;
-    default:
-        typeStr = "UNDEFINED";
-        break;
-    }
+        Level logSeverity = Warning;
+        switch (type)
+        {
+        case GL_DEBUG_TYPE_ERROR:
+            typeStr = "ERROR";
+            logSeverity = Error;
+            break;
+        case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
+            typeStr = "DEPRECATED_BEHAVIOR";
+            break;
+        case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
+            typeStr = "UNDEFINED_BEHAVIOR";
+            break;
+        case GL_DEBUG_TYPE_PORTABILITY:
+            typeStr = "PORTABILITY";
+            break;
+        case GL_DEBUG_TYPE_PERFORMANCE:
+            typeStr = "PERFORMANCE";
+            break;
+        case GL_DEBUG_TYPE_OTHER:
+            typeStr = "OTHER";
+            break;
+        default:
+            typeStr = "UNDEFINED";
+            break;
+        }
 
-    Log(logSeverity) << "OpenGL " << typeStr << " [" << srcStr << "]: " << message;
+        Log(logSeverity) << "OpenGL " << typeStr << " [" << srcStr << "]: " << message;
 #endif
-}
+    }
 
-void enableGLDebugExtension(unsigned int contextID)
-{
+    class PushDebugGroup
+    {
+    public:
+        static std::unique_ptr<PushDebugGroup> sInstance;
+
+        void (GL_APIENTRY * glPushDebugGroup) (GLenum source, GLuint id, GLsizei length, const GLchar * message);
+
+        void (GL_APIENTRY * glPopDebugGroup) (void);
+
+        bool valid()
+        {
+            return glPushDebugGroup && glPopDebugGroup;
+        }
+    };
+
+    std::unique_ptr<PushDebugGroup> PushDebugGroup::sInstance{ std::make_unique<PushDebugGroup>() };
+
+    EnableGLDebugOperation::EnableGLDebugOperation() : osg::GraphicsOperation("EnableGLDebugOperation", false)
+    {
+    }
+
+    void EnableGLDebugOperation::operator()(osg::GraphicsContext* graphicsContext)
+    {
 #ifdef GL_DEBUG_OUTPUT
-    typedef void (GL_APIENTRY *DEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam);
-    typedef void (GL_APIENTRY *GLDebugMessageControlFunction)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled);
-    typedef void (GL_APIENTRY *GLDebugMessageCallbackFunction)(DEBUGPROC, const void* userParam);
-    
-    GLDebugMessageControlFunction glDebugMessageControl = nullptr;
-    GLDebugMessageCallbackFunction glDebugMessageCallback = nullptr;
+        OpenThreads::ScopedLock<OpenThreads::Mutex> lock(mMutex);
 
-    if (osg::isGLExtensionSupported(contextID, "GL_KHR_debug"))
-    {
-        osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallback");
-        osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControl");
-    }
-    else if (osg::isGLExtensionSupported(contextID, "GL_ARB_debug_output"))
-    {
-        osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackARB");
-        osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlARB");
-    }
-    else if (osg::isGLExtensionSupported(contextID, "GL_AMD_debug_output"))
-    {
-        osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackAMD");
-        osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlAMD");
-    }
+        unsigned int contextID = graphicsContext->getState()->getContextID();
 
-    if (glDebugMessageCallback && glDebugMessageControl)
-    {
-        glEnable(GL_DEBUG_OUTPUT);
-        glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_MEDIUM, 0, nullptr, true);
-        glDebugMessageCallback(debugCallback, nullptr);
+        typedef void (GL_APIENTRY *DEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam);
+        typedef void (GL_APIENTRY *GLDebugMessageControlFunction)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled);
+        typedef void (GL_APIENTRY *GLDebugMessageCallbackFunction)(DEBUGPROC, const void* userParam);
 
-        Log(Debug::Info) << "OpenGL debug callback attached.";
-    }
-    else
+        GLDebugMessageControlFunction glDebugMessageControl = nullptr;
+        GLDebugMessageCallbackFunction glDebugMessageCallback = nullptr;
+
+        if (osg::isGLExtensionSupported(contextID, "GL_KHR_debug"))
+        {
+            osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallback");
+            osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControl");
+            osg::setGLExtensionFuncPtr(PushDebugGroup::sInstance->glPushDebugGroup, "glPushDebugGroup");
+            osg::setGLExtensionFuncPtr(PushDebugGroup::sInstance->glPopDebugGroup, "glPopDebugGroup");
+        }
+        else if (osg::isGLExtensionSupported(contextID, "GL_ARB_debug_output"))
+        {
+            osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackARB");
+            osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlARB");
+        }
+        else if (osg::isGLExtensionSupported(contextID, "GL_AMD_debug_output"))
+        {
+            osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackAMD");
+            osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlAMD");
+        }
+
+        if (glDebugMessageCallback && glDebugMessageControl)
+        {
+            glEnable(GL_DEBUG_OUTPUT);
+            glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_MEDIUM, 0, nullptr, true);
+            glDebugMessageCallback(debugCallback, nullptr);
+
+            Log(Info) << "OpenGL debug callback attached.";
+        }
+        else
 #endif
-        Log(Debug::Error) << "Unable to attach OpenGL debug callback.";
-}
-
-Debug::EnableGLDebugOperation::EnableGLDebugOperation() : osg::GraphicsOperation("EnableGLDebugOperation", false)
-{
-}
-
-void Debug::EnableGLDebugOperation::operator()(osg::GraphicsContext* graphicsContext)
-{
-    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(mMutex);
-
-    unsigned int contextID = graphicsContext->getState()->getContextID();
-    enableGLDebugExtension(contextID);
-}
-
-bool Debug::shouldDebugOpenGL()
-{
-    const char* env = std::getenv("OPENMW_DEBUG_OPENGL");
-    if (!env)
-        return false;
-    std::string str(env);
-    if (str.length() == 0)
-        return true;
-
-    return str.find("OFF") == std::string::npos && str.find("0") == std::string::npos && str.find("NO") == std::string::npos;
+            Log(Error) << "Unable to attach OpenGL debug callback.";
+    }
+
+    bool shouldDebugOpenGL()
+    {
+        const char* env = std::getenv("OPENMW_DEBUG_OPENGL");
+        if (!env)
+            return false;
+        std::string str(env);
+        if (str.length() == 0)
+            return true;
+
+        return str.find("OFF") == std::string::npos && str.find("0") == std::string::npos && str.find("NO") == std::string::npos;
+    }
+
+    DebugGroup::DebugGroup(const std::string & message, GLuint id)
+    #ifdef GL_DEBUG_OUTPUT
+        : mSource(GL_DEBUG_SOURCE_APPLICATION)
+    #else
+        : mSource(0x824A)
+    #endif
+        , mId(id)
+        , mMessage(message)
+    {
+    }
+
+    DebugGroup::DebugGroup(const DebugGroup & debugGroup, const osg::CopyOp & copyop)
+        : osg::StateAttribute(debugGroup, copyop)
+        , mSource(debugGroup.mSource)
+        , mId(debugGroup.mId)
+        , mMessage(debugGroup.mMessage)
+    {
+    }
+
+    void DebugGroup::apply(osg::State & state) const
+    {
+        if (!PushDebugGroup::sInstance->valid())
+        {
+            Log(Error) << "OpenGL debug groups not supported on this system, or OPENMW_DEBUG_OPENGL environment variable not set.";
+            return;
+        }
+
+        auto& attributeVec = state.getAttributeVec(this);
+        auto& lastAppliedStack = sLastAppliedStack[state.getContextID()];
+
+        size_t firstNonMatch = 0;
+        while (firstNonMatch < lastAppliedStack.size()
+               && ((firstNonMatch < attributeVec.size() && lastAppliedStack[firstNonMatch] == attributeVec[firstNonMatch].first)
+                   || lastAppliedStack[firstNonMatch] == this))
+            firstNonMatch++;
+
+        for (size_t i = lastAppliedStack.size(); i > firstNonMatch; --i)
+            lastAppliedStack[i - 1]->pop(state);
+        lastAppliedStack.resize(firstNonMatch);
+
+        lastAppliedStack.reserve(attributeVec.size());
+        for (size_t i = firstNonMatch; i < attributeVec.size(); ++i)
+        {
+            const DebugGroup* group = static_cast<const DebugGroup*>(attributeVec[i].first);
+            group->push(state);
+            lastAppliedStack.push_back(group);
+        }
+        if (!(lastAppliedStack.back() == this))
+        {
+            push(state);
+            lastAppliedStack.push_back(this);
+        }
+    }
+
+    int DebugGroup::compare(const StateAttribute & sa) const
+    {
+        COMPARE_StateAttribute_Types(DebugGroup, sa);
+
+        COMPARE_StateAttribute_Parameter(mSource);
+        COMPARE_StateAttribute_Parameter(mId);
+        COMPARE_StateAttribute_Parameter(mMessage);
+
+        return 0;
+    }
+
+    void DebugGroup::releaseGLObjects(osg::State * state) const
+    {
+        if (state)
+            sLastAppliedStack.erase(state->getContextID());
+        else
+            sLastAppliedStack.clear();
+    }
+
+    bool DebugGroup::isValid() const
+    {
+        return mSource || mId || mMessage.length();
+    }
+
+    void DebugGroup::push(osg::State & state) const
+    {
+        if (isValid())
+            PushDebugGroup::sInstance->glPushDebugGroup(mSource, mId, mMessage.size(), mMessage.c_str());
+    }
+
+    void DebugGroup::pop(osg::State & state) const
+    {
+        if (isValid())
+            PushDebugGroup::sInstance->glPopDebugGroup();
+    }
+
+    std::map<unsigned int, std::vector<const DebugGroup *>> DebugGroup::sLastAppliedStack{};
+
 }
diff --git a/components/debug/gldebug.hpp b/components/debug/gldebug.hpp
index 8be747afe..b6f32c9cf 100644
--- a/components/debug/gldebug.hpp
+++ b/components/debug/gldebug.hpp
@@ -17,5 +17,65 @@ namespace Debug
     };
 
     bool shouldDebugOpenGL();
+
+
+    /*
+        Debug groups allow rendering to be annotated, making debugging via APITrace/CodeXL/NSight etc. much clearer.
+
+        Because I've not thought of a quick and clean way of doing it without incurring a small performance cost,
+        there are no uses of this class checked in. For now, add annotations locally when you need them.
+
+        To use this class, add it to a StateSet just like any other StateAttribute. Prefer the string-only constructor.
+        You'll need OPENMW_DEBUG_OPENGL set to true, or shouldDebugOpenGL() redefined to just return true as otherwise
+        the extension function pointers won't get set up. That can maybe be cleaned up in the future.
+
+        Beware that consecutive identical debug groups (i.e. pointers match) won't always get applied due to OSG thinking
+        it's already applied them. Either avoid nesting the same object, add dummy groups so they're not consecutive, or
+        ensure the leaf group isn't identical to its parent.
+    */
+    class DebugGroup : public osg::StateAttribute
+    {
+    public:
+        DebugGroup()
+            : mSource(0)
+            , mId(0)
+            , mMessage("")
+        {}
+
+        DebugGroup(GLenum source, GLuint id, const std::string& message)
+            : mSource(source)
+            , mId(id)
+            , mMessage(message)
+        {}
+
+        DebugGroup(const std::string& message, GLuint id = 0);
+
+        DebugGroup(const DebugGroup& debugGroup, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY);
+
+        META_StateAttribute(Debug, DebugGroup, osg::StateAttribute::Type(101));
+
+        void apply(osg::State& state) const override;
+
+        int compare(const StateAttribute& sa) const override;
+
+        void releaseGLObjects(osg::State* state = nullptr) const override;
+
+        virtual bool isValid() const;
+
+    protected:
+        virtual ~DebugGroup() = default;
+
+        virtual void push(osg::State& state) const;
+
+        virtual void pop(osg::State& state) const;
+
+        GLenum mSource;
+        GLuint mId;
+        std::string mMessage;
+
+        static std::map<unsigned int, std::vector<const DebugGroup *>> sLastAppliedStack;
+
+        friend EnableGLDebugOperation;
+    };
 }
 #endif
diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp
index c47cf9766..142ba590d 100644
--- a/components/detournavigator/navigatorimpl.cpp
+++ b/components/detournavigator/navigatorimpl.cpp
@@ -18,6 +18,8 @@ namespace DetourNavigator
 
     void NavigatorImpl::addAgent(const osg::Vec3f& agentHalfExtents)
     {
+        if(agentHalfExtents.length2() <= 0)
+            return;
         ++mAgents[agentHalfExtents];
         mNavMeshManager.addAgent(agentHalfExtents);
     }
diff --git a/components/esm/variant.cpp b/components/esm/variant.cpp
index 6ff31dc44..cbcb6e002 100644
--- a/components/esm/variant.cpp
+++ b/components/esm/variant.cpp
@@ -62,10 +62,31 @@ ESM::Variant& ESM::Variant::operator= (const Variant& variant)
     return *this;
 }
 
+ESM::Variant& ESM::Variant::operator= (Variant&& variant)
+{
+    if (&variant!=this)
+    {
+        delete mData;
+
+        mType = variant.mType;
+        mData = variant.mData;
+
+        variant.mData = nullptr;
+    }
+
+    return *this;
+}
+
 ESM::Variant::Variant (const Variant& variant)
 : mType (variant.mType), mData (variant.mData ? variant.mData->clone() : nullptr)
 {}
 
+ESM::Variant::Variant(Variant&& variant)
+: mType (variant.mType), mData (variant.mData)
+{
+    variant.mData = nullptr;
+}
+
 ESM::VarType ESM::Variant::getType() const
 {
     return mType;
diff --git a/components/esm/variant.hpp b/components/esm/variant.hpp
index 5f179a7bd..20b3aa76e 100644
--- a/components/esm/variant.hpp
+++ b/components/esm/variant.hpp
@@ -46,8 +46,10 @@ namespace ESM
             ~Variant();
 
             Variant& operator= (const Variant& variant);
+            Variant& operator= (Variant && variant);
 
             Variant (const Variant& variant);
+            Variant (Variant&& variant);
 
             VarType getType() const;
 
diff --git a/components/myguiplatform/myguicompat.h b/components/myguiplatform/myguicompat.h
new file mode 100644
index 000000000..04ca11a79
--- /dev/null
+++ b/components/myguiplatform/myguicompat.h
@@ -0,0 +1,12 @@
+#ifndef OPENMW_COMPONENTS_MYGUIPLATFORM_MYGUICOMPAT_H
+#define OPENMW_COMPONENTS_MYGUIPLATFORM_MYGUICOMPAT_H
+
+#include <MyGUI_Prerequest.h>
+
+#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
+    #define OPENMW_MYGUI_CONST_GETTER_3_4_1 const
+#else
+    #define OPENMW_MYGUI_CONST_GETTER_3_4_1
+#endif
+
+#endif  // OPENMW_COMPONENTS_MYGUIPLATFORM_MYGUICOMPAT_H
diff --git a/components/myguiplatform/myguidatamanager.cpp b/components/myguiplatform/myguidatamanager.cpp
index c90e09221..0310e996b 100644
--- a/components/myguiplatform/myguidatamanager.cpp
+++ b/components/myguiplatform/myguidatamanager.cpp
@@ -15,7 +15,7 @@ void DataManager::setResourcePath(const std::string &path)
     mResourcePath = path;
 }
 
-MyGUI::IDataStream *DataManager::getData(const std::string &name)
+MyGUI::IDataStream *DataManager::getData(const std::string &name) OPENMW_MYGUI_CONST_GETTER_3_4_1
 {
     std::string fullpath = getDataPath(name);
     std::unique_ptr<boost::filesystem::ifstream> stream;
@@ -34,13 +34,13 @@ void DataManager::freeData(MyGUI::IDataStream *data)
     delete data;
 }
 
-bool DataManager::isDataExist(const std::string &name)
+bool DataManager::isDataExist(const std::string &name) OPENMW_MYGUI_CONST_GETTER_3_4_1
 {
     std::string fullpath = mResourcePath + "/" + name;
     return boost::filesystem::exists(fullpath);
 }
 
-const MyGUI::VectorString &DataManager::getDataListNames(const std::string &pattern)
+const MyGUI::VectorString &DataManager::getDataListNames(const std::string &pattern) OPENMW_MYGUI_CONST_GETTER_3_4_1
 {
     // TODO: pattern matching (unused?)
     static MyGUI::VectorString strings;
@@ -49,7 +49,7 @@ const MyGUI::VectorString &DataManager::getDataListNames(const std::string &patt
     return strings;
 }
 
-const std::string &DataManager::getDataPath(const std::string &name)
+const std::string &DataManager::getDataPath(const std::string &name) OPENMW_MYGUI_CONST_GETTER_3_4_1
 {
     static std::string result;
     result.clear();
diff --git a/components/myguiplatform/myguidatamanager.hpp b/components/myguiplatform/myguidatamanager.hpp
index a97c6ad2e..ca2f94899 100644
--- a/components/myguiplatform/myguidatamanager.hpp
+++ b/components/myguiplatform/myguidatamanager.hpp
@@ -3,10 +3,11 @@
 
 #include <MyGUI_DataManager.h>
 
+#include "myguicompat.h"
+
 namespace osgMyGUI
 {
 
-
 class DataManager : public MyGUI::DataManager
 {
 public:
@@ -18,7 +19,7 @@ public:
     /** Get data stream from specified resource name.
         @param _name Resource name (usually file name).
     */
-    MyGUI::IDataStream* getData(const std::string& _name) override;
+    MyGUI::IDataStream* getData(const std::string& _name) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
 
     /** Free data stream.
         @param _data Data stream.
@@ -28,18 +29,18 @@ public:
     /** Is data with specified name exist.
         @param _name Resource name.
     */
-    bool isDataExist(const std::string& _name) override;
+    bool isDataExist(const std::string& _name) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
 
     /** Get all data names with names that matches pattern.
         @param _pattern Pattern to match (for example "*.layout").
     */
-    const MyGUI::VectorString& getDataListNames(const std::string& _pattern) override;
+    const MyGUI::VectorString& getDataListNames(const std::string& _pattern) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
 
     /** Get full path to data.
         @param _name Resource name.
         @return Return full path to specified data.
     */
-    const std::string& getDataPath(const std::string& _name) override;
+    const std::string& getDataPath(const std::string& _name) OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
 
 private:
     std::string mResourcePath;
diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp
index dc771f11f..77a5ee533 100644
--- a/components/myguiplatform/myguirendermanager.cpp
+++ b/components/myguiplatform/myguirendermanager.cpp
@@ -7,6 +7,7 @@
 #include <osg/BlendFunc>
 #include <osg/Texture2D>
 #include <osg/TexMat>
+#include <osg/ValueObject>
 
 #include <osgViewer/Viewer>
 
@@ -14,6 +15,9 @@
 
 #include <components/resource/imagemanager.hpp>
 
+#include <components/debug/debuglog.hpp>
+
+#include "myguicompat.h"
 #include "myguitexture.hpp"
 
 #define MYGUI_PLATFORM_LOG_SECTION "Platform"
@@ -263,7 +267,7 @@ public:
     osg::VertexBufferObject* getVertexBuffer();
 
     void setVertexCount(size_t count) override;
-    size_t getVertexCount() override;
+    size_t getVertexCount() OPENMW_MYGUI_CONST_GETTER_3_4_1 override;
 
     MyGUI::Vertex *lock() override;
     void unlock() override;
@@ -290,7 +294,7 @@ void OSGVertexBuffer::setVertexCount(size_t count)
     mNeedVertexCount = count;
 }
 
-size_t OSGVertexBuffer::getVertexCount()
+size_t OSGVertexBuffer::getVertexCount() OPENMW_MYGUI_CONST_GETTER_3_4_1
 {
     return mNeedVertexCount;
 }
@@ -438,14 +442,23 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text
     batch.mVertexBuffer = static_cast<OSGVertexBuffer*>(buffer)->getVertexBuffer();
     batch.mArray = static_cast<OSGVertexBuffer*>(buffer)->getVertexArray();
     static_cast<OSGVertexBuffer*>(buffer)->markUsed();
+    bool premultipliedAlpha = false;
     if (texture)
     {
         batch.mTexture = static_cast<OSGTexture*>(texture)->getTexture();
         if (batch.mTexture->getDataVariance() == osg::Object::DYNAMIC)
             mDrawable->setDataVariance(osg::Object::DYNAMIC); // only for this frame, reset in begin()
+        batch.mTexture->getUserValue("premultiplied alpha", premultipliedAlpha);
     }
     if (mInjectState)
         batch.mStateSet = mInjectState;
+    else if (premultipliedAlpha)
+    {
+        // This is hacky, but MyGUI made it impossible to use a custom layer for a nested node, so state couldn't be injected 'properly'
+        osg::ref_ptr<osg::StateSet> stateSet = new osg::StateSet();
+        stateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA));
+        batch.mStateSet = stateSet;
+    }
 
     mDrawable->addBatch(batch);
 }
@@ -560,4 +573,14 @@ bool RenderManager::checkTexture(MyGUI::ITexture* _texture)
     return true;
 }
 
+#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
+void RenderManager::registerShader(
+    const std::string& _shaderName,
+    const std::string& _vertexProgramFile,
+    const std::string& _fragmentProgramFile)
+{
+    MYGUI_PLATFORM_LOG(Warning, "osgMyGUI::RenderManager::registerShader is not implemented");
+}
+#endif
+
 }
diff --git a/components/myguiplatform/myguirendermanager.hpp b/components/myguiplatform/myguirendermanager.hpp
index 72abebd18..3c3fb672d 100644
--- a/components/myguiplatform/myguirendermanager.hpp
+++ b/components/myguiplatform/myguirendermanager.hpp
@@ -5,6 +5,8 @@
 
 #include <osg/ref_ptr>
 
+#include "myguicompat.h"
+
 namespace Resource
 {
     class ImageManager;
@@ -70,7 +72,8 @@ public:
     const MyGUI::IntSize& getViewSize() const override { return mViewSize; }
 
     /** @see RenderManager::getVertexFormat */
-    MyGUI::VertexColourType getVertexFormat() override { return mVertexFormat; }
+    MyGUI::VertexColourType getVertexFormat() OPENMW_MYGUI_CONST_GETTER_3_4_1 override
+    { return mVertexFormat; }
 
     /** @see RenderManager::isFormatSupported */
     bool isFormatSupported(MyGUI::PixelFormat format, MyGUI::TextureUsage usage) override;
@@ -102,17 +105,22 @@ public:
     void setInjectState(osg::StateSet* stateSet);
 
     /** @see IRenderTarget::getInfo */
-    const MyGUI::RenderTargetInfo& getInfo() override { return mInfo; }
+    const MyGUI::RenderTargetInfo& getInfo() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mInfo; }
 
     bool checkTexture(MyGUI::ITexture* _texture);
 
     // setViewSize() is a part of MyGUI::RenderManager interface since 3.4.0 release
-#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,4,0)
+#if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3, 4, 0)
     void setViewSize(int width, int height);
 #else
     void setViewSize(int width, int height) override;
 #endif
 
+    // registerShader() is a part of MyGUI::RenderManager interface since 3.4.1 release
+#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
+    void registerShader(const std::string& _shaderName, const std::string& _vertexProgramFile, const std::string& _fragmentProgramFile) override;
+#endif
+
 /*internal:*/
 
     void collectDrawCalls();
diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp
index 598f5a14e..ce7332cc7 100644
--- a/components/myguiplatform/myguitexture.cpp
+++ b/components/myguiplatform/myguitexture.cpp
@@ -115,16 +115,6 @@ namespace osgMyGUI
         Log(Debug::Warning) << "Would save image to file " << fname;
     }
 
-    int OSGTexture::getWidth()
-    {
-        return mWidth;
-    }
-
-    int OSGTexture::getHeight()
-    {
-        return mHeight;
-    }
-
     void *OSGTexture::lock(MyGUI::TextureUsage /*access*/)
     {
         if (!mTexture.valid())
@@ -167,15 +157,14 @@ namespace osgMyGUI
         mLockedImage = nullptr;
     }
 
-    bool OSGTexture::isLocked()
-    {
-        return mLockedImage.valid();
-    }
-
     // Render-to-texture not currently implemented.
     MyGUI::IRenderTarget* OSGTexture::getRenderTarget()
     {
         return nullptr;
     }
 
+#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
+    void OSGTexture::setShader(const std::string& _shaderName)
+    { Log(Debug::Warning) << "OSGTexture::setShader is not implemented"; }
+#endif
 }
diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp
index 6baeb7459..a34f1b762 100644
--- a/components/myguiplatform/myguitexture.hpp
+++ b/components/myguiplatform/myguitexture.hpp
@@ -5,6 +5,12 @@
 
 #include <osg/ref_ptr>
 
+#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
+    #define OPENMW_MYGUI_CONST_GETTER_3_4_1 const
+#else
+    #define OPENMW_MYGUI_CONST_GETTER_3_4_1
+#endif
+
 namespace osg
 {
     class Image;
@@ -47,17 +53,22 @@ namespace osgMyGUI
 
         void* lock(MyGUI::TextureUsage access) override;
         void unlock() override;
-        bool isLocked() override;
+        bool isLocked() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mLockedImage.valid(); }
 
-        int getWidth() override;
-        int getHeight() override;
+        int getWidth() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mWidth; }
+        int getHeight() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mHeight; }
 
-        MyGUI::PixelFormat getFormat() override { return mFormat; }
-        MyGUI::TextureUsage getUsage() override { return mUsage; }
-        size_t getNumElemBytes() override { return mNumElemBytes; }
+        MyGUI::PixelFormat getFormat() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mFormat; }
+        MyGUI::TextureUsage getUsage() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mUsage; }
+        size_t getNumElemBytes() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mNumElemBytes; }
 
         MyGUI::IRenderTarget *getRenderTarget() override;
 
+    // setShader() is a part of MyGUI::RenderManager interface since 3.4.1 release
+#if MYGUI_VERSION > MYGUI_DEFINE_VERSION(3, 4, 0)
+    void setShader(const std::string& _shaderName) override;
+#endif
+
     /*internal:*/
         osg::Texture2D *getTexture() const { return mTexture.get(); }
     };
diff --git a/components/myguiplatform/scalinglayer.cpp b/components/myguiplatform/scalinglayer.cpp
index 07a5161b2..99ed6d07a 100644
--- a/components/myguiplatform/scalinglayer.cpp
+++ b/components/myguiplatform/scalinglayer.cpp
@@ -3,6 +3,8 @@
 #include <MyGUI_RenderManager.h>
 #include <algorithm>
 
+#include "myguicompat.h"
+
 namespace osgMyGUI
 {
 
@@ -37,7 +39,7 @@ namespace osgMyGUI
             mTarget->doRender(_buffer, _texture, _count);
         }
 
-        const MyGUI::RenderTargetInfo& getInfo() override
+        const MyGUI::RenderTargetInfo& getInfo() OPENMW_MYGUI_CONST_GETTER_3_4_1 override
         {
             mInfo = mTarget->getInfo();
             mInfo.hOffset = mHOffset;
@@ -51,7 +53,7 @@ namespace osgMyGUI
         MyGUI::IRenderTarget* mTarget;
         MyGUI::IntSize mViewSize;
         float mHOffset, mVOffset;
-        MyGUI::RenderTargetInfo mInfo;
+        mutable MyGUI::RenderTargetInfo mInfo;
     };
 
     MyGUI::ILayerItem *ScalingLayer::getLayerItemByPoint(int _left, int _top) const
diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp
index ef53bea0d..f40cc04ea 100644
--- a/components/resource/scenemanager.cpp
+++ b/components/resource/scenemanager.cpp
@@ -226,7 +226,7 @@ namespace Resource
         , mAutoUseNormalMaps(false)
         , mAutoUseSpecularMaps(false)
         , mApplyLightingToEnvMaps(false)
-        , mFFPLighting(true)
+        , mLightingMethod(SceneUtil::LightingMethod::FFP)
         , mInstanceCache(new MultiObjectCache)
         , mSharedStateManager(new SharedStateManager)
         , mImageManager(imageManager)
@@ -258,6 +258,12 @@ namespace Resource
         node->accept(*shaderVisitor);
     }
 
+    void SceneManager::reinstateRemovedState(osg::ref_ptr<osg::Node> node)
+    {
+        osg::ref_ptr<Shader::ReinstateRemovedStateVisitor> reinstateRemovedStateVisitor = new Shader::ReinstateRemovedStateVisitor(false);
+        node->accept(*reinstateRemovedStateVisitor);
+    }
+
     void SceneManager::setClampLighting(bool clamp)
     {
         mClampLighting = clamp;
@@ -298,14 +304,19 @@ namespace Resource
         mApplyLightingToEnvMaps = apply;
     }
 
-    void SceneManager::setFFPLighting(bool apply)
+    void SceneManager::setLightingMethod(SceneUtil::LightingMethod method)
     {
-        mFFPLighting = apply;
+        mLightingMethod = method;
     }
 
-    bool SceneManager::getFFPLighting() const
+    SceneUtil::LightingMethod SceneManager::getLightingMethod() const
     {
-        return mFFPLighting;
+        return mLightingMethod;
+    }
+    
+    void SceneManager::setConvertAlphaTestToAlphaToCoverage(bool convert)
+    {
+        mConvertAlphaTestToAlphaToCoverage = convert;
     }
 
     SceneManager::~SceneManager()
@@ -782,6 +793,7 @@ namespace Resource
         shaderVisitor->setAutoUseSpecularMaps(mAutoUseSpecularMaps);
         shaderVisitor->setSpecularMapPattern(mSpecularMapPattern);
         shaderVisitor->setApplyLightingToEnvMaps(mApplyLightingToEnvMaps);
+        shaderVisitor->setConvertAlphaTestToAlphaToCoverage(mConvertAlphaTestToAlphaToCoverage);
         shaderVisitor->setTranslucentFramebuffer(translucentFramebuffer);
         return shaderVisitor;
     }
diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp
index 92800df9f..fdc7d334f 100644
--- a/components/resource/scenemanager.hpp
+++ b/components/resource/scenemanager.hpp
@@ -12,6 +12,8 @@
 
 #include "resourcemanager.hpp"
 
+#include <components/sceneutil/lightmanager.hpp>
+
 namespace Resource
 {
     class ImageManager;
@@ -75,9 +77,14 @@ namespace Resource
 
         Shader::ShaderManager& getShaderManager();
 
-        /// Re-create shaders for this node, need to call this if texture stages or vertex color mode have changed.
+        /// Re-create shaders for this node, need to call this if alpha testing, texture stages or vertex color mode have changed.
         void recreateShaders(osg::ref_ptr<osg::Node> node, const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false, bool forceShadersForNode = false);
 
+        /// Applying shaders to a node may replace some fixed-function state.
+        /// This restores it.
+        /// When editing such state, it should be reinstated before the edits, and shaders should be recreated afterwards.
+        void reinstateRemovedState(osg::ref_ptr<osg::Node> node);
+
         /// @see ShaderVisitor::setForceShaders
         void setForceShaders(bool force);
         bool getForceShaders() const;
@@ -100,8 +107,10 @@ namespace Resource
 
         void setApplyLightingToEnvMaps(bool apply);
 
-        void setFFPLighting(bool apply);
-        bool getFFPLighting() const;
+        void setLightingMethod(SceneUtil::LightingMethod method);
+        SceneUtil::LightingMethod getLightingMethod() const;
+        
+        void setConvertAlphaTestToAlphaToCoverage(bool convert);
 
         void setShaderPath(const std::string& path);
 
@@ -187,7 +196,8 @@ namespace Resource
         bool mAutoUseSpecularMaps;
         std::string mSpecularMapPattern;
         bool mApplyLightingToEnvMaps;
-        bool mFFPLighting;
+        SceneUtil::LightingMethod mLightingMethod;
+        bool mConvertAlphaTestToAlphaToCoverage;
 
         osg::ref_ptr<MultiObjectCache> mInstanceCache;
 
diff --git a/components/sceneutil/lightcontroller.cpp b/components/sceneutil/lightcontroller.cpp
index cc95fe2e4..c759fabc7 100644
--- a/components/sceneutil/lightcontroller.cpp
+++ b/components/sceneutil/lightcontroller.cpp
@@ -11,14 +11,13 @@
 namespace SceneUtil
 {
 
-    LightController::LightController(bool useFFPLighting)
+    LightController::LightController()
         : mType(LT_Normal)
         , mPhase(0.25f + Misc::Rng::rollClosedProbability() * 0.75f)
         , mBrightness(0.675f)
         , mStartTime(0.0)
         , mLastTime(0.0)
         , mTicksToAdvance(0.f)
-        , mFFPLighting(useFFPLighting)
     {
     }
 
@@ -63,11 +62,7 @@ namespace SceneUtil
                 mPhase = mPhase <= 0.5f ? 1.f : 0.25f;
         }
 
-        auto* lightSource = static_cast<SceneUtil::LightSource*>(node);
-        if (mFFPLighting)
-            lightSource->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness);
-        else
-            lightSource->setBrightness(nv->getTraversalNumber(), mBrightness);
+        static_cast<SceneUtil::LightSource*>(node)->getLight(nv->getTraversalNumber())->setDiffuse(mDiffuseColor * mBrightness);
 
         traverse(node, nv);
     }
diff --git a/components/sceneutil/lightcontroller.hpp b/components/sceneutil/lightcontroller.hpp
index f416be724..36b2e868e 100644
--- a/components/sceneutil/lightcontroller.hpp
+++ b/components/sceneutil/lightcontroller.hpp
@@ -20,7 +20,7 @@ namespace SceneUtil
             LT_PulseSlow
         };
 
-        LightController(bool useFFPLighting=true);
+        LightController();
 
         void setType(LightType type);
 
@@ -36,7 +36,6 @@ namespace SceneUtil
         double mStartTime;
         double mLastTime;
         float mTicksToAdvance;
-        bool mFFPLighting;
     };
 
 }
diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp
index 987a95e20..721545920 100644
--- a/components/sceneutil/lightmanager.cpp
+++ b/components/sceneutil/lightmanager.cpp
@@ -2,6 +2,7 @@
 
 #include <osg/BufferObject>
 #include <osg/BufferIndexBinding>
+#include <osg/Endian>
 
 #include <osgUtil/CullVisitor>
 
@@ -29,169 +30,132 @@ namespace
     {
         return left->mViewBound.center().length2() - left->mViewBound.radius2()*81 < right->mViewBound.center().length2() - right->mViewBound.radius2()*81;
     }
+
+    float getLightRadius(const osg::Light* light)
+    {
+        float value = 0.0;
+        light->getUserValue("radius", value);
+        return value;
+    }
+
+    void setLightRadius(osg::Light* light, float value)
+    {
+        light->setUserValue("radius", value);
+    }
 }
 
 namespace SceneUtil
 {
     static int sLightId = 0;
 
-////////////////////////////////////////////////////////////////////////////////
-// Internal Data Structures
-////////////////////////////////////////////////////////////////////////////////
-
     class LightBuffer : public osg::Referenced
     {
     public:
-        LightBuffer(int elements, int count=1)
-            : mNumElements(elements)
-            , mData(new osg::Vec4Array(elements * count))
-            , mDirty(false)
-        {}
 
-        virtual ~LightBuffer() {}
+        LightBuffer(int count) : mData(new osg::Vec4Array(3*count)), mEndian(osg::getCpuByteOrder()) {}
 
-        osg::ref_ptr<osg::Vec4Array> getData() 
+        void setDiffuse(int index, const osg::Vec4& value)
         {
-            return mData;
+            *(unsigned int*)(&(*mData)[3*index][0]) = asRGBA(value);
         }
 
-        bool isDirty() const
+        void setAmbient(int index, const osg::Vec4& value)
         {
-            return mDirty;
+            *(unsigned int*)(&(*mData)[3*index][1]) = asRGBA(value);
         }
 
-        void dirty()
+        void setSpecular(int index, const osg::Vec4& value)
         {
-            mData->dirty();
-            mDirty = false;
+            *(unsigned int*)(&(*mData)[3*index][2]) = asRGBA(value);
         }
 
-    protected:
-        size_t mNumElements;
-        osg::ref_ptr<osg::Vec4Array> mData;
-        bool mDirty;
-    };
-
-    /*
-     *  struct:
-     *      vec4 diffuse
-     *      vec4 ambient
-     *      vec4 specular
-     *      vec4 direction
-     */
-    class SunlightBuffer : public LightBuffer
-    {
-    public:
-
-        enum Type {Diffuse, Ambient, Specular, Direction};
-
-        SunlightBuffer()
-            : LightBuffer(4)
-        {}
-
-        void setValue(Type type, const osg::Vec4& value)
+        void setPosition(int index, const osg::Vec4& value)
         {
-            if (getValue(type) == value)
-                return;
-
-            (*mData)[type] = value;
-
-            mDirty = true;
-        }   
-
-        osg::Vec4 getValue(Type type)
-        {
-            return (*mData)[type];
-        }
-    };
-
-    /*
-     *  struct:
-     *      vec4 diffuse
-     *      vec4 ambient
-     *      vec4 position
-     *      vec4 illumination (constant, linear, quadratic)
-     */
-    class PointLightBuffer : public LightBuffer
-    {
-    public:
-
-        enum Type {Position, Diffuse, Ambient, Attenuation};
-
-        PointLightBuffer(int count)
-            : LightBuffer(4, count)
-        {}
-
-        void setValue(int index, Type type, const osg::Vec4& value)
-        {
-            if (getValue(index, type) == value)
-                return;
-            
-            int indexOffset = mNumElements * index + type;
-            (*mData)[indexOffset] = value;
-            
-            mDirty = true;
+            (*mData)[3*index+1] = value;
         }
 
-        osg::Vec4 getValue(int index, Type type)
+        void setAttenuation(int index, float c, float l, float q)
         {
-            return (*mData)[mNumElements * index + type];
+            (*mData)[3*index+2][0] = c;
+            (*mData)[3*index+2][1] = l;
+            (*mData)[3*index+2][2] = q;
         }
 
+        void setRadius(int index, float value)
+        {
+            (*mData)[3*index+2][3] = value;
+        }
+
+        auto getPosition(int index)
+        {
+            return (*mData)[3*index+1];
+        }
+
+        auto& getData() { return mData; }
+        void dirty() { mData->dirty(); }
+
         static constexpr int queryBlockSize(int sz)
         {
-            return 4 * osg::Vec4::num_components * sizeof(GL_FLOAT) * sz;
+            return 3 * osg::Vec4::num_components * sizeof(GL_FLOAT) * sz;
         }
+
+        unsigned int asRGBA(const osg::Vec4& value) const
+        {
+            return mEndian == osg::BigEndian ? value.asABGR() : value.asRGBA();
+        }
+
+        osg::ref_ptr<osg::Vec4Array> mData;
+        osg::Endian mEndian;
     };
 
     class LightStateCache
     {
     public:
-        osg::Light* lastAppliedLight[8];
+        std::vector<osg::Light*> lastAppliedLight;
     };
 
-    LightStateCache* getLightStateCache(size_t contextid)
+    LightStateCache* getLightStateCache(size_t contextid, size_t size = 8)
     {
         static std::vector<LightStateCache> cacheVector;
         if (cacheVector.size() < contextid+1)
             cacheVector.resize(contextid+1);
+        cacheVector[contextid].lastAppliedLight.resize(size);
         return &cacheVector[contextid];
     }
 
-////////////////////////////////////////////////////////////////////////////////
-// State Attributes
-////////////////////////////////////////////////////////////////////////////////
-
-    SunlightStateAttribute::SunlightStateAttribute()
-        : mBuffer(new SunlightBuffer)
+    void configureStateSetSunOverride(LightingMethod method, const osg::Light* light, osg::StateSet* stateset, int mode)
     {
-        osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
-        mBuffer->getData()->setBufferObject(ubo);
-        mUbb = new osg::UniformBufferBinding(0, mBuffer->getData().get(), 0, mBuffer->getData()->getTotalDataSize());
-    }
+        switch (method)
+        {
+        case LightingMethod::FFP:
+            break;
+        case LightingMethod::PerObjectUniform:
+        {
+            stateset->addUniform(new osg::Uniform("LightBuffer[0].diffuse", light->getDiffuse()), mode);
+            stateset->addUniform(new osg::Uniform("LightBuffer[0].ambient", light->getAmbient()), mode);
+            stateset->addUniform(new osg::Uniform("LightBuffer[0].specular", light->getSpecular()), mode);
+            stateset->addUniform(new osg::Uniform("LightBuffer[0].position", light->getPosition()), mode);
 
-    SunlightStateAttribute::SunlightStateAttribute(const SunlightStateAttribute &copy, const osg::CopyOp &copyop)
-        : osg::StateAttribute(copy, copyop), mBuffer(copy.mBuffer) 
-    {}
+            break;
+        }
+        case LightingMethod::SingleUBO:
+        {
+            osg::ref_ptr<LightBuffer> buffer = new LightBuffer(1);
 
-    int SunlightStateAttribute::compare(const StateAttribute &sa) const
-    {
-        throw std::runtime_error("LightStateAttribute::compare: unimplemented");
-    }
+            buffer->setDiffuse(0, light->getDiffuse());
+            buffer->setAmbient(0, light->getAmbient());
+            buffer->setSpecular(0, light->getSpecular());
+            buffer->setPosition(0, light->getPosition());
 
-    void SunlightStateAttribute::setFromLight(const osg::Light* light)
-    {
-        mBuffer->setValue(SunlightBuffer::Diffuse, light->getDiffuse());
-        mBuffer->setValue(SunlightBuffer::Ambient, light->getAmbient());
-        mBuffer->setValue(SunlightBuffer::Specular, light->getSpecular());
-        mBuffer->setValue(SunlightBuffer::Direction, light->getPosition());
-    }
+            osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
+            buffer->mData->setBufferObject(ubo);
+            osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(static_cast<int>(Shader::UBOBinding::LightBuffer), buffer->mData.get(), 0, buffer->mData->getTotalDataSize());
 
-    void SunlightStateAttribute::setStateSet(osg::StateSet* stateset, int mode)
-    {
-        stateset->setAttributeAndModes(mUbb, mode);
-        stateset->setAssociatedModes(this, mode);
-        mBuffer->dirty();
+            stateset->setAttributeAndModes(ubb, mode);
+
+            break;
+        }
+        }
     }
 
     class DisableLight : public osg::StateAttribute
@@ -325,9 +289,162 @@ namespace SceneUtil
         return nullptr;
     }
 
-////////////////////////////////////////////////////////////////////////////////
-// Node Callbacks
-////////////////////////////////////////////////////////////////////////////////
+    class LightStateAttributePerObjectUniform : public osg::StateAttribute
+    {
+    public:
+        LightStateAttributePerObjectUniform() {}
+        LightStateAttributePerObjectUniform(const std::vector<osg::ref_ptr<osg::Light>>& lights, LightManager* lightManager) :  mLights(lights), mLightManager(lightManager) {}
+
+        LightStateAttributePerObjectUniform(const LightStateAttributePerObjectUniform& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY)
+            : osg::StateAttribute(copy,copyop), mLights(copy.mLights), mLightManager(copy.mLightManager) {}
+
+        int compare(const StateAttribute &sa) const override
+        {
+            throw std::runtime_error("LightStateAttributePerObjectUniform::compare: unimplemented");
+        }
+
+        META_StateAttribute(NifOsg, LightStateAttributePerObjectUniform, osg::StateAttribute::LIGHT)
+
+        void apply(osg::State &state) const override
+        {
+            osg::Matrix modelViewMatrix = state.getModelViewMatrix();
+
+            state.applyModelViewMatrix(state.getInitialViewMatrix());
+
+            LightStateCache* cache = getLightStateCache(state.getContextID(), mLightManager->getMaxLights());
+            for (size_t i = 0; i < mLights.size(); ++i)
+            {
+                osg::Light* current = cache->lastAppliedLight[i];
+                auto light = mLights[i];
+                if (current != light.get())
+                {
+                    mLightManager->getLightUniform(i+1, LightManager::UniformKey::Diffuse)->set(light->getDiffuse());
+                    mLightManager->getLightUniform(i+1, LightManager::UniformKey::Ambient)->set(light->getAmbient());
+                    mLightManager->getLightUniform(i+1, LightManager::UniformKey::Attenuation)->set(osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), getLightRadius(light)));
+                    mLightManager->getLightUniform(i+1, LightManager::UniformKey::Position)->set(light->getPosition() * state.getModelViewMatrix());
+
+                    cache->lastAppliedLight[i] = mLights[i];
+                }
+            }
+            
+            state.applyModelViewMatrix(modelViewMatrix);
+        }
+
+    private:
+        std::vector<osg::ref_ptr<osg::Light>> mLights;
+        LightManager* mLightManager;
+    };
+
+    struct StateSetGenerator
+    {  
+        LightManager* mLightManager;
+
+        virtual ~StateSetGenerator() {}
+
+        virtual osg::ref_ptr<osg::StateSet> generate(const LightManager::LightList& lightList, size_t frameNum) = 0;
+
+        virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {}
+    };
+
+    struct StateSetGeneratorFFP : StateSetGenerator
+    {
+        osg::ref_ptr<osg::StateSet> generate(const LightManager::LightList& lightList, size_t frameNum) override
+        {
+            osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
+
+            std::vector<osg::ref_ptr<osg::Light>> lights;
+            lights.reserve(lightList.size());
+            for (size_t i = 0; i < lightList.size(); ++i)
+                lights.emplace_back(lightList[i]->mLightSource->getLight(frameNum));
+
+            // the first light state attribute handles the actual state setting for all lights
+            // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary
+            // don't use setAttributeAndModes, that does not support light indices!
+            stateset->setAttribute(new FFPLightStateAttribute(mLightManager->getStartLight(), std::move(lights)), osg::StateAttribute::ON);
+
+            for (size_t i = 0; i < lightList.size(); ++i)
+                stateset->setMode(GL_LIGHT0 + mLightManager->getStartLight() + i, osg::StateAttribute::ON);
+
+            // need to push some dummy attributes to ensure proper state tracking
+            // lights need to reset to their default when the StateSet is popped
+            for (size_t i = 1; i < lightList.size(); ++i)
+                stateset->setAttribute(mLightManager->getDummies()[i + mLightManager->getStartLight()].get(), osg::StateAttribute::ON);
+
+            return stateset;
+        }
+    };
+
+    struct StateSetGeneratorSingleUBO : StateSetGenerator
+    {
+        osg::ref_ptr<osg::StateSet> generate(const LightManager::LightList& lightList, size_t frameNum) override
+        {
+            osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
+
+            osg::ref_ptr<osg::IntArray> indices = new osg::IntArray(mLightManager->getMaxLights());
+            osg::ref_ptr<osg::Uniform> indicesUni = new osg::Uniform(osg::Uniform::Type::INT, "PointLightIndex", indices->size());
+            int pointCount = 0;
+
+            for (size_t i = 0; i < lightList.size(); ++i)
+            {
+                int bufIndex = mLightManager->getLightIndexMap(frameNum)[lightList[i]->mLightSource->getId()];
+                indices->at(pointCount++) = bufIndex;
+            }
+            indicesUni->setArray(indices);
+            stateset->addUniform(indicesUni);
+            stateset->addUniform(new osg::Uniform("PointLightCount", pointCount));
+
+            return stateset;
+        }
+
+        // Cached statesets must be re-validated in case the light indicies change. There is no actual link between
+        // a lights ID and the buffer index it will eventually be assigned (or reassigned) to.
+        void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) override
+        {
+            int newCount = 0;
+            int oldCount;
+
+            auto uOldArray = stateset->getUniform("PointLightIndex");
+            auto uOldCount = stateset->getUniform("PointLightCount");
+
+            uOldCount->get(oldCount);
+
+            auto& lightData = mLightManager->getLightIndexMap(frameNum);
+
+            for (int i = 0; i < oldCount; ++i)
+            {
+                auto* lightSource = lightList[i]->mLightSource;
+                auto it = lightData.find(lightSource->getId());
+                if (it != lightData.end())
+                    uOldArray->setElement(newCount++, it->second);
+            }
+
+            uOldArray->dirty();
+            uOldCount->set(newCount);
+        }
+    };
+
+    struct StateSetGeneratorPerObjectUniform : StateSetGenerator
+    {
+        osg::ref_ptr<osg::StateSet> generate(const LightManager::LightList& lightList, size_t frameNum) override
+        {
+            osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
+
+            std::vector<osg::ref_ptr<osg::Light>> lights(lightList.size());
+
+            for (size_t i = 0; i < lightList.size(); ++i)
+            {
+                auto* light = lightList[i]->mLightSource->getLight(frameNum);
+                lights[i] = light;
+                setLightRadius(light, lightList[i]->mLightSource->getRadius());
+            }
+
+            stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform(std::move(lights), mLightManager), osg::StateAttribute::ON);
+
+            stateset->addUniform(new osg::Uniform("PointLightCount", static_cast<int>(lightList.size())));
+
+            return stateset;
+        }
+    };
 
     // Set on a LightSource. Adds the light source to its light manager for the current frame.
     // This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the manager.
@@ -350,9 +467,9 @@ namespace SceneUtil
                 mLightManager = findLightManager(nv->getNodePath());
 
                 if (!mLightManager)
-                    throw std::runtime_error("can't find parent LightManager");
+                    throw std::runtime_error("can't find parent LightManager");                
             }
-
+            
             mLightManager->addLight(static_cast<LightSource*>(node), osg::computeLocalToWorld(nv->getNodePath()), nv->getTraversalNumber());
 
             traverse(node, nv);
@@ -378,16 +495,16 @@ namespace SceneUtil
         void operator()(osg::Node* node, osg::NodeVisitor* nv) override
         {
             LightManager* lightManager = static_cast<LightManager*>(node);
-            lightManager->update();
+            lightManager->update(nv->getTraversalNumber());
 
             traverse(node, nv);
         }
     };
 
-    class SunlightCallback : public osg::NodeCallback
+    class LightManagerCullCallback : public osg::NodeCallback
     {
     public:
-        SunlightCallback(LightManager* lightManager) : mLightManager(lightManager) {}
+        LightManagerCullCallback(LightManager* lightManager) : mLightManager(lightManager) {}
 
         void operator()(osg::Node* node, osg::NodeVisitor* nv) override
         {
@@ -397,20 +514,35 @@ namespace SceneUtil
             {
                 mLastFrameNumber = cv->getTraversalNumber();
 
+                if (mLightManager->getLightingMethod() == LightingMethod::SingleUBO)
+                {   
+                    auto stateset = mLightManager->getStateSet();
+                    auto bo = mLightManager->getLightBuffer(mLastFrameNumber);
+                    osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(static_cast<int>(Shader::UBOBinding::LightBuffer), bo->getData().get(), 0, bo->getData()->getTotalDataSize());
+                    stateset->setAttributeAndModes(ubb.get(), osg::StateAttribute::ON);
+                }
+
                 auto sun = mLightManager->getSunlight();
 
-                if (!sun)
-                    return;
+                if (sun)
+                {
+                    if (mLightManager->getLightingMethod() == LightingMethod::PerObjectUniform)
+                    {
+                        mLightManager->getLightUniform(0, LightManager::UniformKey::Diffuse)->set(sun->getDiffuse());
+                        mLightManager->getLightUniform(0, LightManager::UniformKey::Ambient)->set(sun->getAmbient());
+                        mLightManager->getLightUniform(0, LightManager::UniformKey::Specular)->set(sun->getSpecular());
+                        mLightManager->getLightUniform(0, LightManager::UniformKey::Position)->set(sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix()));
+                    }
+                    else
+                    {
+                        auto buf = mLightManager->getLightBuffer(mLastFrameNumber);
 
-                auto buf = mLightManager->getSunBuffer();
-
-                buf->setValue(SunlightBuffer::Diffuse, sun->getDiffuse());
-                buf->setValue(SunlightBuffer::Ambient, sun->getAmbient());
-                buf->setValue(SunlightBuffer::Specular, sun->getSpecular());
-                buf->setValue(SunlightBuffer::Direction, sun->getPosition() * *cv->getCurrentRenderStage()->getInitialViewMatrix());                           
-
-                if (buf->isDirty())
-                    buf->dirty();
+                        buf->setDiffuse(0, sun->getDiffuse());
+                        buf->setAmbient(0, sun->getAmbient());
+                        buf->setSpecular(0, sun->getSpecular());
+                        buf->setPosition(0, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix()));                           
+                    }
+                }
             }
 
             traverse(node, nv);
@@ -420,7 +552,7 @@ namespace SceneUtil
         LightManager* mLightManager;
         size_t mLastFrameNumber;
     };
-
+    
     class LightManagerStateAttribute : public osg::StateAttribute
     {
     public:
@@ -439,94 +571,144 @@ namespace SceneUtil
 
         void apply(osg::State& state) const override
         {
-            osg::Matrix modelViewMatrix = state.getModelViewMatrix();
-            
-            state.applyModelViewMatrix(state.getInitialViewMatrix());
-                                    
-            for (size_t i = 0; i < mLightManager->mPointLightProxyData.size(); i++)
-            {
-                auto& data = mLightManager->mPointLightProxyData[i];
-                auto pos = data.mPosition * state.getInitialViewMatrix();
-                pos[3] = data.mBrightness;
-                mLightManager->mPointBuffer->setValue(i, PointLightBuffer::Position, pos);
-            }
-            state.applyModelViewMatrix(modelViewMatrix);
-            mLightManager->mPointBuffer->dirty();
+            mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty();
         }
 
         LightManager* mLightManager;
     };
 
+    bool LightManager::isValidLightingModelString(const std::string& value)
+    {
+        static const std::unordered_set<std::string> validLightingModels = {"legacy", "default", "experimental"};
+        return validLightingModels.count(value) != 0;
+    }
+
     LightManager::LightManager(bool ffp)
         : mStartLight(0)
         , mLightingMask(~0u)
         , mSun(nullptr)
-        , mSunBuffer(nullptr)
-        , mFFP(ffp)
+        , mPointLightRadiusMultiplier(std::max(0.0f, Settings::Manager::getFloat("light bounds multiplier", "Shaders")))
     {
-        if (usingFFP())
+        auto lightingModelString = Settings::Manager::getString("lighting method", "Shaders");
+        bool validLightingModel = isValidLightingModelString(lightingModelString);
+        if (!validLightingModel)
+            Log(Debug::Error)   << "Invalid option for 'lighting model':  got '" << lightingModelString 
+                                << "',  expected legacy, default, or experimental.";
+
+        if (ffp || !validLightingModel)
         {
+            setMaxLights(LightManager::mFFPMaxLights);
+            setLightingMethod(LightingMethod::FFP);
+
             for (int i=0; i<getMaxLights(); ++i)
                 mDummies.push_back(new FFPLightStateAttribute(i, std::vector<osg::ref_ptr<osg::Light> >()));
+
             setUpdateCallback(new LightManagerUpdateCallback);
             return;
         }
 
+        osg::GLExtensions* exts = osg::GLExtensions::Get(0, false);
+        bool supportsUBO = exts && exts->isUniformBufferObjectSupported;
+        bool supportsGPU4 = exts && exts->isGpuShader4Supported;    
+
+        if (!supportsUBO)
+            Log(Debug::Info) << "GL_ARB_uniform_buffer_object not supported: using fallback uniforms"; 
+        else if (!supportsGPU4)
+            Log(Debug::Info) << "GL_EXT_gpu_shader4 not supported: using fallback uniforms";
+
+        int targetLights = Settings::Manager::getInt("max lights", "Shaders");
         auto* stateset = getOrCreateStateSet();
 
-        { // sunlight UBO data
-            mSunBuffer = new SunlightBuffer();
-            osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
-            mSunBuffer->getData()->setBufferObject(ubo);
-            osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(0, mSunBuffer->getData().get(), 0, mSunBuffer->getData()->getTotalDataSize());
-            stateset->setAttributeAndModes(ubb.get(), osg::StateAttribute::ON);
+        if (!supportsUBO || !supportsGPU4 || lightingModelString == "default")
+        {
+            setLightingMethod(LightingMethod::PerObjectUniform);
+            setMaxLights(std::max(2, targetLights));
+
+            mLightUniforms.resize(getMaxLights()+1);
+            for (size_t i = 0; i < mLightUniforms.size(); ++i)
+            {
+                osg::ref_ptr<osg::Uniform> udiffuse = new osg::Uniform(osg::Uniform::FLOAT_VEC4, ("LightBuffer[" + std::to_string(i) + "].diffuse").c_str());
+                osg::ref_ptr<osg::Uniform> uspecular = new osg::Uniform(osg::Uniform::FLOAT_VEC4, ("LightBuffer[" + std::to_string(i) + "].specular").c_str());
+                osg::ref_ptr<osg::Uniform> uambient = new osg::Uniform(osg::Uniform::FLOAT_VEC4, ("LightBuffer[" + std::to_string(i) + "].ambient").c_str());
+                osg::ref_ptr<osg::Uniform> uposition = new osg::Uniform(osg::Uniform::FLOAT_VEC4, ("LightBuffer[" + std::to_string(i) + "].position").c_str());
+                osg::ref_ptr<osg::Uniform> uattenuation = new osg::Uniform(osg::Uniform::FLOAT_VEC4, ("LightBuffer[" + std::to_string(i) + "].attenuation").c_str());
+                
+                mLightUniforms[i].emplace(UniformKey::Diffuse, udiffuse);
+                mLightUniforms[i].emplace(UniformKey::Ambient, uambient);
+                mLightUniforms[i].emplace(UniformKey::Specular, uspecular);
+                mLightUniforms[i].emplace(UniformKey::Position, uposition);
+                mLightUniforms[i].emplace(UniformKey::Attenuation, uattenuation);
+
+                stateset->addUniform(udiffuse);
+                stateset->addUniform(uambient);
+                stateset->addUniform(uposition);
+                stateset->addUniform(uattenuation);
+
+                // specular isn't used besides sun, complete waste to upload it
+                if (i == 0)
+                    stateset->addUniform(uspecular);
+            }
         }
+        else
+        {
+            setLightingMethod(LightingMethod::SingleUBO);
+            setMaxLights(std::clamp(targetLights, 2, getMaxLightsInScene() / 2));
 
-        { // point lights UBO data
-            mPointBuffer = new PointLightBuffer(getMaxLightsInScene());
-            osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
-            mPointBuffer->getData()->setBufferObject(ubo);
-            osg::ref_ptr<osg::UniformBufferBinding> ubb = new osg::UniformBufferBinding(1, mPointBuffer->getData().get(), 0, mPointBuffer->getData()->getTotalDataSize());
-            stateset->setAttributeAndModes(ubb.get(), osg::StateAttribute::ON);
+            for (int i = 0; i < 2; ++i)
+            {
+                mLightBuffers[i] = new LightBuffer(getMaxLightsInScene());
+                osg::ref_ptr<osg::UniformBufferObject> ubo = new osg::UniformBufferObject;
+                ubo->setUsage(GL_STREAM_DRAW);
+
+                mLightBuffers[i]->getData()->setBufferObject(ubo);
+            }
+
+            stateset->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON);
         }
-
-        mPointLightProxyData.resize(getMaxLightsInScene());
-
-        stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE);
-        stateset->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON);
+        
         stateset->addUniform(new osg::Uniform("PointLightCount", 0));
 
         setUpdateCallback(new LightManagerUpdateCallback);
-        addCullCallback(new SunlightCallback(this));
+        addCullCallback(new LightManagerCullCallback(this));
     }
 
-////////////////////////////////////////////////////////////////////////////////
-
     LightManager::LightManager(const LightManager &copy, const osg::CopyOp &copyop)
         : osg::Group(copy, copyop)
         , mStartLight(copy.mStartLight)
         , mLightingMask(copy.mLightingMask)
         , mSun(copy.mSun)
-        , mSunBuffer(copy.mSunBuffer)
-        , mFFP(copy.mFFP)
+        , mLightingMethod(copy.mLightingMethod)
+    {
+    }
+
+    LightingMethod LightManager::getLightingMethod() const
+    {
+        return mLightingMethod;
+    }
+
+    LightManager::~LightManager()
     {
     }
 
     bool LightManager::usingFFP() const
     {
-        return mFFP;
+        return mLightingMethod == LightingMethod::FFP;
     }
 
     int LightManager::getMaxLights() const
     {
-        if (usingFFP()) return LightManager::mFFPMaxLights;
-        return std::clamp(Settings::Manager::getInt("max lights", "Shaders"),LightManager::mFFPMaxLights , getMaxLightsInScene());
+        return mMaxLights;
     }
 
+    void LightManager::setMaxLights(int value)
+    {
+        mMaxLights = value;
+    }
+    
     int LightManager::getMaxLightsInScene() const
     {
-        static constexpr int max = 16384 / PointLightBuffer::queryBlockSize(1);        
-        return  max;          
+        static constexpr int max = 16384 / LightBuffer::queryBlockSize(1);        
+        return  max;
     }
 
     Shader::ShaderManager::DefineMap LightManager::getLightDefines() const
@@ -536,28 +718,32 @@ namespace SceneUtil
         bool ffp = usingFFP();
         
         defines["ffpLighting"] = ffp ? "1" : "0";
-        defines["sunDirection"] = ffp ? "gl_LightSource[0].position" : "Sun.direction";
-        defines["sunAmbient"] = ffp ? "gl_LightSource[0].ambient" : "Sun.ambient";
-        defines["sunDiffuse"] = ffp ? "gl_LightSource[0].diffuse" : "Sun.diffuse";
-        defines["sunSpecular"] = ffp ? "gl_LightSource[0].specular" : "Sun.specular";
         defines["maxLights"] = std::to_string(getMaxLights());
         defines["maxLightsInScene"] = std::to_string(getMaxLightsInScene());
+        defines["lightingModel"] = std::to_string(static_cast<int>(mLightingMethod));
+        defines["useUBO"] = std::to_string(mLightingMethod == LightingMethod::SingleUBO);
+        // exposes bitwise operators
+        defines["useGPUShader4"] = std::to_string(mLightingMethod == LightingMethod::SingleUBO);
 
         return defines;
     }
 
-    bool LightManager::queryNonFFPLightingSupport()
+    void LightManager::setLightingMethod(LightingMethod method)
     {
-        osg::GLExtensions* exts = osg::GLExtensions::Get(0, false);
-        if (!exts || !exts->isUniformBufferObjectSupported)
+        mLightingMethod = method;
+        switch (method)
         {
-            auto ffpWarning = Misc::StringUtils::format("GL_ARB_uniform_buffer_object not supported:  Falling back to FFP %zu light limit. Can not set lights to %i."
-                                                    , LightManager::mFFPMaxLights
-                                                    , Settings::Manager::getInt("max lights", "Shaders"));
-            Log(Debug::Warning) << ffpWarning;
-            return false;
+        case LightingMethod::FFP:
+            mStateSetGenerator = std::make_unique<StateSetGeneratorFFP>();
+            break;
+        case LightingMethod::SingleUBO:
+            mStateSetGenerator = std::make_unique<StateSetGeneratorSingleUBO>();
+            break;
+        case LightingMethod::PerObjectUniform:
+            mStateSetGenerator = std::make_unique<StateSetGeneratorPerObjectUniform>();
+            break;
         }
-        return true;
+        mStateSetGenerator->mLightManager = this;
     }
 
     void LightManager::setLightingMask(size_t mask)
@@ -591,19 +777,18 @@ namespace SceneUtil
         return mStartLight;
     }
 
-    void LightManager::update()
+    void LightManager::update(size_t frameNum)
     {   
+        getLightIndexMap(frameNum).clear();
         mLights.clear();
         mLightsInViewSpace.clear();
 
         // Do an occasional cleanup for orphaned lights.
         for (int i=0; i<2; ++i)
         {
-            if (mStateSetCache[i].size() > 5000 || mIndexNeedsRecompiling)
+            if (mStateSetCache[i].size() > 5000) 
                 mStateSetCache[i].clear();
         }
-
-        mIndexNeedsRecompiling = false;
     }
 
     void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum)
@@ -611,11 +796,10 @@ namespace SceneUtil
         LightSourceTransform l;
         l.mLightSource = lightSource;
         l.mWorldMatrix = worldMat;
-        lightSource->getLight(frameNum)->setPosition(osg::Vec4f(worldMat.getTrans().x(),
-                                                        worldMat.getTrans().y(),
-                                                        worldMat.getTrans().z(), 1.f));
-        if (mLights.size() < static_cast<size_t>(getMaxLightsInScene()))
-            mLights.emplace_back(l);
+        osg::Vec3f pos = osg::Vec3f(worldMat.getTrans().x(), worldMat.getTrans().y(), worldMat.getTrans().z());
+        lightSource->getLight(frameNum)->setPosition(osg::Vec4f(pos, 1.f));
+
+        mLights.push_back(l);
     }
 
     void LightManager::setSunlight(osg::ref_ptr<osg::Light> sun)
@@ -630,128 +814,96 @@ namespace SceneUtil
         return mSun;
     }
 
-    osg::ref_ptr<SunlightBuffer> LightManager::getSunBuffer()
-    {
-        return mSunBuffer;
-    }
-
-    osg::ref_ptr<osg::StateSet> LightManager::getLightListStateSet(const LightList &lightList, size_t frameNum)
+    osg::ref_ptr<osg::StateSet> LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix)
     {
         // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists)
         size_t hash = 0;
         for (size_t i=0; i<lightList.size();++i)
-            hash_combine(hash, lightList[i]->mLightSource->getId());
+        {
+            auto id = lightList[i]->mLightSource->getId();
+            hash_combine(hash, id);
 
-        LightStateSetMap& stateSetCache = mStateSetCache[frameNum%2];
-        
-        LightStateSetMap::iterator found = stateSetCache.find(hash);
+            if (getLightingMethod() != LightingMethod::SingleUBO) 
+                continue;
+            
+            if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end())
+                continue;
+
+            int index = getLightIndexMap(frameNum).size() + 1;
+            updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix);
+            getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index);
+        }
+
+        auto& stateSetCache = mStateSetCache[frameNum%2];
+
+        auto found = stateSetCache.find(hash);
         if (found != stateSetCache.end())
+        {
+            mStateSetGenerator->update(found->second, lightList, frameNum);
             return found->second;
+        }
         else
         {
-            osg::ref_ptr<osg::StateSet> stateset = new osg::StateSet;
-
-            if (usingFFP())
-            {
-                std::vector<osg::ref_ptr<osg::Light> > lights;
-                lights.reserve(lightList.size());
-                for (size_t i=0; i<lightList.size();++i)
-                    lights.emplace_back(lightList[i]->mLightSource->getLight(frameNum));
-
-                // the first light state attribute handles the actual state setting for all lights
-                // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary
-                // don't use setAttributeAndModes, that does not support light indices!
-                stateset->setAttribute(new FFPLightStateAttribute(mStartLight, std::move(lights)), osg::StateAttribute::ON);
-
-                for (size_t i=0; i<lightList.size(); ++i)
-                    stateset->setMode(GL_LIGHT0 + mStartLight + i, osg::StateAttribute::ON);
-
-                // need to push some dummy attributes to ensure proper state tracking
-                // lights need to reset to their default when the StateSet is popped
-                for (size_t i=1; i<lightList.size(); ++i)
-                    stateset->setAttribute(mDummies[i+mStartLight].get(), osg::StateAttribute::ON);
-            }
-            else
-            {
-                osg::ref_ptr<osg::IntArray> indices = new osg::IntArray(getMaxLights());
-                osg::ref_ptr<osg::Uniform> indicesUni = new osg::Uniform(osg::Uniform::Type::INT, "PointLightIndex", indices->size());
-                int validCount = 0;
-                for (size_t i = 0; i < lightList.size(); ++i)
-                {
-                    auto it = mLightData.find(lightList[i]->mLightSource->getId());
-                    if (it != mLightData.end())
-                        indices->at(validCount++) = it->second;
-                }
-                indicesUni->setArray(indices);
-                stateset->addUniform(indicesUni);
-                stateset->addUniform(new osg::Uniform("PointLightCount", validCount));
-            }
-
+            auto stateset = mStateSetGenerator->generate(lightList, frameNum);
             stateSetCache.emplace(hash, stateset);
             return stateset;
         }
+        return new osg::StateSet;
     }
 
     const std::vector<LightManager::LightSourceViewBound>& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix, size_t frameNum)
     {
         osg::observer_ptr<osg::Camera> camPtr (camera);
-        std::map<osg::observer_ptr<osg::Camera>, LightSourceViewBoundCollection>::iterator it = mLightsInViewSpace.find(camPtr);
+        auto it = mLightsInViewSpace.find(camPtr);
 
         if (it == mLightsInViewSpace.end())
         {
             it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first;
-
-            for (std::vector<LightSourceTransform>::iterator lightIt = mLights.begin(); lightIt != mLights.end(); ++lightIt)
+            
+            for (const auto& transform : mLights)
             {
-                osg::Matrixf worldViewMat = lightIt->mWorldMatrix * (*viewMatrix);
-                osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), lightIt->mLightSource->getRadius());
+                osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix);
+                
+                float radius = transform.mLightSource->getRadius();
+
+                osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius * mPointLightRadiusMultiplier);
                 transformBoundingSphere(worldViewMat, viewBound);
 
                 LightSourceViewBound l;
-                l.mLightSource = lightIt->mLightSource;
+                l.mLightSource = transform.mLightSource;
                 l.mViewBound = viewBound;
-                it->second.push_back(l);
-
-                if (usingFFP()) continue;
-
-                auto* light = l.mLightSource->getLight(frameNum);
-
-                auto dataIt = mLightData.find(l.mLightSource->getId());
-                if (dataIt != mLightData.end())
-                {
-                    mPointLightProxyData[dataIt->second].mPosition = light->getPosition();
-                    mPointLightProxyData[dataIt->second].mBrightness = l.mLightSource->getBrightness(frameNum);
-                    mPointBuffer->setValue(dataIt->second, PointLightBuffer::Diffuse, light->getDiffuse());
-                    continue;
-                }
-
-                if (mLightData.size() >= static_cast<size_t>(getMaxLightsInScene()))
-                {
-                    mIndexNeedsRecompiling = true;
-                    mLightData.clear();
-                }
-
-                int index = mLightData.size();
-                updateGPUPointLight(index, l.mLightSource, frameNum);
-                mLightData.emplace(l.mLightSource->getId(), index);
+                it->second.push_back(l);  
             }
         }
+
+        if (getLightingMethod() == LightingMethod::SingleUBO)
+        {
+            if (it->second.size() > static_cast<size_t>(getMaxLightsInScene() - 1))
+            {
+                auto sorter = [] (const LightSourceViewBound& left, const LightSourceViewBound& right) {
+                    return left.mViewBound.center().length2() - left.mViewBound.radius2() < right.mViewBound.center().length2() - right.mViewBound.radius2();
+                };
+                std::sort(it->second.begin() + 1, it->second.end(), sorter);
+                it->second.erase((it->second.begin() + 1) + (getMaxLightsInScene() - 2), it->second.end());
+            }
+        }
+
         return it->second;
     }
 
-    void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum)
+    void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix)
     {
         auto* light = lightSource->getLight(frameNum);
-        mPointLightProxyData[index].mPosition = light->getPosition();
-        mPointLightProxyData[index].mBrightness = lightSource->getBrightness(frameNum);
-        mPointBuffer->setValue(index, PointLightBuffer::Diffuse, light->getDiffuse());
-        mPointBuffer->setValue(index, PointLightBuffer::Ambient, light->getAmbient());
-        mPointBuffer->setValue(index, PointLightBuffer::Attenuation, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius()));
+        auto& buf = getLightBuffer(frameNum);
+        buf->setDiffuse(index, light->getDiffuse());
+        buf->setAmbient(index, light->getSpecular());
+        buf->setAttenuation(index, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation());
+        buf->setRadius(index, lightSource->getRadius());
+        buf->setPosition(index, light->getPosition() * (*viewMatrix));
     }
 
     LightSource::LightSource()
         : mRadius(0.f)
-        , mBrightness{1.0,1.0}
     {
         setUpdateCallback(new CollectLightCallback);
         mId = sLightId++;
@@ -798,6 +950,7 @@ namespace SceneUtil
         // makes sure we don't update it more than once per frame when rendering with multiple cameras
         if (mLastFrameNumber != cv->getTraversalNumber())
         {
+
             mLastFrameNumber = cv->getTraversalNumber();
 
             // Don't use Camera::getViewMatrix, that one might be relative to another camera!
@@ -830,6 +983,7 @@ namespace SceneUtil
                     mLightList.push_back(&l);
             }
         }
+
         if (!mLightList.empty())
         {
             size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight();
@@ -840,12 +994,12 @@ namespace SceneUtil
             {
                 // remove lights culled by this camera
                 LightManager::LightList lightList = mLightList;
-                for (LightManager::LightList::iterator it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights; )
+                for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights; )
                 {
                     osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack();
-
+                    
                     osg::BoundingSphere bs = (*it)->mViewBound;
-                    bs._radius = bs._radius*2;
+                    bs._radius = bs._radius * 2.0;
                     osg::CullingSet& cullingSet = stack.front();
                     if (cullingSet.isCulled(bs))
                     {
@@ -863,10 +1017,10 @@ namespace SceneUtil
                     while (lightList.size() > maxLights)
                         lightList.pop_back();
                 }
-                stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber());
+                stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix());
             }
             else
-                stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber());
+                stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix());
 
             cv->pushStateSet(stateset);
             return true;
diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp
index 9c478b294..b430153b2 100644
--- a/components/sceneutil/lightmanager.hpp
+++ b/components/sceneutil/lightmanager.hpp
@@ -5,9 +5,9 @@
 #include <cassert>
 #include <unordered_map>
 #include <unordered_set>
+#include <memory>
 
 #include <osg/Light>
-
 #include <osg/Group>
 #include <osg/NodeVisitor>
 #include <osg/observer_ptr>
@@ -18,38 +18,20 @@ namespace osgUtil
 {
     class CullVisitor;
 }
-
-namespace osg
-{
-    class UniformBufferBinding;
-    class UniformBufferObject;
-}
-
 namespace SceneUtil
 {
-    class SunlightBuffer;
-    class PointLightBuffer;
+    class LightBuffer;
+    class StateSetGenerator;
 
-    // Used to override sun. Rarely useful but necassary for local map. 
-    class SunlightStateAttribute : public osg::StateAttribute
+    enum class LightingMethod
     {
-    public:
-        SunlightStateAttribute();
-        SunlightStateAttribute(const SunlightStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY);
-        
-        int compare(const StateAttribute &sa) const override;
-
-        META_StateAttribute(NifOsg, SunlightStateAttribute, osg::StateAttribute::LIGHT)
-
-        void setFromLight(const osg::Light* light);
-
-        void setStateSet(osg::StateSet* stateset, int mode=osg::StateAttribute::ON);
-
-    private:
-        osg::ref_ptr<SunlightBuffer> mBuffer;
-        osg::ref_ptr<osg::UniformBufferBinding> mUbb;
+        FFP,
+        SingleUBO,
+        PerObjectUniform
     };
 
+    void configureStateSetSunOverride(LightingMethod method, const osg::Light* light, osg::StateSet* stateset, int mode = osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
+
     /// LightSource managed by a LightManager.
     /// @par Typically used for point lights. Spot lights are not supported yet. Directional lights affect the whole scene
     ///     so do not need to be managed by a LightManager - so for directional lights use a plain osg::LightSource instead.
@@ -68,8 +50,6 @@ namespace SceneUtil
 
         int mId;
 
-        float mBrightness[2];
-
     public:
 
         META_Node(SceneUtil, LightSource)
@@ -89,16 +69,6 @@ namespace SceneUtil
             mRadius = radius;
         }
 
-        float getBrightness(size_t frame)
-        {
-            return mBrightness[frame % 2];
-        }
-
-        void setBrightness(size_t frame, float brightness)
-        {
-            mBrightness[frame % 2] = brightness;
-        }
-
         /// Get the osg::Light safe for modification in the given frame.
         /// @par May be used externally to animate the light's color/attenuation properties,
         /// and is used internally to synchronize the light's position with the position of the LightSource.
@@ -128,6 +98,18 @@ namespace SceneUtil
     class LightManager : public osg::Group
     {
     public:
+
+        static bool isValidLightingModelString(const std::string& value);
+
+        enum class UniformKey
+        {
+            Diffuse,
+            Ambient,
+            Specular,
+            Position,
+            Attenuation
+        };
+
         struct LightSourceTransform
         {
             LightSource* mLightSource;
@@ -142,14 +124,14 @@ namespace SceneUtil
 
         using LightList = std::vector<const LightSourceViewBound*>;
 
-        static bool queryNonFFPLightingSupport();
-
         META_Node(SceneUtil, LightManager)
 
         LightManager(bool ffp = true);
 
         LightManager(const LightManager& copy, const osg::CopyOp& copyop);
 
+        ~LightManager();
+
         /// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired.
         /// By default, it's ~0u i.e. always on.
         /// If you have some views that do not require lighting, then set the Camera's cull mask to not include
@@ -162,41 +144,53 @@ namespace SceneUtil
         int getStartLight() const;
 
         /// Internal use only, called automatically by the LightManager's UpdateCallback
-        void update();
+        void update(size_t frameNum);
 
         /// Internal use only, called automatically by the LightSource's UpdateCallback
         void addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum);
 
         const std::vector<LightSourceViewBound>& getLightsInViewSpace(osg::Camera* camera, const osg::RefMatrix* viewMatrix, size_t frameNum);
 
-        osg::ref_ptr<osg::StateSet> getLightListStateSet(const LightList& lightList, size_t frameNum);
+        osg::ref_ptr<osg::StateSet> getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix);
 
         void setSunlight(osg::ref_ptr<osg::Light> sun);
         osg::ref_ptr<osg::Light> getSunlight();
 
-        osg::ref_ptr<SunlightBuffer> getSunBuffer();
-
         bool usingFFP() const;
 
+        LightingMethod getLightingMethod() const;
+
         int getMaxLights() const;
+
         int getMaxLightsInScene() const;
 
-        Shader::ShaderManager::DefineMap getLightDefines() const;
+        auto& getDummies() { return mDummies; }
+
+        auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; }
+        
+        auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; }
+
+        auto& getLightUniform(int index, UniformKey key) { return mLightUniforms[index][key]; }
+
+        std::map<std::string, std::string> getLightDefines() const;
 
     private:
 
         friend class LightManagerStateAttribute;
+        friend class LightManagerCullCallback;
 
-        void updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum);
+        void setLightingMethod(LightingMethod method);
+        void setMaxLights(int value);
+
+        void updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum, const osg::RefMatrix* viewMatrix);
 
-        // Lights collected from the scene graph. Only valid during the cull traversal.
         std::vector<LightSourceTransform> mLights;
 
-        typedef std::vector<LightSourceViewBound> LightSourceViewBoundCollection;
+        using LightSourceViewBoundCollection = std::vector<LightSourceViewBound>;
         std::map<osg::observer_ptr<osg::Camera>, LightSourceViewBoundCollection> mLightsInViewSpace;
-
+        
         // < Light list hash , StateSet >
-        typedef std::map<size_t, osg::ref_ptr<osg::StateSet> > LightStateSetMap;
+        using LightStateSetMap = std::map<size_t, osg::ref_ptr<osg::StateSet>>;
         LightStateSetMap mStateSetCache[2];
 
         std::vector<osg::ref_ptr<osg::StateAttribute>> mDummies;
@@ -206,26 +200,25 @@ namespace SceneUtil
         size_t mLightingMask;
 
         osg::ref_ptr<osg::Light> mSun;
-        osg::ref_ptr<SunlightBuffer> mSunBuffer;
 
-        struct PointLightProxyData
-        {
-            osg::Vec4 mPosition;
-            float mBrightness;
-        };
-
-        std::vector<PointLightProxyData> mPointLightProxyData;
-        osg::ref_ptr<PointLightBuffer> mPointBuffer;
+        osg::ref_ptr<LightBuffer> mLightBuffers[2];
 
         // < Light ID , Buffer Index >
-        using LightDataMap = std::unordered_map<int, int>;
-        LightDataMap mLightData;
+        using LightIndexMap = std::unordered_map<int, int>;
+        LightIndexMap mLightIndexMaps[2];
 
-        bool mIndexNeedsRecompiling;
-        
-        bool mFFP;
+        using UniformMap = std::vector<std::unordered_map<UniformKey, osg::ref_ptr<osg::Uniform>>>;
+        UniformMap mLightUniforms;
 
-        static constexpr int mFFPMaxLights = 8;
+        std::unique_ptr<StateSetGenerator> mStateSetGenerator;
+
+        LightingMethod mLightingMethod;
+
+        float mPointLightRadiusMultiplier;
+
+        int mMaxLights;
+
+        static constexpr auto mFFPMaxLights = 8;
     };
 
     /// To receive lighting, objects must be decorated by a LightListCallback. Light list callbacks must be added via
@@ -264,8 +257,7 @@ namespace SceneUtil
         size_t mLastFrameNumber;
         LightManager::LightList mLightList;
         std::set<SceneUtil::LightSource*> mIgnoredLightSources;
-    };
-
+    }; 
 }
 
 #endif
diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp
index 6c17117d0..07ab44640 100644
--- a/components/sceneutil/lightutil.cpp
+++ b/components/sceneutil/lightutil.cpp
@@ -58,7 +58,7 @@ namespace SceneUtil
         light->setQuadraticAttenuation(quadraticAttenuation);
     }
 
-    void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior, bool useFFPLighting)
+    void addLight(osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior)
     {
         SceneUtil::FindByNameVisitor visitor("AttachLight");
         node->accept(visitor);
@@ -85,21 +85,17 @@ namespace SceneUtil
             attachTo = trans;
         }
 
-        osg::ref_ptr<LightSource> lightSource = createLightSource(esmLight, lightMask, isExterior, osg::Vec4f(0,0,0,1), useFFPLighting);
+        osg::ref_ptr<LightSource> lightSource = createLightSource(esmLight, lightMask, isExterior, osg::Vec4f(0,0,0,1));
         attachTo->addChild(lightSource);
     }
 
-    osg::ref_ptr<LightSource> createLightSource(const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient, bool useFFPLighting)
+    osg::ref_ptr<LightSource> createLightSource(const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient)
     {
         osg::ref_ptr<SceneUtil::LightSource> lightSource (new SceneUtil::LightSource);
         osg::ref_ptr<osg::Light> light (new osg::Light);
         lightSource->setNodeMask(lightMask);
 
         float radius = esmLight->mData.mRadius;
-        // arbitrary multipler to reduce light popping, this is hard to avoid with per-object lighting
-        // we offset this multipler in shaders
-        if (!useFFPLighting)
-            radius *= 2.0;
         lightSource->setRadius(radius);
 
         configureLight(light, radius, isExterior);
@@ -116,7 +112,7 @@ namespace SceneUtil
 
         lightSource->setLight(light);
 
-        osg::ref_ptr<SceneUtil::LightController> ctrl (new SceneUtil::LightController(useFFPLighting));
+        osg::ref_ptr<SceneUtil::LightController> ctrl (new SceneUtil::LightController);
         ctrl->setDiffuse(light->getDiffuse());
         if (esmLight->mData.mFlags & ESM::Light::Flicker)
             ctrl->setType(SceneUtil::LightController::LT_Flicker);
diff --git a/components/sceneutil/lightutil.hpp b/components/sceneutil/lightutil.hpp
index bb28276e9..7096c38b2 100644
--- a/components/sceneutil/lightutil.hpp
+++ b/components/sceneutil/lightutil.hpp
@@ -32,14 +32,14 @@ namespace SceneUtil
     /// @param partsysMask Node mask to ignore when computing the sub graph's bounding box.
     /// @param lightMask Mask to assign to the newly created LightSource.
     /// @param isExterior Is the light outside? May be used for deciding which attenuation settings to use.
-    void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior, bool useFFPLighting=true);
+    void addLight (osg::Group* node, const ESM::Light* esmLight, unsigned int partsysMask, unsigned int lightMask, bool isExterior);
 
     /// @brief Convert an ESM::Light to a SceneUtil::LightSource, and return it.
     /// @param esmLight The light definition coming from the game files containing radius, color, flicker, etc.
     /// @param lightMask Mask to assign to the newly created LightSource.
     /// @param isExterior Is the light outside? May be used for deciding which attenuation settings to use.
     /// @param ambient Ambient component of the light.
-    osg::ref_ptr<LightSource> createLightSource (const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient=osg::Vec4f(0,0,0,1), bool useFFPLighting=true);
+    osg::ref_ptr<LightSource> createLightSource (const ESM::Light* esmLight, unsigned int lightMask, bool isExterior, const osg::Vec4f& ambient=osg::Vec4f(0,0,0,1));
 
 }
 
diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp
index ba903f6db..89e001a98 100644
--- a/components/sceneutil/mwshadowtechnique.cpp
+++ b/components/sceneutil/mwshadowtechnique.cpp
@@ -278,7 +278,7 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
     static osg::ref_ptr<osg::StateSet> ss;
     if (!ss)
     {
-        ShadowsBinAdder adder("ShadowsBin");
+        ShadowsBinAdder adder("ShadowsBin", _vdsm->getCastingPrograms());
         ss = new osg::StateSet;
         ss->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, "ShadowsBin", osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS);
     }
@@ -782,7 +782,8 @@ void MWShadowTechnique::ViewDependentData::releaseGLObjects(osg::State* state) c
 MWShadowTechnique::MWShadowTechnique():
     ShadowTechnique(),
     _enableShadows(false),
-    _debugHud(nullptr)
+    _debugHud(nullptr),
+    _castingPrograms{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }
 {
     _shadowRecievingPlaceholderStateSet = new osg::StateSet;
     mSetDummyStateWhenDisabled = false;
@@ -790,6 +791,7 @@ MWShadowTechnique::MWShadowTechnique():
 
 MWShadowTechnique::MWShadowTechnique(const MWShadowTechnique& vdsm, const osg::CopyOp& copyop):
     ShadowTechnique(vdsm,copyop)
+    , _castingPrograms(vdsm._castingPrograms)
 {
     _shadowRecievingPlaceholderStateSet = new osg::StateSet;
     _enableShadows = vdsm._enableShadows;
@@ -870,7 +872,10 @@ void SceneUtil::MWShadowTechnique::enableFrontFaceCulling()
     _useFrontFaceCulling = true;
 
     if (_shadowCastingStateSet)
+    {
         _shadowCastingStateSet->setAttribute(new osg::CullFace(osg::CullFace::FRONT), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
+        _shadowCastingStateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
+    }
 }
 
 void SceneUtil::MWShadowTechnique::disableFrontFaceCulling()
@@ -878,17 +883,29 @@ void SceneUtil::MWShadowTechnique::disableFrontFaceCulling()
     _useFrontFaceCulling = false;
 
     if (_shadowCastingStateSet)
+    {
+        _shadowCastingStateSet->removeAttribute(osg::StateAttribute::CULLFACE);
         _shadowCastingStateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE);
+    }
 }
 
 void SceneUtil::MWShadowTechnique::setupCastingShader(Shader::ShaderManager & shaderManager)
 {
     // This can't be part of the constructor as OSG mandates that there be a trivial constructor available
-    
-    _castingProgram = new osg::Program();
 
-    _castingProgram->addShader(shaderManager.getShader("shadowcasting_vertex.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::VERTEX));
-    _castingProgram->addShader(shaderManager.getShader("shadowcasting_fragment.glsl", Shader::ShaderManager::DefineMap(), osg::Shader::FRAGMENT));
+    osg::ref_ptr<osg::Shader> castingVertexShader = shaderManager.getShader("shadowcasting_vertex.glsl", {}, osg::Shader::VERTEX);
+    osg::ref_ptr<osg::GLExtensions> exts = osg::GLExtensions::Get(0, false);
+    std::string useGPUShader4 = exts && exts->isGpuShader4Supported ? "1" : "0";
+    for (int alphaFunc = GL_NEVER; alphaFunc <= GL_ALWAYS; ++alphaFunc)
+    {
+        auto& program = _castingPrograms[alphaFunc - GL_NEVER];
+        program = new osg::Program();
+        program->addShader(castingVertexShader);
+        program->addShader(shaderManager.getShader("shadowcasting_fragment.glsl", { {"alphaFunc", std::to_string(alphaFunc)},
+                                                                                    {"alphaToCoverage", "0"},
+                                                                                    {"useGPUShader4", useGPUShader4}
+                                                                                  }, osg::Shader::FRAGMENT));
+    }
 }
 
 MWShadowTechnique::ViewDependentData* MWShadowTechnique::createViewDependentData(osgUtil::CullVisitor* /*cv*/)
@@ -1606,10 +1623,11 @@ void MWShadowTechnique::createShaders()
 
     }
 
-    if (!_castingProgram)
+    if (!_castingPrograms[GL_ALWAYS - GL_NEVER])
         OSG_NOTICE << "Shadow casting shader has not been set up. Remember to call setupCastingShader(Shader::ShaderManager &)" << std::endl;
 
-    _shadowCastingStateSet->setAttributeAndModes(_castingProgram, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
+    // Always use the GL_ALWAYS shader as the shadows bin will change it if necessary
+    _shadowCastingStateSet->setAttributeAndModes(_castingPrograms[GL_ALWAYS - GL_NEVER], osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
     // The casting program uses a sampler, so to avoid undefined behaviour, we must bind a dummy texture in case no other is supplied
     _shadowCastingStateSet->setTextureAttributeAndModes(0, _fallbackBaseTexture.get(), osg::StateAttribute::ON);
     _shadowCastingStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true));
diff --git a/components/sceneutil/mwshadowtechnique.hpp b/components/sceneutil/mwshadowtechnique.hpp
index 7b934b798..de57bf1fd 100644
--- a/components/sceneutil/mwshadowtechnique.hpp
+++ b/components/sceneutil/mwshadowtechnique.hpp
@@ -215,6 +215,8 @@ namespace SceneUtil {
 
         virtual void createShaders();
 
+        virtual std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> getCastingPrograms() const { return _castingPrograms; }
+
         virtual bool selectActiveLights(osgUtil::CullVisitor* cv, ViewDependentData* vdd) const;
 
         virtual osg::Polytope computeLightViewFrustumPolytope(Frustum& frustum, LightData& positionedLight);
@@ -288,7 +290,7 @@ namespace SceneUtil {
         };
 
         osg::ref_ptr<DebugHUD>                  _debugHud;
-        osg::ref_ptr<osg::Program>              _castingProgram;
+        std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> _castingPrograms;
     };
 
 }
diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp
index 520ad0362..5a4096f5c 100644
--- a/components/sceneutil/shadowsbin.cpp
+++ b/components/sceneutil/shadowsbin.cpp
@@ -1,7 +1,9 @@
 #include "shadowsbin.hpp"
 #include <unordered_set>
 #include <osg/StateSet>
+#include <osg/AlphaFunc>
 #include <osg/Material>
+#include <osg/Program>
 #include <osgUtil/StateGraph>
 
 using namespace osgUtil;
@@ -25,9 +27,9 @@ namespace
         osg::StateSet::ModeList::const_iterator mf = l.find(mode);
         if (mf == l.end())
             return;
-        int flags = mf->second;
+        unsigned int flags = mf->second;
         bool newValue = flags & osg::StateAttribute::ON;
-        accumulateState(currentValue, newValue, isOverride, ss->getMode(mode));
+        accumulateState(currentValue, newValue, isOverride, flags);
     }
 
     inline bool materialNeedShadows(osg::Material* m)
@@ -40,6 +42,10 @@ namespace
 namespace SceneUtil
 {
 
+std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> ShadowsBin::sCastingPrograms = {
+    nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
+};
+
 ShadowsBin::ShadowsBin()
 {
     mNoTestStateSet = new osg::StateSet;
@@ -48,10 +54,16 @@ ShadowsBin::ShadowsBin()
 
     mShaderAlphaTestStateSet = new osg::StateSet;
     mShaderAlphaTestStateSet->addUniform(new osg::Uniform("alphaTestShadows", true));
-    mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE);
+    mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE);
+
+    for (size_t i = 0; i < sCastingPrograms.size(); ++i)
+    {
+        mAlphaFuncShaders[i] = new osg::StateSet;
+        mAlphaFuncShaders[i]->setAttribute(sCastingPrograms[i], osg::StateAttribute::ON | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE);
+    }
 }
 
-StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::unordered_set<StateGraph*>& uninterestingCache)
+StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::unordered_set<StateGraph*>& uninterestingCache, bool cullFaceOverridden)
 {
     std::vector<StateGraph*> return_path;
     State state;
@@ -71,7 +83,6 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un
             continue;
 
         accumulateModeState(ss, state.mAlphaBlend, state.mAlphaBlendOverride, GL_BLEND);
-        accumulateModeState(ss, state.mAlphaTest, state.mAlphaTestOverride, GL_ALPHA_TEST);
 
         const osg::StateSet::AttributeList& attributes = ss->getAttributeList();
         osg::StateSet::AttributeList::const_iterator found = attributes.find(std::make_pair(osg::StateAttribute::MATERIAL, 0));
@@ -83,10 +94,21 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un
                 state.mMaterial = nullptr;
         }
 
-        // osg::FrontFace specifies triangle winding, not front-face culling. We can't safely reparent anything under it.
-        found = attributes.find(std::make_pair(osg::StateAttribute::FRONTFACE, 0));
+        found = attributes.find(std::make_pair(osg::StateAttribute::ALPHAFUNC, 0));
         if (found != attributes.end())
-            state.mImportantState = true;
+        {
+            // As force shaders is on, we know this is really a RemovedAlphaFunc
+            const osg::StateSet::RefAttributePair& rap = found->second;
+            accumulateState(state.mAlphaFunc, static_cast<osg::AlphaFunc*>(rap.first.get()), state.mAlphaFuncOverride, rap.second);
+        }
+
+        if (!cullFaceOverridden)
+        {
+            // osg::FrontFace specifies triangle winding, not front-face culling. We can't safely reparent anything under it unless GL_CULL_FACE is off or we flip face culling.
+            found = attributes.find(std::make_pair(osg::StateAttribute::FRONTFACE, 0));
+            if (found != attributes.end())
+                state.mImportantState = true;
+        }
 
         if ((*itr) != sg && !state.interesting())
             uninterestingCache.insert(*itr);
@@ -108,21 +130,45 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un
     if (state.mAlphaBlend)
     {
         sg_new = sg->find_or_insert(mShaderAlphaTestStateSet);
-        for (RenderLeaf* leaf : sg->_leaves)
-        {
+        sg_new->_leaves = std::move(sg->_leaves);
+        for (RenderLeaf* leaf : sg_new->_leaves)
             leaf->_parent = sg_new;
-            sg_new->_leaves.push_back(leaf);
-        }
-        return sg_new;
+        sg = sg_new;
     }
+
+    // GL_ALWAYS is set by default by mwshadowtechnique
+    if (state.mAlphaFunc && state.mAlphaFunc->getFunction() != GL_ALWAYS)
+    {
+        sg_new = sg->find_or_insert(mAlphaFuncShaders[state.mAlphaFunc->getFunction() - GL_NEVER]);
+        sg_new->_leaves = std::move(sg->_leaves);
+        for (RenderLeaf* leaf : sg_new->_leaves)
+            leaf->_parent = sg_new;
+        sg = sg_new;
+    }
+
     return sg;
 }
 
+void ShadowsBin::addPrototype(const std::string & name, const std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1>& castingPrograms)
+{
+    sCastingPrograms = castingPrograms;
+    osg::ref_ptr<osgUtil::RenderBin> bin(new ShadowsBin);
+    osgUtil::RenderBin::addRenderBinPrototype(name, bin);
+}
+
+inline bool ShadowsBin::State::needTexture() const
+{
+    return mAlphaBlend || (mAlphaFunc && mAlphaFunc->getFunction() != GL_ALWAYS);
+}
+
 bool ShadowsBin::State::needShadows() const
 {
-    if (!mMaterial)
-        return true;
-    return materialNeedShadows(mMaterial);
+    if (mAlphaFunc && mAlphaFunc->getFunction() == GL_NEVER)
+        return false;
+    // other alpha func + material combinations might be skippable
+    if (mAlphaBlend && mMaterial)
+        return materialNeedShadows(mMaterial);
+    return true;
 }
 
 void ShadowsBin::sortImplementation()
@@ -139,13 +185,27 @@ void ShadowsBin::sortImplementation()
         root = root->_parent;
         const osg::StateSet* ss = root->getStateSet();
         if (ss->getMode(GL_NORMALIZE) & osg::StateAttribute::ON // that is root stategraph of renderingmanager cpp
-           || ss->getAttribute(osg::StateAttribute::VIEWPORT)) // fallback to rendertargets sg just in case
+           || ss->getAttribute(osg::StateAttribute::VIEWPORT)) // fallback to rendertarget's sg just in case
             break;
         if (!root->_parent)
             return;
     }
     StateGraph* noTestRoot = root->find_or_insert(mNoTestStateSet.get());
-    // root is now a stategraph with useDiffuseMapForShadowAlpha disabled but minimal other state
+    // noTestRoot is now a stategraph with useDiffuseMapForShadowAlpha disabled but minimal other state
+
+    bool cullFaceOverridden = false;
+    while ((root = root->_parent))
+    {
+        if (!root->getStateSet())
+            continue;
+        unsigned int cullFaceFlags = root->getStateSet()->getMode(GL_CULL_FACE);
+        if (cullFaceFlags & osg::StateAttribute::OVERRIDE && !(cullFaceFlags & osg::StateAttribute::ON))
+        {
+            cullFaceOverridden = true;
+            break;
+        }
+    }
+
     noTestRoot->_leaves.reserve(_stateGraphList.size());
     StateGraphList newList;
     std::unordered_set<StateGraph*> uninterestingCache;
@@ -154,7 +214,7 @@ void ShadowsBin::sortImplementation()
         // Render leaves which shouldn't use the diffuse map for shadow alpha but do cast shadows become children of root, so graph is now empty. Don't add to newList.
         // Graphs containing just render leaves which don't cast shadows are discarded. Don't add to newList.
         // Graphs containing other leaves need to be in newList.
-        StateGraph* graphToAdd = cullStateGraph(graph, noTestRoot, uninterestingCache);
+        StateGraph* graphToAdd = cullStateGraph(graph, noTestRoot, uninterestingCache, cullFaceOverridden);
         if (graphToAdd)
             newList.push_back(graphToAdd);
     }
diff --git a/components/sceneutil/shadowsbin.hpp b/components/sceneutil/shadowsbin.hpp
index cc6fd3525..1c63caf4b 100644
--- a/components/sceneutil/shadowsbin.hpp
+++ b/components/sceneutil/shadowsbin.hpp
@@ -1,11 +1,13 @@
 #ifndef OPENMW_COMPONENTS_SCENEUTIL_SHADOWBIN_H
 #define OPENMW_COMPONENTS_SCENEUTIL_SHADOWBIN_H
+#include <array>
 #include <unordered_set>
 #include <osgUtil/RenderBin>
 
 namespace osg
 {
     class Material;
+    class AlphaFunc;
 }
 
 namespace SceneUtil
@@ -15,8 +17,12 @@ namespace SceneUtil
     class ShadowsBin : public osgUtil::RenderBin
     {
     private:
+        static std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1> sCastingPrograms;
+
         osg::ref_ptr<osg::StateSet> mNoTestStateSet;
         osg::ref_ptr<osg::StateSet> mShaderAlphaTestStateSet;
+
+        std::array<osg::ref_ptr<osg::StateSet>, GL_ALWAYS - GL_NEVER + 1> mAlphaFuncShaders;
     public:
         META_Object(SceneUtil, ShadowsBin)
         ShadowsBin();
@@ -24,6 +30,7 @@ namespace SceneUtil
             : osgUtil::RenderBin(rhs, copyop)
             , mNoTestStateSet(rhs.mNoTestStateSet)
             , mShaderAlphaTestStateSet(rhs.mShaderAlphaTestStateSet)
+            , mAlphaFuncShaders(rhs.mAlphaFuncShaders)
             {}
 
         void sortImplementation() override;
@@ -33,8 +40,8 @@ namespace SceneUtil
             State()
                 : mAlphaBlend(false)
                 , mAlphaBlendOverride(false)
-                , mAlphaTest(false)
-                , mAlphaTestOverride(false)
+                , mAlphaFunc(nullptr)
+                , mAlphaFuncOverride(false)
                 , mMaterial(nullptr)
                 , mMaterialOverride(false)
                 , mImportantState(false)
@@ -42,33 +49,29 @@ namespace SceneUtil
 
             bool mAlphaBlend;
             bool mAlphaBlendOverride;
-            bool mAlphaTest;
-            bool mAlphaTestOverride;
+            osg::AlphaFunc* mAlphaFunc;
+            bool mAlphaFuncOverride;
             osg::Material* mMaterial;
             bool mMaterialOverride;
             bool mImportantState;
-            bool needTexture() const { return mAlphaBlend || mAlphaTest; }
+            bool needTexture() const;
             bool needShadows() const;
             // A state is interesting if there's anything about it that might affect whether we can optimise child state
             bool interesting() const
             {
-                return !needShadows() || needTexture() || mAlphaBlendOverride || mAlphaTestOverride || mMaterialOverride || mImportantState;
+                return !needShadows() || needTexture() || mAlphaBlendOverride || mAlphaFuncOverride || mMaterialOverride || mImportantState;
             }
         };
 
-        osgUtil::StateGraph* cullStateGraph(osgUtil::StateGraph* sg, osgUtil::StateGraph* root, std::unordered_set<osgUtil::StateGraph*>& uninteresting);
+        osgUtil::StateGraph* cullStateGraph(osgUtil::StateGraph* sg, osgUtil::StateGraph* root, std::unordered_set<osgUtil::StateGraph*>& uninteresting, bool cullFaceOverridden);
 
-        static void addPrototype(const std::string& name)
-        {
-            osg::ref_ptr<osgUtil::RenderBin> bin (new ShadowsBin);
-            osgUtil::RenderBin::addRenderBinPrototype(name, bin);
-        }
+        static void addPrototype(const std::string& name, const std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1>& castingPrograms);
     };
 
     class ShadowsBinAdder
     {
         public:
-        ShadowsBinAdder(const std::string& name){ ShadowsBin::addPrototype(name); }
+        ShadowsBinAdder(const std::string& name, const std::array<osg::ref_ptr<osg::Program>, GL_ALWAYS - GL_NEVER + 1>& castingPrograms){ ShadowsBin::addPrototype(name, castingPrograms); }
     };
 
 }
diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp
index 2c0d8efa0..fa3c7d26d 100644
--- a/components/sceneutil/util.cpp
+++ b/components/sceneutil/util.cpp
@@ -11,6 +11,7 @@
 
 #include <components/resource/imagemanager.hpp>
 #include <components/resource/scenemanager.hpp>
+#include <components/settings/settings.hpp>
 
 namespace SceneUtil
 {
@@ -260,4 +261,21 @@ osg::ref_ptr<GlowUpdater> addEnchantedGlow(osg::ref_ptr<osg::Node> node, Resourc
     return glowUpdater;
 }
 
+bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture * texture, unsigned int level, unsigned int face, bool mipMapGeneration)
+{
+    unsigned int samples = 0;
+    unsigned int colourSamples = 0;
+    bool addMSAAIntermediateTarget = Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1;
+    if (addMSAAIntermediateTarget)
+    {
+        // Alpha-to-coverage requires a multisampled framebuffer.
+        // OSG will set that up automatically and resolve it to the specified single-sample texture for us.
+        // For some reason, two samples are needed, at least with some drivers.
+        samples = 2;
+        colourSamples = 1;
+    }
+    camera->attach(buffer, texture, level, face, mipMapGeneration, samples, colourSamples);
+    return addMSAAIntermediateTarget;
+}
+
 }
diff --git a/components/sceneutil/util.hpp b/components/sceneutil/util.hpp
index 303d609f5..8103ed87a 100644
--- a/components/sceneutil/util.hpp
+++ b/components/sceneutil/util.hpp
@@ -3,6 +3,7 @@
 
 #include <osg/Matrix>
 #include <osg/BoundingSphere>
+#include <osg/Camera>
 #include <osg/NodeCallback>
 #include <osg/Texture2D>
 #include <osg/Vec4f>
@@ -60,6 +61,9 @@ namespace SceneUtil
     bool hasUserDescription(const osg::Node* node, const std::string pattern);
 
     osg::ref_ptr<GlowUpdater> addEnchantedGlow(osg::ref_ptr<osg::Node> node, Resource::ResourceSystem* resourceSystem, osg::Vec4f glowColor, float glowDuration=-1);
+
+    // Alpha-to-coverage requires a multisampled framebuffer, so we need to set that up for RTTs
+    bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture* texture, unsigned int level = 0, unsigned int face = 0, bool mipMapGeneration = false);
 }
 
 #endif
diff --git a/components/sdlutil/gl4es_init.cpp b/components/sdlutil/gl4es_init.cpp
new file mode 100644
index 000000000..bf9e007b6
--- /dev/null
+++ b/components/sdlutil/gl4es_init.cpp
@@ -0,0 +1,36 @@
+// EGL does not work reliably for feature detection.
+// Instead, we initialize gl4es manually.
+#ifdef OPENMW_GL4ES_MANUAL_INIT
+#include "gl4es_init.h"
+
+// For glHint
+#include <GL/gl.h>
+
+extern "C" {
+
+#include <gl4es/gl4esinit.h>
+#include <gl4es/gl4eshint.h>
+
+static SDL_Window *gWindow;
+
+void openmw_gl4es_GetMainFBSize(int *width, int *height)
+{
+    SDL_GetWindowSize(gWindow, width, height);
+}
+
+void openmw_gl4es_init(SDL_Window *window)
+{
+    gWindow = window;
+    set_getprocaddress(SDL_GL_GetProcAddress);
+    set_getmainfbsize(openmw_gl4es_GetMainFBSize);
+    initialize_gl4es();
+
+    // merge glBegin/glEnd in beams and console
+    glHint(GL_BEGINEND_HINT_GL4ES, 1);
+    // dxt unpacked to 16-bit looks ugly
+    glHint(GL_AVOID16BITS_HINT_GL4ES, 1);
+}
+
+}  // extern "C"
+
+#endif  // OPENMW_GL4ES_MANUAL_INIT
diff --git a/components/sdlutil/gl4es_init.h b/components/sdlutil/gl4es_init.h
new file mode 100644
index 000000000..11371e7ae
--- /dev/null
+++ b/components/sdlutil/gl4es_init.h
@@ -0,0 +1,13 @@
+#ifndef OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H
+#define OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H
+#ifdef OPENMW_GL4ES_MANUAL_INIT
+#include <SDL_video.h>
+
+// Must be called once SDL video mode has been set,
+// which creates a context.
+//
+// GL4ES can then query the context for features and extensions.
+extern "C" void openmw_gl4es_init(SDL_Window *window);
+
+#endif  // OPENMW_GL4ES_MANUAL_INIT
+#endif  // OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H
diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp
index ad7ecd9ae..43284c216 100644
--- a/components/sdlutil/sdlgraphicswindow.cpp
+++ b/components/sdlutil/sdlgraphicswindow.cpp
@@ -2,6 +2,10 @@
 
 #include <SDL_video.h>
 
+#ifdef OPENMW_GL4ES_MANUAL_INIT
+#include "gl4es_init.h"
+#endif
+
 namespace SDLUtil
 {
 
@@ -91,7 +95,7 @@ void GraphicsWindowSDL2::init()
     SDL_Window *oldWin = SDL_GL_GetCurrentWindow();
     SDL_GLContext oldCtx = SDL_GL_GetCurrentContext();
    
-#if defined(ANDROID)
+#if defined(ANDROID) || defined(OPENMW_GL4ES_MANUAL_INIT)
     int major = 1;
     int minor = 1;
     char *ver = getenv("OPENMW_GLES_VERSION");
@@ -116,6 +120,10 @@ void GraphicsWindowSDL2::init()
         return;
     }
 
+#ifdef OPENMW_GL4ES_MANUAL_INIT
+    openmw_gl4es_init(mWindow);
+#endif
+
     setSwapInterval(_traits->vsync);
 
     // Update traits with what we've actually been given
diff --git a/components/shader/removedalphafunc.cpp b/components/shader/removedalphafunc.cpp
new file mode 100644
index 000000000..6b701b890
--- /dev/null
+++ b/components/shader/removedalphafunc.cpp
@@ -0,0 +1,20 @@
+#include "removedalphafunc.hpp"
+
+#include <cassert>
+
+#include <osg/State>
+
+namespace Shader
+{
+    std::array<osg::ref_ptr<RemovedAlphaFunc>, GL_ALWAYS - GL_NEVER + 1> RemovedAlphaFunc::sInstances{
+        nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
+    };
+
+    osg::ref_ptr<RemovedAlphaFunc> RemovedAlphaFunc::getInstance(GLenum func)
+    {
+        assert(func >= GL_NEVER && func <= GL_ALWAYS);
+        if (!sInstances[func - GL_NEVER])
+            sInstances[func - GL_NEVER] = new RemovedAlphaFunc(static_cast<osg::AlphaFunc::ComparisonFunction>(func), 1.0);
+        return sInstances[func - GL_NEVER];
+    }
+}
diff --git a/components/shader/removedalphafunc.hpp b/components/shader/removedalphafunc.hpp
new file mode 100644
index 000000000..c744391e5
--- /dev/null
+++ b/components/shader/removedalphafunc.hpp
@@ -0,0 +1,40 @@
+#ifndef OPENMW_COMPONENTS_REMOVEDALPHAFUNC_H
+#define OPENMW_COMPONENTS_REMOVEDALPHAFUNC_H
+
+#include <array>
+
+#include <osg/AlphaFunc>
+
+namespace Shader
+{
+    // State attribute used when shader visitor replaces the deprecated alpha function with a shader
+    // Prevents redundant glAlphaFunc calls and lets the shadowsbin know the stateset had alpha testing
+    class RemovedAlphaFunc : public osg::AlphaFunc
+    {
+    public:
+        // Get a singleton-like instance with the right func (but a default threshold)
+        static osg::ref_ptr<RemovedAlphaFunc> getInstance(GLenum func);
+
+        RemovedAlphaFunc()
+            : osg::AlphaFunc()
+        {}
+
+        RemovedAlphaFunc(ComparisonFunction func, float ref)
+            : osg::AlphaFunc(func, ref)
+        {}
+
+        RemovedAlphaFunc(const RemovedAlphaFunc& raf, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY)
+            : osg::AlphaFunc(raf, copyop)
+        {}
+
+        META_StateAttribute(Shader, RemovedAlphaFunc, ALPHAFUNC);
+
+        void apply(osg::State& state) const override {}
+
+    protected:
+        virtual ~RemovedAlphaFunc() = default;
+
+        static std::array<osg::ref_ptr<RemovedAlphaFunc>, GL_ALWAYS - GL_NEVER + 1> sInstances;
+    };
+}
+#endif //OPENMW_COMPONENTS_REMOVEDALPHAFUNC_H
diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp
index b1dc145f7..12980dffc 100644
--- a/components/shader/shadermanager.cpp
+++ b/components/shader/shadermanager.cpp
@@ -18,7 +18,7 @@ namespace Shader
 {
 
     ShaderManager::ShaderManager()
-        : mFFPLighting(false)
+        : mLightingMethod(SceneUtil::LightingMethod::FFP)
     {
     }
 
@@ -27,9 +27,9 @@ namespace Shader
         mPath = path;
     }
 
-    void ShaderManager::setFFPLighting(bool useFFP)
+    void ShaderManager::setLightingMethod(SceneUtil::LightingMethod method)
     {
-        mFFPLighting = useFFP;
+        mLightingMethod = method;
     }
 
     bool addLineDirectivesAfterConditionalBlocks(std::string& source)
@@ -356,11 +356,8 @@ namespace Shader
             program->addShader(fragmentShader);
             program->addBindAttribLocation("aOffset", 6);
             program->addBindAttribLocation("aRotation", 7);
-            if (!mFFPLighting)
-            {
-                program->addBindUniformBlock("SunlightBuffer", 0);
-                program->addBindUniformBlock("PointLightBuffer", 1);
-            }
+            if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO)
+                program->addBindUniformBlock("LightBufferBinding", static_cast<int>(UBOBinding::LightBuffer));
             found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first;
         }
         return found->second;
diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp
index c70085485..2450f0d6d 100644
--- a/components/shader/shadermanager.hpp
+++ b/components/shader/shadermanager.hpp
@@ -11,14 +11,26 @@
 
 #include <osgViewer/Viewer>
 
+#include <components/sceneutil/lightmanager.hpp>
+
 namespace Resource
 {
     class SceneManager;
 }
 
+namespace SceneUtil
+{
+    enum class LightingMethod;
+}
+
 namespace Shader
 {
 
+    enum class UBOBinding
+    {
+        LightBuffer
+    };
+
     /// @brief Reads shader template files and turns them into a concrete shader, based on a list of define's.
     /// @par Shader templates can get the value of a define with the syntax @define.
     class ShaderManager
@@ -29,7 +41,7 @@ namespace Shader
 
         void setShaderPath(const std::string& path);
 
-        void setFFPLighting(bool useFFP);
+        void setLightingMethod(SceneUtil::LightingMethod method);
 
         typedef std::map<std::string, std::string> DefineMap;
 
@@ -69,7 +81,7 @@ namespace Shader
         typedef std::map<std::pair<osg::ref_ptr<osg::Shader>, osg::ref_ptr<osg::Shader> >, osg::ref_ptr<osg::Program> > ProgramMap;
         ProgramMap mPrograms;
 
-        bool mFFPLighting;
+        SceneUtil::LightingMethod mLightingMethod;
 
         std::mutex mMutex;
     };
diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp
index 9dec5522c..eae9ad2db 100644
--- a/components/shader/shadervisitor.cpp
+++ b/components/shader/shadervisitor.cpp
@@ -1,7 +1,10 @@
 #include "shadervisitor.hpp"
 
+#include <osg/AlphaFunc>
 #include <osg/Geometry>
+#include <osg/GLExtensions>
 #include <osg/Material>
+#include <osg/Multisample>
 #include <osg/Texture>
 
 #include <osgUtil/TangentSpaceGenerator>
@@ -13,6 +16,7 @@
 #include <components/sceneutil/riggeometry.hpp>
 #include <components/sceneutil/morphgeometry.hpp>
 
+#include "removedalphafunc.hpp"
 #include "shadermanager.hpp"
 
 namespace Shader
@@ -22,6 +26,11 @@ namespace Shader
         : mShaderRequired(false)
         , mColorMode(0)
         , mMaterialOverridden(false)
+        , mAlphaTestOverridden(false)
+        , mAlphaBlendOverridden(false)
+        , mAlphaFunc(GL_ALWAYS)
+        , mAlphaRef(1.0)
+        , mAlphaBlend(false)
         , mNormalHeight(false)
         , mTexStageRequiringTangents(-1)
         , mNode(nullptr)
@@ -77,6 +86,34 @@ namespace Shader
         return newStateSet.get();
     }
 
+    osg::UserDataContainer* getWritableUserDataContainer(osg::Object& object)
+    {
+        if (!object.getUserDataContainer())
+            return object.getOrCreateUserDataContainer();
+
+        osg::ref_ptr<osg::UserDataContainer> newUserData = static_cast<osg::UserDataContainer *>(object.getUserDataContainer()->clone(osg::CopyOp::SHALLOW_COPY));
+        object.setUserDataContainer(newUserData);
+        return newUserData.get();
+    }
+
+    osg::StateSet* getRemovedState(osg::StateSet& stateSet)
+    {
+        if (!stateSet.getUserDataContainer())
+            return nullptr;
+
+        return static_cast<osg::StateSet *>(stateSet.getUserDataContainer()->getUserObject("removedState"));
+    }
+
+    void updateRemovedState(osg::UserDataContainer& userData, osg::StateSet* stateSet)
+    {
+        unsigned int index = userData.getUserObjectIndex("removedState");
+        if (index < userData.getNumUserObjects())
+            userData.setUserObject(index, stateSet);
+        else
+            userData.addUserObject(stateSet);
+        stateSet->setName("removedState");
+    }
+
     const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", "specularMap", "decalMap", "bumpMap" };
     bool isTextureNameRecognized(const std::string& name)
     {
@@ -235,49 +272,76 @@ namespace Shader
         }
 
         const osg::StateSet::AttributeList& attributes = stateset->getAttributeList();
-        for (osg::StateSet::AttributeList::const_iterator it = attributes.begin(); it != attributes.end(); ++it)
+        osg::StateSet::AttributeList removedAttributes;
+        osg::ref_ptr<osg::StateSet> removedState;
+        if (removedState = getRemovedState(*stateset))
+            removedAttributes = removedState->getAttributeList();
+        for (const auto& attributeMap : { attributes, removedAttributes })
         {
-            if (it->first.first == osg::StateAttribute::MATERIAL)
+            for (osg::StateSet::AttributeList::const_iterator it = attributeMap.begin(); it != attributeMap.end(); ++it)
             {
-                // This should probably be moved out of ShaderRequirements and be applied directly now it's a uniform instead of a define
-                if (!mRequirements.back().mMaterialOverridden || it->second.second & osg::StateAttribute::PROTECTED)
+                if (it->first.first == osg::StateAttribute::MATERIAL)
                 {
-                    if (it->second.second & osg::StateAttribute::OVERRIDE)
-                        mRequirements.back().mMaterialOverridden = true;
-
-                    const osg::Material* mat = static_cast<const osg::Material*>(it->second.first.get());
-
-                    if (!writableStateSet)
-                        writableStateSet = getWritableStateSet(node);
-
-                    int colorMode;
-                    switch (mat->getColorMode())
+                    // This should probably be moved out of ShaderRequirements and be applied directly now it's a uniform instead of a define
+                    if (!mRequirements.back().mMaterialOverridden || it->second.second & osg::StateAttribute::PROTECTED)
                     {
-                    case osg::Material::OFF:
-                        colorMode = 0;
-                        break;
-                    case osg::Material::EMISSION:
-                        colorMode = 1;
-                        break;
-                    default:
-                    case osg::Material::AMBIENT_AND_DIFFUSE:
-                        colorMode = 2;
-                        break;
-                    case osg::Material::AMBIENT:
-                        colorMode = 3;
-                        break;
-                    case osg::Material::DIFFUSE:
-                        colorMode = 4;
-                        break;
-                    case osg::Material::SPECULAR:
-                        colorMode = 5;
-                        break;
-                    }
+                        if (it->second.second & osg::StateAttribute::OVERRIDE)
+                            mRequirements.back().mMaterialOverridden = true;
 
-                    mRequirements.back().mColorMode = colorMode;
+                        const osg::Material* mat = static_cast<const osg::Material*>(it->second.first.get());
+
+                        if (!writableStateSet)
+                            writableStateSet = getWritableStateSet(node);
+
+                        int colorMode;
+                        switch (mat->getColorMode())
+                        {
+                        case osg::Material::OFF:
+                            colorMode = 0;
+                            break;
+                        case osg::Material::EMISSION:
+                            colorMode = 1;
+                            break;
+                        default:
+                        case osg::Material::AMBIENT_AND_DIFFUSE:
+                            colorMode = 2;
+                            break;
+                        case osg::Material::AMBIENT:
+                            colorMode = 3;
+                            break;
+                        case osg::Material::DIFFUSE:
+                            colorMode = 4;
+                            break;
+                        case osg::Material::SPECULAR:
+                            colorMode = 5;
+                            break;
+                        }
+
+                        mRequirements.back().mColorMode = colorMode;
+                    }
+                }
+                else if (it->first.first == osg::StateAttribute::ALPHAFUNC)
+                {
+                    if (!mRequirements.back().mAlphaTestOverridden || it->second.second & osg::StateAttribute::PROTECTED)
+                    {
+                        if (it->second.second & osg::StateAttribute::OVERRIDE)
+                            mRequirements.back().mAlphaTestOverridden = true;
+
+                        const osg::AlphaFunc* alpha = static_cast<const osg::AlphaFunc*>(it->second.first.get());
+                        mRequirements.back().mAlphaFunc = alpha->getFunction();
+                        mRequirements.back().mAlphaRef = alpha->getReferenceValue();
+                    }
                 }
             }
-            // Eventually, move alpha testing to discard in shader adn remove deprecated state here
+        }
+
+        unsigned int alphaBlend = stateset->getMode(GL_BLEND);
+        if (alphaBlend != osg::StateAttribute::INHERIT && (!mRequirements.back().mAlphaBlendOverridden || alphaBlend & osg::StateAttribute::PROTECTED))
+        {
+            if (alphaBlend & osg::StateAttribute::OVERRIDE)
+                mRequirements.back().mAlphaBlendOverridden = true;
+
+            mRequirements.back().mAlphaBlend = alphaBlend & osg::StateAttribute::ON;
         }
     }
 
@@ -323,6 +387,57 @@ namespace Shader
 
         writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode));
 
+        defineMap["alphaFunc"] = std::to_string(reqs.mAlphaFunc);
+
+        // back up removed state in case recreateShaders gets rid of the shader later
+        osg::ref_ptr<osg::StateSet> removedState;
+        if ((removedState = getRemovedState(*writableStateSet)) && !mAllowedToModifyStateSets)
+            removedState = new osg::StateSet(*removedState, osg::CopyOp::SHALLOW_COPY);
+        if (!removedState)
+            removedState = new osg::StateSet();
+
+        defineMap["alphaToCoverage"] = "0";
+        if (reqs.mAlphaFunc != osg::AlphaFunc::ALWAYS)
+        {
+            writableStateSet->addUniform(new osg::Uniform("alphaRef", reqs.mAlphaRef));
+
+            const auto* alphaFunc = writableStateSet->getAttributePair(osg::StateAttribute::ALPHAFUNC);
+            if (alphaFunc)
+                removedState->setAttribute(alphaFunc->first, alphaFunc->second);
+            // This prevents redundant glAlphaFunc calls while letting the shadows bin still see the test
+            writableStateSet->setAttribute(RemovedAlphaFunc::getInstance(reqs.mAlphaFunc), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
+
+            // Blending won't work with A2C as we use the alpha channel for coverage. gl_SampleCoverage from ARB_sample_shading would save the day, but requires GLSL 130
+            if (mConvertAlphaTestToAlphaToCoverage && !reqs.mAlphaBlend)
+            {
+                writableStateSet->setMode(GL_SAMPLE_ALPHA_TO_COVERAGE_ARB, osg::StateAttribute::ON);
+                defineMap["alphaToCoverage"] = "1";
+            }
+
+            // Preventing alpha tested stuff shrinking as lower mip levels are used requires knowing the texture size
+            osg::ref_ptr<osg::GLExtensions> exts = osg::GLExtensions::Get(0, false);
+            if (exts && exts->isGpuShader4Supported)
+                defineMap["useGPUShader4"] = "1";
+            // We could fall back to a texture size uniform if EXT_gpu_shader4 is missing
+        }
+
+        if (writableStateSet->getMode(GL_ALPHA_TEST) != osg::StateAttribute::INHERIT)
+            removedState->setMode(GL_ALPHA_TEST, writableStateSet->getMode(GL_ALPHA_TEST));
+        // This disables the deprecated fixed-function alpha test
+        writableStateSet->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);
+
+        if (!removedState->getModeList().empty() || !removedState->getAttributeList().empty())
+        {
+            // user data is normally shallow copied so shared with the original stateset
+            osg::ref_ptr<osg::UserDataContainer> writableUserData;
+            if (mAllowedToModifyStateSets)
+                writableUserData = writableStateSet->getOrCreateUserDataContainer();
+            else
+                writableUserData = getWritableUserDataContainer(*writableStateSet);
+
+            updateRemovedState(*writableUserData, removedState);
+        }
+
         defineMap["translucentFramebuffer"] = mTranslucentFramebuffer ? "1" : "0";
 
         osg::ref_ptr<osg::Shader> vertexShader (mShaderManager.getShader(mDefaultVsTemplate, defineMap, osg::Shader::VERTEX));
@@ -350,6 +465,25 @@ namespace Shader
             writableStateSet = getWritableStateSet(node);
 
         writableStateSet->removeAttribute(osg::StateAttribute::PROGRAM);
+
+        osg::ref_ptr<osg::StateSet> removedState;
+        if (removedState = getRemovedState(*writableStateSet))
+        {
+            // user data is normally shallow copied so shared with the original stateset
+            osg::ref_ptr<osg::UserDataContainer> writableUserData;
+            if (mAllowedToModifyStateSets)
+                writableUserData = writableStateSet->getUserDataContainer();
+            else
+                writableUserData = getWritableUserDataContainer(*writableStateSet);
+            unsigned int index = writableUserData->getUserObjectIndex("removedState");
+            writableUserData->removeUserObject(index);
+
+            for (const auto& [mode, value] : removedState->getModeList())
+                writableStateSet->setMode(mode, value);
+
+            for (const auto& attribute : removedState->getAttributeList())
+                writableStateSet->setAttribute(attribute.second.first, attribute.second.second);
+        }
     }
 
     bool ShaderVisitor::adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs)
@@ -477,9 +611,53 @@ namespace Shader
         mApplyLightingToEnvMaps = apply;
     }
 
+    void ShaderVisitor::setConvertAlphaTestToAlphaToCoverage(bool convert)
+    {
+        mConvertAlphaTestToAlphaToCoverage = convert;
+    }
+
     void ShaderVisitor::setTranslucentFramebuffer(bool translucent)
     {
         mTranslucentFramebuffer = translucent;
     }
 
+    ReinstateRemovedStateVisitor::ReinstateRemovedStateVisitor(bool allowedToModifyStateSets)
+        : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
+        , mAllowedToModifyStateSets(allowedToModifyStateSets)
+    {
+    }
+
+    void ReinstateRemovedStateVisitor::apply(osg::Node& node)
+    {
+        if (node.getStateSet())
+        {
+            osg::ref_ptr<osg::StateSet> removedState = getRemovedState(*node.getStateSet());
+            if (removedState)
+            {
+                osg::ref_ptr<osg::StateSet> writableStateSet;
+                if (mAllowedToModifyStateSets)
+                    writableStateSet = node.getStateSet();
+                else
+                    writableStateSet = getWritableStateSet(node);
+
+                // user data is normally shallow copied so shared with the original stateset
+                osg::ref_ptr<osg::UserDataContainer> writableUserData;
+                if (mAllowedToModifyStateSets)
+                    writableUserData = writableStateSet->getUserDataContainer();
+                else
+                    writableUserData = getWritableUserDataContainer(*writableStateSet);
+                unsigned int index = writableUserData->getUserObjectIndex("removedState");
+                writableUserData->removeUserObject(index);
+
+                for (const auto&[mode, value] : removedState->getModeList())
+                    writableStateSet->setMode(mode, value);
+
+                for (const auto& attribute : removedState->getAttributeList())
+                    writableStateSet->setAttribute(attribute.second.first, attribute.second.second);
+            }
+        }
+
+        traverse(node);
+    }
+
 }
diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp
index 11b37c923..f7c6f8312 100644
--- a/components/shader/shadervisitor.hpp
+++ b/components/shader/shadervisitor.hpp
@@ -40,6 +40,8 @@ namespace Shader
 
         void setApplyLightingToEnvMaps(bool apply);
 
+        void setConvertAlphaTestToAlphaToCoverage(bool convert);
+
         void setTranslucentFramebuffer(bool translucent);
 
         void apply(osg::Node& node) override;
@@ -65,6 +67,8 @@ namespace Shader
 
         bool mApplyLightingToEnvMaps;
 
+        bool mConvertAlphaTestToAlphaToCoverage;
+
         bool mTranslucentFramebuffer;
 
         ShaderManager& mShaderManager;
@@ -83,6 +87,12 @@ namespace Shader
             int mColorMode;
             
             bool mMaterialOverridden;
+            bool mAlphaTestOverridden;
+            bool mAlphaBlendOverridden;
+
+            GLenum mAlphaFunc;
+            float mAlphaRef;
+            bool mAlphaBlend;
 
             bool mNormalHeight; // true if normal map has height info in alpha channel
 
@@ -102,6 +112,17 @@ namespace Shader
         bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs);
     };
 
+    class ReinstateRemovedStateVisitor : public osg::NodeVisitor
+    {
+    public:
+        ReinstateRemovedStateVisitor(bool allowedToModifyStateSets);
+
+        void apply(osg::Node& node) override;
+
+    private:
+        bool mAllowedToModifyStateSets;
+    };
+
 }
 
 #endif
diff --git a/docs/source/reference/modding/extended.rst b/docs/source/reference/modding/extended.rst
index 98b3e7f00..cd739cb1b 100644
--- a/docs/source/reference/modding/extended.rst
+++ b/docs/source/reference/modding/extended.rst
@@ -306,8 +306,11 @@ For example, we do not need to have collision or animation objects for groundcov
 do not need to render groundcover on the map, do not need to render it for the whole visible area (which can be very large with Distant Terrain). It allows to increase performance a lot.
 
 General advices to create assets for this feature:
+
 1. Alpha properties from Nif files are not used, a unified alpha settings are used (alpha testing, "greater of equal" function, 128/255 threshold).
+
 2. Use a single NiTriShape in groundocver mesh, or at least use same properties (texture, alpha, material, etc), so OpenMW can merge them on the fly. Otherwise animations may not work properly.
+
 3. Smooth fading does not work for meshes, which have textures without alpha (e.g. rock).
 
 Groundcover mods can be registered in the openmw.cfg via "groundcover" entries instead of "content" ones:
diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst
index ed43b19a2..acc848299 100644
--- a/docs/source/reference/modding/settings/shaders.rst
+++ b/docs/source/reference/modding/settings/shaders.rst
@@ -147,3 +147,14 @@ radial fog
 By default, the fog becomes thicker proportionally to your distance from the clipping plane set at the clipping distance, which causes distortion at the edges of the screen.
 This setting makes the fog use the actual eye point distance (or so called Euclidean distance) to calculate the fog, which makes the fog look less artificial, especially if you have a wide FOV.
 Note that the rendering will act as if you have 'force shaders' option enabled with this on, which means that shaders will be used to render all objects and the terrain.
+
+antialias alpha test
+---------------------------------------
+
+:Type:		boolean
+:Range:		True/False
+:Default:	False
+
+Convert the alpha test (cutout/punchthrough alpha) to alpha-to-coverage when :ref:`antialiasing` is on.
+This allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation.
+When MSAA is off, this setting will have no visible effect, but might have a performance cost.
diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt
index b4182a105..613de8fa8 100644
--- a/extern/CMakeLists.txt
+++ b/extern/CMakeLists.txt
@@ -31,15 +31,11 @@ if(NOT OPENMW_USE_SYSTEM_BULLET)
     set(USE_DOUBLE_PRECISION ${BULLET_USE_DOUBLES} CACHE BOOL "")
     set(BULLET2_MULTITHREADING ON CACHE BOOL "")
 
-    # Version 3.08 with the following changes:
-    # 1. Fixes the linking of Threads:
-    #    https://github.com/bulletphysics/bullet3/pull/3237
-    # 2. Removes ~300 MiB of files not used here:
-    #    rm -rf build3 data docs examples test Doxyfile
+    # master on 12 Mar 2021
     include(FetchContent)
     FetchContent_Declare(bullet
-        URL https://github.com/glebm/bullet3/archive/ed5256454f4f84bd2c1728c88ddb0405d614e7d2.zip
-        URL_HASH MD5=e3c94fac35a7be885ad8843f828a0f96
+        URL https://github.com/bulletphysics/bullet3/archive/87e668f6b2a883b4ef63db8a07c8e9283916e9d9.zip
+        URL_HASH MD5=9f13246439968494c2b595cf412d83c8
         SOURCE_DIR fetched/bullet
     )
     FetchContent_MakeAvailableExcludeFromAll(bullet)
@@ -60,6 +56,10 @@ if(NOT OPENMW_USE_SYSTEM_MYGUI)
     set(MYGUI_BUILD_PLUGINS OFF CACHE BOOL "")
     set(MYGUI_BUILD_TOOLS OFF CACHE BOOL "")
 
+    # We appear to be using some obsolete properties in the XML.
+    # See https://gitlab.com/OpenMW/openmw/-/issues/5896
+    set(MYGUI_DONT_USE_OBSOLETE OFF CACHE BOOL "")
+
     if(MYGUI_STATIC)
         set(BUILD_SHARED_LIBS OFF)
     else()
@@ -68,8 +68,8 @@ if(NOT OPENMW_USE_SYSTEM_MYGUI)
 
     include(FetchContent)
     FetchContent_Declare(mygui
-        URL https://github.com/MyGUI/mygui/archive/MyGUI3.4.0.zip
-        URL_HASH MD5=9e990a4240430cbf567bfe73488a274e
+        URL https://github.com/MyGUI/mygui/archive/MyGUI3.4.1.zip
+        URL_HASH MD5=952d4033854612c99a5d9bf4b8550c26
         SOURCE_DIR fetched/mygui
     )
     FetchContent_MakeAvailableExcludeFromAll(mygui)
diff --git a/extern/osg-ffmpeg-videoplayer/audiodecoder.cpp b/extern/osg-ffmpeg-videoplayer/audiodecoder.cpp
index 337382679..c32794d2a 100644
--- a/extern/osg-ffmpeg-videoplayer/audiodecoder.cpp
+++ b/extern/osg-ffmpeg-videoplayer/audiodecoder.cpp
@@ -91,7 +91,7 @@ MovieAudioDecoder::~MovieAudioDecoder()
     if(mAudioContext)
         avcodec_free_context(&mAudioContext);
 
-    av_freep(&mFrame);
+    av_frame_free(&mFrame);
     av_freep(&mDataBuf);
 }
 
@@ -222,7 +222,7 @@ int MovieAudioDecoder::audio_decode_frame(AVFrame *frame, int &sample_skip)
             return result;
         }
 
-        av_packet_unref(&mPacket);
+        av_packet_unref(pkt);
         mGetNextPacket = true;
 
         /* next packet */
diff --git a/extern/osg-ffmpeg-videoplayer/videostate.cpp b/extern/osg-ffmpeg-videoplayer/videostate.cpp
index 5858d985a..7c4bddb01 100644
--- a/extern/osg-ffmpeg-videoplayer/videostate.cpp
+++ b/extern/osg-ffmpeg-videoplayer/videostate.cpp
@@ -2,6 +2,7 @@
 
 #include <algorithm>
 #include <cassert>
+#include <cstddef>
 #include <iostream>
 #include <thread>
 #include <chrono>
@@ -49,7 +50,7 @@ VideoState::VideoState()
     , av_sync_type(AV_SYNC_DEFAULT)
     , audio_st(nullptr)
     , video_st(nullptr), frame_last_pts(0.0)
-    , video_clock(0.0), sws_context(nullptr), rgbaFrame(nullptr), pictq_size(0)
+    , video_clock(0.0), sws_context(nullptr), pictq_size(0)
     , pictq_rindex(0), pictq_windex(0)
     , mSeekRequested(false)
     , mSeekPos(0)
@@ -82,10 +83,11 @@ void PacketQueue::put(AVPacket *pkt)
     pkt1 = (AVPacketList*)av_malloc(sizeof(AVPacketList));
     if(!pkt1) throw std::bad_alloc();
 
-    if(pkt != &flush_pkt && !pkt->buf && av_packet_ref(&pkt1->pkt, pkt) < 0)
-        throw std::runtime_error("Failed to duplicate packet");
+    if(pkt == &flush_pkt)
+        pkt1->pkt = *pkt;
+    else
+        av_packet_move_ref(&pkt1->pkt, pkt);
 
-    pkt1->pkt = *pkt;
     pkt1->next = nullptr;
 
     this->mutex.lock ();
@@ -116,7 +118,8 @@ int PacketQueue::get(AVPacket *pkt, VideoState *is)
             this->nb_packets--;
             this->size -= pkt1->pkt.size;
 
-            *pkt = pkt1->pkt;
+            av_packet_unref(pkt);
+            av_packet_move_ref(pkt, &pkt1->pkt);
             av_free(pkt1);
 
             return 1;
@@ -155,6 +158,39 @@ void PacketQueue::clear()
     this->mutex.unlock ();
 }
 
+int VideoPicture::set_dimensions(int w, int h) {
+  if (this->rgbaFrame != nullptr && this->rgbaFrame->width == w &&
+      this->rgbaFrame->height == h) {
+    return 0;
+  }
+
+  std::unique_ptr<AVFrame, VideoPicture::AVFrameDeleter> frame{
+      av_frame_alloc()};
+  if (frame == nullptr) {
+    std::cerr << "av_frame_alloc failed" << std::endl;
+    return -1;
+  }
+
+  constexpr AVPixelFormat kPixFmt = AV_PIX_FMT_RGBA;
+  frame->format = kPixFmt;
+  frame->width = w;
+  frame->height = h;
+  if (av_image_alloc(frame->data, frame->linesize, frame->width, frame->height,
+                     kPixFmt, 1) < 0) {
+    std::cerr << "av_image_alloc failed" << std::endl;
+    return -1;
+  }
+
+  this->rgbaFrame = std::move(frame);
+  return 0;
+}
+
+void VideoPicture::AVFrameDeleter::operator()(AVFrame* frame) const
+{
+    av_freep(frame->data);
+    av_frame_free(&frame);
+}
+
 int VideoState::istream_read(void *user_data, uint8_t *buf, int buf_size)
 {
     try
@@ -220,7 +256,7 @@ void VideoState::video_display(VideoPicture *vp)
         osg::ref_ptr<osg::Image> image = new osg::Image;
 
         image->setImage(this->video_ctx->width, this->video_ctx->height,
-                        1, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, &vp->data[0], osg::Image::NO_DELETE);
+                        1, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, vp->rgbaFrame->data[0], osg::Image::NO_DELETE);
 
         mTexture->setImage(image);
     }
@@ -296,23 +332,27 @@ int VideoState::queue_picture(AVFrame *pFrame, double pts)
     // Convert the image into RGBA format
     // TODO: we could do this in a pixel shader instead, if the source format
     // matches a commonly used format (ie YUV420P)
-    if(this->sws_context == nullptr)
+    const int w = pFrame->width;
+    const int h = pFrame->height;
+    if(this->sws_context == nullptr || this->sws_context_w != w || this->sws_context_h != h)
     {
-        int w = this->video_ctx->width;
-        int h = this->video_ctx->height;
+        if (this->sws_context != nullptr)
+            sws_freeContext(this->sws_context);
         this->sws_context = sws_getContext(w, h, this->video_ctx->pix_fmt,
                                            w, h, AV_PIX_FMT_RGBA, SWS_BICUBIC,
                                            nullptr, nullptr, nullptr);
         if(this->sws_context == nullptr)
             throw std::runtime_error("Cannot initialize the conversion context!\n");
+        this->sws_context_w = w;
+        this->sws_context_h = h;
     }
 
     vp->pts = pts;
-    vp->data.resize(this->video_ctx->width * this->video_ctx->height * 4);
+    if (vp->set_dimensions(w, h) < 0)
+        return -1;
 
-    uint8_t *dst[4] = { &vp->data[0], nullptr, nullptr, nullptr };
     sws_scale(this->sws_context, pFrame->data, pFrame->linesize,
-              0, this->video_ctx->height, dst, this->rgbaFrame->linesize);
+              0, this->video_ctx->height, vp->rgbaFrame->data, vp->rgbaFrame->linesize);
 
     // now we inform our display thread that we have a pic ready
     this->pictq_windex = (this->pictq_windex+1) % VIDEO_PICTURE_ARRAY_SIZE;
@@ -360,13 +400,11 @@ public:
     {
         VideoState* self = mVideoState;
         AVPacket pkt1, *packet = &pkt1;
+        av_init_packet(packet);
         AVFrame *pFrame;
 
         pFrame = av_frame_alloc();
 
-        self->rgbaFrame = av_frame_alloc();
-        av_image_alloc(self->rgbaFrame->data, self->rgbaFrame->linesize, self->video_ctx->width, self->video_ctx->height, AV_PIX_FMT_RGBA, 1);
-
         while(self->videoq.get(packet, self) >= 0)
         {
             if(packet->data == flush_pkt.data)
@@ -407,10 +445,7 @@ public:
 
         av_packet_unref(packet);
 
-        av_free(pFrame);
-
-        av_freep(&self->rgbaFrame->data[0]);
-        av_free(self->rgbaFrame);
+        av_frame_free(&pFrame);
     }
 
 private:
@@ -438,6 +473,7 @@ public:
 
         AVFormatContext *pFormatCtx = self->format_ctx;
         AVPacket pkt1, *packet = &pkt1;
+        av_init_packet(packet);
 
         try
         {
@@ -673,16 +709,21 @@ void VideoState::init(std::shared_ptr<std::istream> inputstream, const std::stri
         {
           if (this->format_ctx->pb != nullptr)
           {
-              av_free(this->format_ctx->pb->buffer);
-              this->format_ctx->pb->buffer = nullptr;
-
-              av_free(this->format_ctx->pb);
-              this->format_ctx->pb = nullptr;
+              av_freep(&this->format_ctx->pb->buffer);
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100)
+              avio_context_free(&this->format_ctx->pb);
+#else
+              av_freep(&this->format_ctx->pb);
+#endif
           }
         }
         // "Note that a user-supplied AVFormatContext will be freed on failure."
         this->format_ctx = nullptr;
-        av_free(ioCtx);
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100)
+        avio_context_free(&ioCtx);
+#else
+        av_freep(&ioCtx);
+#endif
         throw std::runtime_error("Failed to open video input");
     }
 
@@ -756,11 +797,12 @@ void VideoState::deinit()
         ///
         if (this->format_ctx->pb != nullptr)
         {
-            av_free(this->format_ctx->pb->buffer);
-            this->format_ctx->pb->buffer = nullptr;
-
-            av_free(this->format_ctx->pb);
-            this->format_ctx->pb = nullptr;
+            av_freep(&this->format_ctx->pb->buffer);
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 80, 100)
+            avio_context_free(&this->format_ctx->pb);
+#else
+            av_freep(&this->format_ctx->pb);
+#endif
         }
         avformat_close_input(&this->format_ctx);
     }
@@ -771,6 +813,11 @@ void VideoState::deinit()
         mTexture->setImage(nullptr);
         mTexture = nullptr;
     }
+
+    // Dellocate RGBA frame queue.
+    for (std::size_t i = 0; i < VIDEO_PICTURE_ARRAY_SIZE; ++i)
+        this->pictq[i].rgbaFrame = nullptr;
+
 }
 
 double VideoState::get_external_clock()
diff --git a/extern/osg-ffmpeg-videoplayer/videostate.hpp b/extern/osg-ffmpeg-videoplayer/videostate.hpp
index 641fce04b..015656084 100644
--- a/extern/osg-ffmpeg-videoplayer/videostate.hpp
+++ b/extern/osg-ffmpeg-videoplayer/videostate.hpp
@@ -95,7 +95,16 @@ struct VideoPicture {
     VideoPicture() : pts(0.0)
     { }
 
-    std::vector<uint8_t> data;
+    struct AVFrameDeleter {
+        void operator()(AVFrame* frame) const;
+    };
+
+    // Sets frame dimensions.
+    // Must be called before writing to `rgbaFrame`.
+    // Return -1 on error.
+    int set_dimensions(int w, int h);
+
+    std::unique_ptr<AVFrame, AVFrameDeleter> rgbaFrame;
     double pts;
 };
 
@@ -159,8 +168,8 @@ struct VideoState {
     double      video_clock; ///<pts of last decoded frame / predicted pts of next decoded frame
     PacketQueue videoq;
     SwsContext*  sws_context;
+    int sws_context_w, sws_context_h;
     VideoPicture pictq[VIDEO_PICTURE_ARRAY_SIZE];
-    AVFrame*     rgbaFrame; // used as buffer for the frame converted from its native format to RGBA
     int          pictq_size, pictq_rindex, pictq_windex;
     std::mutex pictq_mutex;
     std::condition_variable pictq_cond;
diff --git a/files/settings-default.cfg b/files/settings-default.cfg
index 368d74f6b..cb23f7df7 100644
--- a/files/settings-default.cfg
+++ b/files/settings-default.cfg
@@ -442,11 +442,23 @@ apply lighting to environment maps = false
 # This makes fogging independent from the viewing angle. Shaders will be used to render all objects.
 radial fog = false
 
-# Set maximum number of lights per object, does not include the sun. 
-# This feature may not work on your device, in which case it will fall back to the previous OpenGL limit of 8 lights. 
-# "[Shaders]/clamp lighting" must be set to 'false' and "[Shaders]/force shaders" must be set to 'true' for this to take effect. 
+# Internal handling of lights, values are 'legacy', 'default', 'experimental'
+lighting method = experimental
+
+# Sets the bounding sphere multiplier of light sources, which are used to determine if an object should
+# receive lighting. Higher values will allow for smoother transitions of light sources, but may have a performance cost and
+# requires a higher number of 'max lights' set. It is recommended to keep this at 1.0 with 'legacy' lighting enabled.
+light bounds multiplier =  1.9
+
+# Set maximum number of lights per object. 
+# Only used when 'lighting method' is not set to 'legacy'
 max lights = 16
 
+# Convert the alpha test (cutout/punchthrough alpha) to alpha-to-coverage.
+# This allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation.
+# When MSAA is off, this setting will have no visible effect, but might have a performance cost.
+antialias alpha test = false
+
 [Input]
 
 # Capture control of the cursor prevent movement outside the window.
diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt
index 1a0c5f1a5..7250aa372 100644
--- a/files/shaders/CMakeLists.txt
+++ b/files/shaders/CMakeLists.txt
@@ -12,6 +12,7 @@ set(SHADER_FILES
     water_vertex.glsl
     water_fragment.glsl
     water_nm.png
+    alpha.glsl
     objects_vertex.glsl
     objects_fragment.glsl
     terrain_vertex.glsl
@@ -25,7 +26,6 @@ set(SHADER_FILES
     shadowcasting_vertex.glsl
     shadowcasting_fragment.glsl
     vertexcolors.glsl
-    sun.glsl
 )
 
 copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}")
diff --git a/files/shaders/alpha.glsl b/files/shaders/alpha.glsl
new file mode 100644
index 000000000..05be801e9
--- /dev/null
+++ b/files/shaders/alpha.glsl
@@ -0,0 +1,85 @@
+
+#define FUNC_NEVER                          512 // 0x0200
+#define FUNC_LESS                           513 // 0x0201
+#define FUNC_EQUAL                          514 // 0x0202
+#define FUNC_LEQUAL                         515 // 0x0203
+#define FUNC_GREATER                        516 // 0x0204
+#define FUNC_NOTEQUAL                       517 // 0x0205
+#define FUNC_GEQUAL                         518 // 0x0206
+#define FUNC_ALWAYS                         519 // 0x0207
+
+#if @alphaFunc != FUNC_ALWAYS && @alphaFunc != FUNC_NEVER
+uniform float alphaRef;
+#endif
+
+float mipmapLevel(vec2 scaleduv)
+{
+    vec2 dUVdx = dFdx(scaleduv);
+    vec2 dUVdy = dFdy(scaleduv);
+    float maxDUVSquared = max(dot(dUVdx, dUVdx), dot(dUVdy, dUVdy));
+    return max(0.0, 0.5 * log2(maxDUVSquared));
+}
+
+float coveragePreservingAlphaScale(sampler2D diffuseMap, vec2 uv)
+{
+    #if @alphaFunc != FUNC_ALWAYS && @alphaFunc != FUNC_NEVER
+        vec2 textureSize;
+        #if @useGPUShader4
+            textureSize = textureSize2D(diffuseMap, 0);
+        #else
+            textureSize = vec2(256.0);
+        #endif
+            return 1.0 + mipmapLevel(uv * textureSize) * 0.25;
+    #else
+        return 1.0;
+    #endif
+}
+
+void alphaTest()
+{
+    #if @alphaToCoverage 
+        float coverageAlpha = (gl_FragData[0].a - clamp(alphaRef, 0.0001, 0.9999)) / max(fwidth(gl_FragData[0].a), 0.0001) + 0.5;
+
+        // Some functions don't make sense with A2C or are a pain to think about and no meshes use them anyway
+        // Use regular alpha testing in such cases until someone complains.
+        #if @alphaFunc == FUNC_NEVER
+            discard;
+        #elif @alphaFunc == FUNC_LESS
+            gl_FragData[0].a = 1.0 - coverageAlpha;
+        #elif @alphaFunc == FUNC_EQUAL
+            if (gl_FragData[0].a != alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_LEQUAL
+            gl_FragData[0].a = 1.0 - coverageAlpha;
+        #elif @alphaFunc == FUNC_GREATER
+            gl_FragData[0].a = coverageAlpha;
+        #elif @alphaFunc == FUNC_NOTEQUAL
+            if (gl_FragData[0].a == alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_GEQUAL
+            gl_FragData[0].a = coverageAlpha;
+        #endif
+    #else
+        #if @alphaFunc == FUNC_NEVER
+            discard;
+        #elif @alphaFunc == FUNC_LESS
+            if (gl_FragData[0].a >= alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_EQUAL
+            if (gl_FragData[0].a != alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_LEQUAL
+            if (gl_FragData[0].a > alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_GREATER
+            if (gl_FragData[0].a <= alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_NOTEQUAL
+            if (gl_FragData[0].a == alphaRef)
+                discard;
+        #elif @alphaFunc == FUNC_GEQUAL
+            if (gl_FragData[0].a < alphaRef)
+                discard;
+        #endif
+    #endif
+}
\ No newline at end of file
diff --git a/files/shaders/groundcover_fragment.glsl b/files/shaders/groundcover_fragment.glsl
index 392419d92..5464e3655 100644
--- a/files/shaders/groundcover_fragment.glsl
+++ b/files/shaders/groundcover_fragment.glsl
@@ -1,5 +1,12 @@
 #version 120
-#extension GL_ARB_uniform_buffer_object : enable
+
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 #define GROUNDCOVER
 
@@ -31,11 +38,7 @@ centroid varying vec3 shadowDiffuseLighting;
 
 #include "shadows_fragment.glsl"
 #include "lighting.glsl"
-
-float calc_coverage(float a, float alpha_ref, float falloff_rate)
-{
-    return clamp(falloff_rate * (a - alpha_ref) + alpha_ref, 0.0, 1.0);
-}
+#include "alpha.glsl"
 
 void main()
 {
@@ -56,12 +59,13 @@ void main()
     gl_FragData[0] = vec4(1.0);
 #endif
 
-    gl_FragData[0].a = calc_coverage(gl_FragData[0].a, 128.0/255.0, 4.0);
-
-    float shadowing = unshadowedLightRatio(linearDepth);
     if (euclideanDepth > @groundcoverFadeStart)
         gl_FragData[0].a *= 1.0-smoothstep(@groundcoverFadeStart, @groundcoverFadeEnd, euclideanDepth);
 
+    alphaTest();
+
+    float shadowing = unshadowedLightRatio(linearDepth);
+
     vec3 lighting;
 #if !PER_PIXEL_LIGHTING
     lighting = passLighting + shadowDiffuseLighting * shadowing;
diff --git a/files/shaders/groundcover_vertex.glsl b/files/shaders/groundcover_vertex.glsl
index 3238bc864..221f6528b 100644
--- a/files/shaders/groundcover_vertex.glsl
+++ b/files/shaders/groundcover_vertex.glsl
@@ -1,6 +1,12 @@
 #version 120
 
-#extension GL_ARB_uniform_buffer_object : enable
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 #define GROUNDCOVER
 
diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl
index b243a90c4..36be081ef 100644
--- a/files/shaders/lighting.glsl
+++ b/files/shaders/lighting.glsl
@@ -1,39 +1,86 @@
+#define LIGHTING_MODEL_FFP 0
+#define LIGHTING_MODEL_SINGLE_UBO 1
+#define LIGHTING_MODEL_PER_OBJECT_UNIFORM 2
+
 #if !@ffpLighting
+#define getLight LightBuffer
 
-#include "sun.glsl"
+float quickstep(float x)
+{
+    x = clamp(x, 0.0, 1.0);
+    x = 1.0 - x*x;
+    x = 1.0 - x*x;
+    return x;
+}
 
-#define getLight PointLights
+#if @useUBO
 
-struct PointLight
+const uint mask = uint(0xff);
+
+vec3 unpackRGB(float data)
+{
+    uint colors = uint(data);
+    return vec3( (((colors >>  0) & mask) / 255.0)
+                ,(((colors >>  8) & mask) / 255.0)
+                ,(((colors >> 16) & mask) / 255.0));
+}
+
+vec4 unpackRGBA(float data)
+{
+    uint colors = uint(data);
+    return vec4( (((colors >>  0) & mask) / 255.0)
+                ,(((colors >>  8) & mask) / 255.0)
+                ,(((colors >> 16) & mask) / 255.0)
+                ,(((colors >> 24) & mask) / 255.0));
+}
+
+struct LightData
+{
+    uvec4 packedColors;  // diffuse, ambient, specular
+    vec4 position;
+    vec4 attenuation;   // constant, linear, quadratic, radius
+};
+
+uniform int PointLightIndex[@maxLights];
+uniform int PointLightCount;
+
+layout(std140) uniform LightBufferBinding
+{
+    LightData LightBuffer[@maxLightsInScene];
+};
+
+#else
+
+struct LightData
 {
     vec4 position;
     vec4 diffuse;
     vec4 ambient;
-    float constantAttenuation;
-    float linearAttenuation;
-    float quadraticAttenuation;
-    float radius;
+    vec4 specular;
+    vec4 attenuation;   // constant, linear, quadratic, radius
 };
 
+uniform LightData LightBuffer[@maxLights];
 uniform int PointLightCount;
-uniform int PointLightIndex[@maxLights];
 
-layout(std140) uniform PointLightBuffer
-{
-    PointLight PointLights[@maxLightsInScene];
-};
+#endif
 
 #else
 #define getLight gl_LightSource
 #endif
 
 void perLightSun(out vec3 ambientOut, out vec3 diffuseOut, vec3 viewPos, vec3 viewNormal)
-{
-    vec3 lightDir = @sunDirection.xyz;
-    lightDir = normalize(lightDir);
-
-    ambientOut = @sunAmbient.xyz;
+{    
+    vec3 lightDir = normalize(getLight[0].position.xyz);
 
+#if @lightingModel == LIGHTING_MODEL_SINGLE_UBO
+    vec4 data = getLight[0].packedColors;
+    ambientOut = unpackRGB(data.y);
+    vec3 sunDiffuse = unpackRGB(data.x);
+#else
+    ambientOut = getLight[0].ambient.xyz;
+    vec3 sunDiffuse = getLight[0].diffuse.xyz;
+#endif
     float lambert = dot(viewNormal.xyz, lightDir);
 #ifndef GROUNDCOVER
     lambert = max(lambert, 0.0);
@@ -46,32 +93,33 @@ void perLightSun(out vec3 ambientOut, out vec3 diffuseOut, vec3 viewPos, vec3 vi
     }
     lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0);
 #endif
-    diffuseOut = @sunDiffuse.xyz * lambert;
+
+    diffuseOut = sunDiffuse * lambert;
 }
 
-
-uniform float osg_SimulationTime;
 void perLightPoint(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 viewPos, vec3 viewNormal)
 {
-    vec4 pos = getLight[lightIndex].position;
-    vec3 lightDir = pos.xyz - viewPos;
+    vec3 lightDir = getLight[lightIndex].position.xyz - viewPos;
 
     float lightDistance = length(lightDir);
     lightDir = normalize(lightDir);
 
+#if @ffpLighting
     float illumination = clamp(1.0 / (getLight[lightIndex].constantAttenuation + getLight[lightIndex].linearAttenuation * lightDistance + getLight[lightIndex].quadraticAttenuation * lightDistance * lightDistance), 0.0, 1.0);
-
-// Add an artificial cutoff, otherwise effected objects will be brightly lit and adjacent objects not effected by this light will be dark by contrast
-// This causes nasty artifacts, especially with active grid so it is necassary for now. 
-#if !@ffpLighting
-    float cutoff = getLight[lightIndex].radius * 0.5;
-    illumination *= 1.0 - smoothstep(0.0, 1.0, ((lightDistance / cutoff) - 1.0) * 0.887);
-    illumination = max(0.0, illumination);
+#else
+    float illumination = clamp(1.0 / (getLight[lightIndex].attenuation.x + getLight[lightIndex].attenuation.y * lightDistance + getLight[lightIndex].attenuation.z * lightDistance * lightDistance), 0.0, 1.0);
+    illumination *= 1.0 - quickstep((lightDistance * 0.887 / getLight[lightIndex].attenuation.w) - 0.887);
 #endif
 
+#if @useUBO
+    vec4 data = getLight[lightIndex].packedColors;
+    ambientOut = unpackRGB(data.y) * illumination;
+#else
     ambientOut = getLight[lightIndex].ambient.xyz * illumination;
+#endif
 
     float lambert = dot(viewNormal.xyz, lightDir) * illumination;
+    
 #ifndef GROUNDCOVER
     lambert = max(lambert, 0.0);
 #else
@@ -84,10 +132,10 @@ void perLightPoint(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec
     lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0);
 #endif
 
-#if @ffpLighting
-    diffuseOut = getLight[lightIndex].diffuse.xyz * lambert;
+#if @useUBO
+    diffuseOut = unpackRGB(data.x) * lambert;
 #else
-    diffuseOut = (getLight[lightIndex].diffuse.xyz * pos.w) * lambert;
+    diffuseOut = getLight[lightIndex].diffuse.xyz * lambert;
 #endif
 }
 
@@ -109,17 +157,22 @@ void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 a
 #endif
     ambientLight = gl_LightModel.ambient.xyz;
 
-#if !@ffpLighting
     perLightSun(ambientOut, diffuseOut, viewPos, viewNormal);
     ambientLight += ambientOut;
     diffuseLight += diffuseOut;
-    for (int i=0; i<PointLightCount; ++i)
-    {
-        perLightPoint(ambientOut, diffuseOut, PointLightIndex[i], viewPos, viewNormal);
-#else
-    for (int i=0; i<@maxLights; ++i)
+
+#if @lightingModel == LIGHTING_MODEL_FFP
+    for (int i=1; i < @maxLights; ++i)
     {
         perLightPoint(ambientOut, diffuseOut, i, viewPos, viewNormal);
+#elif @lightingModel == LIGHTING_MODEL_PER_OBJECT_UNIFORM
+    for (int i=1; i <= PointLightCount; ++i)
+    {
+        perLightPoint(ambientOut, diffuseOut, i, viewPos, viewNormal);
+#else 
+    for (int i=0; i < PointLightCount; ++i)
+    {
+        perLightPoint(ambientOut, diffuseOut, PointLightIndex[i], viewPos, viewNormal);
 #endif
         ambientLight += ambientOut;
         diffuseLight += diffuseOut;
@@ -128,11 +181,19 @@ void doLighting(vec3 viewPos, vec3 viewNormal, out vec3 diffuseLight, out vec3 a
 
 vec3 getSpecular(vec3 viewNormal, vec3 viewDirection, float shininess, vec3 matSpec)
 {
-    vec3 lightDir = normalize(@sunDirection.xyz);
+    vec3 sunDir = getLight[0].position.xyz;
+
+#if @lightingModel == LIGHTING_MODEL_SINGLE_UBO
+    vec3 sunSpec = unpackRGB(getLight[0].packedColors.z);
+#else
+    vec3 sunSpec = getLight[0].specular.xyz;
+#endif
+
+    vec3 lightDir = normalize(sunDir);
     float NdotL = dot(viewNormal, lightDir);
     if (NdotL <= 0.0)
         return vec3(0.0);
     vec3 halfVec = normalize(lightDir - viewDirection);
     float NdotH = dot(viewNormal, halfVec);
-    return pow(max(NdotH, 0.0), max(1e-4, shininess)) * @sunSpecular.xyz * matSpec;
+    return pow(max(NdotH, 0.0), max(1e-4, shininess)) * sunSpec * matSpec;
 }
diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl
index 459405b59..d3b7bfc4d 100644
--- a/files/shaders/objects_fragment.glsl
+++ b/files/shaders/objects_fragment.glsl
@@ -1,6 +1,12 @@
 #version 120
 
-#extension GL_ARB_uniform_buffer_object : enable
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 #if @diffuseMap
 uniform sampler2D diffuseMap;
@@ -70,6 +76,7 @@ varying vec3 passNormal;
 #include "shadows_fragment.glsl"
 #include "parallax.glsl"
 #include "lighting.glsl"
+#include "alpha.glsl"
 
 void main()
 {
@@ -111,10 +118,15 @@ void main()
 
 #if @diffuseMap
     gl_FragData[0] = texture2D(diffuseMap, adjustedDiffuseUV);
+    gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, adjustedDiffuseUV);
 #else
     gl_FragData[0] = vec4(1.0);
 #endif
 
+    vec4 diffuseColor = getDiffuseColor();
+    gl_FragData[0].a *= diffuseColor.a;
+    alphaTest();
+
 #if @detailMap
     gl_FragData[0].xyz *= texture2D(detailMap, detailMapUV).xyz * 2.0;
 #endif
@@ -153,9 +165,6 @@ void main()
 
 #endif
 
-    vec4 diffuseColor = getDiffuseColor();
-    gl_FragData[0].a *= diffuseColor.a;
-
     float shadowing = unshadowedLightRatio(linearDepth);
     vec3 lighting;
 #if !PER_PIXEL_LIGHTING
diff --git a/files/shaders/objects_vertex.glsl b/files/shaders/objects_vertex.glsl
index 63a831008..c1eae0722 100644
--- a/files/shaders/objects_vertex.glsl
+++ b/files/shaders/objects_vertex.glsl
@@ -1,6 +1,12 @@
 #version 120
 
-#extension GL_ARB_uniform_buffer_object : enable
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 #if @diffuseMap
 varying vec2 diffuseMapUV;
diff --git a/files/shaders/shadowcasting_fragment.glsl b/files/shaders/shadowcasting_fragment.glsl
index a5410d008..07fad047e 100644
--- a/files/shaders/shadowcasting_fragment.glsl
+++ b/files/shaders/shadowcasting_fragment.glsl
@@ -1,5 +1,9 @@
 #version 120
 
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
+
 uniform sampler2D diffuseMap;
 varying vec2 diffuseMapUV;
 
@@ -8,6 +12,8 @@ varying float alphaPassthrough;
 uniform bool useDiffuseMapForShadowAlpha;
 uniform bool alphaTestShadows;
 
+#include "alpha.glsl"
+
 void main()
 {
     gl_FragData[0].rgb = vec3(1.0);
@@ -16,7 +22,10 @@ void main()
     else
         gl_FragData[0].a = alphaPassthrough;
 
-    // Prevent translucent things casting shadow (including the player using an invisibility effect). For now, rely on the deprecated FF test for non-blended stuff.
+    alphaTest();
+
+    // Prevent translucent things casting shadow (including the player using an invisibility effect).
+    // This replaces alpha blending, which obviously doesn't work with depth buffers
     if (alphaTestShadows && gl_FragData[0].a <= 0.5)
         discard;
 }
diff --git a/files/shaders/sun.glsl b/files/shaders/sun.glsl
deleted file mode 100644
index f9f97af77..000000000
--- a/files/shaders/sun.glsl
+++ /dev/null
@@ -1,14 +0,0 @@
-#if !@ffpLighting
-struct Sunlight
-{
-    vec4 diffuse;
-    vec4 ambient;
-    vec4 specular;
-    vec4 direction;
-};
-
-layout(std140) uniform SunlightBuffer
-{
-    Sunlight Sun;
-};
-#endif
\ No newline at end of file
diff --git a/files/shaders/terrain_fragment.glsl b/files/shaders/terrain_fragment.glsl
index e567dccc3..a8bf73552 100644
--- a/files/shaders/terrain_fragment.glsl
+++ b/files/shaders/terrain_fragment.glsl
@@ -1,6 +1,12 @@
 #version 120
 
-#extension GL_ARB_uniform_buffer_object : enable
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 varying vec2 uv;
 
diff --git a/files/shaders/terrain_vertex.glsl b/files/shaders/terrain_vertex.glsl
index e5f2892ac..2bf74a0e1 100644
--- a/files/shaders/terrain_vertex.glsl
+++ b/files/shaders/terrain_vertex.glsl
@@ -1,6 +1,12 @@
 #version 120
 
-#extension GL_ARB_uniform_buffer_object : enable
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 varying vec2 uv;
 varying float euclideanDepth;
diff --git a/files/shaders/water_fragment.glsl b/files/shaders/water_fragment.glsl
index ba15a8669..c7f2c4657 100644
--- a/files/shaders/water_fragment.glsl
+++ b/files/shaders/water_fragment.glsl
@@ -1,6 +1,12 @@
 #version 120
 
-#extension GL_ARB_uniform_buffer_object : enable
+#if @useUBO
+    #extension GL_ARB_uniform_buffer_object : require
+#endif
+
+#if @useGPUShader4
+    #extension GL_EXT_gpu_shader4: require
+#endif
 
 #define REFRACTION @refraction_enabled
 
@@ -145,7 +151,7 @@ uniform vec3 nodePosition;
 uniform float rainIntensity;
 
 #include "shadows_fragment.glsl"
-#include "sun.glsl"
+#include "lighting.glsl"
 
 float frustumDepth;
 
@@ -195,7 +201,7 @@ void main(void)
                    normal3 * midWaves.y + normal4 * smallWaves.x + normal5 * smallWaves.y + rippleAdd);
     normal = normalize(vec3(-normal.x * bump, -normal.y * bump, normal.z));
 
-    vec3 lVec = normalize((gl_ModelViewMatrixInverse * vec4(@sunDirection.xyz, 0.0)).xyz);
+    vec3 lVec = normalize((gl_ModelViewMatrixInverse * vec4(getLight[0].position.xyz, 0.0)).xyz);
 
     vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz;
     vec3 vVec = normalize(position.xyz - cameraPos.xyz);
@@ -231,6 +237,12 @@ void main(void)
 
     vec3 waterColor = WATER_COLOR * sunFade;
 
+#if @lightingModel == LIGHTING_MODEL_SINGLE_UBO
+  vec4 sunSpec = unpackRGBA(getLight[0].packedColors.z);
+#else
+  vec4 sunSpec = getLight[0].specular;
+#endif
+
 #if REFRACTION
     // refraction
     vec3 refraction = texture2D(refractionMap, screenCoords - screenCoordsOffset).rgb;
@@ -250,11 +262,11 @@ void main(void)
     vec3 scatterColour = mix(SCATTER_COLOUR*vec3(1.0,0.4,0.0), SCATTER_COLOUR, clamp(1.0-exp(-sunHeight*SUN_EXT), 0.0, 1.0));
     vec3 lR = reflect(lVec, lNormal);
     float lightScatter = clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0) * SCATTER_AMOUNT * sunFade * clamp(1.0-exp(-sunHeight), 0.0, 1.0);
-    gl_FragData[0].xyz = mix( mix(refraction,  scatterColour,  lightScatter),  reflection,  fresnel) + specular * @sunSpecular.xyz + vec3(rainRipple.w) * 0.2;
+    gl_FragData[0].xyz = mix( mix(refraction,  scatterColour,  lightScatter),  reflection,  fresnel) + specular * sunSpec.xyz + vec3(rainRipple.w) * 0.2;
     gl_FragData[0].w = 1.0;
 #else
-    gl_FragData[0].xyz = mix(reflection,  waterColor,  (1.0-fresnel)*0.5) + specular * @sunSpecular.xyz + vec3(rainRipple.w) * 0.7;
-    gl_FragData[0].w = clamp(fresnel*6.0 + specular * @sunSpecular.w, 0.0, 1.0);     //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0);
+    gl_FragData[0].xyz = mix(reflection,  waterColor,  (1.0-fresnel)*0.5) + specular * sunSpec.xyz + vec3(rainRipple.w) * 0.7;
+    gl_FragData[0].w = clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0);     //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0);
 #endif
 
     // fog