diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
index 178e122c32..593a3c020b 100644
--- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Clement on 4/22/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -39,46 +40,46 @@ ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity)
 void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) {
     if (_stage) {
         if (!LightStage::isIndexInvalid(_sunIndex)) {
-            _stage->removeLight(_sunIndex);
+            _stage->removeElement(_sunIndex);
             _sunIndex = INVALID_INDEX;
         }
         if (!LightStage::isIndexInvalid(_ambientIndex)) {
-            _stage->removeLight(_ambientIndex);
+            _stage->removeElement(_ambientIndex);
             _ambientIndex = INVALID_INDEX;
         }
     }
 
     if (_backgroundStage) {
         if (!BackgroundStage::isIndexInvalid(_backgroundIndex)) {
-            _backgroundStage->removeBackground(_backgroundIndex);
+            _backgroundStage->removeElement(_backgroundIndex);
             _backgroundIndex = INVALID_INDEX;
         }
     }
 
     if (_hazeStage) {
         if (!HazeStage::isIndexInvalid(_hazeIndex)) {
-            _hazeStage->removeHaze(_hazeIndex);
+            _hazeStage->removeElement(_hazeIndex);
             _hazeIndex = INVALID_INDEX;
         }
     }
 
     if (_bloomStage) {
         if (!BloomStage::isIndexInvalid(_bloomIndex)) {
-            _bloomStage->removeBloom(_bloomIndex);
+            _bloomStage->removeElement(_bloomIndex);
             _bloomIndex = INVALID_INDEX;
         }
     }
 
     if (_tonemappingStage) {
         if (!TonemappingStage::isIndexInvalid(_tonemappingIndex)) {
-            _tonemappingStage->removeTonemapping(_tonemappingIndex);
+            _tonemappingStage->removeElement(_tonemappingIndex);
             _tonemappingIndex = INVALID_INDEX;
         }
     }
 
     if (_ambientOcclusionStage) {
         if (!AmbientOcclusionStage::isIndexInvalid(_ambientOcclusionIndex)) {
-            _ambientOcclusionStage->removeAmbientOcclusion(_ambientOcclusionIndex);
+            _ambientOcclusionStage->removeElement(_ambientOcclusionIndex);
             _ambientOcclusionIndex = INVALID_INDEX;
         }
     }
@@ -123,7 +124,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
     { // Sun 
         if (_needSunUpdate) {
             if (LightStage::isIndexInvalid(_sunIndex)) {
-                _sunIndex = _stage->addLight(_sunLight);
+                _sunIndex = _stage->addElement(_sunLight);
             } else {
                 _stage->updateLightArrayBuffer(_sunIndex);
             }
@@ -136,7 +137,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
 
         if (_needAmbientUpdate) {
             if (LightStage::isIndexInvalid(_ambientIndex)) {
-                _ambientIndex = _stage->addLight(_ambientLight);
+                _ambientIndex = _stage->addElement(_ambientLight);
             } else {
                 _stage->updateLightArrayBuffer(_ambientIndex);
             }
@@ -149,7 +150,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
 
         if (_needBackgroundUpdate) {
             if (BackgroundStage::isIndexInvalid(_backgroundIndex)) {
-                _backgroundIndex = _backgroundStage->addBackground(_background);
+                _backgroundIndex = _backgroundStage->addElement(_background);
             }
             _needBackgroundUpdate = false;
         }
@@ -158,7 +159,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
     {
         if (_needHazeUpdate) {
             if (HazeStage::isIndexInvalid(_hazeIndex)) {
-                _hazeIndex = _hazeStage->addHaze(_haze);
+                _hazeIndex = _hazeStage->addElement(_haze);
             }
             _needHazeUpdate = false;
         }
@@ -167,7 +168,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
     {
         if (_needBloomUpdate) {
             if (BloomStage::isIndexInvalid(_bloomIndex)) {
-                _bloomIndex = _bloomStage->addBloom(_bloom);
+                _bloomIndex = _bloomStage->addElement(_bloom);
             }
             _needBloomUpdate = false;
         }
@@ -176,7 +177,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
     {
         if (_needTonemappingUpdate) {
             if (TonemappingStage::isIndexInvalid(_tonemappingIndex)) {
-                _tonemappingIndex = _tonemappingStage->addTonemapping(_tonemapping);
+                _tonemappingIndex = _tonemappingStage->addElement(_tonemapping);
             }
             _needTonemappingUpdate = false;
         }
@@ -185,7 +186,7 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
     {
         if (_needAmbientOcclusionUpdate) {
             if (AmbientOcclusionStage::isIndexInvalid(_ambientOcclusionIndex)) {
-                _ambientOcclusionIndex = _ambientOcclusionStage->addAmbientOcclusion(_ambientOcclusion);
+                _ambientOcclusionIndex = _ambientOcclusionStage->addElement(_ambientOcclusion);
             }
             _needAmbientOcclusionUpdate = false;
         }
@@ -205,9 +206,9 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
         }
 
         if (_skyboxMode == COMPONENT_MODE_DISABLED) {
-            _backgroundStage->_currentFrame.pushBackground(INVALID_INDEX);
+            _backgroundStage->_currentFrame.pushElement(INVALID_INDEX);
         } else if (_skyboxMode == COMPONENT_MODE_ENABLED) {
-            _backgroundStage->_currentFrame.pushBackground(_backgroundIndex);
+            _backgroundStage->_currentFrame.pushElement(_backgroundIndex);
         }
 
         if (_ambientLightMode == COMPONENT_MODE_DISABLED) {
@@ -218,25 +219,25 @@ void ZoneEntityRenderer::doRender(RenderArgs* args) {
 
         // Haze only if the mode is not inherit, as the model deals with on/off
         if (_hazeMode != COMPONENT_MODE_INHERIT) {
-            _hazeStage->_currentFrame.pushHaze(_hazeIndex);
+            _hazeStage->_currentFrame.pushElement(_hazeIndex);
         }
 
         if (_bloomMode == COMPONENT_MODE_DISABLED) {
-            _bloomStage->_currentFrame.pushBloom(INVALID_INDEX);
+            _bloomStage->_currentFrame.pushElement(INVALID_INDEX);
         } else if (_bloomMode == COMPONENT_MODE_ENABLED) {
-            _bloomStage->_currentFrame.pushBloom(_bloomIndex);
+            _bloomStage->_currentFrame.pushElement(_bloomIndex);
         }
 
         if (_tonemappingMode == COMPONENT_MODE_DISABLED) {
-            _tonemappingStage->_currentFrame.pushTonemapping(0); // Use the fallback tonemapping for "off"
+            _tonemappingStage->_currentFrame.pushElement(0);  // Use the fallback tonemapping for "off"
         } else if (_tonemappingMode == COMPONENT_MODE_ENABLED) {
-            _tonemappingStage->_currentFrame.pushTonemapping(_tonemappingIndex);
+            _tonemappingStage->_currentFrame.pushElement(_tonemappingIndex);
         }
 
         if (_ambientOcclusionMode == COMPONENT_MODE_DISABLED) {
-            _ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(INVALID_INDEX);
+            _ambientOcclusionStage->_currentFrame.pushElement(INVALID_INDEX);
         } else if (_ambientOcclusionMode == COMPONENT_MODE_ENABLED) {
-            _ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(_ambientOcclusionIndex);
+            _ambientOcclusionStage->_currentFrame.pushElement(_ambientOcclusionIndex);
         }
     }
 
diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp
index ab1acc3c91..1a76777888 100644
--- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp
+++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Niraj Venkat on 7/15/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -606,8 +607,8 @@ void AmbientOcclusionEffect::run(const render::RenderContextPointer& renderConte
     graphics::AmbientOcclusionPointer ambientOcclusion;
     if (_debug) {
         ambientOcclusion = _debugAmbientOcclusion;
-    } else if (ambientOcclusionStage && ambientOcclusionFrame->_ambientOcclusions.size()) {
-        ambientOcclusion = ambientOcclusionStage->getAmbientOcclusion(ambientOcclusionFrame->_ambientOcclusions.front());
+    } else if (ambientOcclusionStage && ambientOcclusionFrame->_elements.size()) {
+        ambientOcclusion = ambientOcclusionStage->getElement(ambientOcclusionFrame->_elements.front());
     }
 
     if (!ambientOcclusion || !lightingModel->isAmbientOcclusionEnabled()) {
diff --git a/libraries/render-utils/src/AmbientOcclusionStage.cpp b/libraries/render-utils/src/AmbientOcclusionStage.cpp
index 6b3763a39c..55fa290ab7 100644
--- a/libraries/render-utils/src/AmbientOcclusionStage.cpp
+++ b/libraries/render-utils/src/AmbientOcclusionStage.cpp
@@ -7,50 +7,8 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
+
 #include "AmbientOcclusionStage.h"
 
-#include <gpu/Context.h>
-
-std::string AmbientOcclusionStage::_stageName { "AMBIENT_OCCLUSION_STAGE" };
-const AmbientOcclusionStage::Index AmbientOcclusionStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
-
-AmbientOcclusionStage::Index AmbientOcclusionStage::findAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) const {
-    auto found = _ambientOcclusionMap.find(ambientOcclusion);
-    if (found != _ambientOcclusionMap.end()) {
-        return INVALID_INDEX;
-    } else {
-        return (*found).second;
-    }
-}
-
-AmbientOcclusionStage::Index AmbientOcclusionStage::addAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) {
-    auto found = _ambientOcclusionMap.find(ambientOcclusion);
-    if (found == _ambientOcclusionMap.end()) {
-        auto ambientOcclusionId = _ambientOcclusions.newElement(ambientOcclusion);
-        // Avoid failing to allocate a ambientOcclusion, just pass
-        if (ambientOcclusionId != INVALID_INDEX) {
-            // Insert the ambientOcclusion and its index in the reverse map
-            _ambientOcclusionMap.insert(AmbientOcclusionMap::value_type(ambientOcclusion, ambientOcclusionId));
-        }
-        return ambientOcclusionId;
-    } else {
-        return (*found).second;
-    }
-}
-
-AmbientOcclusionStage::AmbientOcclusionPointer AmbientOcclusionStage::removeAmbientOcclusion(Index index) {
-    AmbientOcclusionPointer removed = _ambientOcclusions.freeElement(index);
-    if (removed) {
-        _ambientOcclusionMap.erase(removed);
-    }
-    return removed;
-}
-
-AmbientOcclusionStageSetup::AmbientOcclusionStageSetup() {}
-
-void AmbientOcclusionStageSetup::run(const render::RenderContextPointer& renderContext) {
-    auto stage = renderContext->_scene->getStage(AmbientOcclusionStage::getName());
-    if (!stage) {
-        renderContext->_scene->resetStage(AmbientOcclusionStage::getName(), std::make_shared<AmbientOcclusionStage>());
-    }
-}
+template <>
+std::string render::PointerStage<graphics::AmbientOcclusion, graphics::AmbientOcclusionPointer>::_name { "AMBIENT_OCCLUSION_STAGE" };
diff --git a/libraries/render-utils/src/AmbientOcclusionStage.h b/libraries/render-utils/src/AmbientOcclusionStage.h
index d5dee344ba..1b54a0828e 100644
--- a/libraries/render-utils/src/AmbientOcclusionStage.h
+++ b/libraries/render-utils/src/AmbientOcclusionStage.h
@@ -11,74 +11,17 @@
 #ifndef hifi_render_utils_AmbientOcclusionStage_h
 #define hifi_render_utils_AmbientOcclusionStage_h
 
-#include <graphics/Stage.h>
-#include <set>
-#include <unordered_map>
-#include <render/IndexedContainer.h>
-#include <render/Stage.h>
-
-#include <render/Forward.h>
-#include <render/DrawTask.h>
 #include <graphics/AmbientOcclusion.h>
+#include <render/Stage.h>
+#include <render/StageSetup.h>
 
 // AmbientOcclusion stage to set up ambientOcclusion-related rendering tasks
-class AmbientOcclusionStage : public render::Stage {
-public:
-    static std::string _stageName;
-    static const std::string& getName() { return _stageName; }
-
-    using Index = render::indexed_container::Index;
-    static const Index INVALID_INDEX;
-    static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-    
-    using AmbientOcclusionPointer = graphics::AmbientOcclusionPointer;
-    using AmbientOcclusions = render::indexed_container::IndexedPointerVector<graphics::AmbientOcclusion>;
-    using AmbientOcclusionMap = std::unordered_map<AmbientOcclusionPointer, Index>;
-
-    using AmbientOcclusionIndices = std::vector<Index>;
-
-    Index findAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion) const;
-    Index addAmbientOcclusion(const AmbientOcclusionPointer& ambientOcclusion);
-
-    AmbientOcclusionPointer removeAmbientOcclusion(Index index);
-    
-    bool checkAmbientOcclusionId(Index index) const { return _ambientOcclusions.checkIndex(index); }
-
-    Index getNumAmbientOcclusions() const { return _ambientOcclusions.getNumElements(); }
-    Index getNumFreeAmbientOcclusions() const { return _ambientOcclusions.getNumFreeIndices(); }
-    Index getNumAllocatedAmbientOcclusions() const { return _ambientOcclusions.getNumAllocatedIndices(); }
-
-    AmbientOcclusionPointer getAmbientOcclusion(Index ambientOcclusionId) const {
-        return _ambientOcclusions.get(ambientOcclusionId);
-    }
-
-    AmbientOcclusions _ambientOcclusions;
-    AmbientOcclusionMap _ambientOcclusionMap;
-
-    class Frame {
-    public:
-        Frame() {}
-        
-        void clear() { _ambientOcclusions.clear(); }
-
-        void pushAmbientOcclusion(AmbientOcclusionStage::Index index) { _ambientOcclusions.emplace_back(index); }
-
-        AmbientOcclusionStage::AmbientOcclusionIndices _ambientOcclusions;
-    };
-    using FramePointer = std::shared_ptr<Frame>;
-    
-    Frame _currentFrame;
-};
+class AmbientOcclusionStage : public render::PointerStage<graphics::AmbientOcclusion, graphics::AmbientOcclusionPointer> {};
 using AmbientOcclusionStagePointer = std::shared_ptr<AmbientOcclusionStage>;
 
-class AmbientOcclusionStageSetup {
+class AmbientOcclusionStageSetup : public render::StageSetup<AmbientOcclusionStage> {
 public:
     using JobModel = render::Job::Model<AmbientOcclusionStageSetup>;
-
-    AmbientOcclusionStageSetup();
-    void run(const render::RenderContextPointer& renderContext);
-
-protected:
 };
 
 #endif
diff --git a/libraries/render-utils/src/AssembleLightingStageTask.cpp b/libraries/render-utils/src/AssembleLightingStageTask.cpp
index bc040582bd..0646f77d51 100644
--- a/libraries/render-utils/src/AssembleLightingStageTask.cpp
+++ b/libraries/render-utils/src/AssembleLightingStageTask.cpp
@@ -1,6 +1,7 @@
 //
 //  Created by Samuel Gateau on 2018/12/06
 //  Copyright 2013-2018 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
diff --git a/libraries/render-utils/src/BackgroundStage.cpp b/libraries/render-utils/src/BackgroundStage.cpp
index 455f356a45..f3f287bdac 100644
--- a/libraries/render-utils/src/BackgroundStage.cpp
+++ b/libraries/render-utils/src/BackgroundStage.cpp
@@ -3,58 +3,20 @@
 //
 //  Created by Sam Gateau on 5/9/2017.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
+
 #include "BackgroundStage.h"
 
 #include "DeferredLightingEffect.h"
 
-#include <gpu/Context.h>
-
 #include <graphics/ShaderConstants.h>
 
-std::string BackgroundStage::_stageName { "BACKGROUND_STAGE" };
-const BackgroundStage::Index BackgroundStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
-
-BackgroundStage::Index BackgroundStage::findBackground(const BackgroundPointer& background) const {
-    auto found = _backgroundMap.find(background);
-    if (found != _backgroundMap.end()) {
-        return INVALID_INDEX;
-    } else {
-        return (*found).second;
-    }
-
-}
-
-BackgroundStage::Index BackgroundStage::addBackground(const BackgroundPointer& background) {
-
-    auto found = _backgroundMap.find(background);
-    if (found == _backgroundMap.end()) {
-        auto backgroundId = _backgrounds.newElement(background);
-        // Avoid failing to allocate a background, just pass
-        if (backgroundId != INVALID_INDEX) {
-
-            // Insert the background and its index in the reverse map
-            _backgroundMap.insert(BackgroundMap::value_type(background, backgroundId));
-        }
-        return backgroundId;
-    } else {
-        return (*found).second;
-    }
-}
-
-
-BackgroundStage::BackgroundPointer BackgroundStage::removeBackground(Index index) {
-    BackgroundPointer removed = _backgrounds.freeElement(index);
-    
-    if (removed) {
-        _backgroundMap.erase(removed);
-    }
-    return removed;
-}
-
+template <>
+std::string render::PointerStage<graphics::SunSkyStage, graphics::SunSkyStagePointer>::_name { "BACKGROUND_STAGE" };
 
 void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) {
     const auto& lightingModel = inputs.get0();
@@ -66,8 +28,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext,
     const auto& backgroundStage = renderContext->_scene->getStage<BackgroundStage>();
     const auto& backgroundFrame = inputs.get1();
     graphics::SkyboxPointer skybox;
-    if (backgroundStage && backgroundFrame->_backgrounds.size()) {
-        const auto& background = backgroundStage->getBackground(backgroundFrame->_backgrounds.front());
+    if (backgroundStage && backgroundFrame->_elements.size()) {
+        const auto& background = backgroundStage->getElement(backgroundFrame->_elements.front());
         if (background) {
             skybox = background->getSkybox();
         }   
@@ -98,8 +60,8 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext,
             // If we're using forward rendering, we need to calculate haze
             if (args->_renderMethod == render::Args::RenderMethod::FORWARD) {
                 const auto& hazeStage = args->_scene->getStage<HazeStage>();
-                if (hazeStage && hazeFrame->_hazes.size() > 0) {
-                    const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front());
+                if (hazeStage && hazeFrame->_elements.size() > 0) {
+                    const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front());
                     if (hazePointer) {
                         batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer());
                     }
@@ -111,14 +73,3 @@ void DrawBackgroundStage::run(const render::RenderContextPointer& renderContext,
         args->_batch = nullptr;
     }
 }
-
-BackgroundStageSetup::BackgroundStageSetup() {
-}
-
-void BackgroundStageSetup::run(const render::RenderContextPointer& renderContext) {
-    auto stage = renderContext->_scene->getStage(BackgroundStage::getName());
-    if (!stage) {
-        renderContext->_scene->resetStage(BackgroundStage::getName(), std::make_shared<BackgroundStage>());
-    }
-}
-
diff --git a/libraries/render-utils/src/BackgroundStage.h b/libraries/render-utils/src/BackgroundStage.h
index 3015b721b1..4da8fbf9fb 100644
--- a/libraries/render-utils/src/BackgroundStage.h
+++ b/libraries/render-utils/src/BackgroundStage.h
@@ -1,8 +1,9 @@
 //
 //  BackgroundStage.h
-
+//
 //  Created by Sam Gateau on 5/9/2017.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -12,72 +13,19 @@
 #define hifi_render_utils_BackgroundStage_h
 
 #include <graphics/Stage.h>
-#include <set>
-#include <unordered_map>
-#include <render/IndexedContainer.h>
 #include <render/Stage.h>
-#include "HazeStage.h"
+#include <render/StageSetup.h>
 
+#include "HazeStage.h"
 #include "LightingModel.h"
 
-
 // Background stage to set up background-related rendering tasks
-class BackgroundStage : public render::Stage {
-public:
-    static std::string _stageName;
-    static const std::string& getName() { return _stageName; }
-
-    using Index = render::indexed_container::Index;
-    static const Index INVALID_INDEX;
-    static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-    
-    using BackgroundPointer = graphics::SunSkyStagePointer;
-    using Backgrounds = render::indexed_container::IndexedPointerVector<graphics::SunSkyStage>;
-    using BackgroundMap = std::unordered_map<BackgroundPointer, Index>;
-
-    using BackgroundIndices = std::vector<Index>;
-
-
-    Index findBackground(const BackgroundPointer& background) const;
-    Index addBackground(const BackgroundPointer& background);
-
-    BackgroundPointer removeBackground(Index index);
-    
-    bool checkBackgroundId(Index index) const { return _backgrounds.checkIndex(index); }
-
-    Index getNumBackgrounds() const { return _backgrounds.getNumElements(); }
-    Index getNumFreeBackgrounds() const { return _backgrounds.getNumFreeIndices(); }
-    Index getNumAllocatedBackgrounds() const { return _backgrounds.getNumAllocatedIndices(); }
-
-    BackgroundPointer getBackground(Index backgroundId) const {
-        return _backgrounds.get(backgroundId);
-    }
-
-    Backgrounds _backgrounds;
-    BackgroundMap _backgroundMap;
-
-    class Frame {
-    public:
-        Frame() {}
-        
-        void clear() { _backgrounds.clear(); }
-
-        void pushBackground(BackgroundStage::Index index) { _backgrounds.emplace_back(index); }
-
-        BackgroundStage::BackgroundIndices _backgrounds;
-    };
-    using FramePointer = std::shared_ptr<Frame>;
-    
-    Frame _currentFrame;
-};
+class BackgroundStage : public render::PointerStage<graphics::SunSkyStage, graphics::SunSkyStagePointer> {};
 using BackgroundStagePointer = std::shared_ptr<BackgroundStage>;
 
-class BackgroundStageSetup {
+class BackgroundStageSetup : public render::StageSetup<BackgroundStage> {
 public:
     using JobModel = render::Job::Model<BackgroundStageSetup>;
-
-    BackgroundStageSetup();
-    void run(const render::RenderContextPointer& renderContext);
 };
 
 class DrawBackgroundStage {
diff --git a/libraries/render-utils/src/BloomEffect.cpp b/libraries/render-utils/src/BloomEffect.cpp
index d27403440c..763f12cf0f 100644
--- a/libraries/render-utils/src/BloomEffect.cpp
+++ b/libraries/render-utils/src/BloomEffect.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Olivier Prat on 09/25/17.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -43,8 +44,8 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons
     const auto lightingModel = inputs.get3();
     const auto& bloomStage = renderContext->_scene->getStage<BloomStage>();
     graphics::BloomPointer bloom;
-    if (bloomStage && bloomFrame->_blooms.size()) {
-        bloom = bloomStage->getBloom(bloomFrame->_blooms.front());
+    if (bloomStage && bloomFrame->_elements.size()) {
+        bloom = bloomStage->getElement(bloomFrame->_elements.front());
     }
     if (!bloom || (lightingModel && !lightingModel->isBloomEnabled())) {
         renderContext->taskFlow.abortTask();
diff --git a/libraries/render-utils/src/BloomStage.cpp b/libraries/render-utils/src/BloomStage.cpp
index 2bedfeea96..23ec4596bd 100644
--- a/libraries/render-utils/src/BloomStage.cpp
+++ b/libraries/render-utils/src/BloomStage.cpp
@@ -3,56 +3,13 @@
 //
 //  Created by Sam Gondelman on 8/7/2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
+
 #include "BloomStage.h"
 
-#include "DeferredLightingEffect.h"
-
-#include <gpu/Context.h>
-
-std::string BloomStage::_stageName { "BLOOM_STAGE" };
-const BloomStage::Index BloomStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
-
-BloomStage::Index BloomStage::findBloom(const BloomPointer& bloom) const {
-    auto found = _bloomMap.find(bloom);
-    if (found != _bloomMap.end()) {
-        return INVALID_INDEX;
-    } else {
-        return (*found).second;
-    }
-}
-
-BloomStage::Index BloomStage::addBloom(const BloomPointer& bloom) {
-    auto found = _bloomMap.find(bloom);
-    if (found == _bloomMap.end()) {
-        auto bloomId = _blooms.newElement(bloom);
-        // Avoid failing to allocate a bloom, just pass
-        if (bloomId != INVALID_INDEX) {
-            // Insert the bloom and its index in the reverse map
-            _bloomMap.insert(BloomMap::value_type(bloom, bloomId));
-        }
-        return bloomId;
-    } else {
-        return (*found).second;
-    }
-}
-
-BloomStage::BloomPointer BloomStage::removeBloom(Index index) {
-    BloomPointer removed = _blooms.freeElement(index);
-    if (removed) {
-        _bloomMap.erase(removed);
-    }
-    return removed;
-}
-
-BloomStageSetup::BloomStageSetup() {}
-
-void BloomStageSetup::run(const render::RenderContextPointer& renderContext) {
-    auto stage = renderContext->_scene->getStage(BloomStage::getName());
-    if (!stage) {
-        renderContext->_scene->resetStage(BloomStage::getName(), std::make_shared<BloomStage>());
-    }
-}
+template <>
+std::string render::PointerStage<graphics::Bloom, graphics::BloomPointer>::_name { "BLOOM_STAGE" };
diff --git a/libraries/render-utils/src/BloomStage.h b/libraries/render-utils/src/BloomStage.h
index bb03e181af..37e72bacea 100644
--- a/libraries/render-utils/src/BloomStage.h
+++ b/libraries/render-utils/src/BloomStage.h
@@ -3,6 +3,7 @@
 
 //  Created by Sam Gondelman on 8/7/2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -11,74 +12,17 @@
 #ifndef hifi_render_utils_BloomStage_h
 #define hifi_render_utils_BloomStage_h
 
-#include <graphics/Stage.h>
-#include <set>
-#include <unordered_map>
-#include <render/IndexedContainer.h>
-#include <render/Stage.h>
-
-#include <render/Forward.h>
-#include <render/DrawTask.h>
 #include <graphics/Bloom.h>
+#include <render/Stage.h>
+#include <render/StageSetup.h>
 
 // Bloom stage to set up bloom-related rendering tasks
-class BloomStage : public render::Stage {
-public:
-    static std::string _stageName;
-    static const std::string& getName() { return _stageName; }
-
-    using Index = render::indexed_container::Index;
-    static const Index INVALID_INDEX;
-    static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-    
-    using BloomPointer = graphics::BloomPointer;
-    using Blooms = render::indexed_container::IndexedPointerVector<graphics::Bloom>;
-    using BloomMap = std::unordered_map<BloomPointer, Index>;
-
-    using BloomIndices = std::vector<Index>;
-
-    Index findBloom(const BloomPointer& bloom) const;
-    Index addBloom(const BloomPointer& bloom);
-
-    BloomPointer removeBloom(Index index);
-    
-    bool checkBloomId(Index index) const { return _blooms.checkIndex(index); }
-
-    Index getNumBlooms() const { return _blooms.getNumElements(); }
-    Index getNumFreeBlooms() const { return _blooms.getNumFreeIndices(); }
-    Index getNumAllocatedBlooms() const { return _blooms.getNumAllocatedIndices(); }
-
-    BloomPointer getBloom(Index bloomId) const {
-        return _blooms.get(bloomId);
-    }
-
-    Blooms _blooms;
-    BloomMap _bloomMap;
-
-    class Frame {
-    public:
-        Frame() {}
-        
-        void clear() { _blooms.clear(); }
-
-        void pushBloom(BloomStage::Index index) { _blooms.emplace_back(index); }
-
-        BloomStage::BloomIndices _blooms;
-    };
-    using FramePointer = std::shared_ptr<Frame>;
-    
-    Frame _currentFrame;
-};
+class BloomStage : public render::PointerStage<graphics::Bloom, graphics::BloomPointer> {};
 using BloomStagePointer = std::shared_ptr<BloomStage>;
 
-class BloomStageSetup {
+class BloomStageSetup : public render::StageSetup<BloomStage> {
 public:
     using JobModel = render::Job::Model<BloomStageSetup>;
-
-    BloomStageSetup();
-    void run(const render::RenderContextPointer& renderContext);
-
-protected:
 };
 
 #endif
diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp
index 0ff1c435e1..f2f6639f88 100644
--- a/libraries/render-utils/src/DeferredLightingEffect.cpp
+++ b/libraries/render-utils/src/DeferredLightingEffect.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Andrzej Kapolka on 9/11/14.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -375,7 +376,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext,
         // Global directional light, maybe shadow and ambient pass
         auto lightStage = renderContext->_scene->getStage<LightStage>();
         assert(lightStage);
-        assert(lightStage->getNumLights() > 0);
+        assert(lightStage->getNumElements() > 0);
         auto keyLight = lightStage->getCurrentKeyLight(*lightFrame);
 
         // Check if keylight casts shadows
@@ -391,7 +392,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext,
         // Global Ambient light
         graphics::LightPointer ambientLight;
         if (lightStage && lightFrame->_ambientLights.size()) {
-            ambientLight = lightStage->getLight(lightFrame->_ambientLights.front());
+            ambientLight = lightStage->getElement(lightFrame->_ambientLights.front());
         }
         bool hasAmbientMap = (ambientLight != nullptr);
 
@@ -430,8 +431,8 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext,
 
         // Haze
         const auto& hazeStage = args->_scene->getStage<HazeStage>();
-        if (hazeStage && hazeFrame->_hazes.size() > 0) {
-            const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front());
+        if (hazeStage && hazeFrame->_elements.size() > 0) {
+            const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front());
             if (hazePointer) {
                 batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer());
             }
@@ -636,7 +637,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) {
 
             // Add the global light to the light stage (for later shadow rendering)
             // Set this light to be the default
-            _defaultLightID = lightStage->addLight(lp, true);
+            _defaultLightID = lightStage->addElement(lp, true);
         }
 
         auto backgroundStage = renderContext->_scene->getStage<BackgroundStage>();
@@ -649,7 +650,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) {
             _defaultBackground = background;
 
             // Add the global light to the light stage (for later shadow rendering)
-            _defaultBackgroundID = backgroundStage->addBackground(_defaultBackground);
+            _defaultBackgroundID = backgroundStage->addElement(_defaultBackground);
         }
     }
 
@@ -659,7 +660,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) {
             auto haze = std::make_shared<graphics::Haze>();
 
             _defaultHaze = haze;
-            _defaultHazeID = hazeStage->addHaze(_defaultHaze);
+            _defaultHazeID = hazeStage->addElement(_defaultHaze);
         }
     }
 
@@ -669,7 +670,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) {
             auto tonemapping = std::make_shared<graphics::Tonemapping>();
 
             _defaultTonemapping = tonemapping;
-            _defaultTonemappingID = tonemappingStage->addTonemapping(_defaultTonemapping);
+            _defaultTonemappingID = tonemappingStage->addElement(_defaultTonemapping);
         }
     }
 }
diff --git a/libraries/render-utils/src/DrawHaze.cpp b/libraries/render-utils/src/DrawHaze.cpp
index 134f12174b..f7e62d75f4 100644
--- a/libraries/render-utils/src/DrawHaze.cpp
+++ b/libraries/render-utils/src/DrawHaze.cpp
@@ -40,8 +40,8 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu
     const auto hazeFrame = inputs.get0();
     const auto& hazeStage = renderContext->args->_scene->getStage<HazeStage>();
     graphics::HazePointer haze;
-    if (hazeStage && hazeFrame->_hazes.size() > 0) {
-        haze = hazeStage->getHaze(hazeFrame->_hazes.front());
+    if (hazeStage && hazeFrame->_elements.size() > 0) {
+        haze = hazeStage->getElement(hazeFrame->_elements.front());
     }
 
     if (!haze) {
diff --git a/libraries/render-utils/src/FadeEffect.cpp b/libraries/render-utils/src/FadeEffect.cpp
index 88018924d8..1d25c0a372 100644
--- a/libraries/render-utils/src/FadeEffect.cpp
+++ b/libraries/render-utils/src/FadeEffect.cpp
@@ -3,6 +3,7 @@
 
 //  Created by Olivier Prat on 17/07/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -45,7 +46,7 @@ render::ShapePipeline::ItemSetter FadeEffect::getItemUniformSetter() {
             const auto& scene = args->_scene;
             const auto& batch = args->_batch;
             auto transitionStage = scene->getStage<render::TransitionStage>();
-            auto& transitionState = transitionStage->getTransition(item.getTransitionId());
+            auto& transitionState = transitionStage->getElement(item.getTransitionId());
 
             if (transitionState.paramsBuffer._size != sizeof(gpu::StructBuffer<FadeObjectParams>)) {
                 static_assert(sizeof(transitionState.paramsBuffer) == sizeof(gpu::StructBuffer<FadeObjectParams>), "Assuming gpu::StructBuffer is a helper class for gpu::BufferView");
diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp
index 122b543c05..72edbacbf0 100644
--- a/libraries/render-utils/src/FadeEffectJobs.cpp
+++ b/libraries/render-utils/src/FadeEffectJobs.cpp
@@ -3,6 +3,7 @@
 
 //  Created by Olivier Prat on 07/07/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -572,7 +573,7 @@ void FadeJob::run(const render::RenderContextPointer& renderContext, FadeJob::Ou
 
     // And now update fade effect
     for (auto transitionId : *transitionStage) {
-        auto& state = transitionStage->editTransition(transitionId);
+        auto& state = transitionStage->editElement(transitionId);
 #ifdef DEBUG
         auto& item = scene->getItem(state.itemId);
         assert(item.getTransitionId() == transitionId);
diff --git a/libraries/render-utils/src/HazeStage.cpp b/libraries/render-utils/src/HazeStage.cpp
index 9251e1e2f9..49ed7a1ea4 100644
--- a/libraries/render-utils/src/HazeStage.cpp
+++ b/libraries/render-utils/src/HazeStage.cpp
@@ -3,59 +3,13 @@
 //
 //  Created by Nissim Hadar on 9/26/2017.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
+
 #include "HazeStage.h"
 
-#include "DeferredLightingEffect.h"
-
-#include <gpu/Context.h>
-
-std::string HazeStage::_stageName { "HAZE_STAGE" };
-const HazeStage::Index HazeStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
-
-HazeStage::Index HazeStage::findHaze(const HazePointer& haze) const {
-    auto found = _hazeMap.find(haze);
-    if (found != _hazeMap.end()) {
-        return INVALID_INDEX;
-    } else {
-        return (*found).second;
-    }
-}
-
-HazeStage::Index HazeStage::addHaze(const HazePointer& haze) {
-    auto found = _hazeMap.find(haze);
-    if (found == _hazeMap.end()) {
-        auto hazeId = _hazes.newElement(haze);
-        // Avoid failing to allocate a haze, just pass
-        if (hazeId != INVALID_INDEX) {
-
-            // Insert the haze and its index in the reverse map
-            _hazeMap.insert(HazeMap::value_type(haze, hazeId));
-        }
-        return hazeId;
-    } else {
-        return (*found).second;
-    }
-}
-
-HazeStage::HazePointer HazeStage::removeHaze(Index index) {
-    HazePointer removed = _hazes.freeElement(index);
-    
-    if (removed) {
-        _hazeMap.erase(removed);
-    }
-    return removed;
-}
-
-HazeStageSetup::HazeStageSetup() {
-}
-
-void HazeStageSetup::run(const render::RenderContextPointer& renderContext) {
-    auto stage = renderContext->_scene->getStage(HazeStage::getName());
-    if (!stage) {
-        renderContext->_scene->resetStage(HazeStage::getName(), std::make_shared<HazeStage>());
-    }
-}
\ No newline at end of file
+template <>
+std::string render::PointerStage<graphics::Haze, graphics::HazePointer>::_name { "HAZE_STAGE" };
diff --git a/libraries/render-utils/src/HazeStage.h b/libraries/render-utils/src/HazeStage.h
index b1c7d0384c..70a33b27eb 100644
--- a/libraries/render-utils/src/HazeStage.h
+++ b/libraries/render-utils/src/HazeStage.h
@@ -3,6 +3,7 @@
 
 //  Created by Nissim Hadar on 9/26/2017.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -11,74 +12,17 @@
 #ifndef hifi_render_utils_HazeStage_h
 #define hifi_render_utils_HazeStage_h
 
-#include <graphics/Stage.h>
-#include <set>
-#include <unordered_map>
-#include <render/IndexedContainer.h>
-#include <render/Stage.h>
-
-#include <render/Forward.h>
-#include <render/DrawTask.h>
 #include <graphics/Haze.h>
+#include <render/Stage.h>
+#include <render/StageSetup.h>
 
 // Haze stage to set up haze-related rendering tasks
-class HazeStage : public render::Stage {
-public:
-    static std::string _stageName;
-    static const std::string& getName() { return _stageName; }
-
-    using Index = render::indexed_container::Index;
-    static const Index INVALID_INDEX;
-    static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-    
-    using HazePointer = graphics::HazePointer;
-    using Hazes = render::indexed_container::IndexedPointerVector<graphics::Haze>;
-    using HazeMap = std::unordered_map<HazePointer, Index>;
-
-    using HazeIndices = std::vector<Index>;
-
-    Index findHaze(const HazePointer& haze) const;
-    Index addHaze(const HazePointer& haze);
-
-    HazePointer removeHaze(Index index);
-    
-    bool checkHazeId(Index index) const { return _hazes.checkIndex(index); }
-
-    Index getNumHazes() const { return _hazes.getNumElements(); }
-    Index getNumFreeHazes() const { return _hazes.getNumFreeIndices(); }
-    Index getNumAllocatedHazes() const { return _hazes.getNumAllocatedIndices(); }
-
-    HazePointer getHaze(Index hazeId) const {
-        return _hazes.get(hazeId);
-    }
-
-    Hazes _hazes;
-    HazeMap _hazeMap;
-
-    class Frame {
-    public:
-        Frame() {}
-        
-        void clear() { _hazes.clear(); }
-
-        void pushHaze(HazeStage::Index index) { _hazes.emplace_back(index); }
-
-        HazeStage::HazeIndices _hazes;
-    };
-    using FramePointer = std::shared_ptr<Frame>;
-    
-    Frame _currentFrame;
-};
+class HazeStage : public render::PointerStage<graphics::Haze, graphics::HazePointer> {};
 using HazeStagePointer = std::shared_ptr<HazeStage>;
 
-class HazeStageSetup {
+class HazeStageSetup : public render::StageSetup<HazeStage> {
 public:
     using JobModel = render::Job::Model<HazeStageSetup>;
-
-    HazeStageSetup();
-    void run(const render::RenderContextPointer& renderContext);
-
-protected:
 };
 
 class FetchHazeConfig : public render::Job::Config {
diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp
index 2525427b61..755d0a60be 100644
--- a/libraries/render-utils/src/HighlightEffect.cpp
+++ b/libraries/render-utils/src/HighlightEffect.cpp
@@ -161,7 +161,7 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c
 
     if (!inShapes.empty() && !render::HighlightStage::isIndexInvalid(highlightId)) {
         auto resources = inputs.get1();
-        auto& highlight = highlightStage->getHighlight(highlightId);
+        auto& highlight = highlightStage->getElement(highlightId);
 
         RenderArgs* args = renderContext->args;
 
@@ -247,7 +247,7 @@ void DrawHighlight::run(const render::RenderContextPointer& renderContext, const
             auto highlightStage = renderContext->_scene->getStage<render::HighlightStage>();
             auto highlightId = _sharedParameters->_highlightIds[_highlightPassIndex];
             if (!render::HighlightStage::isIndexInvalid(highlightId)) {
-                auto& highlight = highlightStage->getHighlight(highlightId);
+                auto& highlight = highlightStage->getElement(highlightId);
                 auto pipeline = getPipeline(highlight._style);
                 {
                     auto& shaderParameters = _configuration.edit();
@@ -432,7 +432,7 @@ void SelectionToHighlight::run(const render::RenderContextPointer& renderContext
     auto highlightList = highlightStage->getActiveHighlightIds();
 
     for (auto styleId : highlightList) {
-        auto highlight = highlightStage->getHighlight(styleId);
+        auto highlight = highlightStage->getElement(styleId);
 
         if (!scene->isSelectionEmpty(highlight._selectionName)) {
             auto highlightId = highlightStage->getHighlightIdBySelection(highlight._selectionName);
@@ -572,8 +572,8 @@ void HighlightCleanup::run(const render::RenderContextPointer& renderContext, co
     auto highlightStage = scene->getStage<render::HighlightStage>();
 
     for (auto index : inputs.get0()) {
-        std::string selectionName = highlightStage->getHighlight(index)._selectionName;
-        highlightStage->removeHighlight(index);
+        std::string selectionName = highlightStage->getElement(index)._selectionName;
+        highlightStage->removeElement(index);
         scene->removeSelection(selectionName);
     }
 
diff --git a/libraries/render-utils/src/LightClusters.cpp b/libraries/render-utils/src/LightClusters.cpp
index 3425e08783..f0ec35238f 100644
--- a/libraries/render-utils/src/LightClusters.cpp
+++ b/libraries/render-utils/src/LightClusters.cpp
@@ -348,7 +348,7 @@ glm::ivec3 LightClusters::updateClusters() {
     uint32_t numClusteredLights = 0;
     for (size_t lightNum = 1; lightNum < _visibleLightIndices.size(); ++lightNum) {
         auto lightId = _visibleLightIndices[lightNum];
-        auto light = _lightStage->getLight(lightId);
+        auto light = _lightStage->getElement(lightId);
         if (!light) {
             continue;
         }
@@ -567,9 +567,9 @@ void LightClusteringPass::run(const render::RenderContextPointer& renderContext,
     output = _lightClusters;
 
     auto config = std::static_pointer_cast<Config>(renderContext->jobConfig);
-    config->numSceneLights = lightStage->getNumLights();
-    config->numFreeSceneLights = lightStage->getNumFreeLights();
-    config->numAllocatedSceneLights = lightStage->getNumAllocatedLights();
+    config->numSceneLights = lightStage->getNumElements();
+    config->numFreeSceneLights = lightStage->getNumFreeElements();
+    config->numAllocatedSceneLights = lightStage->getNumAllocatedElements();
     config->setNumInputLights(clusteringStats.x);
     config->setNumClusteredLights(clusteringStats.y);
     config->setNumClusteredLightReferences(clusteringStats.z);
diff --git a/libraries/render-utils/src/LightClusters.h b/libraries/render-utils/src/LightClusters.h
index 6192105914..94e1e37ae3 100644
--- a/libraries/render-utils/src/LightClusters.h
+++ b/libraries/render-utils/src/LightClusters.h
@@ -95,7 +95,7 @@ public:
 
     FrustumGrid::Planes _gridPlanes[3];
 
-    LightStage::LightIndices _visibleLightIndices;
+    render::ElementIndices _visibleLightIndices;
     gpu::BufferView _lightIndicesBuffer;
 
     const uint32_t EMPTY_CLUSTER { 0x0000FFFF };
diff --git a/libraries/render-utils/src/LightPayload.cpp b/libraries/render-utils/src/LightPayload.cpp
index d1018982d0..43280c9a2a 100644
--- a/libraries/render-utils/src/LightPayload.cpp
+++ b/libraries/render-utils/src/LightPayload.cpp
@@ -52,7 +52,7 @@ LightPayload::LightPayload() :
 LightPayload::~LightPayload() {
     if (!LightStage::isIndexInvalid(_index)) {
         if (_stage) {
-            _stage->removeLight(_index);
+            _stage->removeElement(_index);
         }
     }
 }
@@ -64,7 +64,7 @@ void LightPayload::render(RenderArgs* args) {
     }
     // Do we need to allocate the light in the stage ?
     if (LightStage::isIndexInvalid(_index)) {
-        _index = _stage->addLight(_light);
+        _index = _stage->addElement(_light);
         _needUpdate = false;
     }
     // Need an update ?
@@ -122,7 +122,7 @@ _light(std::make_shared<graphics::Light>())
 KeyLightPayload::~KeyLightPayload() {
     if (!LightStage::isIndexInvalid(_index)) {
         if (_stage) {
-            _stage->removeLight(_index);
+            _stage->removeElement(_index);
         }
     }
 }
@@ -134,7 +134,7 @@ void KeyLightPayload::render(RenderArgs* args) {
     }
     // Do we need to allocate the light in the stage ?
     if (LightStage::isIndexInvalid(_index)) {
-        _index = _stage->addLight(_light);
+        _index = _stage->addElement(_light);
         _needUpdate = false;
     }
     // Need an update ?
diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp
index 58f32cdc4e..395ca15487 100644
--- a/libraries/render-utils/src/LightStage.cpp
+++ b/libraries/render-utils/src/LightStage.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Zach Pomerantz on 1/14/2015.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -15,7 +16,9 @@
 
 #include "ViewFrustum.h"
 
-std::string LightStage::_stageName { "LIGHT_STAGE" };
+template <>
+std::string render::PointerStage<graphics::Light, graphics::LightPointer, LightFrame>::_name { "LIGHT_STAGE" };
+
 // The bias matrix goes from homogeneous coordinates to UV coords (see http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/#basic-shader)
 const glm::mat4 LightStage::Shadow::_biasMatrix {
     0.5, 0.0, 0.0, 0.0,
@@ -24,8 +27,6 @@ const glm::mat4 LightStage::Shadow::_biasMatrix {
     0.5, 0.5, 0.5, 1.0 };
 const int LightStage::Shadow::MAP_SIZE = 1024;
 
-const LightStage::Index LightStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
-
 LightStage::LightStage() {
     // Add off lights
     const LightPointer ambientOffLight { std::make_shared<graphics::Light>() };
@@ -33,28 +34,28 @@ LightStage::LightStage() {
     ambientOffLight->setColor(graphics::Vec3(0.0));
     ambientOffLight->setIntensity(0.0f);
     ambientOffLight->setType(graphics::Light::Type::AMBIENT);
-    _ambientOffLightId = addLight(ambientOffLight);
+    _ambientOffLightId = addElement(ambientOffLight);
 
     const LightPointer pointOffLight { std::make_shared<graphics::Light>() };
     pointOffLight->setAmbientIntensity(0.0f);
     pointOffLight->setColor(graphics::Vec3(0.0));
     pointOffLight->setIntensity(0.0f);
     pointOffLight->setType(graphics::Light::Type::POINT);
-    _pointOffLightId = addLight(pointOffLight);
+    _pointOffLightId = addElement(pointOffLight);
 
     const LightPointer spotOffLight { std::make_shared<graphics::Light>() };
     spotOffLight->setAmbientIntensity(0.0f);
     spotOffLight->setColor(graphics::Vec3(0.0));
     spotOffLight->setIntensity(0.0f);
     spotOffLight->setType(graphics::Light::Type::SPOT);
-    _spotOffLightId = addLight(spotOffLight);
+    _spotOffLightId = addElement(spotOffLight);
 
     const LightPointer sunOffLight { std::make_shared<graphics::Light>() };
     sunOffLight->setAmbientIntensity(0.0f);
     sunOffLight->setColor(graphics::Vec3(0.0));
     sunOffLight->setIntensity(0.0f);
     sunOffLight->setType(graphics::Light::Type::SUN);
-    _sunOffLightId = addLight(sunOffLight);
+    _sunOffLightId = addElement(sunOffLight);
 
     // Set default light to the off ambient light (until changed)
     _defaultLightId = _ambientOffLightId;
@@ -323,21 +324,12 @@ void LightStage::Shadow::setCascadeFrustum(unsigned int cascadeIndex, const View
     schemaCascade.reprojection = _biasMatrix * shadowFrustum.getProjection() * viewInverse.getMatrix();
 }
 
-LightStage::Index LightStage::findLight(const LightPointer& light) const {
-    auto found = _lightMap.find(light);
-    if (found != _lightMap.end()) {
-        return INVALID_INDEX;
-    } else {
-        return (*found).second;
-    }
-}
-
-LightStage::Index LightStage::addLight(const LightPointer& light, const bool shouldSetAsDefault) {
+LightStage::Index LightStage::addElement(const LightPointer& light, const bool shouldSetAsDefault) {
     Index lightId;
 
-    auto found = _lightMap.find(light);
-    if (found == _lightMap.end()) {
-        lightId = _lights.newElement(light);
+    auto found = _elementMap.find(light);
+    if (found == _elementMap.end()) {
+        lightId = _elements.newElement(light);
         // Avoid failing to allocate a light, just pass
         if (lightId != INVALID_INDEX) {
 
@@ -349,8 +341,8 @@ LightStage::Index LightStage::addLight(const LightPointer& light, const bool sho
                 _descs[lightId] = Desc();
             }
 
-            // INsert the light and its index in the reverese map
-            _lightMap.insert(LightMap::value_type(light, lightId));
+            // Insert the light and its index in the reverese map
+            _elementMap[light] = lightId;
 
             updateLightArrayBuffer(lightId);
         }
@@ -365,30 +357,30 @@ LightStage::Index LightStage::addLight(const LightPointer& light, const bool sho
     return lightId;
 }
 
-LightStage::LightPointer LightStage::removeLight(Index index) {
-    LightPointer removedLight = _lights.freeElement(index);
+LightStage::LightPointer LightStage::removeElement(Index index) {
+    LightPointer removedLight = _elements.freeElement(index);
     if (removedLight) {
-        _lightMap.erase(removedLight);
+        _elementMap.erase(removedLight);
         _descs[index] = Desc();
     }
     assert(_descs.size() <= (size_t)index || _descs[index].shadowId == INVALID_INDEX);
     return removedLight;
 }
 
-LightStage::LightPointer LightStage::getCurrentKeyLight(const LightStage::Frame& frame) const {
+LightStage::LightPointer LightStage::getCurrentKeyLight(const LightFrame& frame) const {
     Index keyLightId { _defaultLightId };
     if (!frame._sunLights.empty()) {
         keyLightId = frame._sunLights.front();
     }
-    return _lights.get(keyLightId);
+    return _elements.get(keyLightId);
 }
 
-LightStage::LightPointer LightStage::getCurrentAmbientLight(const LightStage::Frame& frame) const {
+LightStage::LightPointer LightStage::getCurrentAmbientLight(const LightFrame& frame) const {
     Index keyLightId { _defaultLightId };
     if (!frame._ambientLights.empty()) {
         keyLightId = frame._ambientLights.front();
     }
-    return _lights.get(keyLightId);
+    return _elements.get(keyLightId);
 }
 
 void LightStage::updateLightArrayBuffer(Index lightId) {
@@ -404,7 +396,7 @@ void LightStage::updateLightArrayBuffer(Index lightId) {
     }
 
     // lightArray is big enough so we can remap
-    auto light = _lights._elements[lightId];
+    auto light = _elements._elements[lightId];
     if (light) {
         const auto& lightSchema = light->getLightSchemaBuffer().get();
         _lightArrayBuffer->setSubData<graphics::Light::LightSchema>(lightId, lightSchema);
@@ -412,17 +404,3 @@ void LightStage::updateLightArrayBuffer(Index lightId) {
         // this should not happen ?
     }
 }
-
-LightStageSetup::LightStageSetup() {
-}
-
-void LightStageSetup::run(const render::RenderContextPointer& renderContext) {
-    if (renderContext->_scene) {
-        auto stage = renderContext->_scene->getStage(LightStage::getName());
-        if (!stage) {
-            stage = std::make_shared<LightStage>();
-            renderContext->_scene->resetStage(LightStage::getName(), stage);
-        }
-    }
-}
-
diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h
index 36e62c614f..5b0a90ddb6 100644
--- a/libraries/render-utils/src/LightStage.h
+++ b/libraries/render-utils/src/LightStage.h
@@ -4,6 +4,7 @@
 //
 //  Created by Zach Pomerantz on 1/14/2015.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -12,34 +13,44 @@
 #ifndef hifi_render_utils_LightStage_h
 #define hifi_render_utils_LightStage_h
 
-#include <set>
-#include <unordered_map>
-
 #include <gpu/Framebuffer.h>
-
 #include <graphics/Light.h>
-
-#include <render/IndexedContainer.h>
 #include <render/Stage.h>
-#include <render/Engine.h>
+#include <render/StageSetup.h>
 
 class ViewFrustum;
 
-// Light stage to set up light-related rendering tasks
-class LightStage : public render::Stage {
+class LightFrame {
 public:
-    static std::string _stageName;
-    static const std::string& getName() { return _stageName; }
+    LightFrame() {}
 
     using Index = render::indexed_container::Index;
-    static const Index INVALID_INDEX;
-    static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-    
-    using LightPointer = graphics::LightPointer;
-    using Lights = render::indexed_container::IndexedPointerVector<graphics::Light>;
-    using LightMap = std::unordered_map<LightPointer, Index>;
 
-    using LightIndices = std::vector<Index>;
+    void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); }
+    void pushLight(Index index, graphics::Light::Type type) {
+        switch (type) {
+            case graphics::Light::POINT: { pushPointLight(index); break; }
+            case graphics::Light::SPOT: { pushSpotLight(index); break; }
+            case graphics::Light::SUN: { pushSunLight(index); break; }
+            case graphics::Light::AMBIENT: { pushAmbientLight(index); break; }
+            default: { break; }
+        }
+    }
+    void pushPointLight(Index index) { _pointLights.emplace_back(index); }
+    void pushSpotLight(Index index) { _spotLights.emplace_back(index); }
+    void pushSunLight(Index index) { _sunLights.emplace_back(index); }
+    void pushAmbientLight(Index index) { _ambientLights.emplace_back(index); }
+
+    render::ElementIndices _pointLights;
+    render::ElementIndices _spotLights;
+    render::ElementIndices _sunLights;
+    render::ElementIndices _ambientLights;
+};
+
+// Light stage to set up light-related rendering tasks
+class LightStage : public render::PointerStage<graphics::Light, graphics::LightPointer, LightFrame> {
+public:
+    using LightPointer = graphics::LightPointer;
 
     class Shadow {
     public:
@@ -74,9 +85,9 @@ public:
                                      float left, float right, float bottom, float top, float viewMaxShadowDistance) const;
         };
 
-        Shadow(graphics::LightPointer light, unsigned int cascadeCount = 1);
+        Shadow(LightPointer light, unsigned int cascadeCount = 1);
 
-        void setLight(graphics::LightPointer light);
+        void setLight(LightPointer light);
 
         void setKeylightFrustum(const ViewFrustum& viewFrustum,
                                 float nearDepth = 1.0f, float farDepth = 1000.0f);
@@ -93,83 +104,46 @@ public:
         float getMaxDistance() const { return _maxDistance; }
         void setMaxDistance(float value);
 
-        const graphics::LightPointer& getLight() const { return _light; }
+        const LightPointer& getLight() const { return _light; }
 
         gpu::TexturePointer map;
 #include "Shadows_shared.slh"
         class Schema : public ShadowParameters {
         public:
-
             Schema();
-
         };
+
     protected:
 
         using Cascades = std::vector<Cascade>;
 
         static const glm::mat4 _biasMatrix;
 
-        graphics::LightPointer _light;
+        LightPointer _light;
         float _maxDistance{ 0.0f };
         Cascades _cascades;
 
         UniformBufferView _schemaBuffer = nullptr;
     };
-
     using ShadowPointer = std::shared_ptr<Shadow>;
 
-    Index findLight(const LightPointer& light) const;
-    Index addLight(const LightPointer& light, const bool shouldSetAsDefault = false);
-    
+    Index addElement(const LightPointer& light) override { return addElement(light, false); }
+    Index addElement(const LightPointer& light, const bool shouldSetAsDefault);
+    LightPointer removeElement(Index index) override;
+
     Index getDefaultLight() { return _defaultLightId; }
 
-    LightPointer removeLight(Index index);
-    
-    bool checkLightId(Index index) const { return _lights.checkIndex(index); }
-
-    Index getNumLights() const { return _lights.getNumElements(); }
-    Index getNumFreeLights() const { return _lights.getNumFreeIndices(); }
-    Index getNumAllocatedLights() const { return _lights.getNumAllocatedIndices(); }
-
-    LightPointer getLight(Index lightId) const { return _lights.get(lightId); }
-
     LightStage();
 
     gpu::BufferPointer getLightArrayBuffer() const { return _lightArrayBuffer; }
     void updateLightArrayBuffer(Index lightId);
 
-    class Frame {
-    public:
-        Frame() {}
-        
-        void clear() { _pointLights.clear(); _spotLights.clear(); _sunLights.clear(); _ambientLights.clear(); }
-        void pushLight(LightStage::Index index, graphics::Light::Type type) {
-            switch (type) {
-                case graphics::Light::POINT: { pushPointLight(index); break; }
-                case graphics::Light::SPOT: { pushSpotLight(index); break; }
-                case graphics::Light::SUN: { pushSunLight(index); break; }
-                case graphics::Light::AMBIENT: { pushAmbientLight(index); break; }
-                default: { break; }
-            }
-        }
-        void pushPointLight(LightStage::Index index) { _pointLights.emplace_back(index); }
-        void pushSpotLight(LightStage::Index index) { _spotLights.emplace_back(index); }
-        void pushSunLight(LightStage::Index index) { _sunLights.emplace_back(index); }
-        void pushAmbientLight(LightStage::Index index) { _ambientLights.emplace_back(index); }
-
-        LightStage::LightIndices _pointLights;
-        LightStage::LightIndices _spotLights;
-        LightStage::LightIndices _sunLights;
-        LightStage::LightIndices _ambientLights;
-    };
-    using FramePointer = std::shared_ptr<Frame>;
-    
     class ShadowFrame {
     public:
         ShadowFrame() {}
-        
+
         void clear() {}
-        
+
         using Object = ShadowPointer;
         using Objects = std::vector<Object>;
 
@@ -177,20 +151,17 @@ public:
             _objects.emplace_back(shadow);
         }
 
-
         Objects _objects;
     };
     using ShadowFramePointer = std::shared_ptr<ShadowFrame>;
 
-    Frame _currentFrame;
-    
     Index getAmbientOffLight() { return _ambientOffLightId; }
     Index getPointOffLight() { return _pointOffLightId; }
     Index getSpotOffLight() { return _spotOffLightId; }
     Index getSunOffLight() { return _sunOffLightId; }
 
-    LightPointer getCurrentKeyLight(const LightStage::Frame& frame) const;
-    LightPointer getCurrentAmbientLight(const LightStage::Frame& frame) const;
+    LightPointer getCurrentKeyLight(const LightFrame& frame) const;
+    LightPointer getCurrentAmbientLight(const LightFrame& frame) const;
 
 protected:
 
@@ -201,9 +172,7 @@ protected:
 
     gpu::BufferPointer _lightArrayBuffer;
 
-    Lights _lights;
     Descs _descs;
-    LightMap _lightMap;
 
     // define off lights
     Index _ambientOffLightId;
@@ -216,16 +185,9 @@ protected:
 };
 using LightStagePointer = std::shared_ptr<LightStage>;
 
-
-class LightStageSetup {
+class LightStageSetup : public render::StageSetup<LightStage> {
 public:
     using JobModel = render::Job::Model<LightStageSetup>;
-
-    LightStageSetup();
-    void run(const render::RenderContextPointer& renderContext);
-
-protected:
 };
 
-
 #endif
diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp
index c79ac87551..ba0460417c 100644
--- a/libraries/render-utils/src/RenderCommonTask.cpp
+++ b/libraries/render-utils/src/RenderCommonTask.cpp
@@ -1,6 +1,9 @@
 //
+//  RenderCommonTask.cpp
+//
 //  Created by Bradley Austin Davis on 2018/01/09
 //  Copyright 2013-2018 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -78,9 +81,9 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs&
 
     graphics::HazePointer haze;
     const auto& hazeStage = renderContext->args->_scene->getStage<HazeStage>();
-    if (hazeStage && hazeFrame->_hazes.size() > 0) {
+    if (hazeStage && hazeFrame->_elements.size() > 0) {
         // We use _hazes.back() here because the last haze object will always have haze disabled.
-        haze = hazeStage->getHaze(hazeFrame->_hazes.back());
+        haze = hazeStage->getElement(hazeFrame->_elements.back());
     }
 
     // Clear the framebuffer without stereo
diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h
index 255fcb6392..5d6395aceb 100644
--- a/libraries/render-utils/src/RenderCommonTask.h
+++ b/libraries/render-utils/src/RenderCommonTask.h
@@ -1,6 +1,9 @@
 //
+//  RenderCommonTask.h
+//
 //  Created by Bradley Austin Davis on 2018/01/09
 //  Copyright 2013-2018 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp
index 05253178f1..261658039c 100644
--- a/libraries/render-utils/src/RenderDeferredTask.cpp
+++ b/libraries/render-utils/src/RenderDeferredTask.cpp
@@ -5,6 +5,7 @@
 //
 //  Created by Sam Gateau on 5/29/15.
 //  Copyright 2016 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -511,8 +512,8 @@ void RenderTransparentDeferred::run(const RenderContextPointer& renderContext, c
 
         // Setup haze if current zone has haze
         const auto& hazeStage = args->_scene->getStage<HazeStage>();
-        if (hazeStage && hazeFrame->_hazes.size() > 0) {
-            const auto& hazePointer = hazeStage->getHaze(hazeFrame->_hazes.front());
+        if (hazeStage && hazeFrame->_elements.size() > 0) {
+            const auto& hazePointer = hazeStage->getElement(hazeFrame->_elements.front());
             if (hazePointer) {
                 batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, hazePointer->getHazeParametersBuffer());
             }
diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp
index 74cdf1b044..85ea0facbe 100644
--- a/libraries/render-utils/src/RenderForwardTask.cpp
+++ b/libraries/render-utils/src/RenderForwardTask.cpp
@@ -5,6 +5,7 @@
 //
 //  Created by Zach Pomerantz on 12/13/2016.
 //  Copyright 2016 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -271,8 +272,8 @@ void DrawForward::run(const RenderContextPointer& renderContext, const Inputs& i
 
     graphics::HazePointer haze;
     const auto& hazeStage = renderContext->args->_scene->getStage<HazeStage>();
-    if (hazeStage && hazeFrame->_hazes.size() > 0) {
-        haze = hazeStage->getHaze(hazeFrame->_hazes.front());
+    if (hazeStage && hazeFrame->_elements.size() > 0) {
+        haze = hazeStage->getElement(hazeFrame->_elements.front());
     }
 
     gpu::doInBatch("DrawForward::run", args->_context, [&](gpu::Batch& batch) {
diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp
index 9f1cf8421a..5964a1737c 100644
--- a/libraries/render-utils/src/SubsurfaceScattering.cpp
+++ b/libraries/render-utils/src/SubsurfaceScattering.cpp
@@ -473,8 +473,8 @@ void DebugSubsurfaceScattering::run(const render::RenderContextPointer& renderCo
 
     auto lightStage = renderContext->_scene->getStage<LightStage>();
     assert(lightStage);
-   // const auto light = DependencyManager::get<DeferredLightingEffect>()->getLightStage()->getLight(0);
-    const auto light = lightStage->getLight(0);
+   // const auto light = DependencyManager::get<DeferredLightingEffect>()->getLightStage()->getElement(0);
+    const auto light = lightStage->getElement(0);
     if (!_debugParams) {
         _debugParams = std::make_shared<gpu::Buffer>(sizeof(glm::vec4), nullptr);
         _debugParams->setSubData(0, _debugCursorTexcoord);
diff --git a/libraries/render-utils/src/ToneMapAndResampleTask.cpp b/libraries/render-utils/src/ToneMapAndResampleTask.cpp
index d906d82aa7..c72776f1b6 100644
--- a/libraries/render-utils/src/ToneMapAndResampleTask.cpp
+++ b/libraries/render-utils/src/ToneMapAndResampleTask.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Anna Brewer on 7/3/19.
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -74,8 +75,8 @@ void ToneMapAndResample::run(const RenderContextPointer& renderContext, const In
 
     const auto& tonemappingStage = renderContext->_scene->getStage<TonemappingStage>();
     graphics::TonemappingPointer tonemapping;
-    if (tonemappingStage && tonemappingFrame->_tonemappings.size()) {
-        tonemapping = tonemappingStage->getTonemapping(tonemappingFrame->_tonemappings.front());
+    if (tonemappingStage && tonemappingFrame->_elements.size()) {
+        tonemapping = tonemappingStage->getElement(tonemappingFrame->_elements.front());
     }
 
     if (_debug) {
diff --git a/libraries/render-utils/src/TonemappingStage.cpp b/libraries/render-utils/src/TonemappingStage.cpp
index 9b6029ca1b..1bb382b77d 100644
--- a/libraries/render-utils/src/TonemappingStage.cpp
+++ b/libraries/render-utils/src/TonemappingStage.cpp
@@ -7,50 +7,8 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
+
 #include "TonemappingStage.h"
 
-#include <gpu/Context.h>
-
-std::string TonemappingStage::_stageName { "TONEMAPPING_STAGE" };
-const TonemappingStage::Index TonemappingStage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
-
-TonemappingStage::Index TonemappingStage::findTonemapping(const TonemappingPointer& tonemapping) const {
-    auto found = _tonemappingMap.find(tonemapping);
-    if (found != _tonemappingMap.end()) {
-        return INVALID_INDEX;
-    } else {
-        return (*found).second;
-    }
-}
-
-TonemappingStage::Index TonemappingStage::addTonemapping(const TonemappingPointer& tonemapping) {
-    auto found = _tonemappingMap.find(tonemapping);
-    if (found == _tonemappingMap.end()) {
-        auto tonemappingId = _tonemappings.newElement(tonemapping);
-        // Avoid failing to allocate a tonemapping, just pass
-        if (tonemappingId != INVALID_INDEX) {
-            // Insert the tonemapping and its index in the reverse map
-            _tonemappingMap.insert(TonemappingMap::value_type(tonemapping, tonemappingId));
-        }
-        return tonemappingId;
-    } else {
-        return (*found).second;
-    }
-}
-
-TonemappingStage::TonemappingPointer TonemappingStage::removeTonemapping(Index index) {
-    TonemappingPointer removed = _tonemappings.freeElement(index);
-    if (removed) {
-        _tonemappingMap.erase(removed);
-    }
-    return removed;
-}
-
-TonemappingStageSetup::TonemappingStageSetup() {}
-
-void TonemappingStageSetup::run(const render::RenderContextPointer& renderContext) {
-    auto stage = renderContext->_scene->getStage(TonemappingStage::getName());
-    if (!stage) {
-        renderContext->_scene->resetStage(TonemappingStage::getName(), std::make_shared<TonemappingStage>());
-    }
-}
+template <>
+std::string render::PointerStage<graphics::Tonemapping, graphics::TonemappingPointer>::_name { "TONEMAPPING_STAGE" };
diff --git a/libraries/render-utils/src/TonemappingStage.h b/libraries/render-utils/src/TonemappingStage.h
index 15161094a2..ee05c0ace4 100644
--- a/libraries/render-utils/src/TonemappingStage.h
+++ b/libraries/render-utils/src/TonemappingStage.h
@@ -11,74 +11,17 @@
 #ifndef hifi_render_utils_TonemappingStage_h
 #define hifi_render_utils_TonemappingStage_h
 
-#include <graphics/Stage.h>
-#include <set>
-#include <unordered_map>
-#include <render/IndexedContainer.h>
-#include <render/Stage.h>
-
-#include <render/Forward.h>
-#include <render/DrawTask.h>
 #include <graphics/Tonemapping.h>
+#include <render/Stage.h>
+#include <render/StageSetup.h>
 
 // Tonemapping stage to set up tonemapping-related rendering tasks
-class TonemappingStage : public render::Stage {
-public:
-    static std::string _stageName;
-    static const std::string& getName() { return _stageName; }
-
-    using Index = render::indexed_container::Index;
-    static const Index INVALID_INDEX;
-    static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-    
-    using TonemappingPointer = graphics::TonemappingPointer;
-    using Tonemappings = render::indexed_container::IndexedPointerVector<graphics::Tonemapping>;
-    using TonemappingMap = std::unordered_map<TonemappingPointer, Index>;
-
-    using TonemappingIndices = std::vector<Index>;
-
-    Index findTonemapping(const TonemappingPointer& tonemapping) const;
-    Index addTonemapping(const TonemappingPointer& tonemapping);
-
-    TonemappingPointer removeTonemapping(Index index);
-    
-    bool checkTonemappingId(Index index) const { return _tonemappings.checkIndex(index); }
-
-    Index getNumTonemappings() const { return _tonemappings.getNumElements(); }
-    Index getNumFreeTonemappings() const { return _tonemappings.getNumFreeIndices(); }
-    Index getNumAllocatedTonemappings() const { return _tonemappings.getNumAllocatedIndices(); }
-
-    TonemappingPointer getTonemapping(Index tonemappingId) const {
-        return _tonemappings.get(tonemappingId);
-    }
-
-    Tonemappings _tonemappings;
-    TonemappingMap _tonemappingMap;
-
-    class Frame {
-    public:
-        Frame() {}
-        
-        void clear() { _tonemappings.clear(); }
-
-        void pushTonemapping(TonemappingStage::Index index) { _tonemappings.emplace_back(index); }
-
-        TonemappingStage::TonemappingIndices _tonemappings;
-    };
-    using FramePointer = std::shared_ptr<Frame>;
-    
-    Frame _currentFrame;
-};
+class TonemappingStage : public render::PointerStage<graphics::Tonemapping, graphics::TonemappingPointer> {};
 using TonemappingStagePointer = std::shared_ptr<TonemappingStage>;
 
-class TonemappingStageSetup {
+class TonemappingStageSetup : public render::StageSetup<TonemappingStage> {
 public:
     using JobModel = render::Job::Model<TonemappingStageSetup>;
-
-    TonemappingStageSetup();
-    void run(const render::RenderContextPointer& renderContext);
-
-protected:
 };
 
 #endif
diff --git a/libraries/render-utils/src/ZoneRenderer.cpp b/libraries/render-utils/src/ZoneRenderer.cpp
index a8fb349e4d..5d958d8a84 100644
--- a/libraries/render-utils/src/ZoneRenderer.cpp
+++ b/libraries/render-utils/src/ZoneRenderer.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 4/4/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -86,11 +87,11 @@ void SetupZones::run(const RenderContextPointer& context, const Input& input) {
     // Finally add the default lights and background:
     lightStage->_currentFrame.pushSunLight(lightStage->getDefaultLight());
     lightStage->_currentFrame.pushAmbientLight(lightStage->getDefaultLight());
-    backgroundStage->_currentFrame.pushBackground(0);
-    hazeStage->_currentFrame.pushHaze(0);
-    bloomStage->_currentFrame.pushBloom(INVALID_INDEX);
-    tonemappingStage->_currentFrame.pushTonemapping(0);
-    ambientOcclusionStage->_currentFrame.pushAmbientOcclusion(INVALID_INDEX);
+    backgroundStage->_currentFrame.pushElement(0);
+    hazeStage->_currentFrame.pushElement(0);
+    bloomStage->_currentFrame.pushElement(INVALID_INDEX);
+    tonemappingStage->_currentFrame.pushElement(0);
+    ambientOcclusionStage->_currentFrame.pushElement(INVALID_INDEX);
 }
 
 gpu::PipelinePointer DebugZoneLighting::_keyLightPipeline;
@@ -144,22 +145,22 @@ void DebugZoneLighting::run(const render::RenderContextPointer& context, const I
     std::vector<graphics::LightPointer> keyLightStack;
     if (lightStage && lightFrame->_sunLights.size()) {
         for (auto index : lightFrame->_sunLights) {
-            keyLightStack.push_back(lightStage->getLight(index));
+            keyLightStack.push_back(lightStage->getElement(index));
         }
     }
 
     std::vector<graphics::LightPointer> ambientLightStack;
     if (lightStage && lightFrame->_ambientLights.size()) {
         for (auto index : lightFrame->_ambientLights) {
-            ambientLightStack.push_back(lightStage->getLight(index));
+            ambientLightStack.push_back(lightStage->getElement(index));
         }
     }
 
     auto backgroundStage = context->_scene->getStage<BackgroundStage>(BackgroundStage::getName());
     std::vector<graphics::SkyboxPointer> skyboxStack;
-    if (backgroundStage && backgroundFrame->_backgrounds.size()) {
-        for (auto index : backgroundFrame->_backgrounds) {
-            auto background = backgroundStage->getBackground(index);
+    if (backgroundStage && backgroundFrame->_elements.size()) {
+        for (auto index : backgroundFrame->_elements) {
+            auto background = backgroundStage->getElement(index);
             if (background) {
                 skyboxStack.push_back(background->getSkybox());
             }
diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp
index 925cffdae1..d722197205 100644
--- a/libraries/render/src/render/DrawStatus.cpp
+++ b/libraries/render/src/render/DrawStatus.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Niraj Venkat on 6/29/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -163,7 +164,7 @@ void DrawStatus::run(const RenderContextPointer& renderContext, const Input& inp
                                 status.setColor(Item::Status::Value::RED);
                             }
                             // Set icon based on transition type
-                            auto& transition = transitionStage->getTransition(transitionID);
+                            auto& transition = transitionStage->getElement(transitionID);
                             switch (transition.eventType) {
                             case Transition::Type::USER_ENTER_DOMAIN:
                                 status.setIcon((unsigned char)Item::Status::Icon::USER_TRANSITION_IN);
diff --git a/libraries/render/src/render/HighlightStage.cpp b/libraries/render/src/render/HighlightStage.cpp
index c9f097b387..07fb5b5635 100644
--- a/libraries/render/src/render/HighlightStage.cpp
+++ b/libraries/render/src/render/HighlightStage.cpp
@@ -1,33 +1,31 @@
+//
+//  HighlightStage.cpp
+//
+//  Created by Olivier Prat on 07/07/2017.
+//  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
 #include "HighlightStage.h"
 
+#include "Engine.h"
+
 using namespace render;
 
-std::string HighlightStage::_name("Highlight");
-const HighlightStage::Index HighlightStage::INVALID_INDEX{ render::indexed_container::INVALID_INDEX };
+template <>
+std::string TypedStage<Highlight>::_name { "HIGHLIGHT_STAGE" };
 
 HighlightStage::Index HighlightStage::addHighlight(const std::string& selectionName, const HighlightStyle& style) {
-    Highlight outline{ selectionName, style };
-    Index id;
-
-    id = _highlights.newElement(outline);
-    _activeHighlightIds.push_back(id);
-
-    return id;
+    Highlight outline { selectionName, style };
+    return addElement(outline);
 }
 
-void HighlightStage::removeHighlight(Index index) {
-    HighlightIdList::iterator  idIterator = std::find(_activeHighlightIds.begin(), _activeHighlightIds.end(), index);
-    if (idIterator != _activeHighlightIds.end()) {
-        _activeHighlightIds.erase(idIterator);
-    }
-    if (!_highlights.isElementFreed(index)) {
-        _highlights.freeElement(index);
-    }
-}
-
-Index HighlightStage::getHighlightIdBySelection(const std::string& selectionName) const {
-    for (auto outlineId : _activeHighlightIds) {
-        const auto& outline = _highlights.get(outlineId);
+HighlightStage::Index HighlightStage::getHighlightIdBySelection(const std::string& selectionName) const {
+    for (auto outlineId : _activeElementIDs) {
+        const auto& outline = _elements.get(outlineId);
         if (outline._selectionName == selectionName) {
             return outlineId;
         }
@@ -100,9 +98,6 @@ void HighlightStageConfig::setOccludedFillOpacity(float value) {
     emit dirty();
 }
 
-HighlightStageSetup::HighlightStageSetup() {
-}
-
 void HighlightStageSetup::configure(const Config& config) {
     // Copy the styles here but update the stage with the new styles in run to be sure everything is
     // thread safe...
@@ -112,8 +107,7 @@ void HighlightStageSetup::configure(const Config& config) {
 void HighlightStageSetup::run(const render::RenderContextPointer& renderContext) {
     auto stage = renderContext->_scene->getStage<HighlightStage>(HighlightStage::getName());
     if (!stage) {
-        stage = std::make_shared<HighlightStage>();
-        renderContext->_scene->resetStage(HighlightStage::getName(), stage);
+        renderContext->_scene->resetStage(HighlightStage::getName(), std::make_shared<HighlightStage>());
     }
 
     if (!_styles.empty()) {
@@ -127,4 +121,3 @@ void HighlightStageSetup::run(const render::RenderContextPointer& renderContext)
         _styles.clear();
     }
 }
-
diff --git a/libraries/render/src/render/HighlightStage.h b/libraries/render/src/render/HighlightStage.h
index 91d8cc3f81..d675ed9f6b 100644
--- a/libraries/render/src/render/HighlightStage.h
+++ b/libraries/render/src/render/HighlightStage.h
@@ -3,6 +3,7 @@
 
 //  Created by Olivier Prat on 07/07/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -11,56 +12,27 @@
 #ifndef hifi_render_utils_HighlightStage_h
 #define hifi_render_utils_HighlightStage_h
 
-#include "Stage.h"
 #include "Engine.h"
-#include "IndexedContainer.h"
 #include "HighlightStyle.h"
+#include "Stage.h"
 
 namespace render {
 
-    // Highlight stage to set up HighlightStyle-related effects
-    class HighlightStage : public Stage {
+    class Highlight {
     public:
+        Highlight(const std::string& selectionName, const HighlightStyle& style) : _selectionName{ selectionName }, _style{ style } { }
 
-        class Highlight {
-        public:
-
-            Highlight(const std::string& selectionName, const HighlightStyle& style) : _selectionName{ selectionName }, _style{ style } { }
-
-            std::string _selectionName;
-            HighlightStyle _style;
-
-        };
-
-        static const std::string& getName() { return _name; }
-
-        using Index = render::indexed_container::Index;
-        static const Index INVALID_INDEX;
-        using HighlightIdList = render::indexed_container::Indices;
-
-        static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-
-        bool checkHighlightId(Index index) const { return _highlights.checkIndex(index); }
-
-        const Highlight& getHighlight(Index index) const { return _highlights.get(index); }
-        Highlight& editHighlight(Index index) { return _highlights.edit(index); }
+        std::string _selectionName;
+        HighlightStyle _style;
+    };
 
+    // Highlight stage to set up HighlightStyle-related effects
+    class HighlightStage : public TypedStage<Highlight> {
+    public:
         Index addHighlight(const std::string& selectionName, const HighlightStyle& style = HighlightStyle());
         Index getHighlightIdBySelection(const std::string& selectionName) const;
-        void removeHighlight(Index index);
 
-        HighlightIdList::iterator begin() { return _activeHighlightIds.begin(); }
-        HighlightIdList::iterator end() { return _activeHighlightIds.end(); }
-        const HighlightIdList&  getActiveHighlightIds() const { return _activeHighlightIds; }
-
-    private:
-
-        using Highlights = render::indexed_container::IndexedVector<Highlight>;
-
-        static std::string _name;
-
-        Highlights _highlights;
-        HighlightIdList _activeHighlightIds;
+        const IDList& getActiveHighlightIds() const { return _activeElementIDs; }
     };
     using HighlightStagePointer = std::shared_ptr<HighlightStage>;
 
@@ -122,7 +94,7 @@ namespace render {
         using Config = HighlightStageConfig;
         using JobModel = render::Job::Model<HighlightStageSetup, Config>;
 
-        HighlightStageSetup();
+        HighlightStageSetup() {}
 
         void configure(const Config& config);
         void run(const RenderContextPointer& renderContext);
diff --git a/libraries/render/src/render/HighlightStyle.h b/libraries/render/src/render/HighlightStyle.h
index 138674ffbb..266ce71262 100644
--- a/libraries/render/src/render/HighlightStyle.h
+++ b/libraries/render/src/render/HighlightStyle.h
@@ -17,6 +17,8 @@
 
 #include <string>
 
+#include <ViewFrustum.h>
+
 namespace render {
 
     // This holds the configuration for a particular outline style
diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp
index a9bb2a5856..3776caef0d 100644
--- a/libraries/render/src/render/Scene.cpp
+++ b/libraries/render/src/render/Scene.cpp
@@ -440,7 +440,7 @@ void Scene::queryTransitionItems(const Transaction::TransitionQueries& transacti
             auto transitionId = item.getTransitionId();
 
             if (!TransitionStage::isIndexInvalid(transitionId)) {
-                auto& transition = transitionStage->getTransition(transitionId);
+                auto& transition = transitionStage->getElement(transitionId);
                 func(itemId, &transition);
             } else {
                 func(itemId, nullptr);
@@ -477,7 +477,7 @@ void Scene::resetHighlights(const Transaction::HighlightResets& transactions) {
             if (HighlightStage::isIndexInvalid(outlineId)) {
                 outlineStage->addHighlight(selectionName, newStyle);
             } else {
-                outlineStage->editHighlight(outlineId)._style = newStyle;
+                outlineStage->editElement(outlineId)._style = newStyle;
             }
         }
     }
@@ -490,7 +490,7 @@ void Scene::removeHighlights(const Transaction::HighlightRemoves& transactions)
             auto outlineId = outlineStage->getHighlightIdBySelection(selectionName);
 
             if (!HighlightStage::isIndexInvalid(outlineId)) {
-                outlineStage->removeHighlight(outlineId);
+                outlineStage->removeElement(outlineId);
             }
         }
     }
@@ -505,7 +505,7 @@ void Scene::queryHighlights(const Transaction::HighlightQueries& transactions) {
             auto outlineId = outlineStage->getHighlightIdBySelection(selectionName);
 
             if (!HighlightStage::isIndexInvalid(outlineId)) {
-                func(&outlineStage->editHighlight(outlineId)._style);
+                func(&outlineStage->editElement(outlineId)._style);
             } else {
                 func(nullptr);
             }
@@ -559,7 +559,7 @@ void Scene::removeItemTransition(ItemID itemId) {
     auto& item = _items[itemId];
     TransitionStage::Index transitionId = item.getTransitionId();
     if (!render::TransitionStage::isIndexInvalid(transitionId)) {
-        const auto& transition = transitionStage->getTransition(transitionId);
+        const auto& transition = transitionStage->getElement(transitionId);
         const auto transitionOwner = transition.itemId;
         if (transitionOwner == itemId) {
             // No more items will be using this transition. Clean it up.
@@ -570,7 +570,7 @@ void Scene::removeItemTransition(ItemID itemId) {
                 }
             }
             _transitionFinishedOperatorMap.erase(transitionId);
-            transitionStage->removeTransition(transitionId);
+            transitionStage->removeElement(transitionId);
         }
 
         setItemTransition(itemId, render::TransitionStage::INVALID_INDEX);
diff --git a/libraries/render/src/render/Stage.cpp b/libraries/render/src/render/Stage.cpp
index 1ee9b1d6ff..d5335f07ed 100644
--- a/libraries/render/src/render/Stage.cpp
+++ b/libraries/render/src/render/Stage.cpp
@@ -4,23 +4,13 @@
 //
 //  Created by Sam Gateau on 6/14/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 #include "Stage.h"
 
-
 using namespace render;
 
-
-
-Stage::~Stage() {
-
-}
-
-Stage::Stage() :
-    _name()
-{
-}
-
+const Stage::Index Stage::INVALID_INDEX { render::indexed_container::INVALID_INDEX };
diff --git a/libraries/render/src/render/Stage.h b/libraries/render/src/render/Stage.h
index 5145810671..2bb81771e5 100644
--- a/libraries/render/src/render/Stage.h
+++ b/libraries/render/src/render/Stage.h
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 6/14/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -12,27 +13,141 @@
 #ifndef hifi_render_Stage_h
 #define hifi_render_Stage_h
 
-#include <memory>
 #include <map>
+#include <memory>
+#include <unordered_map>
 #include <string>
 
+#include "IndexedContainer.h"
+
 namespace render {
 
+    using ElementIndices = std::vector<indexed_container::Index>;
+
     class Stage {
     public:
+        Stage() {}
+        virtual ~Stage() {}
+
         using Name = std::string;
+        using Index = indexed_container::Index;
+        static const Index INVALID_INDEX;
+        using IDList = indexed_container::Indices;
 
-        Stage();
-        virtual ~Stage();
-
-    protected:
-        Name _name;
+        static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
     };
 
     using StagePointer = std::shared_ptr<Stage>;
-
     using StageMap = std::map<const Stage::Name, StagePointer>;
 
+    template<typename T>
+    class TypedStage : public Stage {
+    public:
+        TypedStage() {}
+        virtual ~TypedStage() {}
+
+        static const Name& getName() { return _name; }
+
+        bool checkId(Index index) const { return _elements.checkIndex(index); }
+
+        const T& getElement(Index id) const { return _elements.get(id); }
+        T& editElement(Index id) { return _elements.edit(id); }
+        Index addElement(const T& element) {
+            Index id = _elements.newElement(element);
+            _activeElementIDs.push_back(id);
+            return id;
+        }
+
+        void removeElement(Index index) {
+            IDList::iterator idIterator = std::find(_activeElementIDs.begin(), _activeElementIDs.end(), index);
+            if (idIterator != _activeElementIDs.end()) {
+                _activeElementIDs.erase(idIterator);
+            }
+            if (!_elements.isElementFreed(index)) {
+                _elements.freeElement(index);
+            }
+        }
+
+        IDList::iterator begin() { return _activeElementIDs.begin(); }
+        IDList::iterator end() { return _activeElementIDs.end(); }
+    protected:
+        static Name _name;
+
+        indexed_container::IndexedVector<T> _elements;
+        IDList _activeElementIDs;
+    };
+
+    class Frame {
+    public:
+        Frame() {}
+
+        using Index = indexed_container::Index;
+
+        void clear() { _elements.clear(); }
+        void pushElement(Index index) { _elements.emplace_back(index); }
+
+        ElementIndices _elements;
+    };
+
+    template<typename T, typename P, typename F = Frame>
+    class PointerStage : public Stage {
+    public:
+        PointerStage() {}
+        virtual ~PointerStage() {}
+
+        static const Name& getName() { return _name; }
+
+        bool checkId(Index index) const { return _elements.checkIndex(index); }
+
+        Index getNumElements() const { return _elements.getNumElements(); }
+        Index getNumFreeElements() const { return _elements.getNumFreeIndices(); }
+        Index getNumAllocatedElements() const { return _elements.getNumAllocatedIndices(); }
+
+        P getElement(Index id) const { return _elements.get(id); }
+
+        Index findElement(const P& element) const {
+            auto found = _elementMap.find(element);
+            if (found != _elementMap.end()) {
+                return INVALID_INDEX;
+            } else {
+                return (*found).second;
+            }
+        }
+
+        virtual Index addElement(const P& element) {
+            auto found = _elementMap.find(element);
+            if (found == _elementMap.end()) {
+                auto id = _elements.newElement(element);
+                // Avoid failing to allocate an element, just pass
+                if (id != INVALID_INDEX) {
+                    // Insert the element and its index in the reverse map
+                    _elementMap[element] = id;
+                }
+                return id;
+            } else {
+                return (*found).second;
+            }
+        }
+
+        virtual P removeElement(Index index) {
+            P removed = _elements.freeElement(index);
+
+            if (removed) {
+                _elementMap.erase(removed);
+            }
+            return removed;
+        }
+
+        using Frame = F;
+        using FramePointer = std::shared_ptr<F>;
+        F _currentFrame;
+
+    protected:
+        static Name _name;
+
+        indexed_container::IndexedPointerVector<T> _elements;
+        std::unordered_map<P, Index> _elementMap;
+    };
 }
 
 #endif // hifi_render_Stage_h
diff --git a/libraries/render/src/render/StageSetup.h b/libraries/render/src/render/StageSetup.h
new file mode 100644
index 0000000000..5691bca207
--- /dev/null
+++ b/libraries/render/src/render/StageSetup.h
@@ -0,0 +1,35 @@
+//
+//  StageSetup.h
+//  render/src/render
+//
+//  Created by HifiExperiments on 10/16/24
+//  Copyright 2024 Overte e.V.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_render_StageSetup_h
+#define hifi_render_StageSetup_h
+
+#include "Engine.h"
+
+namespace render {
+
+    template <typename T>
+    class StageSetup {
+    public:
+        StageSetup() {}
+
+        void run(const RenderContextPointer& renderContext) {
+            if (renderContext->_scene) {
+                auto stage = renderContext->_scene->getStage(T::getName());
+                if (!stage) {
+                    renderContext->_scene->resetStage(T::getName(), std::make_shared<T>());
+                }
+            }
+        }
+    };
+}
+
+#endif // hifi_render_StageSetup_h
diff --git a/libraries/render/src/render/TransitionStage.cpp b/libraries/render/src/render/TransitionStage.cpp
index 9ddc72f4db..72637cc3d9 100644
--- a/libraries/render/src/render/TransitionStage.cpp
+++ b/libraries/render/src/render/TransitionStage.cpp
@@ -1,43 +1,25 @@
-#include "TransitionStage.h"
+//
+//  TransitionStage.cpp
+//
+//  Created by Olivier Prat on 07/07/2017.
+//  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
 
-#include <algorithm>
+#include "TransitionStage.h"
 
 using namespace render;
 
-std::string TransitionStage::_name("Transition");
-const TransitionStage::Index TransitionStage::INVALID_INDEX{ indexed_container::INVALID_INDEX };
+template <>
+std::string TypedStage<Transition>::_name { "TRANSITION_STAGE" };
 
 TransitionStage::Index TransitionStage::addTransition(ItemID itemId, Transition::Type type, ItemID boundId) {
     Transition transition;
-    Index id;
-    
     transition.eventType = type;
     transition.itemId = itemId;
     transition.boundItemId = boundId;
-    id = _transitions.newElement(transition);
-    _activeTransitionIds.push_back(id);
-
-    return id;
+    return addElement(transition);
 }
-
-void TransitionStage::removeTransition(Index index) {
-    TransitionIdList::iterator  idIterator = std::find(_activeTransitionIds.begin(), _activeTransitionIds.end(), index);
-    if (idIterator != _activeTransitionIds.end()) {
-        _activeTransitionIds.erase(idIterator);
-    }
-    if (!_transitions.isElementFreed(index)) {
-        _transitions.freeElement(index);
-    }
-}
-
-TransitionStageSetup::TransitionStageSetup() {
-}
-
-void TransitionStageSetup::run(const RenderContextPointer& renderContext) {
-    auto stage = renderContext->_scene->getStage(TransitionStage::getName());
-    if (!stage) {
-        stage = std::make_shared<TransitionStage>();
-        renderContext->_scene->resetStage(TransitionStage::getName(), stage);
-    }
-}
-
diff --git a/libraries/render/src/render/TransitionStage.h b/libraries/render/src/render/TransitionStage.h
index abfdca9a06..11256fb346 100644
--- a/libraries/render/src/render/TransitionStage.h
+++ b/libraries/render/src/render/TransitionStage.h
@@ -1,8 +1,9 @@
 //
 //  TransitionStage.h
-
+//
 //  Created by Olivier Prat on 07/07/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 Overte e.V.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -12,55 +13,22 @@
 #define hifi_render_TransitionStage_h
 
 #include "Stage.h"
-#include "IndexedContainer.h"
-#include "Engine.h"
+#include "StageSetup.h"
 #include "Transition.h"
 
 namespace render {
 
     // Transition stage to set up Transition-related effects
-    class TransitionStage : public render::Stage {
+    class TransitionStage : public TypedStage<Transition> {
     public:
-
-        static const std::string& getName() { return _name; }
-
-        using Index = indexed_container::Index;
-        static const Index INVALID_INDEX;
-        using TransitionIdList = indexed_container::Indices;
-
-        static bool isIndexInvalid(Index index) { return index == INVALID_INDEX; }
-
-        bool isTransitionUsed(Index index) const { return _transitions.checkIndex(index) && !_transitions.isElementFreed(index); }
-
-        const Transition& getTransition(Index TransitionId) const { return _transitions.get(TransitionId); }
-
-        Transition& editTransition(Index TransitionId) { return _transitions.edit(TransitionId); }
-
+        bool isTransitionUsed(Index index) const { return _elements.checkIndex(index) && !_elements.isElementFreed(index); }
         Index addTransition(ItemID itemId, Transition::Type type, ItemID boundId);
-        void removeTransition(Index index);
-
-        TransitionIdList::iterator begin() { return _activeTransitionIds.begin(); }
-        TransitionIdList::iterator end() { return _activeTransitionIds.end(); }
-
-    private:
-
-        using Transitions = indexed_container::IndexedVector<Transition>;
-
-        static std::string _name;
-
-        Transitions _transitions;
-        TransitionIdList _activeTransitionIds;
     };
     using TransitionStagePointer = std::shared_ptr<TransitionStage>;
 
-    class TransitionStageSetup {
+    class TransitionStageSetup : public StageSetup<TransitionStage> {
     public:
-        using JobModel = render::Job::Model<TransitionStageSetup>;
-
-        TransitionStageSetup();
-        void run(const RenderContextPointer& renderContext);
-
-    protected:
+        using JobModel = Job::Model<TransitionStageSetup>;
     };
 
 }