diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp
index e765e45e80d2aad8c84b4ad4db66e579f6424eb3..9e4a0fa5035ea316e7b28a1e9791599e314b853e 100644
--- a/indra/llrender/llshadermgr.cpp
+++ b/indra/llrender/llshadermgr.cpp
@@ -1413,6 +1413,10 @@ void LLShaderMgr::initAttribsAndUniforms()
     mReservedUniforms.push_back("sun_up_factor");
     mReservedUniforms.push_back("moonlight_color");
 
+	// Alchemy
+	mReservedUniforms.push_back("colorgrade_lut");
+	mReservedUniforms.push_back("colorgrade_lut_size");
+
 	llassert(mReservedUniforms.size() == END_RESERVED_UNIFORMS);
 
 	std::set<std::string> dupe_check;
diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h
index 97515df40a00804f866ee8d96064e05f483d32b3..df7c8bdceb6c13077d6536f4cb60d1176fde9d45 100644
--- a/indra/llrender/llshadermgr.h
+++ b/indra/llrender/llshadermgr.h
@@ -257,6 +257,8 @@ class LLShaderMgr
         WATER_EDGE_FACTOR,                  //  "water_edge"
         SUN_UP_FACTOR,                      //  "sun_up_factor"
         MOONLIGHT_COLOR,                    //  "moonlight_color"
+        COLORGRADE_LUT,                     //  "colorgrade_lut"
+        COLORGRADE_LUT_SIZE,                //  "colorgrade_lut_size"
         END_RESERVED_UNIFORMS
     } eGLSLReservedUniforms;
     // clang-format on
diff --git a/indra/newview/alrenderutils.cpp b/indra/newview/alrenderutils.cpp
index cfe81f898be244f009eac4a0fd049e3b2be5c366..0238e601e839c41422d9c18422b1575e0c886f61 100644
--- a/indra/newview/alrenderutils.cpp
+++ b/indra/newview/alrenderutils.cpp
@@ -28,6 +28,9 @@
 
 #include "alrenderutils.h"
 
+#include "llimagepng.h"
+#include "llimagetga.h"
+#include "llimagewebp.h"
 #include "llrendertarget.h"
 #include "llvertexbuffer.h"
 
@@ -50,6 +53,7 @@ static LLStaticHashedString tone_uncharted_c("tone_uncharted_c");
 ALRenderUtil::ALRenderUtil()
 {
 	// Connect settings
+	gSavedSettings.getControl("RenderColorGradeLUT")->getSignal()->connect(boost::bind(&ALRenderUtil::setupColorGrade, this));
 	gSavedSettings.getControl("RenderToneMapType")->getSignal()->connect(boost::bind(&ALRenderUtil::setupTonemap, this));
 	gSavedSettings.getControl("RenderToneMapExposure")->getSignal()->connect(boost::bind(&ALRenderUtil::setupTonemap, this));
 	gSavedSettings.getControl("RenderToneMapLottesA")->getSignal()->connect(boost::bind(&ALRenderUtil::setupTonemap, this));
@@ -85,9 +89,19 @@ void ALRenderUtil::resetVertexBuffers()
 	mRenderBuffer = nullptr;
 }
 
+void ALRenderUtil::releaseGLBuffers()
+{
+	if (mCGLut)
+	{
+		LLImageGL::deleteTextures(1, &mCGLut);
+		mCGLut = 0;
+	}
+}
+
 void ALRenderUtil::refreshState()
 {
 	setupTonemap();
+	setupColorGrade();
 }
 
 bool ALRenderUtil::setupTonemap()
@@ -131,7 +145,7 @@ void ALRenderUtil::renderTonemap(LLRenderTarget* src, LLRenderTarget* dst)
 
 	dst->bindTarget();
 
-	LLGLSLShader* tone_shader = &gDeferredPostTonemapProgram[mTonemapType];
+	LLGLSLShader* tone_shader = (mCGLut != 0 ) ? &gDeferredPostColorGradeLUTProgram[mTonemapType] : &gDeferredPostTonemapProgram[mTonemapType];
 
 	tone_shader->bind();
 
@@ -168,6 +182,19 @@ void ALRenderUtil::renderTonemap(LLRenderTarget* src, LLRenderTarget* dst)
 	}
 	}
 
+	if (mCGLut != 0)
+	{
+		S32 channel = tone_shader->enableTexture(LLShaderMgr::COLORGRADE_LUT, LLTexUnit::TT_TEXTURE);
+		if (channel > -1)
+		{
+			gGL.getTexUnit(channel)->bindManual(LLTexUnit::TT_TEXTURE, mCGLut);
+			gGL.getTexUnit(channel)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR);
+			gGL.getTexUnit(channel)->setTextureAddressMode(LLTexUnit::TAM_CLAMP);
+		}
+
+		tone_shader->uniform4fv(LLShaderMgr::COLORGRADE_LUT_SIZE, 1, mCGLutSize.mV);
+	}
+
 	mRenderBuffer->setBuffer(LLVertexBuffer::MAP_VERTEX);
 	mRenderBuffer->drawArrays(LLRender::TRIANGLES, 0, 3);
 	stop_glerror();
@@ -179,4 +206,121 @@ void ALRenderUtil::renderTonemap(LLRenderTarget* src, LLRenderTarget* dst)
 	gGL.popMatrix();
 	gGL.matrixMode(LLRender::MM_MODELVIEW);
 	gGL.popMatrix();
+}
+
+bool ALRenderUtil::setupColorGrade()
+{
+	if (mCGLut)
+	{
+		LLImageGL::deleteTextures(1, &mCGLut);
+		mCGLut = 0;
+	}
+
+	std::string lut_name = gSavedSettings.getString("RenderColorGradeLUT");
+	if (!lut_name.empty())
+	{
+		enum class ELutExt
+		{
+			EXT_IMG_TGA = 0,
+			EXT_IMG_PNG,
+			EXT_IMG_WEBP,
+			EXT_NONE
+		};
+		std::string lut_path = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "colorlut", lut_name);
+		if (!lut_path.empty())
+		{
+			std::string temp_exten = gDirUtilp->getExtension(lut_path);
+
+			ELutExt extension = ELutExt::EXT_NONE;
+			if (temp_exten == "tga")
+			{
+				extension = ELutExt::EXT_IMG_TGA;
+			}
+			else if (temp_exten == "png")
+			{
+				extension = ELutExt::EXT_IMG_PNG;
+			}
+			else if (temp_exten == "webp")
+			{
+				extension = ELutExt::EXT_IMG_WEBP;
+			}
+
+			LLPointer<LLImageRaw> raw_image = new LLImageRaw;
+			bool decode_success = false;
+
+			switch (extension)
+			{
+			default:
+				break;
+			case ELutExt::EXT_IMG_TGA:
+			{
+				LLPointer<LLImageTGA> tga_image = new LLImageTGA;
+				if (tga_image->load(lut_path) && tga_image->decode(raw_image, 0.0f))
+				{
+					decode_success = true;
+				}
+				break;
+			}
+			case ELutExt::EXT_IMG_PNG:
+			{
+				LLPointer<LLImagePNG> png_image = new LLImagePNG;
+				if (png_image->load(lut_path) && png_image->decode(raw_image, 0.0f))
+				{
+					decode_success = true;
+				}
+				break;
+			}
+			case ELutExt::EXT_IMG_WEBP:
+			{
+				LLPointer<LLImageWebP> webp_image = new LLImageWebP;
+				if (webp_image->load(lut_path) && webp_image->decode(raw_image, 0.0f))
+				{
+					decode_success = true;
+				}
+				break;
+			}
+			}
+
+			if(decode_success)
+			{
+				U32 primary_format = 0;
+				U32 int_format = 0;
+				switch (raw_image->getComponents())
+				{
+				case 3:
+				{
+					primary_format = GL_RGB;
+					int_format = GL_RGB8;
+					break;
+				}
+				case 4:
+				{
+					primary_format = GL_RGBA;
+					int_format = GL_RGBA8;
+					break;
+				}
+				default:
+					return true;
+				};
+
+				S32 image_height = raw_image->getHeight();
+				S32 image_width = raw_image->getWidth();
+				if ((image_height > 0 && image_height <= 64)	   // within dimension limit
+				&& !(image_height & (image_height - 1))			   // height is power of 2
+				&& ((image_height * image_height) == image_width)) // width is height * height
+				{
+					mCGLutSize = LLVector4(1.f / image_width, 1.f / image_height, (F32)image_width, (F32)image_height);
+
+					LLImageGL::generateTextures(1, &mCGLut);
+					gGL.getTexUnit(0)->bindManual(LLTexUnit::TT_TEXTURE, mCGLut);
+					LLImageGL::setManualImage(LLTexUnit::getInternalType(LLTexUnit::TT_TEXTURE), 0, int_format, image_width,
+						image_height, primary_format, GL_UNSIGNED_BYTE, raw_image->getData(), false);
+					stop_glerror();
+					gGL.getTexUnit(0)->setTextureFilteringOption(LLTexUnit::TFO_BILINEAR);
+					gGL.getTexUnit(0)->setTextureAddressMode(LLTexUnit::TAM_CLAMP);
+				}
+			}
+		}
+	}
+	return true;
 }
\ No newline at end of file
diff --git a/indra/newview/alrenderutils.h b/indra/newview/alrenderutils.h
index 94c64a7d402ab6b52d5e7a9f9ee0286d0829ba60..cf4681b2cc07cfeb837fb796b04a1940fdb7251c 100644
--- a/indra/newview/alrenderutils.h
+++ b/indra/newview/alrenderutils.h
@@ -42,6 +42,8 @@ class ALRenderUtil
 	void restoreVertexBuffers();
 	void resetVertexBuffers();
 
+	void releaseGLBuffers();
+
 	void refreshState();
 
 	// Deferred Only Functions
@@ -63,6 +65,8 @@ class ALRenderUtil
 	void renderTonemap(LLRenderTarget* src, LLRenderTarget* dst);
 	// End Deferred Only
 
+	bool setupColorGrade();
+
 private:
 	// Parameters
 	F32 mTonemapExposure = 1.f;
@@ -77,6 +81,10 @@ class ALRenderUtil
 	LLVector3 mToneUnchartedParamB;
 	LLVector3 mToneUnchartedParamC;
 
+	// Texture Data
+	U32 mCGLut;
+	LLVector4 mCGLutSize;
+
 	// Vertex Buffers
 	LLPointer<LLVertexBuffer> mRenderBuffer;
 };
diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml
index eb543c3dec2b9b8259725f2b41d7e2c601c84a9e..aad8ee9f6c7099ed1460bc1f1ec9708660f25cd1 100644
--- a/indra/newview/app_settings/settings_alchemy.xml
+++ b/indra/newview/app_settings/settings_alchemy.xml
@@ -695,6 +695,17 @@
       <key>Value</key>
       <real>0.275</real>
     </map>
+    <key>RenderColorGradeLUT</key>
+    <map>
+      <key>Comment</key>
+      <string>Name of image file for color grading LUT(TGA, PNG, or WebP)</string>
+      <key>Persist</key>
+      <integer>1</integer>
+      <key>Type</key>
+      <string>String</string>
+      <key>Value</key>
+      <string></string>
+    </map>
     <key>RenderToneMapExposure</key>
     <map>
       <key>Comment</key>
diff --git a/indra/newview/app_settings/shaders/class1/alchemy/toneMapF.glsl b/indra/newview/app_settings/shaders/class1/alchemy/toneMapF.glsl
index c53cb298663e9ca8656e850fe69f587168e8281f..2955f24cd07bf8c0a989a01e259c6347f845d467 100644
--- a/indra/newview/app_settings/shaders/class1/alchemy/toneMapF.glsl
+++ b/indra/newview/app_settings/shaders/class1/alchemy/toneMapF.glsl
@@ -37,6 +37,11 @@ uniform sampler2DRect diffuseRect;
 uniform vec2 screen_res;
 uniform float exposure;
 
+#if COLOR_GRADE_LUT
+uniform sampler2D colorgrade_lut;
+uniform vec4 colorgrade_lut_size;
+#endif
+
 vec3 linear_to_srgb(vec3 cl);
 
 vec3 reinhard(vec3 x)
@@ -204,6 +209,30 @@ void main()
 #if NEEDS_GAMMA_CORRECT
     diff.rgb = linear_to_srgb(diff.rgb);
 #endif
+
+#if COLOR_GRADE_LUT
+    // Invert coord for compat with DX-style LUT
+    diff.y	= 1.0 - diff.y;
+
+	// Convert to texel coords
+	vec3 lutRange = diff.rgb * ( colorgrade_lut_size.w - 1);
+
+    // Calculate coords in texel space
+	vec2 lutX = vec2(floor(lutRange.z)*colorgrade_lut_size.w+lutRange.x, lutRange.y);
+	vec2 lutY = vec2(ceil(lutRange.z)*colorgrade_lut_size.w+lutRange.x, lutRange.y);
+
+	// texel to ndc
+	lutX = (lutX+0.5)*colorgrade_lut_size.xy;
+	lutY = (lutY+0.5)*colorgrade_lut_size.xy;
+
+	// LUT interpolation
+	diff.rgb = mix(
+        texture2D(colorgrade_lut, lutX).rgb, 
+        texture2D(colorgrade_lut, lutY).rgb, 
+        fract(lutRange.z)
+	);
+#endif
+
     frag_color = diff;
 }
 
diff --git a/indra/newview/llviewershadermgr.cpp b/indra/newview/llviewershadermgr.cpp
index 34be5447ba50047748139cba5512b8dbdd199271..53c9b20cbb93de332611a23bed52e0ce69650b70 100644
--- a/indra/newview/llviewershadermgr.cpp
+++ b/indra/newview/llviewershadermgr.cpp
@@ -246,6 +246,7 @@ LLGLSLShader			gDeferredSkinnedFullbrightProgram;
 LLGLSLShader			gNormalMapGenProgram;
 LLGLSLShader            gDeferredPostCASProgram;
 LLGLSLShader			gDeferredPostTonemapProgram[AL_TONEMAP_COUNT];
+LLGLSLShader			gDeferredPostColorGradeLUTProgram[AL_TONEMAP_COUNT];
 // [RLVa:KB] - @setsphere
 LLGLSLShader			gRlvSphereProgram;
 // [/RLVa:KB]
@@ -835,6 +836,7 @@ void LLViewerShaderMgr::unloadShaders()
 	for (U32 i = 0; i < AL_TONEMAP_COUNT; ++i)
 	{
 		gDeferredPostTonemapProgram[i].unload();
+		gDeferredPostColorGradeLUTProgram[i].unload();
 	}
 
 	mShaderLevel[SHADER_LIGHTING] = 0;
@@ -1300,6 +1302,7 @@ BOOL LLViewerShaderMgr::loadShadersDeferred()
 		for (U32 i = 0; i < AL_TONEMAP_COUNT; ++i)
 		{
 			gDeferredPostTonemapProgram[i].unload();
+			gDeferredPostColorGradeLUTProgram[i].unload();
 		}
 		return TRUE;
 	}
@@ -2997,6 +3000,22 @@ BOOL LLViewerShaderMgr::loadShadersDeferred()
 
 			success = gDeferredPostTonemapProgram[i].createShader(NULL, NULL);
 		}
+
+		if (success)
+		{
+			gDeferredPostColorGradeLUTProgram[i].mName = "Color Grading Shader " + std::to_string(i);
+			gDeferredPostColorGradeLUTProgram[i].mFeatures.hasSrgb = true;
+			gDeferredPostColorGradeLUTProgram[i].mShaderFiles.clear();
+			gDeferredPostColorGradeLUTProgram[i].mShaderFiles.push_back(make_pair("alchemy/postNoTCV.glsl", GL_VERTEX_SHADER));
+			gDeferredPostColorGradeLUTProgram[i].mShaderFiles.push_back(make_pair("alchemy/toneMapF.glsl", GL_FRAGMENT_SHADER));
+			gDeferredPostColorGradeLUTProgram[i].mShaderLevel = mShaderLevel[SHADER_DEFERRED];
+
+			gDeferredPostColorGradeLUTProgram[i].clearPermutations();
+			gDeferredPostColorGradeLUTProgram[i].addPermutation("COLOR_GRADE_LUT", std::to_string(1));
+			gDeferredPostColorGradeLUTProgram[i].addPermutation("TONEMAP_METHOD", std::to_string(i));
+
+			success = gDeferredPostColorGradeLUTProgram[i].createShader(NULL, NULL);
+		}
 	}
 
 	return success;
diff --git a/indra/newview/llviewershadermgr.h b/indra/newview/llviewershadermgr.h
index 0afa02480fa63439adf76f4d4069b58fc31b60d4..b34aa3646dab73981da520191ab243a406372277 100644
--- a/indra/newview/llviewershadermgr.h
+++ b/indra/newview/llviewershadermgr.h
@@ -332,6 +332,7 @@ extern LLGLSLShader			gDeferredSkinnedFullbrightProgram;
 extern LLGLSLShader			gNormalMapGenProgram;
 extern LLGLSLShader         gDeferredPostCASProgram;
 extern LLGLSLShader			gDeferredPostTonemapProgram[AL_TONEMAP_COUNT];
+extern LLGLSLShader			gDeferredPostColorGradeLUTProgram[AL_TONEMAP_COUNT];
 // [RLVa:KB] - @setsphere
 extern LLGLSLShader			gRlvSphereProgram;
 // [/RLVa:KB]
diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp
index dfe947845c64e9420fc9779663188d7f90ebd0a8..060b66a1d478c14c0f9dd4701551b6f1e240a4a4 100644
--- a/indra/newview/pipeline.cpp
+++ b/indra/newview/pipeline.cpp
@@ -1294,6 +1294,8 @@ void LLPipeline::releaseGLBuffers()
         mSampleMap = 0;
     }
 
+	mALRenderUtil->releaseGLBuffers();
+
 	releaseLUTBuffers();
 
 	mWaterRef.release();