Explorar el Código

Added importance sampling for reflection probes

BearishSun hace 8 años
padre
commit
2602318a10

+ 8 - 4
Data/Raw/Engine/DataList.json

@@ -166,10 +166,6 @@
             "Path": "PPTonemapping.bsl",
             "Path": "PPTonemapping.bsl",
             "UUID": "a8aa01e7-7e72-4f84-89f8-a0097250b0d3"
             "UUID": "a8aa01e7-7e72-4f84-89f8-a0097250b0d3"
         },
         },
-        {
-            "Path": "ReflectionCubemapFilter.bsl",
-            "UUID": "31b4b9ae-2afa-4cd8-b532-fe82a7f42baa"
-        },
         {
         {
             "Path": "Resolve.bsl",
             "Path": "Resolve.bsl",
             "UUID": "9d5f5101-2d7e-432c-b8ad-1998de9ca5c7"
             "UUID": "9d5f5101-2d7e-432c-b8ad-1998de9ca5c7"
@@ -217,6 +213,14 @@
         {
         {
             "Path": "FlatFramebufferToTexture.bsl",
             "Path": "FlatFramebufferToTexture.bsl",
             "UUID": "c469c67d-6a43-4961-8e8c-d769f88546b7"
             "UUID": "c469c67d-6a43-4961-8e8c-d769f88546b7"
+        },
+        {
+            "Path": "ReflectionCubeDownsample.bsl",
+            "UUID": "0efe7fe5-f7da-4ac8-a634-522bf6791dad"
+        },
+        {
+            "Path": "ReflectionCubeImportanceSample.bsl",
+            "UUID": "ed16fdd9-7982-4f5c-ae63-8303c786e9ad"
         }
         }
     ],
     ],
     "Skin": [
     "Skin": [

+ 0 - 12
Data/Raw/Engine/Shaders/ReflectionCubemapFilter.bsl → Data/Raw/Engine/Shaders/ReflectionCubeDownsample.bsl

@@ -30,12 +30,6 @@ Technique : inherits("PPBase") =
 
 
 			float4 main(VStoFS input) : SV_Target0
 			float4 main(VStoFS input) : SV_Target0
 			{
 			{
-				// Note: This is a rough approximation rather than something physically correct.
-				// For a more correct version we should sample along the specular lobe and weight
-				// the contributions. But even that wouldn't be fully correct as specular lobe
-				// shape changes according to view/normal due to occlusion. So instead we just 
-				// approximate everything.
-			
 				float2 scaledUV = input.uv0 * 2.0f - 1.0f;
 				float2 scaledUV = input.uv0 * 2.0f - 1.0f;
 				
 				
 				float3 dir;
 				float3 dir;
@@ -82,12 +76,6 @@ Technique : inherits("PPBase") =
 			
 			
 			void main()
 			void main()
 			{
 			{
-				// Note: This is a rough approximation rather than something physically correct.
-				// For a more correct version we should sample along the specular lobe and weight
-				// the contributions. But even that wouldn't be fully correct as specular lobe
-				// shape changes according to view/normal due to occlusion. So instead we just 
-				// approximate everything.
-			
 				vec2 scaledUV = FSInput.uv0 * 2.0f - 1.0f;
 				vec2 scaledUV = FSInput.uv0 * 2.0f - 1.0f;
 				
 				
 				vec3 dir;
 				vec3 dir;

+ 210 - 0
Data/Raw/Engine/Shaders/ReflectionCubeImportanceSample.bsl

@@ -0,0 +1,210 @@
+#include "$ENGINE$\PPBase.bslinc"
+
+Parameters =
+{
+	int			gCubeFace;
+	SamplerCUBE	gInputSamp : alias("gInputTex");
+	TextureCUBE gInputTex;
+};
+
+Blocks =
+{
+	Block Input;
+};
+
+Technique : inherits("PPBase") =
+{
+	Language = "HLSL11";
+	
+	Pass =
+	{
+		Fragment =
+		{
+			#define PI 3.1415926
+		
+			// From Hacker's Delight
+			float reverseBits(uint bits) 
+			{
+				bits = (bits << 16u) | (bits >> 16u);
+				bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
+				bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
+				bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
+				bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
+				
+				return float(bits) * 2.3283064365386963e-10; // 0x100000000
+			}
+			
+			float2 hammersleySequence(uint i, uint count)
+			{
+				float2 output;
+				output.x = i / (float)count;
+				output.y = reverseBits(i);
+				
+				return output;
+			}
+			
+			// Returns cos(theta) in x and phi in y
+			float2 importanceSampleGGX(float2 e, float roughness4)
+			{
+				// See GGXImportanceSample.nb for derivation (essentially, take base GGX, normalize it,
+				// generate PDF, split PDF into marginal probability for theta and conditional probability
+				// for phi. Plug those into the CDF, invert it.)				
+				float cosTheta = sqrt((1.0f - e.x) / (1.0f + (roughness4 - 1.0f) * e.y));
+				float phi = 2.0f * PI * e.y;
+				
+				return float2(cosTheta, phi);
+			}
+			
+			float3 sphericalToCartesian(float cosTheta, float sinTheta, float phi)
+			{
+				float3 output;
+				output.x = sinTheta * cos(phi);
+				output.y = sinTheta * sin(phi);
+				output.z = cosTheta;
+				
+				return output;
+			}
+			
+			float pdfGGX(float cosTheta, float sinTheta, float roughness4)
+			{
+				float d = (cosTheta*roughness4 - cosTheta) * cosTheta + 1;
+				return roughness4 * cosTheta * sinTheta / (d*d*PI);
+			}
+			
+			float mapRoughnessToMipLevel(float roughness, uint maxMipLevel)
+			{
+				// We use the following equation:
+				//    mipLevel = log10(roughness) / log10(dropPercent)
+				//
+				// Where dropPercent represent by what % to drop the roughness with each mip level.
+				// We convert to log2 and a assume a drop percent value of 0.6. This gives us:
+				//    mipLevel = -1.35692 * log2(roughness);
+				
+				return max(0, maxMipLevel - (-1.35692 * log2(roughness)));
+			}
+			
+			float mapMipLevelToRoughness(uint mipLevel)
+			{
+				// Inverse of mapRoughnessToMipLevel()
+				return 1.0f - exp2(-0.7369655941662063 * mipLevel);
+			}
+			
+			cbuffer Input
+			{
+				int gCubeFace;
+				int gMipLevel;
+				float gPrecomputedMipFactor;
+			}	
+		
+			SamplerState gInputSamp;
+			TextureCube gInputTex;
+
+			float4 main(VStoFS input) : SV_Target0
+			{
+				float2 scaledUV = input.uv0 * 2.0f - 1.0f;
+														
+				float3 N;
+				if(gCubeFace == 0)
+					N = float3(1.0f, -scaledUV.y, -scaledUV.x);
+				else if(gCubeFace == 1)
+					N = float3(-1.0f, -scaledUV.y, scaledUV.x);
+				else if(gCubeFace == 2)
+					N = float3(scaledUV.x, 1.0f, scaledUV.y);
+				else if(gCubeFace == 3)
+					N = float3(scaledUV.x, -1.0f, -scaledUV.y);
+				else if(gCubeFace == 4)
+					N = float3(scaledUV.x, -scaledUV.y, 1.0f);
+				else
+					N = float3(-scaledUV.x, -scaledUV.y, -1.0f);
+				
+				N = normalize(N);
+				
+				// Determine which mip level to sample from depending on PDF and cube -> sphere mapping distortion
+				float distortion = rcp(pow(N.x * N.x + N.y * N.y + N.z * N.z, 3.0f/2.0f));
+				
+				float roughness = mapMipLevelToRoughness(gMipLevel);
+				float roughness2 = roughness * roughness;
+				float roughness4 = roughness2 * roughness2;
+				
+				float4 sum = 0;
+				for(uint i = 0; i < NUM_SAMPLES; i++)
+				{
+					float random = hammersleySequence(i, NUM_SAMPLES);
+					float2 sphericalH = importanceSampleGGX(random, roughness4);
+					
+					float cosTheta = sphericalH.x;
+					float phi = sphericalH.y;
+					
+					float sinTheta = sqrt(1.0f - cosTheta);
+					
+					float3 H = sphericalToCartesian(cosTheta, sinTheta, phi);
+					float PDF = pdfGGX(cosTheta, sinTheta, roughness4);
+					
+					// Transform H to world space
+					float3 up = abs(H.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0);
+					float3 tangentX = normalize(cross(up, N));
+					float3 tangentY = cross(N, tangentX);
+					
+					H = tangentX * H.x + tangentY * H.y + N * H.z; 
+					
+					// Calculating mip level from distortion and pdf and described by http://http.developer.nvidia.com/GPUGems3/gpugems3_ch20.html
+					int mipLevel = max(gPrecomputedMipFactor - 0.5f * log2(PDF * distortion), 0);
+					
+					// Note: Adding +1 bias as it looks better
+					mipLevel++;
+					
+					// sum += H * GGX / PDF. In GGX/PDF most factors cancel out and we're left with 1/sin*cos
+					sum += gInputTex.SampleLevel(gInputSamp, H, mipLevel) / (cosTheta * sinTheta);
+				}
+				
+				return sum / NUM_SAMPLES;
+			}	
+		};
+	};
+};
+
+Technique : inherits("PPBase") =
+{
+	Language = "GLSL";
+	
+	Pass =
+	{
+		Fragment =
+		{
+			in VStoFS
+			{
+				layout(location = 0) vec2 uv0;
+			} FSInput;		
+		
+			layout(location = 0) out vec4 fragColor;
+		
+			layout(binding = 0) uniform Input
+			{
+				int gCubeFace;
+			};
+			
+			layout(binding = 1) uniform samplerCube gInputTex;
+			
+			void main()
+			{
+				vec2 scaledUV = FSInput.uv0 * 2.0f - 1.0f;
+				
+				vec3 dir;
+				if(gCubeFace == 0)
+					dir = vec3(1.0f, -scaledUV.y, -scaledUV.x);
+				else if(gCubeFace == 1)
+					dir = vec3(-1.0f, -scaledUV.y, scaledUV.x);
+				else if(gCubeFace == 2)
+					dir = vec3(scaledUV.x, 1.0f, scaledUV.y);
+				else if(gCubeFace == 3)
+					dir = vec3(scaledUV.x, -1.0f, -scaledUV.y);
+				else if(gCubeFace == 4)
+					dir = vec3(scaledUV.x, -scaledUV.y, 1.0f);
+				else
+					dir = vec3(-scaledUV.x, -scaledUV.y, -1.0f);
+				
+				fragColor = texture(gInputTex, dir);
+			}	
+		};
+	};
+};

+ 39 - 7
Source/RenderBeast/Include/BsReflectionProbes.h

@@ -12,19 +12,19 @@ namespace bs { namespace ct
 	 *  @{
 	 *  @{
 	 */
 	 */
 
 
-	BS_PARAM_BLOCK_BEGIN(ReflectionCubemapFilterParamDef)
+	BS_PARAM_BLOCK_BEGIN(ReflectionCubeDownsampleParamDef)
 		BS_PARAM_BLOCK_ENTRY(int, gCubeFace)
 		BS_PARAM_BLOCK_ENTRY(int, gCubeFace)
 	BS_PARAM_BLOCK_END
 	BS_PARAM_BLOCK_END
 
 
-	extern ReflectionCubemapFilterParamDef gReflectionCubemapFilterDef;
+	extern ReflectionCubeDownsampleParamDef gReflectionCubeDownsampleParamDef;
 
 
-	/** Performs filtering on cubemap faces in order for make them suitable for specular evaluation. */
-	class ReflectionCubemapFilterMat : public RendererMaterial<ReflectionCubemapFilterMat>
+	/** Performs filtering on cubemap faces in order to prepare them for importance sampling. */
+	class ReflectionCubeDownsampleMat : public RendererMaterial<ReflectionCubeDownsampleMat>
 	{
 	{
-		RMAT_DEF("ReflectionCubemapFilter.bsl")
+		RMAT_DEF("ReflectionCubeDownsample.bsl")
 
 
 	public:
 	public:
-		ReflectionCubemapFilterMat();
+		ReflectionCubeDownsampleMat();
 
 
 		/** Downsamples the provided texture face and outputs it to the provided target. */
 		/** Downsamples the provided texture face and outputs it to the provided target. */
 		void execute(const SPtr<Texture>& source, UINT32 face, const TextureSurface& surface, 
 		void execute(const SPtr<Texture>& source, UINT32 face, const TextureSurface& surface, 
@@ -35,6 +35,33 @@ namespace bs { namespace ct
 		GpuParamTexture mInputTexture;
 		GpuParamTexture mInputTexture;
 	};
 	};
 
 
+	BS_PARAM_BLOCK_BEGIN(ReflectionCubeImportanceSampleParamDef)
+		BS_PARAM_BLOCK_ENTRY(int, gCubeFace)
+		BS_PARAM_BLOCK_ENTRY(int, gMipLevel)
+		BS_PARAM_BLOCK_ENTRY(float, gPrecomputedMipFactor)
+	BS_PARAM_BLOCK_END
+
+	extern ReflectionCubeImportanceSampleParamDef gReflectionCubeImportanceSampleParamDef;
+
+	/** Performs importance sampling on cubemap faces in order for make them suitable for specular evaluation. */
+	class ReflectionCubeImportanceSampleMat : public RendererMaterial<ReflectionCubeImportanceSampleMat>
+	{
+		RMAT_DEF("ReflectionCubeImportanceSample.bsl")
+
+	public:
+		ReflectionCubeImportanceSampleMat();
+
+		/** Importance samples the provided texture face and outputs it to the provided target. */
+		void execute(const SPtr<Texture>& source, UINT32 face, UINT32 mip, const SPtr<RenderTarget>& target);
+
+	private:
+		static const UINT32 NUM_SAMPLES;
+
+		SPtr<GpuParamBlockBuffer> mParamBuffer;
+		GpuParamTexture mInputTexture;
+	};
+
+
 	/** Helper class that handles generation and processing of textures used for reflection probes. */
 	/** Helper class that handles generation and processing of textures used for reflection probes. */
 	class ReflectionProbes
 	class ReflectionProbes
 	{
 	{
@@ -42,8 +69,13 @@ namespace bs { namespace ct
 		/**
 		/**
 		 * Performs filtering on the cubemap, populating its mip-maps with filtered values that can be used for
 		 * Performs filtering on the cubemap, populating its mip-maps with filtered values that can be used for
 		 * evaluating specular reflections.
 		 * evaluating specular reflections.
+		 * 
+		 * @param[in, out]	cubemap		Cubemap to filter. Its mip level 0 will be read, filtered and written into
+		 *								other mip levels.
+		 * @param[in]		scratch		Temporary cubemap texture to use for the filtering process. Must match the size of
+		 *								the source cubemap.
 		 */
 		 */
-		static void filterCubemapForSpecular(const SPtr<Texture>& cubemap);
+		static void filterCubemapForSpecular(const SPtr<Texture>& cubemap, const SPtr<Texture>& scratch);
 
 
 		static const UINT32 REFLECTION_CUBEMAP_SIZE;
 		static const UINT32 REFLECTION_CUBEMAP_SIZE;
 	};
 	};

+ 81 - 10
Source/RenderBeast/Source/BsReflectionProbes.cpp

@@ -8,26 +8,65 @@
 
 
 namespace bs { namespace ct
 namespace bs { namespace ct
 {
 {
-	ReflectionCubemapFilterParamDef gReflectionCubemapFilterDef;
+	ReflectionCubeDownsampleParamDef gReflectionCubeDownsampleParamDef;
 
 
-	ReflectionCubemapFilterMat::ReflectionCubemapFilterMat()
+	ReflectionCubeDownsampleMat::ReflectionCubeDownsampleMat()
 	{
 	{
-		mParamBuffer = gReflectionCubemapFilterDef.createBuffer();
+		mParamBuffer = gReflectionCubeDownsampleParamDef.createBuffer();
 
 
 		mParamsSet->setParamBlockBuffer("Input", mParamBuffer);
 		mParamsSet->setParamBlockBuffer("Input", mParamBuffer);
 		mParamsSet->getGpuParams()->getTextureParam(GPT_FRAGMENT_PROGRAM, "gInputTex", mInputTexture);
 		mParamsSet->getGpuParams()->getTextureParam(GPT_FRAGMENT_PROGRAM, "gInputTex", mInputTexture);
 	}
 	}
 
 
-	void ReflectionCubemapFilterMat::_initDefines(ShaderDefines& defines)
+	void ReflectionCubeDownsampleMat::_initDefines(ShaderDefines& defines)
 	{
 	{
 		// Do nothing
 		// Do nothing
 	}
 	}
 
 
-	void ReflectionCubemapFilterMat::execute(const SPtr<Texture>& source, UINT32 face, const TextureSurface& surface, 
+	void ReflectionCubeDownsampleMat::execute(const SPtr<Texture>& source, UINT32 face, const TextureSurface& surface, 
 											 const SPtr<RenderTarget>& target)
 											 const SPtr<RenderTarget>& target)
 	{
 	{
 		mInputTexture.set(source, surface);
 		mInputTexture.set(source, surface);
-		gReflectionCubemapFilterDef.gCubeFace.set(mParamBuffer, face);
+		gReflectionCubeDownsampleParamDef.gCubeFace.set(mParamBuffer, face);
+
+		RenderAPI& rapi = RenderAPI::instance();
+		rapi.setRenderTarget(target);
+
+		gRendererUtility().setPass(mMaterial);
+		gRendererUtility().setPassParams(mParamsSet);
+		gRendererUtility().drawScreenQuad();
+	}
+
+	const UINT32 ReflectionCubeImportanceSampleMat::NUM_SAMPLES = 1024;
+	ReflectionCubeImportanceSampleParamDef gReflectionCubeImportanceSampleParamDef;
+
+	ReflectionCubeImportanceSampleMat::ReflectionCubeImportanceSampleMat()
+	{
+		mParamBuffer = gReflectionCubeImportanceSampleParamDef.createBuffer();
+
+		mParamsSet->setParamBlockBuffer("Input", mParamBuffer);
+		mParamsSet->getGpuParams()->getTextureParam(GPT_FRAGMENT_PROGRAM, "gInputTex", mInputTexture);
+	}
+
+	void ReflectionCubeImportanceSampleMat::_initDefines(ShaderDefines& defines)
+	{
+		defines.set("NUM_SAMPLES", NUM_SAMPLES); 
+	}
+
+	void ReflectionCubeImportanceSampleMat::execute(const SPtr<Texture>& source, UINT32 face, UINT32 mip, 
+													const SPtr<RenderTarget>& target)
+	{
+		mInputTexture.set(source);
+		gReflectionCubeImportanceSampleParamDef.gCubeFace.set(mParamBuffer, face);
+		gReflectionCubeImportanceSampleParamDef.gMipLevel.set(mParamBuffer, mip);
+
+		float width = (float)source->getProperties().getWidth();
+		float height = (float)source->getProperties().getHeight();
+
+		// First part of the equation for determining mip level for sample from.
+		// See http://http.developer.nvidia.com/GPUGems3/gpugems3_ch20.html
+		float mipFactor = 0.5f * std::log2(width * height / NUM_SAMPLES); // TODO
+		gReflectionCubeImportanceSampleParamDef.gPrecomputedMipFactor.set(mParamBuffer, mipFactor);
 
 
 		RenderAPI& rapi = RenderAPI::instance();
 		RenderAPI& rapi = RenderAPI::instance();
 		rapi.setRenderTarget(target);
 		rapi.setRenderTarget(target);
@@ -39,18 +78,33 @@ namespace bs { namespace ct
 
 
 	const UINT32 ReflectionProbes::REFLECTION_CUBEMAP_SIZE = 256;
 	const UINT32 ReflectionProbes::REFLECTION_CUBEMAP_SIZE = 256;
 
 
-	void ReflectionProbes::filterCubemapForSpecular(const SPtr<Texture>& cubemap)
+	void ReflectionProbes::filterCubemapForSpecular(const SPtr<Texture>& cubemap, const SPtr<Texture>& scratch)
 	{
 	{
-		static ReflectionCubemapFilterMat filterMaterial;
+		static ReflectionCubeDownsampleMat downsampleMat;
+		static ReflectionCubeImportanceSampleMat importanceSampleMat;
 
 
+		// We sample the cubemaps using importance sampling to generate roughness
 		UINT32 numMips = cubemap->getProperties().getNumMipmaps();
 		UINT32 numMips = cubemap->getProperties().getNumMipmaps();
 
 
+		// Before importance sampling the cubemaps we first create box filtered versions for each mip level. This helps fix
+		// the aliasing artifacts that would otherwise be noticeable on importance sampled cubemaps. The aliasing happens
+		// because: 
+		//  1. We use the same random samples for all pixels, which appears to duplicate reflections instead of creating
+		//     noise, which is usually more acceptable 
+		//  2. Even if we were to use fully random samples we would need a lot to avoid noticeable noise, which isn't
+		//     practical
+
+		// Copy base mip level to scratch cubemap
+		for (UINT32 face = 0; face < 6; face++)
+			cubemap->copy(scratch, face, 0, face, 0);
+
+		// Fill out remaining scratch mip levels by downsampling
 		for (UINT32 mip = 1; mip < numMips; mip++)
 		for (UINT32 mip = 1; mip < numMips; mip++)
 		{
 		{
 			for (UINT32 face = 0; face < 6; face++)
 			for (UINT32 face = 0; face < 6; face++)
 			{
 			{
 				RENDER_TEXTURE_DESC cubeFaceRTDesc;
 				RENDER_TEXTURE_DESC cubeFaceRTDesc;
-				cubeFaceRTDesc.colorSurfaces[0].texture = cubemap;
+				cubeFaceRTDesc.colorSurfaces[0].texture = scratch;
 				cubeFaceRTDesc.colorSurfaces[0].face = face;
 				cubeFaceRTDesc.colorSurfaces[0].face = face;
 				cubeFaceRTDesc.colorSurfaces[0].numFaces = 1;
 				cubeFaceRTDesc.colorSurfaces[0].numFaces = 1;
 				cubeFaceRTDesc.colorSurfaces[0].mipLevel = mip;
 				cubeFaceRTDesc.colorSurfaces[0].mipLevel = mip;
@@ -59,7 +113,24 @@ namespace bs { namespace ct
 
 
 				UINT32 sourceMip = mip - 1;
 				UINT32 sourceMip = mip - 1;
 				TextureSurface sourceSurface(sourceMip, 1, 0, 6);
 				TextureSurface sourceSurface(sourceMip, 1, 0, 6);
-				filterMaterial.execute(cubemap, face, sourceSurface, target);
+				downsampleMat.execute(scratch, face, sourceSurface, target);
+			}
+		}
+
+		// Importance sample
+		for (UINT32 mip = 1; mip < numMips; mip++)
+		{
+			for (UINT32 face = 0; face < 6; face++)
+			{
+				RENDER_TEXTURE_DESC cubeFaceRTDesc;
+				cubeFaceRTDesc.colorSurfaces[0].texture = cubemap;
+				cubeFaceRTDesc.colorSurfaces[0].face = face;
+				cubeFaceRTDesc.colorSurfaces[0].numFaces = 1;
+				cubeFaceRTDesc.colorSurfaces[0].mipLevel = mip;
+
+				SPtr<RenderTarget> target = RenderTexture::create(cubeFaceRTDesc);
+
+				importanceSampleMat.execute(scratch, face, mip, target);
 			}
 			}
 		}
 		}
 	}
 	}

+ 15 - 9
Source/RenderBeast/Source/BsRenderBeast.cpp

@@ -108,6 +108,8 @@ namespace bs { namespace ct
 		mRenderables.clear();
 		mRenderables.clear();
 		mRenderableVisibility.clear();
 		mRenderableVisibility.clear();
 
 
+		mCubemapArrayTex = nullptr;
+
 		PostProcessing::shutDown();
 		PostProcessing::shutDown();
 		GpuResourcePool::shutDown();
 		GpuResourcePool::shutDown();
 
 
@@ -1176,7 +1178,7 @@ namespace bs { namespace ct
 			{
 			{
 				TEXTURE_DESC cubeMapDesc;
 				TEXTURE_DESC cubeMapDesc;
 				cubeMapDesc.type = TEX_TYPE_CUBE_MAP;
 				cubeMapDesc.type = TEX_TYPE_CUBE_MAP;
-				cubeMapDesc.format = PF_FLOAT16_RGBA;
+				cubeMapDesc.format = PF_FLOAT_R11G11B10;
 				cubeMapDesc.width = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
 				cubeMapDesc.width = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
 				cubeMapDesc.height = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
 				cubeMapDesc.height = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
 				cubeMapDesc.numMips = PixelUtil::getMaxMipmaps(cubeMapDesc.width, cubeMapDesc.height, 1, cubeMapDesc.format);
 				cubeMapDesc.numMips = PixelUtil::getMaxMipmaps(cubeMapDesc.width, cubeMapDesc.height, 1, cubeMapDesc.format);
@@ -1189,6 +1191,17 @@ namespace bs { namespace ct
 
 
 			auto& cubemapArrayProps = mCubemapArrayTex->getProperties();
 			auto& cubemapArrayProps = mCubemapArrayTex->getProperties();
 
 
+			TEXTURE_DESC cubemapDesc;
+			cubemapDesc.type = TEX_TYPE_CUBE_MAP;
+			cubemapDesc.format = PF_FLOAT_R11G11B10;
+			cubemapDesc.width = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
+			cubemapDesc.height = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
+			cubemapDesc.numMips = PixelUtil::getMaxMipmaps(cubemapDesc.width, cubemapDesc.height, 1, cubemapDesc.format);
+
+			SPtr<Texture> scratchCubemap;
+			if (numProbes > 0)
+				scratchCubemap = Texture::create(cubemapDesc);
+
 			FrameQueue<UINT32> emptySlots;
 			FrameQueue<UINT32> emptySlots;
 			for (UINT32 i = 0; i < numProbes; i++)
 			for (UINT32 i = 0; i < numProbes; i++)
 			{
 			{
@@ -1202,17 +1215,10 @@ namespace bs { namespace ct
 
 
 						if (probeInfo.texture == nullptr || probeInfo.textureDirty)
 						if (probeInfo.texture == nullptr || probeInfo.textureDirty)
 						{
 						{
-							TEXTURE_DESC cubemapDesc;
-							cubemapDesc.type = TEX_TYPE_CUBE_MAP;
-							cubemapDesc.format = PF_FLOAT16_RGBA;
-							cubemapDesc.width = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
-							cubemapDesc.height = ReflectionProbes::REFLECTION_CUBEMAP_SIZE;
-							cubemapDesc.numMips = PixelUtil::getMaxMipmaps(cubemapDesc.width, cubemapDesc.height, 1, cubemapDesc.format);
-
 							probeInfo.texture = Texture::create(cubemapDesc);
 							probeInfo.texture = Texture::create(cubemapDesc);
 
 
 							captureSceneCubeMap(probeInfo.texture, probeInfo.probe->getPosition(), true, frameInfo);
 							captureSceneCubeMap(probeInfo.texture, probeInfo.probe->getPosition(), true, frameInfo);
-							ReflectionProbes::filterCubemapForSpecular(probeInfo.texture);
+							ReflectionProbes::filterCubemapForSpecular(probeInfo.texture, scratchCubemap);
 
 
 							ReflectionCubemapCache::instance().setCachedTexture(probeInfo.probe->getUUID(), probeInfo.texture);
 							ReflectionCubemapCache::instance().setCachedTexture(probeInfo.probe->getUUID(), probeInfo.texture);
 						}
 						}