diff --git a/src/gl/renderer/gl_postprocess.cpp b/src/gl/renderer/gl_postprocess.cpp
index fde8da353..ac5a6336d 100644
--- a/src/gl/renderer/gl_postprocess.cpp
+++ b/src/gl/renderer/gl_postprocess.cpp
@@ -121,7 +121,7 @@ CUSTOM_CVAR(Int, gl_ssao_portals, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
 }
 
 CVAR(Float, gl_ssao_strength, 0.7, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
-CVAR(Bool, gl_ssao_debug, false, 0)
+CVAR(Int, gl_ssao_debug, 0, 0)
 CVAR(Float, gl_ssao_bias, 0.5f, 0)
 CVAR(Float, gl_ssao_radius, 100.0f, 0)
 CUSTOM_CVAR(Float, gl_ssao_blur_amount, 4.0f, 0)
@@ -162,13 +162,12 @@ void FGLRenderer::AmbientOccludeScene()
 	FGLDebug::PushGroup("AmbientOccludeScene");
 
 	FGLPostProcessState savedState;
-	savedState.SaveTextureBinding1();
+	savedState.SaveTextureBindings(3);
 
 	float bias = gl_ssao_bias;
 	float aoRadius = gl_ssao_radius;
 	const float blurAmount = gl_ssao_blur_amount;
 	float aoStrength = gl_ssao_strength;
-	bool multisample = gl_multisample > 1;
 
 	//float tanHalfFovy = tan(fovy * (M_PI / 360.0f));
 	float tanHalfFovy = 1.0f / gl_RenderState.mProjectionMatrix.get()[5];
@@ -189,16 +188,16 @@ void FGLRenderer::AmbientOccludeScene()
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 	glActiveTexture(GL_TEXTURE0);
-	mLinearDepthShader->Bind(multisample);
-	mLinearDepthShader->DepthTexture[multisample].Set(0);
-	mLinearDepthShader->ColorTexture[multisample].Set(1);
-	if (multisample) mLinearDepthShader->SampleCount[multisample].Set(gl_multisample);
-	mLinearDepthShader->LinearizeDepthA[multisample].Set(1.0f / GetZFar() - 1.0f / GetZNear());
-	mLinearDepthShader->LinearizeDepthB[multisample].Set(MAX(1.0f / GetZNear(), 1.e-8f));
-	mLinearDepthShader->InverseDepthRangeA[multisample].Set(1.0f);
-	mLinearDepthShader->InverseDepthRangeB[multisample].Set(0.0f);
-	mLinearDepthShader->Scale[multisample].Set(mBuffers->AmbientWidth * 2.0f / (float)mScreenViewport.width, mBuffers->AmbientHeight * 2.0f / (float)mScreenViewport.height);
-	mLinearDepthShader->Offset[multisample].Set(mSceneViewport.left / (float)mScreenViewport.width, mSceneViewport.top / (float)mScreenViewport.height);
+	mLinearDepthShader->Bind();
+	mLinearDepthShader->DepthTexture.Set(0);
+	mLinearDepthShader->ColorTexture.Set(1);
+	if (gl_multisample > 1) mLinearDepthShader->SampleCount.Set(gl_multisample);
+	mLinearDepthShader->LinearizeDepthA.Set(1.0f / GetZFar() - 1.0f / GetZNear());
+	mLinearDepthShader->LinearizeDepthB.Set(MAX(1.0f / GetZNear(), 1.e-8f));
+	mLinearDepthShader->InverseDepthRangeA.Set(1.0f);
+	mLinearDepthShader->InverseDepthRangeB.Set(0.0f);
+	mLinearDepthShader->Scale.Set(mBuffers->AmbientWidth * 2.0f / (float)mScreenViewport.width, mBuffers->AmbientHeight * 2.0f / (float)mScreenViewport.height);
+	mLinearDepthShader->Offset.Set(mSceneViewport.left / (float)mScreenViewport.width, mSceneViewport.top / (float)mScreenViewport.height);
 	RenderScreenQuad();
 
 	// Apply ambient occlusion
@@ -212,10 +211,14 @@ void FGLRenderer::AmbientOccludeScene()
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+	mBuffers->BindSceneNormalTexture(2);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 	glActiveTexture(GL_TEXTURE0);
 	mSSAOShader->Bind();
 	mSSAOShader->DepthTexture.Set(0);
 	mSSAOShader->RandomTexture.Set(1);
+	mSSAOShader->NormalTexture.Set(2);
 	mSSAOShader->UVToViewA.Set(2.0f * invFocalLenX, -2.0f * invFocalLenY);
 	mSSAOShader->UVToViewB.Set(-invFocalLenX, invFocalLenY);
 	mSSAOShader->InvFullResolution.Set(1.0f / mBuffers->AmbientWidth, 1.0f / mBuffers->AmbientHeight);
@@ -246,26 +249,29 @@ void FGLRenderer::AmbientOccludeScene()
 
 	// Add SSAO back to scene texture:
 	mBuffers->BindSceneFB(false);
-	GLenum buffers[] = { GL_COLOR_ATTACHMENT0 };
-	glDrawBuffers(1, buffers);
 	glViewport(mSceneViewport.left, mSceneViewport.top, mSceneViewport.width, mSceneViewport.height);
 	glEnable(GL_BLEND);
 	glBlendEquation(GL_FUNC_ADD);
-	if (gl_ssao_debug)
+	if (gl_ssao_debug > 1)
 		glBlendFunc(GL_ONE, GL_ZERO);
 	else
 		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	if (gl_ssao_debug == 1)
+	{
+		glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+	}
 	glActiveTexture(GL_TEXTURE0);
 	glBindTexture(GL_TEXTURE_2D, mBuffers->AmbientTexture1);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-	mBuffers->BindSceneDataTexture(1);
-	mSSAOCombineShader->Bind(multisample);
-	mSSAOCombineShader->AODepthTexture[multisample].Set(0);
-	mSSAOCombineShader->SceneDataTexture[multisample].Set(1);
-	if (multisample) mSSAOCombineShader->SampleCount[multisample].Set(gl_multisample);
-	mSSAOCombineShader->Scale[multisample].Set(mBuffers->AmbientWidth * 2.0f / (float)mScreenViewport.width, mBuffers->AmbientHeight * 2.0f / (float)mScreenViewport.height);
-	mSSAOCombineShader->Offset[multisample].Set(mSceneViewport.left / (float)mScreenViewport.width, mSceneViewport.top / (float)mScreenViewport.height);
+	mBuffers->BindSceneFogTexture(1);
+	mSSAOCombineShader->Bind();
+	mSSAOCombineShader->AODepthTexture.Set(0);
+	mSSAOCombineShader->SceneFogTexture.Set(1);
+	if (gl_multisample > 1) mSSAOCombineShader->SampleCount.Set(gl_multisample);
+	mSSAOCombineShader->Scale.Set(mBuffers->AmbientWidth * 2.0f / (float)mScreenViewport.width, mBuffers->AmbientHeight * 2.0f / (float)mScreenViewport.height);
+	mSSAOCombineShader->Offset.Set(mSceneViewport.left / (float)mScreenViewport.width, mSceneViewport.top / (float)mScreenViewport.height);
 	RenderScreenQuad();
 
 	FGLDebug::PopGroup();
@@ -285,7 +291,7 @@ void FGLRenderer::UpdateCameraExposure()
 	FGLDebug::PushGroup("UpdateCameraExposure");
 
 	FGLPostProcessState savedState;
-	savedState.SaveTextureBinding1();
+	savedState.SaveTextureBindings(2);
 
 	// Extract light level from scene texture:
 	const auto &level0 = mBuffers->ExposureLevels[0];
@@ -358,7 +364,7 @@ void FGLRenderer::BloomScene()
 	FGLDebug::PushGroup("BloomScene");
 
 	FGLPostProcessState savedState;
-	savedState.SaveTextureBinding1();
+	savedState.SaveTextureBindings(2);
 
 	const float blurAmount = gl_bloom_amount;
 	int sampleCount = gl_bloom_kernel_size;
@@ -461,7 +467,7 @@ void FGLRenderer::TonemapScene()
 	}
 	else
 	{
-		savedState.SaveTextureBinding1();
+		savedState.SaveTextureBindings(2);
 		glActiveTexture(GL_TEXTURE1);
 		glBindTexture(GL_TEXTURE_2D, mBuffers->ExposureTexture);
 		glActiveTexture(GL_TEXTURE0);
diff --git a/src/gl/renderer/gl_postprocessstate.cpp b/src/gl/renderer/gl_postprocessstate.cpp
index 57b7862f5..fead1435a 100644
--- a/src/gl/renderer/gl_postprocessstate.cpp
+++ b/src/gl/renderer/gl_postprocessstate.cpp
@@ -45,17 +45,7 @@ FGLPostProcessState::FGLPostProcessState()
 {
 	glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTex);
 	glActiveTexture(GL_TEXTURE0);
-	glGetIntegerv(GL_TEXTURE_BINDING_2D, &textureBinding[0]);
-	glBindTexture(GL_TEXTURE_2D, 0);
-	if (gl.flags & RFL_SAMPLER_OBJECTS)
-	{
-		glGetIntegerv(GL_SAMPLER_BINDING, &samplerBinding[0]);
-		glBindSampler(0, 0);
-		glActiveTexture(GL_TEXTURE0 + 1);
-		glGetIntegerv(GL_SAMPLER_BINDING, &samplerBinding[1]);
-		glBindSampler(1, 0);
-		glActiveTexture(GL_TEXTURE0);
-	}
+	SaveTextureBindings(1);
 
 	glGetBooleanv(GL_BLEND, &blendEnabled);
 	glGetBooleanv(GL_SCISSOR_TEST, &scissorEnabled);
@@ -75,12 +65,26 @@ FGLPostProcessState::FGLPostProcessState()
 	glDisable(GL_BLEND);
 }
 
-void FGLPostProcessState::SaveTextureBinding1()
+void FGLPostProcessState::SaveTextureBindings(unsigned int numUnits)
 {
-	glActiveTexture(GL_TEXTURE1);
-	glGetIntegerv(GL_TEXTURE_BINDING_2D, &textureBinding[1]);
-	glBindTexture(GL_TEXTURE_2D, 0);
-	textureBinding1Saved = true;
+	while (textureBinding.Size() < numUnits)
+	{
+		unsigned int i = textureBinding.Size();
+
+		GLint texture;
+		glActiveTexture(GL_TEXTURE0 + i);
+		glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture);
+		glBindTexture(GL_TEXTURE_2D, 0);
+		textureBinding.Push(texture);
+
+		if (gl.flags & RFL_SAMPLER_OBJECTS)
+		{
+			GLint sampler;
+			glGetIntegerv(GL_SAMPLER_BINDING, &sampler);
+			glBindSampler(i, 0);
+			samplerBinding.Push(sampler);
+		}
+	}
 	glActiveTexture(GL_TEXTURE0);
 }
 
@@ -117,25 +121,22 @@ FGLPostProcessState::~FGLPostProcessState()
 
 	glUseProgram(currentProgram);
 
-	if (textureBinding1Saved)
+	// Fully unbind to avoid incomplete texture warnings from Nvidia's driver when gl_debug_level 4 is active
+	for (unsigned int i = 0; i < textureBinding.Size(); i++)
 	{
-		glActiveTexture(GL_TEXTURE1);
+		glActiveTexture(GL_TEXTURE0 + i);
 		glBindTexture(GL_TEXTURE_2D, 0);
 	}
 
-	glActiveTexture(GL_TEXTURE0);
-	glBindTexture(GL_TEXTURE_2D, 0);
-	if (gl.flags & RFL_SAMPLER_OBJECTS)
+	for (unsigned int i = 0; i < samplerBinding.Size(); i++)
 	{
-		glBindSampler(0, samplerBinding[0]);
-		glBindSampler(1, samplerBinding[1]);
+		glBindSampler(i, samplerBinding[i]);
 	}
-	glBindTexture(GL_TEXTURE_2D, textureBinding[0]);
 
-	if (textureBinding1Saved)
+	for (unsigned int i = 0; i < textureBinding.Size(); i++)
 	{
-		glActiveTexture(GL_TEXTURE1);
-		glBindTexture(GL_TEXTURE_2D, textureBinding[1]);
+		glActiveTexture(GL_TEXTURE0 + i);
+		glBindTexture(GL_TEXTURE_2D, textureBinding[i]);
 	}
 
 	glActiveTexture(activeTex);
diff --git a/src/gl/renderer/gl_postprocessstate.h b/src/gl/renderer/gl_postprocessstate.h
index bf53aa7de..795a7d4ba 100644
--- a/src/gl/renderer/gl_postprocessstate.h
+++ b/src/gl/renderer/gl_postprocessstate.h
@@ -14,15 +14,15 @@ public:
 	FGLPostProcessState();
 	~FGLPostProcessState();
 
-	void SaveTextureBinding1();
+	void SaveTextureBindings(unsigned int numUnits);
 
 private:
 	FGLPostProcessState(const FGLPostProcessState &) = delete;
 	FGLPostProcessState &operator=(const FGLPostProcessState &) = delete;
 
 	GLint activeTex;
-	GLint textureBinding[2];
-	GLint samplerBinding[2];
+	TArray<GLint> textureBinding;
+	TArray<GLint> samplerBinding;
 	GLboolean blendEnabled;
 	GLboolean scissorEnabled;
 	GLboolean depthEnabled;
@@ -34,7 +34,6 @@ private:
 	GLint blendSrcAlpha;
 	GLint blendDestRgb;
 	GLint blendDestAlpha;
-	bool textureBinding1Saved = false;
 };
 
 #endif
diff --git a/src/gl/renderer/gl_renderbuffers.cpp b/src/gl/renderer/gl_renderbuffers.cpp
index 9915b024c..1a1509d7a 100644
--- a/src/gl/renderer/gl_renderbuffers.cpp
+++ b/src/gl/renderer/gl_renderbuffers.cpp
@@ -86,13 +86,15 @@ void FGLRenderBuffers::ClearScene()
 	if (mSceneUsesTextures)
 	{
 		DeleteTexture(mSceneMultisample);
-		DeleteTexture(mSceneData);
+		DeleteTexture(mSceneFog);
+		DeleteTexture(mSceneNormal);
 		DeleteTexture(mSceneDepthStencil);
 	}
 	else
 	{
 		DeleteRenderBuffer(mSceneMultisample);
-		DeleteRenderBuffer(mSceneData);
+		DeleteRenderBuffer(mSceneFog);
+		DeleteRenderBuffer(mSceneNormal);
 		DeleteRenderBuffer(mSceneDepthStencil);
 	}
 }
@@ -264,16 +266,17 @@ void FGLRenderBuffers::CreateScene(int width, int height, int samples, bool need
 		{
 			mSceneMultisample = Create2DMultisampleTexture("SceneMultisample", GL_RGBA16F, width, height, samples, false);
 			mSceneDepthStencil = Create2DMultisampleTexture("SceneDepthStencil", GL_DEPTH24_STENCIL8, width, height, samples, false);
-			mSceneData = Create2DMultisampleTexture("SceneSSAOData", GL_RGBA8, width, height, samples, false);
-			mSceneFB = CreateFrameBuffer("SceneFB", mSceneMultisample, 0, mSceneDepthStencil, true);
-			mSceneDataFB = CreateFrameBuffer("SSAOSceneFB", mSceneMultisample, mSceneData, mSceneDepthStencil, true);
+			mSceneFog = Create2DMultisampleTexture("SceneFog", GL_RGBA8, width, height, samples, false);
+			mSceneNormal = Create2DMultisampleTexture("SceneNormal", GL_RGB10_A2, width, height, samples, false);
+			mSceneFB = CreateFrameBuffer("SceneFB", mSceneMultisample, 0, 0, mSceneDepthStencil, true);
+			mSceneDataFB = CreateFrameBuffer("SceneGBufferFB", mSceneMultisample, mSceneFog, mSceneNormal, mSceneDepthStencil, true);
 		}
 		else
 		{
 			mSceneMultisample = CreateRenderBuffer("SceneMultisample", GL_RGBA16F, width, height, samples);
 			mSceneDepthStencil = CreateRenderBuffer("SceneDepthStencil", GL_DEPTH24_STENCIL8, width, height, samples);
 			mSceneFB = CreateFrameBuffer("SceneFB", mSceneMultisample, mSceneDepthStencil, true);
-			mSceneDataFB = CreateFrameBuffer("SSAOSceneFB", mSceneMultisample, mSceneDepthStencil, true);
+			mSceneDataFB = CreateFrameBuffer("SceneGBufferFB", mSceneMultisample, mSceneDepthStencil, true);
 		}
 	}
 	else
@@ -281,15 +284,16 @@ void FGLRenderBuffers::CreateScene(int width, int height, int samples, bool need
 		if (needsSceneTextures)
 		{
 			mSceneDepthStencil = Create2DTexture("SceneDepthStencil", GL_DEPTH24_STENCIL8, width, height);
-			mSceneData = Create2DTexture("SceneSSAOData", GL_RGBA8, width, height);
-			mSceneFB = CreateFrameBuffer("SceneFB", mPipelineTexture[0], 0, mSceneDepthStencil, false);
-			mSceneDataFB = CreateFrameBuffer("SSAOSceneFB", mPipelineTexture[0], mSceneData, mSceneDepthStencil, false);
+			mSceneFog = Create2DTexture("SceneFog", GL_RGBA8, width, height);
+			mSceneNormal = Create2DTexture("SceneNormal", GL_RGB10_A2, width, height);
+			mSceneFB = CreateFrameBuffer("SceneFB", mPipelineTexture[0], 0, 0, mSceneDepthStencil, false);
+			mSceneDataFB = CreateFrameBuffer("SceneGBufferFB", mPipelineTexture[0], mSceneFog, mSceneNormal, mSceneDepthStencil, false);
 		}
 		else
 		{
 			mSceneDepthStencil = CreateRenderBuffer("SceneDepthStencil", GL_DEPTH24_STENCIL8, width, height);
 			mSceneFB = CreateFrameBuffer("SceneFB", mPipelineTexture[0], mSceneDepthStencil, false);
-			mSceneDataFB = CreateFrameBuffer("SSAOSceneFB", mPipelineTexture[0], mSceneDepthStencil, false);
+			mSceneDataFB = CreateFrameBuffer("SceneGBufferFB", mPipelineTexture[0], mSceneDepthStencil, false);
 		}
 	}
 }
@@ -471,6 +475,7 @@ GLuint FGLRenderBuffers::Create2DTexture(const FString &name, GLuint format, int
 	case GL_RGBA32F:			dataformat = GL_RGBA; datatype = GL_FLOAT; break;
 	case GL_R32F:				dataformat = GL_RED; datatype = GL_FLOAT; break;
 	case GL_RG32F:				dataformat = GL_RG; datatype = GL_FLOAT; break;
+	case GL_RGB10_A2:			dataformat = GL_RGBA; datatype = GL_UNSIGNED_INT_10_10_10_2; break;
 	case GL_DEPTH_COMPONENT24:	dataformat = GL_DEPTH_COMPONENT; datatype = GL_FLOAT; break;
 	case GL_STENCIL_INDEX8:		dataformat = GL_STENCIL_INDEX; datatype = GL_INT; break;
 	case GL_DEPTH24_STENCIL8:	dataformat = GL_DEPTH_STENCIL; datatype = GL_UNSIGNED_INT_24_8; break;
@@ -560,7 +565,7 @@ GLuint FGLRenderBuffers::CreateFrameBuffer(const FString &name, GLuint colorbuff
 	return handle;
 }
 
-GLuint FGLRenderBuffers::CreateFrameBuffer(const FString &name, GLuint colorbuffer0, GLuint colorbuffer1, GLuint depthstencil, bool multisample)
+GLuint FGLRenderBuffers::CreateFrameBuffer(const FString &name, GLuint colorbuffer0, GLuint colorbuffer1, GLuint colorbuffer2, GLuint depthstencil, bool multisample)
 {
 	GLuint handle = 0;
 	glGenFramebuffers(1, &handle);
@@ -571,6 +576,8 @@ GLuint FGLRenderBuffers::CreateFrameBuffer(const FString &name, GLuint colorbuff
 		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorbuffer0, 0);
 		if (colorbuffer1 != 0)
 			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, colorbuffer1, 0);
+		if (colorbuffer2 != 0)
+			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D_MULTISAMPLE, colorbuffer2, 0);
 		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D_MULTISAMPLE, depthstencil, 0);
 	}
 	else
@@ -578,6 +585,8 @@ GLuint FGLRenderBuffers::CreateFrameBuffer(const FString &name, GLuint colorbuff
 		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorbuffer0, 0);
 		if (colorbuffer1 != 0)
 			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, colorbuffer1, 0);
+		if (colorbuffer2 != 0)
+			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, colorbuffer2, 0);
 		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthstencil, 0);
 	}
 	if (CheckFrameBufferCompleteness())
@@ -742,17 +751,32 @@ void FGLRenderBuffers::BindSceneColorTexture(int index)
 
 //==========================================================================
 //
-// Binds the scene data texture to the specified texture unit
+// Binds the scene fog data texture to the specified texture unit
 //
 //==========================================================================
 
-void FGLRenderBuffers::BindSceneDataTexture(int index)
+void FGLRenderBuffers::BindSceneFogTexture(int index)
 {
 	glActiveTexture(GL_TEXTURE0 + index);
 	if (mSamples > 1)
-		glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, mSceneData);
+		glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, mSceneFog);
 	else
-		glBindTexture(GL_TEXTURE_2D, mSceneData);
+		glBindTexture(GL_TEXTURE_2D, mSceneFog);
+}
+
+//==========================================================================
+//
+// Binds the scene normal data texture to the specified texture unit
+//
+//==========================================================================
+
+void FGLRenderBuffers::BindSceneNormalTexture(int index)
+{
+	glActiveTexture(GL_TEXTURE0 + index);
+	if (mSamples > 1)
+		glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, mSceneNormal);
+	else
+		glBindTexture(GL_TEXTURE_2D, mSceneNormal);
 }
 
 //==========================================================================
diff --git a/src/gl/renderer/gl_renderbuffers.h b/src/gl/renderer/gl_renderbuffers.h
index 3fcc2c07b..9508f7009 100644
--- a/src/gl/renderer/gl_renderbuffers.h
+++ b/src/gl/renderer/gl_renderbuffers.h
@@ -34,7 +34,8 @@ public:
 
 	void BindSceneFB(bool sceneData);
 	void BindSceneColorTexture(int index);
-	void BindSceneDataTexture(int index);
+	void BindSceneFogTexture(int index);
+	void BindSceneNormalTexture(int index);
 	void BindSceneDepthTexture(int index);
 	void BlitSceneToTexture();
 
@@ -93,7 +94,7 @@ private:
 	GLuint CreateRenderBuffer(const FString &name, GLuint format, int samples, int width, int height);
 	GLuint CreateFrameBuffer(const FString &name, GLuint colorbuffer);
 	GLuint CreateFrameBuffer(const FString &name, GLuint colorbuffer, GLuint depthstencil, bool colorIsARenderBuffer);
-	GLuint CreateFrameBuffer(const FString &name, GLuint colorbuffer0, GLuint colorbuffer1, GLuint depthstencil, bool multisample);
+	GLuint CreateFrameBuffer(const FString &name, GLuint colorbuffer0, GLuint colorbuffer1, GLuint colorbuffer2, GLuint depthstencil, bool multisample);
 	bool CheckFrameBufferCompleteness();
 	void ClearFrameBuffer(bool stencil, bool depth);
 	void DeleteTexture(GLuint &handle);
@@ -113,7 +114,8 @@ private:
 	// Buffers for the scene
 	GLuint mSceneMultisample = 0;
 	GLuint mSceneDepthStencil = 0;
-	GLuint mSceneData = 0;
+	GLuint mSceneFog = 0;
+	GLuint mSceneNormal = 0;
 	GLuint mSceneFB = 0;
 	GLuint mSceneDataFB = 0;
 	bool mSceneUsesTextures = false;
diff --git a/src/gl/scene/gl_scene.cpp b/src/gl/scene/gl_scene.cpp
index b6120f940..13b210d91 100644
--- a/src/gl/scene/gl_scene.cpp
+++ b/src/gl/scene/gl_scene.cpp
@@ -160,8 +160,8 @@ void FGLRenderer::Set3DViewport(bool mainview)
 	{
 		bool useSSAO = (gl_ssao != 0);
 		mBuffers->BindSceneFB(useSSAO);
-		GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
-		glDrawBuffers(useSSAO ? 2 : 1, buffers);
+		GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
+		glDrawBuffers(useSSAO ? 3 : 1, buffers);
 		gl_RenderState.SetPassType(useSSAO ? GBUFFER_PASS : NORMAL_PASS);
 		gl_RenderState.Apply();
 	}
@@ -507,8 +507,8 @@ void FGLRenderer::DrawScene(int drawmode)
 	// If SSAO is active, switch to gbuffer shaders and use the framebuffer with gbuffers
 	if (applySSAO)
 	{
-		GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
-		glDrawBuffers(2, buffers);
+		GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
+		glDrawBuffers(3, buffers);
 		gl_RenderState.SetPassType(GBUFFER_PASS);
 		gl_RenderState.Apply();
 		gl_RenderState.ApplyMatrices();
diff --git a/src/gl/shaders/gl_ambientshader.cpp b/src/gl/shaders/gl_ambientshader.cpp
index 43adb5c0e..6348d24cd 100644
--- a/src/gl/shaders/gl_ambientshader.cpp
+++ b/src/gl/shaders/gl_ambientshader.cpp
@@ -31,54 +31,65 @@
 #include "gl/system/gl_cvars.h"
 #include "gl/shaders/gl_ambientshader.h"
 
-void FLinearDepthShader::Bind(bool multisample)
+void FLinearDepthShader::Bind()
 {
-	auto &shader = mShader[multisample];
-	if (!shader)
+	bool multisample = (gl_multisample > 1);
+	if (mMultisample != multisample)
+		mShader.reset();
+
+	if (!mShader)
 	{
-		shader.Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 330);
-		shader.Compile(FShaderProgram::Fragment, "shaders/glsl/lineardepth.fp", multisample ? "#define MULTISAMPLE\n" : "", 330);
-		shader.SetFragDataLocation(0, "FragColor");
-		shader.Link("shaders/glsl/lineardepth");
-		shader.SetAttribLocation(0, "PositionInProjection");
-		DepthTexture[multisample].Init(shader, "DepthTexture");
-		ColorTexture[multisample].Init(shader, "ColorTexture");
-		SampleCount[multisample].Init(shader, "SampleCount");
-		LinearizeDepthA[multisample].Init(shader, "LinearizeDepthA");
-		LinearizeDepthB[multisample].Init(shader, "LinearizeDepthB");
-		InverseDepthRangeA[multisample].Init(shader, "InverseDepthRangeA");
-		InverseDepthRangeB[multisample].Init(shader, "InverseDepthRangeB");
-		Scale[multisample].Init(shader, "Scale");
-		Offset[multisample].Init(shader, "Offset");
+		mShader = std::make_unique<FShaderProgram>();
+		mShader->Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 330);
+		mShader->Compile(FShaderProgram::Fragment, "shaders/glsl/lineardepth.fp", multisample ? "#define MULTISAMPLE\n" : "", 330);
+		mShader->SetFragDataLocation(0, "FragColor");
+		mShader->Link("shaders/glsl/lineardepth");
+		mShader->SetAttribLocation(0, "PositionInProjection");
+		DepthTexture.Init(*mShader, "DepthTexture");
+		ColorTexture.Init(*mShader, "ColorTexture");
+		SampleCount.Init(*mShader, "SampleCount");
+		LinearizeDepthA.Init(*mShader, "LinearizeDepthA");
+		LinearizeDepthB.Init(*mShader, "LinearizeDepthB");
+		InverseDepthRangeA.Init(*mShader, "InverseDepthRangeA");
+		InverseDepthRangeB.Init(*mShader, "InverseDepthRangeB");
+		Scale.Init(*mShader, "Scale");
+		Offset.Init(*mShader, "Offset");
+		mMultisample = multisample;
 	}
-	shader.Bind();
+	mShader->Bind();
 }
 
 void FSSAOShader::Bind()
 {
-	auto &shader = mShader[gl_ssao];
-	if (!shader)
+	bool multisample = (gl_multisample > 1);
+	if (mCurrentQuality != gl_ssao || mMultisample != multisample)
+		mShader.reset();
+
+	if (!mShader)
 	{
-		shader.Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 330);
-		shader.Compile(FShaderProgram::Fragment, "shaders/glsl/ssao.fp", GetDefines(gl_ssao), 330);
-		shader.SetFragDataLocation(0, "FragColor");
-		shader.Link("shaders/glsl/ssao");
-		shader.SetAttribLocation(0, "PositionInProjection");
-		DepthTexture.Init(shader, "DepthTexture");
-		RandomTexture.Init(shader, "RandomTexture");
-		UVToViewA.Init(shader, "UVToViewA");
-		UVToViewB.Init(shader, "UVToViewB");
-		InvFullResolution.Init(shader, "InvFullResolution");
-		NDotVBias.Init(shader, "NDotVBias");
-		NegInvR2.Init(shader, "NegInvR2");
-		RadiusToScreen.Init(shader, "RadiusToScreen");
-		AOMultiplier.Init(shader, "AOMultiplier");
-		AOStrength.Init(shader, "AOStrength");
+		mShader = std::make_unique<FShaderProgram>();
+		mShader->Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 330);
+		mShader->Compile(FShaderProgram::Fragment, "shaders/glsl/ssao.fp", GetDefines(gl_ssao, multisample), 330);
+		mShader->SetFragDataLocation(0, "FragColor");
+		mShader->Link("shaders/glsl/ssao");
+		mShader->SetAttribLocation(0, "PositionInProjection");
+		DepthTexture.Init(*mShader, "DepthTexture");
+		NormalTexture.Init(*mShader, "NormalTexture");
+		RandomTexture.Init(*mShader, "RandomTexture");
+		UVToViewA.Init(*mShader, "UVToViewA");
+		UVToViewB.Init(*mShader, "UVToViewB");
+		InvFullResolution.Init(*mShader, "InvFullResolution");
+		NDotVBias.Init(*mShader, "NDotVBias");
+		NegInvR2.Init(*mShader, "NegInvR2");
+		RadiusToScreen.Init(*mShader, "RadiusToScreen");
+		AOMultiplier.Init(*mShader, "AOMultiplier");
+		AOStrength.Init(*mShader, "AOStrength");
+		mMultisample = multisample;
 	}
-	shader.Bind();
+	mShader->Bind();
 }
 
-FString FSSAOShader::GetDefines(int mode)
+FString FSSAOShader::GetDefines(int mode, bool multisample)
 {
 	int numDirections, numSteps;
 	switch (gl_ssao)
@@ -96,6 +107,9 @@ FString FSSAOShader::GetDefines(int mode)
 		#define NUM_DIRECTIONS %d.0
 		#define NUM_STEPS %d.0
 	)", numDirections, numSteps);
+
+	if (multisample)
+		defines += "\n#define MULTISAMPLE\n";
 	return defines;
 }
 
@@ -117,21 +131,26 @@ void FDepthBlurShader::Bind(bool vertical)
 	shader.Bind();
 }
 
-void FSSAOCombineShader::Bind(bool multisample)
+void FSSAOCombineShader::Bind()
 {
-	auto &shader = mShader[multisample];
-	if (!shader)
+	bool multisample = (gl_multisample > 1);
+	if (mMultisample != multisample)
+		mShader.reset();
+
+	if (!mShader)
 	{
-		shader.Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 330);
-		shader.Compile(FShaderProgram::Fragment, "shaders/glsl/ssaocombine.fp", multisample ? "#define MULTISAMPLE\n" : "", 330);
-		shader.SetFragDataLocation(0, "FragColor");
-		shader.Link("shaders/glsl/ssaocombine");
-		shader.SetAttribLocation(0, "PositionInProjection");
-		AODepthTexture[multisample].Init(shader, "AODepthTexture");
-		SceneDataTexture[multisample].Init(shader, "SceneDataTexture");
-		SampleCount[multisample].Init(shader, "SampleCount");
-		Scale[multisample].Init(shader, "Scale");
-		Offset[multisample].Init(shader, "Offset");
+		mShader = std::make_unique<FShaderProgram>();
+		mShader->Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 330);
+		mShader->Compile(FShaderProgram::Fragment, "shaders/glsl/ssaocombine.fp", multisample ? "#define MULTISAMPLE\n" : "", 330);
+		mShader->SetFragDataLocation(0, "FragColor");
+		mShader->Link("shaders/glsl/ssaocombine");
+		mShader->SetAttribLocation(0, "PositionInProjection");
+		AODepthTexture.Init(*mShader, "AODepthTexture");
+		SceneFogTexture.Init(*mShader, "SceneFogTexture");
+		SampleCount.Init(*mShader, "SampleCount");
+		Scale.Init(*mShader, "Scale");
+		Offset.Init(*mShader, "Offset");
+		mMultisample = multisample;
 	}
-	shader.Bind();
+	mShader->Bind();
 }
diff --git a/src/gl/shaders/gl_ambientshader.h b/src/gl/shaders/gl_ambientshader.h
index 704729b24..5c68892c3 100644
--- a/src/gl/shaders/gl_ambientshader.h
+++ b/src/gl/shaders/gl_ambientshader.h
@@ -6,20 +6,21 @@
 class FLinearDepthShader
 {
 public:
-	void Bind(bool multisample);
+	void Bind();
 
-	FBufferedUniformSampler DepthTexture[2];
-	FBufferedUniformSampler ColorTexture[2];
-	FBufferedUniform1i SampleCount[2];
-	FBufferedUniform1f LinearizeDepthA[2];
-	FBufferedUniform1f LinearizeDepthB[2];
-	FBufferedUniform1f InverseDepthRangeA[2];
-	FBufferedUniform1f InverseDepthRangeB[2];
-	FBufferedUniform2f Scale[2];
-	FBufferedUniform2f Offset[2];
+	FBufferedUniformSampler DepthTexture;
+	FBufferedUniformSampler ColorTexture;
+	FBufferedUniform1i SampleCount;
+	FBufferedUniform1f LinearizeDepthA;
+	FBufferedUniform1f LinearizeDepthB;
+	FBufferedUniform1f InverseDepthRangeA;
+	FBufferedUniform1f InverseDepthRangeB;
+	FBufferedUniform2f Scale;
+	FBufferedUniform2f Offset;
 
 private:
-	FShaderProgram mShader[2];
+	std::unique_ptr<FShaderProgram> mShader;
+	bool mMultisample = false;
 };
 
 class FSSAOShader
@@ -28,6 +29,7 @@ public:
 	void Bind();
 
 	FBufferedUniformSampler DepthTexture;
+	FBufferedUniformSampler NormalTexture;
 	FBufferedUniformSampler RandomTexture;
 	FBufferedUniform2f UVToViewA;
 	FBufferedUniform2f UVToViewB;
@@ -48,9 +50,11 @@ private:
 		NumQualityModes
 	};
 
-	FString GetDefines(int mode);
+	FString GetDefines(int mode, bool multisample);
 
-	FShaderProgram mShader[NumQualityModes];
+	std::unique_ptr<FShaderProgram> mShader;
+	Quality mCurrentQuality = Off;
+	bool mMultisample = false;
 };
 
 class FDepthBlurShader
@@ -70,16 +74,17 @@ private:
 class FSSAOCombineShader
 {
 public:
-	void Bind(bool multisample);
+	void Bind();
 
-	FBufferedUniformSampler AODepthTexture[2];
-	FBufferedUniformSampler SceneDataTexture[2];
-	FBufferedUniform1i SampleCount[2];
-	FBufferedUniform2f Scale[2];
-	FBufferedUniform2f Offset[2];
+	FBufferedUniformSampler AODepthTexture;
+	FBufferedUniformSampler SceneFogTexture;
+	FBufferedUniform1i SampleCount;
+	FBufferedUniform2f Scale;
+	FBufferedUniform2f Offset;
 
 private:
-	FShaderProgram mShader[2];
+	std::unique_ptr<FShaderProgram> mShader;
+	bool mMultisample = false;
 };
 
 #endif
\ No newline at end of file
diff --git a/src/gl/shaders/gl_shader.cpp b/src/gl/shaders/gl_shader.cpp
index 9cbd69ce2..b7d8335f8 100644
--- a/src/gl/shaders/gl_shader.cpp
+++ b/src/gl/shaders/gl_shader.cpp
@@ -182,7 +182,8 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char *
 	glBindAttribLocation(hShader, VATTR_NORMAL, "aNormal");
 
 	glBindFragDataLocation(hShader, 0, "FragColor");
-	glBindFragDataLocation(hShader, 1, "FragData");
+	glBindFragDataLocation(hShader, 1, "FragFog");
+	glBindFragDataLocation(hShader, 2, "FragNormal");
 
 	glLinkProgram(hShader);
 
diff --git a/src/gl/system/gl_cvars.h b/src/gl/system/gl_cvars.h
index 836787117..3b425d261 100644
--- a/src/gl/system/gl_cvars.h
+++ b/src/gl/system/gl_cvars.h
@@ -53,7 +53,7 @@ EXTERN_CVAR(Float, gl_lens_chromatic)
 EXTERN_CVAR(Int, gl_ssao)
 EXTERN_CVAR(Int, gl_ssao_portals)
 EXTERN_CVAR(Float, gl_ssao_strength)
-EXTERN_CVAR(Bool, gl_ssao_debug)
+EXTERN_CVAR(Int, gl_ssao_debug)
 EXTERN_CVAR(Float, gl_ssao_bias)
 EXTERN_CVAR(Float, gl_ssao_radius)
 EXTERN_CVAR(Float, gl_ssao_blur_amount)
diff --git a/wadsrc/static/shaders/glsl/main.fp b/wadsrc/static/shaders/glsl/main.fp
index 1f270728d..e26b2fd4a 100644
--- a/wadsrc/static/shaders/glsl/main.fp
+++ b/wadsrc/static/shaders/glsl/main.fp
@@ -8,7 +8,8 @@ in vec4 vColor;
 
 out vec4 FragColor;
 #ifdef GBUFFER_PASS
-out vec4 FragData;
+out vec4 FragFog;
+out vec4 FragNormal;
 #endif
 
 #ifdef SHADER_STORAGE_LIGHTS
@@ -408,7 +409,8 @@ void main()
 	}
 	FragColor = frag;
 #ifdef GBUFFER_PASS
-	FragData = vec4(AmbientOcclusionColor(), 1.0);
+	FragFog = vec4(AmbientOcclusionColor(), 1.0);
+	FragNormal = vec4(vEyeNormal.xyz * 0.5 + 0.5, 1.0);
 #endif
 }
 
diff --git a/wadsrc/static/shaders/glsl/ssao.fp b/wadsrc/static/shaders/glsl/ssao.fp
index e0d972c57..758f7dbaa 100644
--- a/wadsrc/static/shaders/glsl/ssao.fp
+++ b/wadsrc/static/shaders/glsl/ssao.fp
@@ -15,6 +15,12 @@ uniform float AOStrength;
 
 uniform sampler2D DepthTexture;
 
+#if defined(MULTISAMPLE)
+uniform sampler2DMS NormalTexture;
+#else
+uniform sampler2D NormalTexture;
+#endif
+
 #if defined(USE_RANDOM_TEXTURE)
 uniform sampler2D RandomTexture;
 #endif
@@ -28,6 +34,20 @@ vec3 FetchViewPos(vec2 uv)
     return vec3((UVToViewA * uv + UVToViewB) * z, z);
 }
 
+#if defined(MULTISAMPLE)
+vec3 FetchNormal(vec2 uv)
+{
+	ivec2 texSize = textureSize(NormalTexture);
+	ivec2 ipos = ivec2(uv * vec2(texSize));
+	return normalize(texelFetch(NormalTexture, ipos, 0).xyz * 2.0 - 1.0);
+}
+#else
+vec3 FetchNormal(vec2 uv)
+{
+	return normalize(texture(NormalTexture, uv).xyz * 2.0 - 1.0);
+}
+#endif
+
 vec3 MinDiff(vec3 p, vec3 pr, vec3 pl)
 {
     vec3 v1 = pr - p;
@@ -106,7 +126,8 @@ float ComputeAO(vec3 viewPosition, vec3 viewNormal)
 void main()
 {
     vec3 viewPosition = FetchViewPos(TexCoord);
-    vec3 viewNormal = ReconstructNormal(viewPosition);
+    //vec3 viewNormal = ReconstructNormal(viewPosition);
+	vec3 viewNormal = FetchNormal(TexCoord);
 	float occlusion = ComputeAO(viewPosition, viewNormal) * AOStrength + (1.0 - AOStrength);
 	FragColor = vec4(occlusion, viewPosition.z, 0.0, 1.0);
 }
diff --git a/wadsrc/static/shaders/glsl/ssaocombine.fp b/wadsrc/static/shaders/glsl/ssaocombine.fp
index 7cd096276..377516587 100644
--- a/wadsrc/static/shaders/glsl/ssaocombine.fp
+++ b/wadsrc/static/shaders/glsl/ssaocombine.fp
@@ -5,10 +5,10 @@ out vec4 FragColor;
 uniform sampler2D AODepthTexture;
 
 #if defined(MULTISAMPLE)
-uniform sampler2DMS SceneDataTexture;
+uniform sampler2DMS SceneFogTexture;
 uniform int SampleCount;
 #else
-uniform sampler2D SceneDataTexture;
+uniform sampler2D SceneFogTexture;
 #endif
 
 uniform vec2 Scale;
@@ -18,19 +18,19 @@ void main()
 {
 	vec2 uv = Offset + TexCoord * Scale;
 #if defined(MULTISAMPLE)
-	ivec2 texSize = textureSize(SceneDataTexture);
+	ivec2 texSize = textureSize(SceneFogTexture);
 #else
-	ivec2 texSize = textureSize(SceneDataTexture, 0);
+	ivec2 texSize = textureSize(SceneFogTexture, 0);
 #endif
 	ivec2 ipos = ivec2(max(floor(uv * vec2(texSize) - 0.75), vec2(0.0)));
 
 #if defined(MULTISAMPLE)
 	vec3 fogColor = vec3(0.0);
 	for (int i = 0; i < SampleCount; i++)
-		fogColor += texelFetch(SceneDataTexture, ipos, i).rgb;
+		fogColor += texelFetch(SceneFogTexture, ipos, i).rgb;
 	fogColor /= float(SampleCount);
 #else
-	vec3 fogColor = texelFetch(SceneDataTexture, ipos, 0).rgb;
+	vec3 fogColor = texelFetch(SceneFogTexture, ipos, 0).rgb;
 #endif
 
 	float attenutation = texture(AODepthTexture, TexCoord).x;