diff --git a/indra/llprimitive/llprimitive.cpp b/indra/llprimitive/llprimitive.cpp
index 5dfce4ae16abdbaecd02f1b6f79dbe94260fef0c..b45ead92d30b1bd99b93090b6b1c9d9e8bedaa24 100644
--- a/indra/llprimitive/llprimitive.cpp
+++ b/indra/llprimitive/llprimitive.cpp
@@ -404,6 +404,11 @@ S32  LLPrimitive::setTEFullbright(const U8 index, const U8 fullbright)
 	return mTextureList.setFullbright(index, fullbright);
 }
 
+S32 LLPrimitive::setTERenderableTarget(const U8 te, const LLTextureEntry::eRenderableTarget target)
+{
+    return mTextureList.setRenderableTarget(te, target);
+}
+
 S32  LLPrimitive::setTEMediaFlags(const U8 index, const U8 media_flags)
 {
 	return mTextureList.setMediaFlags(index, media_flags);
diff --git a/indra/llprimitive/llprimitive.h b/indra/llprimitive/llprimitive.h
index d2adfa4a3d692744d8698c71d030bea0eea9a7a5..520c7c7ac8348d7f6360903d9ceb0fccedeaa394 100644
--- a/indra/llprimitive/llprimitive.h
+++ b/indra/llprimitive/llprimitive.h
@@ -485,6 +485,7 @@ class LLPrimitive : public LLXform
 	virtual S32 setTETexGen(const U8 te, const U8 texgen);
 	virtual S32 setTEShiny(const U8 te, const U8 shiny);
 	virtual S32 setTEFullbright(const U8 te, const U8 fullbright);
+    virtual S32 setTERenderableTarget(const U8 te, const LLTextureEntry::eRenderableTarget target);
 	virtual S32 setTEMediaFlags(const U8 te, const U8 flags);
 	virtual S32 setTEGlow(const U8 te, const F32 glow);
 	virtual S32 setTEMaterialID(const U8 te, const LLMaterialID& pMaterialID);
diff --git a/indra/llprimitive/llprimtexturelist.cpp b/indra/llprimitive/llprimtexturelist.cpp
index f4f08248b82dde355796be88bd2abce6c72abc59..49b59e35cbfe6fe4619f8e958467b952d0ffb21c 100644
--- a/indra/llprimitive/llprimtexturelist.cpp
+++ b/indra/llprimitive/llprimtexturelist.cpp
@@ -341,6 +341,16 @@ S32 LLPrimTextureList::setFullbright(const U8 index, const U8 fullbright)
 	return TEM_CHANGE_NONE;
 }
 
+S32 LLPrimTextureList::setRenderableTarget(const U8 index, const U8 target)
+{
+    if (index < mEntryList.size())
+    {
+        return mEntryList[index]->setRenderableTarget((LLTextureEntry::eRenderableTarget)target);
+    }
+    
+    return TEM_CHANGE_NONE;
+}
+
 S32 LLPrimTextureList::setMediaFlags(const U8 index, const U8 media_flags)
 {
 	if (index < mEntryList.size())
diff --git a/indra/llprimitive/llprimtexturelist.h b/indra/llprimitive/llprimtexturelist.h
index 49c636e40f1866004e0831facd4a2f4d7f7cfc5d..34b87d879f6ad2adb36a4c3ba06ec49a190ac677 100644
--- a/indra/llprimitive/llprimtexturelist.h
+++ b/indra/llprimitive/llprimtexturelist.h
@@ -102,6 +102,7 @@ class LLPrimTextureList
 	S32 setTexGen(const U8 index, const U8 texgen);
 	S32 setShiny(const U8 index, const U8 shiny);
 	S32 setFullbright(const U8 index, const U8 t);
+    S32 setRenderableTarget(const U8 index, const U8 target);
 	S32 setMediaFlags(const U8 index, const U8 media_flags);
 	S32 setGlow(const U8 index, const F32 glow);
 	S32 setMaterialID(const U8 index, const LLMaterialID& pMaterialID);
diff --git a/indra/llprimitive/lltextureentry.cpp b/indra/llprimitive/lltextureentry.cpp
index 71caff1686938dfd99b20c61443b25503f1210d4..d057e5ab29e5cbb0788da5c43f10ec33f327d853 100644
--- a/indra/llprimitive/lltextureentry.cpp
+++ b/indra/llprimitive/lltextureentry.cpp
@@ -504,6 +504,16 @@ S32 LLTextureEntry::setFullbright(U8 fullbright)
 	return TEM_CHANGE_NONE;
 }
 
+S32 LLTextureEntry::setRenderableTarget(eRenderableTarget target)
+{
+    if (getRenderableTarget() != target) {
+        mRenderableTarget = target;
+        return TEM_CHANGE_TEXTURE;
+    }
+    
+    return TEM_CHANGE_NONE;
+}
+
 S32 LLTextureEntry::setShiny(U8 shiny)
 {
 	shiny &= TEM_SHINY_MASK;
@@ -685,6 +695,15 @@ S32 LLTextureEntry::setMaterialParams(const LLMaterialPtr pMaterialParams)
 		mMaterialUpdatePending = true;
 	}
 	mMaterial = pMaterialParams;
+    
+    // TODO: GZ: We should avoid magic UUIDs in the future, but for development we're using one for the time being.  Remove this later.
+    if (mMaterial->getSpecularID().asString() == "da7ecda1-e780-423f-ce27-26df7dc69cb6")
+    {
+        setRenderableTarget(RT_MIRROR);
+    } else {
+        setRenderableTarget(RT_DISABLED);
+    }
+    
 	return TEM_CHANGE_TEXTURE;
 }
 
diff --git a/indra/llprimitive/lltextureentry.h b/indra/llprimitive/lltextureentry.h
index f5f2c0172d89021e9b0cf4aff9097787cfd3b16a..2c0832e1d46543ce03ff8cb5142e0ce3ca6f3de6 100644
--- a/indra/llprimitive/lltextureentry.h
+++ b/indra/llprimitive/lltextureentry.h
@@ -80,6 +80,12 @@ class LLTextureEntry
 		TEX_GEN_SPHERICAL		= 0x04,
 		TEX_GEN_CYLINDRICAL		= 0x06
 	} eTexGen;
+    
+    
+    typedef enum e_renderable_target {
+        RT_DISABLED = 0x00,
+        RT_MIRROR = 0x02
+    } eRenderableTarget;
 
 	LLTextureEntry();
 	LLTextureEntry(const LLUUID& tex_id);
@@ -134,7 +140,9 @@ class LLTextureEntry
     S32  setGlow(F32 glow);
 	S32  setMaterialID(const LLMaterialID& pMaterialID);
 	S32  setMaterialParams(const LLMaterialPtr pMaterialParams);
-	
+    
+    S32  setRenderableTarget(eRenderableTarget target);
+    
 	virtual const LLUUID &getID() const { return mID; }
 	const LLColor4 &getColor() const { return mColor; }
     const F32 getAlpha() const { return mColor.mV[VALPHA]; }
@@ -152,6 +160,7 @@ class LLTextureEntry
 
 	U8	 getBumpmap() const { return mBump & TEM_BUMP_MASK; }
 	U8	 getFullbright() const { return (mBump>>TEM_FULLBRIGHT_SHIFT) & TEM_FULLBRIGHT_MASK; }
+    eRenderableTarget getRenderableTarget() const { return mRenderableTarget; }
 	U8	 getShiny() const { return (mBump>>TEM_SHINY_SHIFT) & TEM_SHINY_MASK; }
 	U8	 getBumpShiny() const { return mBump & TEM_BUMP_SHINY_MASK; }
  	U8	 getBumpShinyFullbright() const { return mBump; }
@@ -233,6 +242,7 @@ class LLTextureEntry
 	LLColor4			mColor;
 	U8					mBump;					// Bump map, shiny, and fullbright
 	U8					mMediaFlags;			// replace with web page, movie, etc.
+    eRenderableTarget   mRenderableTarget;
 	F32                 mGlow;
 	bool                mMaterialUpdatePending;
 	LLMaterialID        mMaterialID;
diff --git a/indra/llrender/llglslshader.cpp b/indra/llrender/llglslshader.cpp
index ccfb8f69bef9d08e2eaac3454cc4356461832eb2..be5ad08fbea6eb04c6a201cbd6ebdace0403625e 100644
--- a/indra/llrender/llglslshader.cpp
+++ b/indra/llrender/llglslshader.cpp
@@ -1184,12 +1184,14 @@ S32 LLGLSLShader::getTextureChannel(S32 uniform) const
 S32 LLGLSLShader::enableTexture(S32 uniform, LLTexUnit::eTextureType mode, LLTexUnit::eTextureColorSpace space)
 {
     LL_PROFILE_ZONE_SCOPED_CATEGORY_SHADER;
-
+    
     if (uniform < 0 || uniform >= (S32)mTexture.size())
     {
         LL_SHADER_UNIFORM_ERRS() << "Uniform out of range: " << uniform << LL_ENDL;
         return -1;
     }
+
+    
     S32 index = mTexture[uniform];
     if (index != -1)
     {
diff --git a/indra/llrender/llglslshader.h b/indra/llrender/llglslshader.h
index b8071248e2861f9789905744eeb7abbd81060e7f..71b9b0f4fb6d239635924a564fca016f4a92c66f 100644
--- a/indra/llrender/llglslshader.h
+++ b/indra/llrender/llglslshader.h
@@ -57,6 +57,7 @@ class LLShaderFeatures
     bool hasAlphaMask = false;
     bool hasReflectionProbes = false;
     bool attachNothing = false;
+    bool hasHeroProbes = false;
 };
 
 // ============= Structure for caching shader uniforms ===============
diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index 75d6ef6c4613409293ae3736d9bfbd8e8ff7bb48..177cad25086b318b661a678f315f77189e6b390b 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -238,6 +238,14 @@ BOOL LLShaderMgr::attachShaderFeatures(LLGLSLShader * shader)
             return FALSE;
         }
 	}
+    
+    if (features->hasHeroProbes)
+    {
+        if (!shader->attachFragmentObject("deferred/heroProbesUtil.glsl"))
+        {
+            return FALSE;
+        }
+    }
 
     if (features->hasShadows)
 	{
@@ -1260,6 +1268,7 @@ void LLShaderMgr::initAttribsAndUniforms()
     mReservedUniforms.push_back("sceneDepth");
     mReservedUniforms.push_back("reflectionProbes");
     mReservedUniforms.push_back("irradianceProbes");
+    mReservedUniforms.push_back("heroProbes");
 	mReservedUniforms.push_back("cloud_noise_texture");
     mReservedUniforms.push_back("cloud_noise_texture_next");
 	mReservedUniforms.push_back("fullbright");
diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h
index 46f352aa58c29fa69c0d51b7405d9aab15f8b296..cfa824c3612f1e2d1f1eb3fdf4baecd8f2025257 100644
--- a/indra/llrender/llshadermgr.h
+++ b/indra/llrender/llshadermgr.h
@@ -96,6 +96,7 @@ class LLShaderMgr
         SCENE_DEPTH,                        //  "sceneDepth"
         REFLECTION_PROBES,                  //  "reflectionProbes"
         IRRADIANCE_PROBES,                  //  "irradianceProbes"
+        HERO_PROBE,                         //  "heroProbes"
         CLOUD_NOISE_MAP,                    //  "cloud_noise_texture"
         CLOUD_NOISE_MAP_NEXT,               //  "cloud_noise_texture_next"
         FULLBRIGHT,                         //  "fullbright"
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 2b2a2d9d1b63c7fcad86ec1c1eb392f6f56f179c..270d5e6869ca6e8aec75ca06786b892a345373ea 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -9175,6 +9175,17 @@
       <real>0.00</real>
     </array>
   </map>
+  <key>RenderMirrors</key>
+  <map>
+    <key>Comment</key>
+    <string>Renders realtime mirrors.</string>
+    <key>Persist</key>
+    <integer>1</integer>
+    <key>Type</key>
+    <string>Boolean</string>
+    <key>Value</key>
+    <integer>1</integer>
+  </map>
   <key>RenderScreenSpaceReflections</key>
   <map>
     <key>Comment</key>
diff --git a/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl b/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl
index 1b2a34ef015efdb91f526660825007449a5cca36..298ddf92a7523a21ffe445cf5bcf0f11948dacb3 100644
--- a/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl
+++ b/indra/newview/app_settings/shaders/class3/deferred/reflectionProbeF.glsl
@@ -31,6 +31,7 @@ float tapScreenSpaceReflection(int totalSamples, vec2 tc, vec3 viewPos, vec3 n,
 
 uniform samplerCubeArray   reflectionProbes;
 uniform samplerCubeArray   irradianceProbes;
+
 uniform sampler2D sceneMap;
 uniform int cube_snapshot;
 uniform float max_probe_lod;
@@ -70,6 +71,9 @@ layout (std140) uniform ReflectionProbes
 
     // number of reflection probes present in refSphere
     int refmapCount;
+    
+    vec4 heroPosition[1];
+    int heroProbeCount;
 };
 
 // Inputs
@@ -513,6 +517,10 @@ vec3 tapRefMap(vec3 pos, vec3 dir, out float w, out float dw, float lod, vec3 c,
 
     v = env_mat * v;
     
+#if defined(HERO_PROBES)
+    vec3 mirror = textureLod(heroProbes, vec4(v.xyz, 0), lod).rgb;
+#endif
+    
     vec4 ret = textureLod(reflectionProbes, vec4(v.xyz, refIndex[i].x), lod) * refParams[i].y;
 
     return ret.rgb;
diff --git a/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl b/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl
index 4ef003e0cb4a42f6b601e1c0da0dd1d14ccd9593..a63d44511518552cd93447b86ef447489eee9605 100644
--- a/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl
+++ b/indra/newview/app_settings/shaders/class3/deferred/softenLightF.glsl
@@ -32,6 +32,14 @@ uniform sampler2D specularRect;
 uniform sampler2D normalMap;
 uniform sampler2D emissiveRect; // PBR linear packed Occlusion, Roughness, Metal. See: pbropaqueF.glsl
 
+uniform samplerCubeArray   heroProbes;
+
+layout (std140) uniform HeroProbeData
+{
+    vec4 heroPosition[1];
+    int heroProbeCount;
+};
+
 const float M_PI = 3.14159265;
 
 #if defined(HAS_SUN_SHADOW) || defined(HAS_SSAO)
@@ -290,7 +298,7 @@ void main()
         vec4 fogged = applyWaterFogViewLinear(pos.xyz, vec4(color, bloom));
         color       = fogged.rgb;
     #endif
-
-    frag_color.rgb = max(color.rgb, vec3(0)); //output linear since local lights will be added to this shader's results
+    frag_color.rgb = textureLod(heroProbes, vec4(norm.xyz, 0), 0).rgb;
+    //frag_color.rgb = max(color.rgb, vec3(0)); //output linear since local lights will be added to this shader's results
     frag_color.a = 0.0;
 }
diff --git a/indra/newview/llpanelface.cpp b/indra/newview/llpanelface.cpp
index 7d6015f557e03a351cd0decad240bfacbbe6d446..6b737b6895257e3426b4e5f6f96617a7d405d081 100644
--- a/indra/newview/llpanelface.cpp
+++ b/indra/newview/llpanelface.cpp
@@ -592,6 +592,17 @@ void LLPanelFace::sendFullbright()
 	LLSelectMgr::getInstance()->selectionSetFullbright( fullbright );
 }
 
+void LLPanelFace::sendMirror()
+{
+    LLCheckBoxCtrl* mCheckMirror = getChild<LLCheckBoxCtrl>("checkbox mirror");
+    
+    if (!mCheckMirror)
+        return;
+    
+    LLTextureEntry::eRenderableTarget target = mCheckMirror->get() ? LLTextureEntry::RT_MIRROR : LLTextureEntry::RT_DISABLED;
+    LLSelectMgr::getInstance()->selectionSetRenderableTarget(target);
+}
+
 void LLPanelFace::sendColor()
 {
 	
@@ -1686,6 +1697,7 @@ void LLPanelFace::updateUI(bool force_set_values /*false*/)
 					getChild<LLUICtrl>("shinyOffsetV")->setValue(offset_y);
 					getChild<LLUICtrl>("glossiness")->setValue(material->getSpecularLightExponent());
 					getChild<LLUICtrl>("environment")->setValue(material->getEnvironmentIntensity());
+                    getChild<LLUICtrl>("mirror")->setValue(material->getEnvironmentIntensity());
 
 					updateShinyControls(!material->getSpecularID().isNull(), true);
 		}
@@ -2994,6 +3006,12 @@ void LLPanelFace::onCommitFullbright(LLUICtrl* ctrl, void* userdata)
 	self->sendFullbright();
 }
 
+void LLPanelFace::onCommitMirror(LLUICtrl* ctrl, void* userdata)
+{
+    LLPanelFace* self = (LLPanelFace*) userdata;
+    self->sendMirror();
+}
+
 // static
 void LLPanelFace::onCommitGlow(LLUICtrl* ctrl, void* userdata)
 {
diff --git a/indra/newview/llpanelface.h b/indra/newview/llpanelface.h
index 106259b84aa18bb1f12d3c448943a04fd38ab412..f6d6d99d9f0b0c105a3ad94647e1abaf0a714d78 100644
--- a/indra/newview/llpanelface.h
+++ b/indra/newview/llpanelface.h
@@ -138,6 +138,7 @@ class LLPanelFace : public LLPanel
 	void			sendTexGen();				// applies and sends bump map
 	void			sendShiny(U32 shininess);			// applies and sends shininess
 	void			sendFullbright();		// applies and sends full bright
+    void            sendMirror();
 	void        sendGlow();
     void            alignTestureLayer();
 
@@ -227,7 +228,8 @@ class LLPanelFace : public LLPanel
 	static void		onCommitShiny(				LLUICtrl* ctrl, void* userdata);
 	static void		onCommitAlphaMode(		LLUICtrl* ctrl, void* userdata);
 	static void		onCommitFullbright(		LLUICtrl* ctrl, void* userdata);
-	static void    onCommitGlow(				LLUICtrl* ctrl, void *userdata);
+    static void     onCommitMirror(LLUICtrl* ctrl, void* userdata);
+	static void     onCommitGlow(				LLUICtrl* ctrl, void *userdata);
 	static void		onCommitPlanarAlign(		LLUICtrl* ctrl, void* userdata);
 	static void		onCommitRepeatsPerMeter(	LLUICtrl* ctrl, void* userinfo);
 
diff --git a/indra/newview/llreflectionmap.h b/indra/newview/llreflectionmap.h
index 7ea0fe6187ca448c9ff2c29ef4d8b5f182d658de..831a6358eee3dee77cceadeb15a2a1229cb715d9 100644
--- a/indra/newview/llreflectionmap.h
+++ b/indra/newview/llreflectionmap.h
@@ -36,6 +36,15 @@ class alignas(16) LLReflectionMap : public LLRefCount
 {
     LL_ALIGN_NEW
 public:
+    
+    enum class ProbeType
+    {
+        ALL = 0,
+        RADIANCE,
+        IRRADIANCE,
+        REFLECTION
+    };
+    
     // allocate an environment map of the given resolution 
     LLReflectionMap();
 
@@ -127,5 +136,7 @@ class alignas(16) LLReflectionMap : public LLRefCount
     GLuint mOcclusionQuery = 0;
     bool mOccluded = false;
     U32 mOcclusionPendingFrames = 0;
+    
+    ProbeType mType;
 };
 
diff --git a/indra/newview/llreflectionmapmanager.cpp b/indra/newview/llreflectionmapmanager.cpp
index e30aba3df758881aa918ae7eac9000e885f9d99d..8e7d073167021b2990a4f79e2de9a2d886b487b2 100644
--- a/indra/newview/llreflectionmapmanager.cpp
+++ b/indra/newview/llreflectionmapmanager.cpp
@@ -130,6 +130,13 @@ void LLReflectionMapManager::update()
         U32 targetRes = mProbeResolution * 4; // super sample
         mRenderTarget.allocate(targetRes, targetRes, color_fmt, true);
     }
+    
+    if (!mHeroRenderTarget.isComplete())
+    {
+        U32 color_fmt = GL_RGB16F;
+        U32 targetRes = mHeroProbeResolution * 2;
+        mHeroRenderTarget.allocate(targetRes, targetRes, color_fmt, true);
+    }
 
     if (mMipChain.empty())
     {
@@ -315,7 +322,7 @@ void LLReflectionMapManager::update()
         mRadiancePass = mRealtimeRadiancePass;
         for (U32 i = 0; i < 6; ++i)
         {
-            updateProbeFace(closestDynamic, i);
+            updateProbeFace(closestDynamic, i, mProbeResolution);
         }
         mRealtimeRadiancePass = !mRealtimeRadiancePass;
 
@@ -354,6 +361,10 @@ void LLReflectionMapManager::update()
         oldestOccluded->autoAdjustOrigin();
         oldestOccluded->mLastUpdateTime = gFrameTimeSeconds;
     }
+    
+    
+    doHeroProbeUpdate();
+    
 }
 
 LLReflectionMap* LLReflectionMapManager::addProbe(LLSpatialGroup* group)
@@ -528,7 +539,7 @@ void LLReflectionMapManager::doProbeUpdate()
     LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
     llassert(mUpdatingProbe != nullptr);
 
-    updateProbeFace(mUpdatingProbe, mUpdatingFace);
+    updateProbeFace(mUpdatingProbe, mUpdatingFace, mProbeResolution);
     
     if (++mUpdatingFace == 6)
     {
@@ -547,6 +558,21 @@ void LLReflectionMapManager::doProbeUpdate()
     }
 }
 
+void LLReflectionMapManager::doHeroProbeUpdate()
+{
+    LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY;
+    llassert(mHeroProbe != nullptr);
+    
+    touch_default_probe(mHeroProbe);
+    
+    for (int i = 0; i < 6; i++)
+    {
+        gPipeline.mRT = &gPipeline.mAuxillaryRT;
+        mHeroProbe->update(mHeroProbeResolution, i);
+        gPipeline.mRT = &gPipeline.mMainRT;
+    }
+}
+
 // Do the reflection map update render passes.
 // For every 12 calls of this function, one complete reflection probe radiance map and irradiance map is generated
 // First six passes render the scene with direct lighting only into a scratch space cube map at the end of the cube map array and generate 
@@ -555,10 +581,20 @@ void LLReflectionMapManager::doProbeUpdate()
 // The next six passes render the scene with both radiance and irradiance into the same scratch space cube map and generate a simple mip chain.
 // At the end of these passes, a radiance map is generated for this probe and placed into the radiance cube map array at the index for this probe.
 // In effect this simulates single-bounce lighting.
-void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
+void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face, U32 probeResolution, LLPointer<LLCubeMapArray> cubeArray)
 {
     // hacky hot-swap of camera specific render targets
     gPipeline.mRT = &gPipeline.mAuxillaryRT;
+    
+    LLRenderTarget* target = &mRenderTarget;
+    
+    S32 sourceIdx = mReflectionProbeCount;
+    
+    if (probeResolution == mHeroProbeResolution)
+    {
+        sourceIdx = 0;
+        target = &mHeroRenderTarget;
+    }
 
     mLightScale = 1.f;
     static LLCachedControl<F32> max_local_light_ambiance(gSavedSettings, "RenderReflectionProbeMaxLocalLightAmbiance", 8.f);
@@ -577,20 +613,18 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
         gPipeline.andRenderTypeMask(LLPipeline::RENDER_TYPE_SKY, LLPipeline::RENDER_TYPE_WL_SKY,
             LLPipeline::RENDER_TYPE_WATER, LLPipeline::RENDER_TYPE_VOIDWATER, LLPipeline::RENDER_TYPE_CLOUDS, LLPipeline::RENDER_TYPE_TERRAIN, LLPipeline::END_RENDER_TYPES);
         
-        probe->update(mRenderTarget.getWidth(), face);
+        probe->update(target->getWidth(), face);
 
         gPipeline.popRenderTypeMask();
     }
     else
     {
-        probe->update(mRenderTarget.getWidth(), face);
+        probe->update(target->getWidth(), face);
     }
     
     gPipeline.mRT = &gPipeline.mMainRT;
 
-    S32 sourceIdx = mReflectionProbeCount;
-
-    if (probe != mUpdatingProbe)
+    if (probe != mUpdatingProbe && probe->mType != LLReflectionMap::ProbeType::REFLECTION)
     { // this is the "realtime" probe that's updating every frame, use the secondary scratch space channel
         sourceIdx += 1;
     }
@@ -611,7 +645,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
         gGL.loadIdentity();
 
         gGL.flush();
-        U32 res = mProbeResolution * 2;
+        U32 res = probeResolution * 2;
 
         static LLStaticHashedString resScale("resScale");
         static LLStaticHashedString direction("direction");
@@ -623,20 +657,20 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
         // perform a gaussian blur on the super sampled render before downsampling
         {
             gGaussianProgram.bind();
-            gGaussianProgram.uniform1f(resScale, 1.f / (mProbeResolution * 2));
+            gGaussianProgram.uniform1f(resScale, 1.f / (probeResolution * 2));
             S32 diffuseChannel = gGaussianProgram.enableTexture(LLShaderMgr::DEFERRED_DIFFUSE, LLTexUnit::TT_TEXTURE);
 
             // horizontal
             gGaussianProgram.uniform2f(direction, 1.f, 0.f);
             gGL.getTexUnit(diffuseChannel)->bind(screen_rt);
-            mRenderTarget.bindTarget();
+            target->bindTarget();
             gPipeline.mScreenTriangleVB->setBuffer();
             gPipeline.mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3);
-            mRenderTarget.flush();
+            target->flush();
 
             // vertical
             gGaussianProgram.uniform2f(direction, 0.f, 1.f);
-            gGL.getTexUnit(diffuseChannel)->bind(&mRenderTarget);
+            gGL.getTexUnit(diffuseChannel)->bind(target);
             screen_rt->bindTarget();
             gPipeline.mScreenTriangleVB->setBuffer();
             gPipeline.mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3);
@@ -644,7 +678,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
         }
 
 
-        S32 mips = log2((F32)mProbeResolution) + 0.5f;
+        S32 mips = log2((F32)probeResolution) + 0.5f;
 
         gReflectionMipProgram.bind();
         S32 diffuseChannel = gReflectionMipProgram.enableTexture(LLShaderMgr::DEFERRED_DIFFUSE, LLTexUnit::TT_TEXTURE);
@@ -663,7 +697,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
             }
 
             
-            gReflectionMipProgram.uniform1f(resScale, 1.f/(mProbeResolution*2));
+            gReflectionMipProgram.uniform1f(resScale, 1.f/(probeResolution*2));
             
             gPipeline.mScreenTriangleVB->setBuffer();
             gPipeline.mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3);
@@ -675,14 +709,14 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
             if (mip >= 0)
             {
                 LL_PROFILE_GPU_ZONE("probe mip copy");
-                mTexture->bind(0);
+                probe->mCubeArray->bind(0);
                 //glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, probe->mCubeIndex * 6 + face, 0, 0, res, res);
                 glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, sourceIdx * 6 + face, 0, 0, res, res);
                 //if (i == 0)
                 //{
                     //glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, mip, 0, 0, probe->mCubeIndex * 6 + face, 0, 0, res, res);
                 //}
-                mTexture->unbind();
+                probe->mCubeArray->unbind();
             }
             mMipChain[i].flush();
         }
@@ -695,7 +729,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
         gReflectionMipProgram.unbind();
     }
 
-    if (face == 5)
+    if (face == 5 && probe->mType != LLReflectionMap::ProbeType::REFLECTION)
     {
         mMipChain[0].bindTarget();
         static LLStaticHashedString sSourceIdx("sourceIdx");
@@ -707,7 +741,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
             mVertexBuffer->setBuffer();
 
             S32 channel = gRadianceGenProgram.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
-            mTexture->bind(channel);
+            probe->mCubeArray->bind(channel);
             gRadianceGenProgram.uniform1i(sSourceIdx, sourceIdx);
             gRadianceGenProgram.uniform1f(LLShaderMgr::REFLECTION_PROBE_MAX_LOD, mMaxProbeLOD);
 
@@ -722,7 +756,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
 
                 gRadianceGenProgram.uniform1f(sRoughness, (F32)i / (F32)(mMipChain.size() - 1));
                 gRadianceGenProgram.uniform1f(sMipLevel, i);
-                gRadianceGenProgram.uniform1i(sWidth, mProbeResolution);
+                gRadianceGenProgram.uniform1i(sWidth, probeResolution);
 
                 for (int cf = 0; cf < 6; ++cf)
                 { // for each cube face
@@ -752,7 +786,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
             //generate irradiance map
             gIrradianceGenProgram.bind();
             S32 channel = gIrradianceGenProgram.enableTexture(LLShaderMgr::REFLECTION_PROBES, LLTexUnit::TT_CUBE_MAP_ARRAY);
-            mTexture->bind(channel);
+            probe->mCubeArray->bind(channel);
 
             gIrradianceGenProgram.uniform1i(sSourceIdx, sourceIdx);
             gIrradianceGenProgram.uniform1f(LLShaderMgr::REFLECTION_PROBE_MAX_LOD, mMaxProbeLOD);
@@ -787,7 +821,7 @@ void LLReflectionMapManager::updateProbeFace(LLReflectionMap* probe, U32 face)
                     S32 res = mMipChain[i].getWidth();
                     mIrradianceMaps->bind(channel);
                     glCopyTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, i - start_mip, 0, 0, probe->mCubeIndex * 6 + cf, 0, 0, res, res);
-                    mTexture->bind(channel);
+                    probe->mCubeArray->bind(channel);
                 }
             }
         }
@@ -890,7 +924,7 @@ void LLReflectionMapManager::updateUniforms()
 
         GLint refBucket[256][4]; //lookup table for which index to start with for the given Z depth
         // numbrer of active refmaps
-        GLint refmapCount;  
+        GLint refmapCount;
     };
 
     mReflectionMaps.resize(mReflectionProbeCount);
@@ -1068,7 +1102,7 @@ void LLReflectionMapManager::updateUniforms()
 #endif
 
     rpd.refmapCount = count;
-
+    
     //copy rpd into uniform buffer object
     if (mUBO == 0)
     {
@@ -1081,7 +1115,35 @@ void LLReflectionMapManager::updateUniforms()
         glBufferData(GL_UNIFORM_BUFFER, sizeof(ReflectionProbeData), &rpd, GL_STREAM_DRAW);
         glBindBuffer(GL_UNIFORM_BUFFER, 0);
     }
+    
+    struct HeroProbeData
+    {
+        LLVector4 heroPosition[1];
+        GLint heroProbeCount = 1;
+    };
+    
+    HeroProbeData hpd;
+    
+    modelview.loadu(gGLModelView);
+    
+    oa.set(0, 0, 0, 0);
+    hpd.heroProbeCount = 1;
+    modelview.affineTransform(mHeroProbe->mOrigin, oa);
+    hpd.heroPosition[0].set(oa.getF32ptr());
+
+    //copy rpd into uniform buffer object
+    if (mUBO == 0)
+    {
+        glGenBuffers(1, &mHeroUBO);
+    }
 
+    {
+        LL_PROFILE_ZONE_NAMED_CATEGORY_DISPLAY("rmmsu - update buffer");
+        glBindBuffer(GL_UNIFORM_BUFFER, mHeroUBO);
+        glBufferData(GL_UNIFORM_BUFFER, sizeof(HeroProbeData), &hpd, GL_STREAM_DRAW);
+        glBindBuffer(GL_UNIFORM_BUFFER, 0);
+    }
+    
 #if 0
     if (!gCubeSnapshot)
     {
@@ -1214,7 +1276,7 @@ void LLReflectionMapManager::initReflectionMaps()
 {
     U32 count = LL_MAX_REFLECTION_PROBE_COUNT;
 
-    if (mTexture.isNull() || mReflectionProbeCount != count || mReset)
+    if (mTexture.isNull() || mReflectionProbeCount != count || mReset || mHeroArray.isNull())
     {
         mReset = false;
         mReflectionProbeCount = count;
@@ -1263,6 +1325,22 @@ void LLReflectionMapManager::initReflectionMaps()
         mDefaultProbe->mRadius = 4096.f;
         mDefaultProbe->mProbeIndex = 0;
         touch_default_probe(mDefaultProbe);
+        
+        mHeroProbeResolution = 512;
+        
+        mHeroArray = new LLCubeMapArray();
+        mHeroArray->allocate(mHeroProbeResolution, 3, 1);
+        
+        if (mHeroProbe.isNull()) {
+            mHeroProbe = new LLReflectionMap();
+        }
+        
+        mHeroProbe->mCubeIndex = 0;
+        mHeroProbe->mCubeArray = mHeroArray;
+        mHeroProbe->mDistance = 64.f;
+        mHeroProbe->mRadius = 4096.f;
+        mHeroProbe->mProbeIndex = 0;
+        touch_default_probe(mHeroProbe);
 
     }
 
@@ -1291,11 +1369,13 @@ void LLReflectionMapManager::cleanup()
 { 
     mVertexBuffer = nullptr;
     mRenderTarget.release();
+    mHeroRenderTarget.release();
 
     mMipChain.clear();
 
     mTexture = nullptr;
     mIrradianceMaps = nullptr;
+    mHeroArray = nullptr;
 
     mProbes.clear();
     mKillList.clear();
@@ -1306,9 +1386,14 @@ void LLReflectionMapManager::cleanup()
     
     mDefaultProbe = nullptr;
     mUpdatingProbe = nullptr;
+    
+    mHeroProbe = nullptr;
 
     glDeleteBuffers(1, &mUBO);
     mUBO = 0;
+    
+    glDeleteBuffers(1, &mHeroUBO);
+    mHeroUBO = 0;
 
     // note: also called on teleport (not just shutdown), so make sure we're in a good "starting" state
     initCubeFree();
diff --git a/indra/newview/llreflectionmapmanager.h b/indra/newview/llreflectionmapmanager.h
index 5a3901cae9f29aca4886af3afda31a01bc455da2..2ca9a8d5987a657197d376ac5ac63d33e13fae91 100644
--- a/indra/newview/llreflectionmapmanager.h
+++ b/indra/newview/llreflectionmapmanager.h
@@ -125,11 +125,15 @@ class alignas(16) LLReflectionMapManager
     // render target for cube snapshots
     // used to generate mipmaps without doing a copy-to-texture
     LLRenderTarget mRenderTarget;
+    
+    LLRenderTarget mHeroRenderTarget;
 
     std::vector<LLRenderTarget> mMipChain;
 
     // storage for reflection probe radiance maps (plus two scratch space cubemaps)
     LLPointer<LLCubeMapArray> mTexture;
+    
+    LLPointer<LLCubeMapArray> mHeroArray;
 
     // vertex buffer for pushing verts to filter shaders
     LLPointer<LLVertexBuffer> mVertexBuffer;
@@ -142,9 +146,11 @@ class alignas(16) LLReflectionMapManager
 
     // perform an update on the currently updating Probe
     void doProbeUpdate();
+    
+    void doHeroProbeUpdate();
 
     // update the specified face of the specified probe
-    void updateProbeFace(LLReflectionMap* probe, U32 face);
+    void updateProbeFace(LLReflectionMap* probe, U32 face, U32 probeResolution);
     
     // list of active reflection maps
     std::vector<LLPointer<LLReflectionMap> > mProbes;
@@ -157,6 +163,9 @@ class alignas(16) LLReflectionMapManager
 
     // handle to UBO
     U32 mUBO = 0;
+    
+    // Hero UBO
+    U32 mHeroUBO = 0;
 
     // list of maps being used for rendering
     std::vector<LLReflectionMap*> mReflectionMaps;
@@ -176,12 +185,16 @@ class alignas(16) LLReflectionMapManager
     bool mRealtimeRadiancePass = false;
 
     LLPointer<LLReflectionMap> mDefaultProbe;  // default reflection probe to fall back to for pixels with no probe influences (should always be at cube index 0)
+    
+    LLPointer<LLReflectionMap> mHeroProbe;
 
     // number of reflection probes to use for rendering
     U32 mReflectionProbeCount;
 
     // resolution of reflection probes
     U32 mProbeResolution = 128;
+    
+    U32 mHeroProbeResolution = 512;
 
     // maximum LoD of reflection probes (mip levels - 1)
     F32 mMaxProbeLOD = 6.f;
diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp
index dca341e5a2e441b1d23eb4128206bd22f5e71c8c..19ce8f6a55c04646a8d4c3b4edd348ae4f79d34e 100644
--- a/indra/newview/llselectmgr.cpp
+++ b/indra/newview/llselectmgr.cpp
@@ -2347,6 +2347,47 @@ void LLSelectMgr::selectionSetFullbright(U8 fullbright)
 	getSelection()->applyToObjects(&sendfunc);
 }
 
+void LLSelectMgr::selectionSetRenderableTarget(LLTextureEntry::eRenderableTarget target)
+{
+    struct f : public LLSelectedTEFunctor
+    {
+        LLTextureEntry::eRenderableTarget mRenderableTarget;
+        
+        f(const LLTextureEntry::eRenderableTarget& t) : mRenderableTarget(t) {}
+        
+        bool apply(LLViewerObject* object, S32 te)
+        {
+            if (object->permModify())
+            {
+                object->setTERenderableTarget(te, mRenderableTarget);
+            }
+            
+            return true;
+        }
+    } setfunc(target);
+    
+    getSelection()->applyToTEs(&setfunc);
+    
+    struct g : public LLSelectedObjectFunctor
+    {
+        LLTextureEntry::eRenderableTarget mRenderableTarget;
+        
+        g(const LLTextureEntry::eRenderableTarget& t) : mRenderableTarget(t) {}
+        
+        virtual bool apply(LLViewerObject* object)
+        {
+            if (object->permModify())
+            {
+                object->sendTEUpdate();
+            }
+            
+            return true;
+        }
+    } sendfunc(target);
+    
+    getSelection()->applyToObjects(&sendfunc);
+}
+
 // This function expects media_data to be a map containing relevant
 // media data name/value pairs (e.g. home_url, etc.)
 void LLSelectMgr::selectionSetMedia(U8 media_type, const LLSD &media_data)
diff --git a/indra/newview/llselectmgr.h b/indra/newview/llselectmgr.h
index ca9a32f0db7e6c831177db2733125416a42d1734..93d750215b396f0f88f187b074b01556f4d7627d 100644
--- a/indra/newview/llselectmgr.h
+++ b/indra/newview/llselectmgr.h
@@ -653,6 +653,7 @@ class LLSelectMgr : public LLEditMenuHandler, public LLSimpleton<LLSelectMgr>
 	void selectionSetGlow(const F32 glow);
 	void selectionSetMaterialParams(LLSelectedTEMaterialFunctor* material_func, int specific_te = -1);
 	void selectionRemoveMaterial();
+    void selectionSetRenderableTarget(LLTextureEntry::eRenderableTarget target);
 
 	void selectionSetObjectPermissions(U8 perm_field, BOOL set, U32 perm_mask, BOOL override = FALSE);
 	void selectionSetObjectName(const std::string& name);
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index e67750af7cc16b71bde8679210d82152d302aabd..8930b2453bdb5cd09152f6a70b74f23ed260ba77 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -5284,6 +5284,28 @@ S32 LLViewerObject::setTEFullbright(const U8 te, const U8 fullbright)
 	return retval;
 }
 
+S32 LLViewerObject::setTERenderableTarget(const U8 te, const LLTextureEntry::eRenderableTarget target)
+{
+    S32 retval = 0;
+    
+    const LLTextureEntry *tep = getTE(te);
+    if (!tep)
+    {
+        LL_WARNS() << "No texture entry for te " << (S32)te << ", object " << mID << LL_ENDL;
+    }
+    else if (target != tep->getRenderableTarget())
+    {
+        retval = LLPrimitive::setTERenderableTarget(te, target);
+        setChanged(TEXTURE);
+        if (mDrawable.notNull() && retval)
+        {
+            //gPipeline.markMirror(mDrawable);
+        }
+    }
+    
+    return retval;
+}
+
 
 S32 LLViewerObject::setTEMediaFlags(const U8 te, const U8 media_flags)
 {
diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h
index bf82c43cd35bd2f265f55a05281f6c74ad56b595..f4f596bfd07d09480a0a3119cedace35b1a57508 100644
--- a/indra/newview/llviewerobject.h
+++ b/indra/newview/llviewerobject.h
@@ -359,6 +359,7 @@ class LLViewerObject
 	/*virtual*/	S32		setTEMediaTexGen(const U8 te, const U8 media ); // *FIXME: this confusingly acts upon a superset of setTETexGen's flags without absorbing its semantics
 	/*virtual*/	S32		setTEShiny(const U8 te, const U8 shiny );
 	/*virtual*/	S32		setTEFullbright(const U8 te, const U8 fullbright );
+    /*virtual*/ S32     setTERenderableTarget(const U8 te, const LLTextureEntry::eRenderableTarget target);
 	/*virtual*/	S32		setTEMediaFlags(const U8 te, const U8 media_flags );
 	/*virtual*/ S32     setTEGlow(const U8 te, const F32 glow);
 	/*virtual*/ S32     setTEMaterialID(const U8 te, const LLMaterialID& pMaterialID);
diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp
index 5eae1dfb187e2fe239386efd2768f0d2b829ec11..f6f1cf6854566e8020f16b3247ead8fa6d2e90eb 100644
--- a/indra/newview/llviewershadermgr.cpp
+++ b/indra/newview/llviewershadermgr.cpp
@@ -2228,6 +2228,8 @@ BOOL LLViewerShaderMgr::loadShadersDeferred()
         {
             gDeferredSoftenProgram.addPermutation("LOCAL_LIGHT_KILL", "1");
         }
+        
+        gDeferredSoftenProgram.addPermutation("HERO_PROBES", "1");
 
 		if (gSavedSettings.getBOOL("RenderDeferredSSAO"))
 		{ //if using SSAO, take screen space light map into account as if shadows are enabled
diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp
index 8160785d751d009a5a72854cd3fd549b9367a688..8eacdd2162a45a1029528a2bc615b942b3441644 100644
--- a/indra/newview/llvovolume.cpp
+++ b/indra/newview/llvovolume.cpp
@@ -2344,6 +2344,16 @@ S32 LLVOVolume::setTEFullbright(const U8 te, const U8 fullbright)
 	return  res;
 }
 
+S32 LLVOVolume::setTERenderableTarget(const U8 te, const LLTextureEntry::eRenderableTarget mirror)
+{
+    S32 res = LLViewerObject::setTERenderableTarget(te, mirror);
+    if (res)
+    {
+        //gPipeline.markMirror(mDrawable);
+    }
+    return res;
+}
+
 S32 LLVOVolume::setTEBumpShinyFullbright(const U8 te, const U8 bump)
 {
 	S32 res = LLViewerObject::setTEBumpShinyFullbright(te, bump);
diff --git a/indra/newview/llvovolume.h b/indra/newview/llvovolume.h
index acba18383f9fc6fd8ea75a14bfb684df2623b2e7..f2162ef2441a62ae36ee94a3344a42869b21b327 100644
--- a/indra/newview/llvovolume.h
+++ b/indra/newview/llvovolume.h
@@ -209,6 +209,7 @@ class LLVOVolume : public LLViewerObject
 	/*virtual*/ S32		setTEBumpmap(const U8 te, const U8 bump) override;
 	/*virtual*/ S32		setTEShiny(const U8 te, const U8 shiny) override;
 	/*virtual*/ S32		setTEFullbright(const U8 te, const U8 fullbright) override;
+    /*virtual*/ S32     setTERenderableTarget(const U8 te, const LLTextureEntry::eRenderableTarget target) override;
 	/*virtual*/ S32		setTEBumpShinyFullbright(const U8 te, const U8 bump) override;
 	/*virtual*/ S32		setTEMediaFlags(const U8 te, const U8 media_flags) override;
 	/*virtual*/ S32		setTEGlow(const U8 te, const F32 glow) override;
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index 38f27d4dfa1aa0bfd4643be5c5e19a0bd1d636f9..d6e0bf1b0981da4cd3d419804c1e02c3cdb01a2f 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -8419,6 +8419,13 @@ void LLPipeline::bindReflectionProbes(LLGLSLShader& shader)
         mReflectionMapManager.mIrradianceMaps->bind(channel);
         bound = true;
     }
+    
+    channel = shader.enableTexture(LLShaderMgr::HERO_PROBE, LLTexUnit::TT_CUBE_MAP_ARRAY);
+    if (channel > -1 && mReflectionMapManager.mHeroArray.notNull())
+    {
+        mReflectionMapManager.mHeroArray->bind(channel);
+        bound = true;
+    }
 
     if (bound)
     {