Przeglądaj źródła

Updated LightProbe generation code
Light probe SH coefficients are now saved with the light probe volume

BearishSun 8 lat temu
rodzic
commit
1ce5ec1bb4

+ 1 - 0
Source/BansheeCore/Include/BsCorePrerequisites.h

@@ -572,6 +572,7 @@ namespace bs
 		TID_Skybox = 1134,
 		TID_CSkybox = 1135,
 		TID_LightProbeVolume = 1136,
+		TID_SavedLightProbeInfo = 1137,
 
 		// Moved from Engine layer
 		TID_CCamera = 30000,

+ 22 - 22
Source/BansheeCore/Include/BsHardwareBuffer.h

@@ -18,9 +18,9 @@ namespace bs
 	 * @note	Be aware that reading from non-system memory hardware buffers is usually slow and should be avoided.
 	 */
 	class BS_CORE_EXPORT HardwareBuffer
-    {
-    public:
-        virtual ~HardwareBuffer() {}
+	{
+	public:
+		virtual ~HardwareBuffer() {}
 
 		/**
 		 * Locks a portion of the buffer and returns pointer to the locked area. You must call unlock() when done.
@@ -35,15 +35,15 @@ namespace bs
 		 * @param[in]	queueIdx	Device queue to perform any read/write operations on. See @ref queuesDoc.
 		 */
 		virtual void* lock(UINT32 offset, UINT32 length, GpuLockOptions options, UINT32 deviceIdx = 0, UINT32 queueIdx = 0)
-        {
-            assert(!isLocked() && "Cannot lock this buffer, it is already locked!");
-            void* ret = map(offset, length, options, deviceIdx, queueIdx);
-            mIsLocked = true;
+		{
+			assert(!isLocked() && "Cannot lock this buffer, it is already locked!");
+			void* ret = map(offset, length, options, deviceIdx, queueIdx);
+			mIsLocked = true;
 
 			mLockStart = offset;
 			mLockSize = length;
-            return ret;
-        }
+			return ret;
+		}
 
 		/**
 		 * Locks the entire buffer and returns pointer to the locked area. You must call unlock() when done.
@@ -55,19 +55,19 @@ namespace bs
 		 *							the method returns null.
 		 * @param[in]	queueIdx	Device queue to perform any read/write operations on. See @ref queuesDoc.
 		 */
-        void* lock(GpuLockOptions options, UINT32 deviceIdx = 0, UINT32 queueIdx = 0)
-        {
-            return this->lock(0, mSize, options, deviceIdx, queueIdx);
-        }
+		void* lock(GpuLockOptions options, UINT32 deviceIdx = 0, UINT32 queueIdx = 0)
+		{
+			return this->lock(0, mSize, options, deviceIdx, queueIdx);
+		}
 
 		/**	Releases the lock on this buffer. */
 		virtual void unlock()
-        {
-            assert(isLocked() && "Cannot unlock this buffer, it is not locked!");
+		{
+			assert(isLocked() && "Cannot unlock this buffer, it is not locked!");
 
-            unmap();
-            mIsLocked = false;
-        }
+			unmap();
+			mIsLocked = false;
+		}
 
 		/**
 		 * Reads data from a portion of the buffer and copies it to the destination buffer. Caller must ensure destination 
@@ -81,7 +81,7 @@ namespace bs
 		 *							no data will be read.		
 		 * @param[in]	queueIdx	Device queue to perform the read operation on. See @ref queuesDoc.
 		 */
-        virtual void readData(UINT32 offset, UINT32 length, void* dest, UINT32 deviceIdx = 0, UINT32 queueIdx = 0) = 0;
+		virtual void readData(UINT32 offset, UINT32 length, void* dest, UINT32 deviceIdx = 0, UINT32 queueIdx = 0) = 0;
 
 		/**
 		 * Writes data into a portion of the buffer from the source memory. 
@@ -93,7 +93,7 @@ namespace bs
 		 * @param[in]	writeFlags	Optional write flags that may affect performance.
 		 * @param[in]	queueIdx	Device queue to perform the write operation on. See @ref queuesDoc.
 		 */
-        virtual void writeData(UINT32 offset, UINT32 length, const void* source,
+		virtual void writeData(UINT32 offset, UINT32 length, const void* source,
 				BufferWriteType writeFlags = BWT_NORMAL, UINT32 queueIdx = 0) = 0;
 
 		/**
@@ -125,10 +125,10 @@ namespace bs
 		}
 			
 		/** Returns the size of this buffer in bytes. */
-        UINT32 getSize() const { return mSize; }
+		UINT32 getSize() const { return mSize; }
 
 		/**	Returns whether or not this buffer is currently locked. */
-        bool isLocked() const { return mIsLocked; }	
+		bool isLocked() const { return mIsLocked; }
 
 	protected:
 		friend class HardwareBufferManager;

+ 73 - 12
Source/BansheeCore/Include/BsLightProbeVolume.h

@@ -10,6 +10,11 @@
 
 namespace bs
 {
+	namespace ct
+	{
+		class RendererTask;
+	}
+
 	/** @addtogroup Implementation
 	 *  @{
 	 */
@@ -64,6 +69,21 @@ namespace bs
 
 	namespace ct { class LightProbeVolume; }
 
+	/** Vector representing spherical harmonic coefficients for a light probe. */
+	struct LightProbeSHCoefficients
+	{
+		float coeffsR[9];
+		float coeffsG[9];
+		float coeffsB[9];
+	};
+
+	/** SH coefficients for a specific light probe, and its handle. */
+	struct LightProbeCoefficientInfo
+	{
+		UINT32 handle;
+		LightProbeSHCoefficients coefficients;
+	};
+
 	/** 
 	 * Allows you to define a volume of light probes that will be used for indirect lighting. Lighting information in the
 	 * scene will be interpolated from nearby probes to calculate the amount of indirect lighting at that position. It is
@@ -83,8 +103,13 @@ namespace bs
 
 			LightProbeFlags flags;
 			Vector3 position;
+
+			/** Coefficients are only valid directly after deserialization, or after updateCoefficients() is called. */
+			LightProbeSHCoefficients coefficients;
 		};
 	public:
+		~LightProbeVolume();
+
 		/** Adds a new probe at the specified position and returns a handle to the probe. */
 		UINT32 addProbe(const Vector3& position);
 
@@ -100,6 +125,18 @@ namespace bs
 		 */
 		void removeProbe(UINT32 handle);
 
+		/**
+		 * Causes the information for this specific light probe to be updated. You generally want to call this when the
+		 * probe is moved or the scene around the probe changes.
+		 */
+		void renderProbe(UINT32 handle);
+
+		/**
+		 * Causes the information for all lights probes to be updated. You generally want to call this if you move the
+		 * entire light volume or the scene around the volume changes.
+		 */
+		void renderProbes();
+
 		/**	Retrieves an implementation of the object usable only from the core thread. */
 		SPtr<ct::LightProbeVolume> getCore() const;
 
@@ -118,6 +155,15 @@ namespace bs
 
 		LightProbeVolume(const AABox& volume, const Vector3& density);
 
+		/** Renders the light probe data on the core thread. */
+		void runRenderProbeTask();
+
+		/** 
+		 * Fetches latest SH coefficient data from the core thread. Note this method will block the caller thread until
+		 * the data is fetched from the core thread. It will also force any in-progress light probe updated to finish.
+		 */
+		void updateCoefficients();
+
 		/** @copydoc CoreObject::createCore */
 		SPtr<ct::CoreObject> createCore() const override;
 
@@ -133,6 +179,7 @@ namespace bs
 	private:
 		UnorderedMap<UINT32, ProbeInfo> mProbes;
 		UINT32 mNextProbeId = 0;
+		SPtr<ct::RendererTask> mRendererTask;
 
 		/************************************************************************/
 		/* 								RTTI		                     		*/
@@ -173,20 +220,11 @@ namespace bs
 		/**	Retrieves an ID that can be used for uniquely identifying this object by the renderer. */
 		UINT32 getRendererId() const { return mRendererId; }
 
-		/** 
-		 * Parses the list of probes and reorganizes it by removing gaps so that all probes are sequential. 
-		 * 
-		 * @param[out]	freedEntries	A list of entries mapping to the GPU buffer where probe SH coefficients are stored.
-		 *								These are the entries that have been freed since the last call to prune().
-		 * @param[in]	freeAll			If true, all probes held by this volume will be marked as freed.
-		 */
-		void prune(Vector<UINT32>& freedEntries, bool freeAll = false);
-
-		/** Returns information about all light probes. */
-		Vector<LightProbeInfo>& getLightProbeInfos() { return mProbeInfos; }
-
 		/** Returns a list of positions for all light probes. */
 		Vector<Vector3>& getLightProbePositions() { return mProbePositions; }
+
+		/** Populates the vector with SH coefficients for each light probe. Involves reading the GPU buffer. */
+		void getProbeCoefficients(Vector<LightProbeCoefficientInfo>& output) const;
 	protected:
 		friend class bs::LightProbeVolume;
 
@@ -198,11 +236,34 @@ namespace bs
 		/** @copydoc CoreObject::syncToCore */
 		void syncToCore(const CoreSyncData& data) override;
 
+		/** 
+		 * Renders dirty probes and updates their SH coefficients in the local GPU buffer. 
+		 *
+		 * @param[in]	maxProbes	Maximum number of probes to render. Set to zero to render all dirty probes. Limiting the
+		 *							number of probes allows the rendering to be distributed over multiple frames.
+		 * @return					True if there are no more dirty probes to process.
+		 */
+		bool renderProbes(UINT32 maxProbes);
+
+		/** 
+		 * Resizes the internal GPU buffer that stores light probe SH coefficients, to the specified size (in the number
+		 * of probes). 
+		 */
+		void resizeCoefficientBuffer(UINT32 count);
+
 		UINT32 mRendererId = 0;
 		UnorderedMap<UINT32, UINT32> mProbeMap; // Map from static indices to compact list of probes
+		UINT32 mFirstDirtyProbe = 0;
 
 		Vector<Vector3> mProbePositions;
 		Vector<LightProbeInfo> mProbeInfos;
+
+		// Contains SH coefficients for the probes
+		SPtr<GpuBuffer> mCoefficients;
+		UINT32 mCoeffBufferSize = 0;
+
+		// Temporary until initialization
+		Vector<LightProbeSHCoefficients> mInitCoefficients;
 	};
 	}
 

+ 122 - 2
Source/BansheeCore/Include/BsLightProbeVolumeRTTI.h

@@ -5,6 +5,9 @@
 #include "BsCorePrerequisites.h"
 #include "BsRTTIType.h"
 #include "BsLightProbeVolume.h"
+#include "BsRenderer.h"
+#include "BsCoreThread.h"
+#include "BsTextureRTTI.h"
 
 namespace bs
 {
@@ -13,6 +16,72 @@ namespace bs
 	 *  @{
 	 */
 
+	BS_ALLOW_MEMCPY_SERIALIZATION(LightProbeSHCoefficients)
+
+	/** Serializable information about a single light probe. */
+	struct SavedLightProbeInfo
+	{
+		Vector<Vector3> positions;
+		Vector<LightProbeSHCoefficients> coefficients;
+	};
+
+	template<> struct RTTIPlainType<SavedLightProbeInfo>
+	{	
+		enum { id = TID_SavedLightProbeInfo }; enum { hasDynamicSize = 1 };
+
+		static void toMemory(const SavedLightProbeInfo& data, char* memory)
+		{ 
+			UINT32 size = getDynamicSize(data);
+
+			UINT32 curSize = sizeof(UINT32);
+			memcpy(memory, &size, curSize);
+			memory += curSize;
+
+			UINT32 version = 0;
+
+			memory = rttiWriteElem(version, memory);
+			memory = rttiWriteElem(data.positions, memory);
+			memory = rttiWriteElem(data.coefficients, memory);
+		}
+
+		static UINT32 fromMemory(SavedLightProbeInfo& data, char* memory)
+		{ 
+			UINT32 size;
+			memcpy(&size, memory, sizeof(UINT32)); 
+			memory += sizeof(UINT32);
+
+			UINT32 version;
+			memory = rttiReadElem(version, memory);
+
+			switch(version)
+			{
+			case 0:
+				rttiReadElem(data.positions, memory);
+				rttiReadElem(data.coefficients, memory);
+				break;
+			default:
+				LOGERR("Unknown version of SavedLightProbeInfo data. Unable to deserialize.");
+				break;
+			}
+
+			return size;
+		}
+
+		static UINT32 getDynamicSize(const SavedLightProbeInfo& data)	
+		{ 
+			UINT64 dataSize = rttiGetElemSize(data.positions) + rttiGetElemSize(data.coefficients) + sizeof(UINT32) * 2;
+
+#if BS_DEBUG_MODE
+			if(dataSize > std::numeric_limits<UINT32>::max())
+			{
+				BS_EXCEPT(InternalErrorException, "Data overflow! Size doesn't fit into 32 bits.");
+			}
+#endif
+
+			return (UINT32)dataSize;
+		}	
+	}; 
+
 	class BS_CORE_EXPORT LightProbeVolumeRTTI : public RTTIType <LightProbeVolume, IReflectable, LightProbeVolumeRTTI>
 	{
 	private:
@@ -20,10 +89,61 @@ namespace bs
 			BS_RTTI_MEMBER_PLAIN(mPosition, 0)
 			BS_RTTI_MEMBER_PLAIN(mRotation, 1)
 		BS_END_RTTI_MEMBERS
+
+		SavedLightProbeInfo& getProbeInfo(LightProbeVolume* obj)
+		{
+			obj->updateCoefficients();
+
+			UINT32 numProbes = (UINT32)obj->mProbes.size();
+			SavedLightProbeInfo savedLightProbeInfo;
+			savedLightProbeInfo.coefficients.resize(numProbes);
+			savedLightProbeInfo.positions.resize(numProbes);
+
+			UINT32 idx = 0;
+			for(auto& entry : obj->mProbes)
+			{
+				savedLightProbeInfo.positions[idx] = entry.second.position;
+				savedLightProbeInfo.coefficients[idx] = entry.second.coefficients;
+
+				idx++;
+			}
+
+			obj->mRTTIData = savedLightProbeInfo;
+			return any_cast_ref<SavedLightProbeInfo>(obj->mRTTIData);
+		}
+
+		void setProbeInfo(LightProbeVolume* obj, SavedLightProbeInfo& data)
+		{
+			obj->mProbes.clear();
+
+			UINT32 numProbes = (UINT32)data.positions.size();
+			for(UINT32 i = 0; i < numProbes; ++i)
+			{
+				UINT32 handle = obj->mNextProbeId++;
+
+				LightProbeVolume::ProbeInfo probeInfo;
+				probeInfo.flags = LightProbeFlags::Clean;
+				probeInfo.position = data.positions[i];
+				probeInfo.coefficients = data.coefficients[i];
+
+				obj->mProbes[handle] = probeInfo;
+			}
+		}
 	public:
 		LightProbeVolumeRTTI()
 			:mInitMembers(this)
-		{ }
+		{
+			
+			addPlainField("mProbeInfo", 2, &LightProbeVolumeRTTI::getProbeInfo, &LightProbeVolumeRTTI::setProbeInfo, 
+				RTTI_Flag_SkipInReferenceSearch);
+		}
+
+		void onSerializationEnded(IReflectable* obj, const UnorderedMap<String, UINT64>& params) override
+		{
+			// Clear temporary data
+			LightProbeVolume* volume = static_cast<LightProbeVolume*>(obj);
+			volume->mRTTIData = nullptr;
+		}
 
 		void onDeserializationEnded(IReflectable* obj, const UnorderedMap<String, UINT64>& params) override
 		{
@@ -52,4 +172,4 @@ namespace bs
 
 	/** @} */
 	/** @endcond */
-}
+}

+ 2 - 0
Source/BansheeCore/Include/BsReflectionProbe.h

@@ -153,6 +153,8 @@ namespace bs
 	class BS_CORE_EXPORT ReflectionProbe : public IReflectable, public CoreObject, public ReflectionProbeBase
 	{
 	public:
+		~ReflectionProbe();
+
 		/** 
 		 * Allows you assign a custom texture to use as a reflection map. This will disable automatic generation of
 		 * reflections. To re-enable auto-generation call this with a null parameter.

+ 1 - 1
Source/BansheeCore/Include/BsRenderer.h

@@ -159,7 +159,7 @@ namespace bs
 		 *
 		 * @note	Core thread.
 		 */
-		virtual void notifyLightProbeVolumeUpdated(LightProbeVolume* volume) { }
+		virtual void notifyLightProbeVolumeUpdated(LightProbeVolume* volume, bool coefficientsUpdated) { }
 
 		/**
 		 * Called whenever a light probe volume is destroyed.

+ 2 - 0
Source/BansheeCore/Include/BsSkybox.h

@@ -95,6 +95,8 @@ namespace bs
 	class BS_CORE_EXPORT Skybox : public IReflectable, public CoreObject, public TSkybox<false>
 	{
 	public:
+		~Skybox();
+		
 		/**	Retrieves an implementation of the skybox usable only from the core thread. */
 		SPtr<ct::Skybox> getCore() const;
 

+ 238 - 67
Source/BansheeCore/Source/BsLightProbeVolume.cpp

@@ -5,7 +5,9 @@
 #include "BsFrameAlloc.h"
 #include "BsRenderer.h"
 #include "BsLight.h"
-#include <atlalloc.h>
+#include "BsGpuBuffer.h"
+#include "BsTexture.h"
+#include "BsIBLUtility.h"
 
 namespace bs
 {
@@ -21,10 +23,16 @@ namespace bs
 		// TODO - Generates probes in the grid volume
 	}
 
+	LightProbeVolume::~LightProbeVolume()
+	{
+		if (mRendererTask)
+			mRendererTask->cancel();
+	}
+
 	UINT32 LightProbeVolume::addProbe(const Vector3& position)
 	{
 		UINT32 handle = mNextProbeId++;
-		mProbes[handle] = ProbeInfo(LightProbeFlags::Dirty, position);
+		mProbes[handle] = ProbeInfo(LightProbeFlags::Clean, position);
 
 		_markCoreDirty();
 		return handle;
@@ -45,7 +53,6 @@ namespace bs
 		auto iterFind = mProbes.find(handle);
 		if (iterFind != mProbes.end())
 		{
-			iterFind->second.flags = LightProbeFlags::Dirty;
 			iterFind->second.position = position;
 			_markCoreDirty();
 		}
@@ -60,6 +67,94 @@ namespace bs
 		return Vector3::ZERO;
 	}
 
+	void LightProbeVolume::renderProbe(UINT32 handle)
+	{
+		auto iterFind = mProbes.find(handle);
+		if (iterFind != mProbes.end())
+		{
+			if (iterFind->second.flags == LightProbeFlags::Clean)
+			{
+				iterFind->second.flags = LightProbeFlags::Dirty;
+
+				_markCoreDirty();
+				runRenderProbeTask();
+			}
+		}
+	}
+
+	void LightProbeVolume::renderProbes()
+	{
+		bool anyModified = false;
+		for(auto& entry : mProbes)
+		{
+			if (entry.second.flags == LightProbeFlags::Clean)
+			{
+				entry.second.flags = LightProbeFlags::Dirty;
+				anyModified = true;
+			}
+		}
+
+		if (anyModified)
+		{
+			_markCoreDirty();
+			runRenderProbeTask();
+		}
+	}
+
+	void LightProbeVolume::runRenderProbeTask()
+	{
+		// If a task is already running cancel it
+		// Note: If the task is just about to start processing, cancelling it will skip the update this frame
+		// (which might be fine if we just changed positions of dirty probes it was about to update, but it might also
+		// waste a frame if those positions needed to be updated anyway). For now I'm ignoring it as it seems like a rare
+		// enough situation, plus it's one that will only happen during development time.
+		if (mRendererTask)
+			mRendererTask->cancel();
+
+		auto renderComplete = [this]()
+		{
+			mRendererTask = nullptr;
+		};
+
+		SPtr<ct::LightProbeVolume> coreProbeVolume = getCore();
+		auto renderProbes = [coreProbeVolume]()
+		{
+			return coreProbeVolume->renderProbes(3);
+		};
+
+		mRendererTask = ct::RendererTask::create("RenderLightProbes", renderProbes);
+
+		mRendererTask->onComplete.connect(renderComplete);
+		ct::gRenderer()->addTask(mRendererTask);
+	}
+
+	void LightProbeVolume::updateCoefficients()
+	{
+		// Ensure all light probe coefficients are generated
+		if (mRendererTask)
+			mRendererTask->wait();
+
+		ct::LightProbeVolume* coreVolume = getCore().get();
+
+		Vector<LightProbeCoefficientInfo> coeffInfo;
+		auto getSaveData = [coreVolume, &coeffInfo]()
+		{
+			coreVolume->getProbeCoefficients(coeffInfo);
+		};
+
+		gCoreThread().queueCommand(getSaveData);
+		gCoreThread().submit(true);
+
+		for(auto& entry : coeffInfo)
+		{
+			auto iterFind = mProbes.find(entry.handle);
+			if (iterFind == mProbes.end())
+				continue;
+
+			iterFind->second.coefficients = entry.coefficients;
+		}
+	}
+
 	SPtr<ct::LightProbeVolume> LightProbeVolume::getCore() const
 	{
 		return std::static_pointer_cast<ct::LightProbeVolume>(mCoreSpecific);
@@ -116,6 +211,9 @@ namespace bs
 				}
 			}
 
+			for (auto& probe : removedProbes)
+				mProbes.erase(probe);
+
 			UINT32 numDirtyProbes = (UINT32)dirtyProbes.size();
 			UINT32 numRemovedProbes = (UINT32)removedProbes.size();
 
@@ -170,6 +268,8 @@ namespace bs
 	{
 	LightProbeVolume::LightProbeVolume(const UnorderedMap<UINT32, bs::LightProbeVolume::ProbeInfo>& probes)
 	{
+		mInitCoefficients.resize(probes.size());
+
 		UINT32 probeIdx = 0;
 		for(auto& entry : probes)
 		{
@@ -178,10 +278,12 @@ namespace bs
 			
 			LightProbeInfo probeInfo;
 			probeInfo.flags = LightProbeFlags::Dirty;
-			probeInfo.bufferIdx = -1;
-			probeInfo.handle = probeIdx;
+			probeInfo.bufferIdx = probeIdx;
+			probeInfo.handle = entry.first;
 
 			mProbeInfos[probeIdx] = probeInfo;
+			mInitCoefficients[probeIdx] = entry.second.coefficients;
+
 			probeIdx++;
 		}
 	}
@@ -193,73 +295,55 @@ namespace bs
 
 	void LightProbeVolume::initialize()
 	{
-		gRenderer()->notifyLightProbeVolumeAdded(this);
+		// Set SH coefficients loaded from the file
+		UINT32 numCoefficients = (UINT32)mInitCoefficients.size();
+		assert(mInitCoefficients.size() == mProbeMap.size());
+
+		resizeCoefficientBuffer(std::max(32U, numCoefficients));
+		mCoefficients->writeData(0, sizeof(LightProbeSHCoefficients) * numCoefficients, mInitCoefficients.data());
+		mInitCoefficients.clear();
 
+		gRenderer()->notifyLightProbeVolumeAdded(this);
 		CoreObject::initialize();
 	}
 
-	void LightProbeVolume::prune(Vector<UINT32>& freedEntries, bool freeAll)
+	bool LightProbeVolume::renderProbes(UINT32 maxProbes)
 	{
-		UINT32 numProbes = (UINT32)mProbeInfos.size();
-		INT32 lastSearchIdx = numProbes - 1;
-		
-		for (UINT32 i = 0; i < (UINT32)mProbeInfos.size(); ++i)
+		// Probe map only contains active probes
+		UINT32 numUsedProbes = (UINT32)mProbeMap.size();
+		if(numUsedProbes > mCoeffBufferSize)
+			resizeCoefficientBuffer(std::max(32U, numUsedProbes * 2));
+
+		UINT32 numProbeUpdates = 0;
+		for (; mFirstDirtyProbe < (UINT32)mProbeInfos.size(); ++mFirstDirtyProbe)
 		{
-			LightProbeInfo& info = mProbeInfos[i];
+			LightProbeInfo& probeInfo = mProbeInfos[mFirstDirtyProbe];
 
-			if (info.flags == LightProbeFlags::Removed)
+			if(probeInfo.flags == LightProbeFlags::Dirty)
 			{
-				if (info.bufferIdx != -1)
-					freedEntries.push_back(info.bufferIdx);
-
-				info.flags = LightProbeFlags::Empty;
-
-				// Replace the empty spot with an element from the back
-				while (lastSearchIdx >= (INT32)i)
-				{
-					bool foundNonEmpty = false;
-					LightProbeFlags flags = mProbeInfos[lastSearchIdx].flags;
-					if (flags != LightProbeFlags::Empty)
-					{
-						std::swap(mProbeInfos[i], mProbeInfos[lastSearchIdx]);
-						std::swap(mProbePositions[i], mProbePositions[lastSearchIdx]);
+				TEXTURE_DESC cubemapDesc;
+				cubemapDesc.type = TEX_TYPE_CUBE_MAP;
+				cubemapDesc.format = PF_FLOAT16_RGB;
+				cubemapDesc.width = IBLUtility::IRRADIANCE_CUBEMAP_SIZE;
+				cubemapDesc.height = IBLUtility::IRRADIANCE_CUBEMAP_SIZE;
+				cubemapDesc.usage = TU_STATIC | TU_RENDERTARGET;
 
-						mProbeMap[mProbeInfos[lastSearchIdx].handle] = i;
-						foundNonEmpty = true;
-					}
+				SPtr<Texture> cubemap = Texture::create(cubemapDesc);
 
-					// Remove last element
-					mProbeInfos.erase(mProbeInfos.begin() + lastSearchIdx);
-					mProbePositions.erase(mProbePositions.begin() + lastSearchIdx);
-					lastSearchIdx--;
+				gRenderer()->captureSceneCubeMap(cubemap, mProbePositions[mFirstDirtyProbe], true);
+				gIBLUtility().filterCubemapForIrradiance(cubemap, mCoefficients, probeInfo.bufferIdx);
 
-					// Search is done, we found an element to fill the empty spot
-					if (foundNonEmpty)
-						break;
-				}
+				probeInfo.flags = LightProbeFlags::Clean;
+				numProbeUpdates++;
 			}
-		}
 
-		if(freeAll)
-		{
-			// Add all remaining (non-removed) probes to the free list, and mark them as dirty so when/if those probes
-			// get used again, the systems knows they are out of date
-			for (UINT32 i = 0; i < (UINT32)mProbeInfos.size(); ++i)
-			{
-				LightProbeInfo& info = mProbeInfos[i];
+			if (maxProbes != 0 && numProbeUpdates >= maxProbes)
+				break;
+		}
 
-				if (info.flags != LightProbeFlags::Empty)
-				{
-					if (info.bufferIdx != -1)
-					{
-						freedEntries.push_back(info.bufferIdx);
-						info.bufferIdx = -1;
-					}
+		gRenderer()->notifyLightProbeVolumeUpdated(this, true);
 
-					info.flags = LightProbeFlags::Dirty;
-				}
-			}
-		}
+		return mFirstDirtyProbe == (UINT32)mProbeInfos.size();
 	}
 
 	void LightProbeVolume::syncToCore(const CoreSyncData& data)
@@ -290,27 +374,59 @@ namespace bs
 			auto iterFind = mProbeMap.find(handle);
 			if(iterFind != mProbeMap.end())
 			{
+				// Update existing probe information
 				UINT32 compactIdx = iterFind->second;
 				
 				mProbeInfos[compactIdx].flags = LightProbeFlags::Dirty;
 				mProbePositions[compactIdx] = position;
+
+				mFirstDirtyProbe = std::min(compactIdx, mFirstDirtyProbe);
 			}
-			else
+			else // Add a new probe
 			{
-				UINT32 compactIdx = (UINT32)mProbeInfos.size();
+				// Empty slots always start at a specific index because we always move them to the back of the array
+				UINT32 emptyProbeStartIdx = (UINT32)mProbeMap.size();
+				UINT32 numProbes = (UINT32)mProbeInfos.size();
 
-				LightProbeInfo info;
-				info.flags = LightProbeFlags::Dirty;
-				info.bufferIdx = -1;
-				info.handle = handle;
+				// Find an empty slot to place the probe information at
+				UINT32 compactIdx = -1;
+				for(UINT32 j = emptyProbeStartIdx; j < numProbes; ++j)
+				{
+					if(mProbeInfos[j].flags == LightProbeFlags::Empty)
+					{
+						compactIdx = j;
+						break;
+					}
+				}
+
+				// Found an empty slot
+				if (compactIdx == -1)
+				{
+					compactIdx = (UINT32)mProbeInfos.size();
+
+					LightProbeInfo info;
+					info.flags = LightProbeFlags::Dirty;
+					info.bufferIdx = compactIdx;
+					info.handle = handle;
+
+					mProbeInfos.push_back(info);
+					mProbePositions.push_back(position);
+				}
+				else // No empty slot, add a new one
+				{
+					LightProbeInfo& info = mProbeInfos[compactIdx];
+					info.flags = LightProbeFlags::Dirty;
+					info.handle = handle;
 
-				mProbeInfos.push_back(info);
-				mProbePositions.push_back(position);
+					mProbePositions[compactIdx] = position;
+				}
 
 				mProbeMap[handle] = compactIdx;
+				mFirstDirtyProbe = std::min(compactIdx, mFirstDirtyProbe);
 			}
 		}
 
+		// Mark slots for removed probes as empty, and move them back to the end of the array
 		for (UINT32 i = 0; i < numRemovedProbes; ++i)
 		{
 			UINT32 idx;
@@ -322,7 +438,25 @@ namespace bs
 				UINT32 compactIdx = iterFind->second;
 				
 				LightProbeInfo& info = mProbeInfos[compactIdx];
-				info.flags = LightProbeFlags::Removed;
+				info.flags = LightProbeFlags::Empty;
+
+				// Move the empty info to the back of the array so all non-empty probes are contiguous
+				// Search from back to current index, and find first non-empty probe to switch switch
+				UINT32 lastSearchIdx = (UINT32)mProbeInfos.size() - 1;
+				while (lastSearchIdx >= (INT32)compactIdx)
+				{
+					LightProbeFlags flags = mProbeInfos[lastSearchIdx].flags;
+					if (flags != LightProbeFlags::Empty)
+					{
+						std::swap(mProbeInfos[i], mProbeInfos[lastSearchIdx]);
+						std::swap(mProbePositions[i], mProbePositions[lastSearchIdx]);
+
+						mProbeMap[mProbeInfos[lastSearchIdx].handle] = i;
+						break;
+					}
+
+					lastSearchIdx--;
+				}
 				
 				mProbeMap.erase(iterFind);
 			}
@@ -338,7 +472,44 @@ namespace bs
 		else
 		{
 			if(mIsActive)
-				gRenderer()->notifyLightProbeVolumeUpdated(this);
+				gRenderer()->notifyLightProbeVolumeUpdated(this, false);
 		}
 	}
+
+	void LightProbeVolume::getProbeCoefficients(Vector<LightProbeCoefficientInfo>& output) const
+	{
+		UINT32 numActiveProbes = (UINT32)mProbeMap.size();
+		if (numActiveProbes == 0)
+			return;
+
+		output.resize(numActiveProbes);
+
+		LightProbeSHCoefficients* coefficients = bs_stack_alloc<LightProbeSHCoefficients>(numActiveProbes);
+		mCoefficients->readData(0, sizeof(LightProbeSHCoefficients) * numActiveProbes, coefficients);
+
+		for(UINT32 i = 0; i < numActiveProbes; ++i)
+		{
+			output[i].coefficients = coefficients[mProbeInfos[i].bufferIdx];
+			output[i].handle = mProbeInfos[i].handle;
+		}
+
+		bs_stack_free(coefficients);
+	}
+
+	void LightProbeVolume::resizeCoefficientBuffer(UINT32 count)
+	{
+		GPU_BUFFER_DESC desc;
+		desc.type = GBT_STRUCTURED;
+		desc.elementSize = sizeof(LightProbeSHCoefficients);
+		desc.elementCount = count;
+		desc.usage = GBU_STATIC;
+		desc.format = BF_UNKNOWN;
+
+		SPtr<GpuBuffer> newBuffer = GpuBuffer::create(desc);
+		if (mCoefficients)
+			newBuffer->copyData(*mCoefficients, 0, 0, mCoefficients->getSize(), true);
+
+		mCoefficients = newBuffer;
+		mCoeffBufferSize = count;
+	}
 }}

+ 6 - 0
Source/BansheeCore/Source/BsReflectionProbe.cpp

@@ -47,6 +47,12 @@ namespace bs
 		updateBounds();
 	}
 
+	ReflectionProbe::~ReflectionProbe()
+	{
+		if (mRendererTask)
+			mRendererTask->cancel();
+	}
+
 	void ReflectionProbe::capture()
 	{
 		if (mCustomTexture != nullptr)

+ 6 - 0
Source/BansheeCore/Source/BsSkybox.cpp

@@ -34,6 +34,12 @@ namespace bs
 		}
 	}
 
+	Skybox::~Skybox()
+	{
+		if (mRendererTask != nullptr)
+			mRendererTask->cancel();
+	}
+
 	void Skybox::filterTexture()
 	{
 		// If previous rendering task exists, cancel it

+ 2 - 19
Source/RenderBeast/Include/BsLightProbes.h

@@ -27,8 +27,6 @@ namespace bs { namespace ct
 			SPtr<LightProbeVolume> volume;
 			/** Remains true as long as there are dirty probes in the volume. */
 			bool isDirty;
-			/** Keeps track of which dirty probe was last updated, so we can perform the update over multiple frames. */
-			UINT32 lastUpdatedProbe; 
 		};
 
 		/** 
@@ -53,22 +51,8 @@ namespace bs { namespace ct
 		/** Notifies the manager that all the probes in the provided volume have been removed. */
 		void notifyRemoved(const SPtr<LightProbeVolume>& volume);
 
-		/**
-		 * Updates any dirty light probes by rendering the scene from their perspective and generating their SH 
-		 * coefficients.
-		 *
-		 * @param[in]	frameInfo		Information about the current frame.
-		 * @param[in]	maxProbes		Places a limit of how many probes can be updated in a single call to this method.
-		 *								Any probes that weren't updated will be updated when the method is called next 
-		 *								(up to the @p maxProbes limit), as so on.
-		 *								 
-		 *								This limit is provided to ensure there are no massive framerate spikes caused up
-		 *								updating many probes in a single frame - instead this method allows the updates to
-		 *								be distributed over multiple frames. 
-		 *								
-		 *								Provide a limit of 0 to force all probes to be updated.
-		 */
-		void updateProbes(const FrameInfo& frameInfo, UINT32 maxProbes = 3);
+		/** Updates light probe tetrahedron data after probes changed (added/removed/moved). */
+		void updateProbes();
 
 		/** Generates GPU buffers that contain a list of probe tetrahedrons visible from the provided view. */
 		void updateVisibleProbes(const RendererView& view, VisibleLightProbeData& output);
@@ -96,7 +80,6 @@ namespace bs { namespace ct
 
 		UINT32 mNumAllocatedEntries;
 		UINT32 mNumUsedEntries;
-		Vector<UINT32> mEmptyEntries;
 
 		Vector<AABox> mTetrahedronBounds;
 		Vector<TetrahedronData> mTetrahedronInfos;

+ 1 - 1
Source/RenderBeast/Include/BsRenderBeast.h

@@ -113,7 +113,7 @@ namespace bs
 		void notifyLightProbeVolumeAdded(LightProbeVolume* volume) override;
 
 		/** @copydoc Renderer::notifyLightProbeVolumeUpdated */
-		void notifyLightProbeVolumeUpdated(LightProbeVolume* volume) override;
+		void notifyLightProbeVolumeUpdated(LightProbeVolume* volume, bool coefficientsUpdated) override;
 
 		/** @copydoc Renderer::notifyLightProbeVolumeRemoved */
 		void notifyLightProbeVolumeRemoved(LightProbeVolume* volume) override;

+ 2 - 67
Source/RenderBeast/Source/BsLightProbes.cpp

@@ -5,7 +5,6 @@
 #include "BsGpuBuffer.h"
 #include "BsRendererView.h"
 #include "BsRenderBeastIBLUtility.h"
-#include "BsRendererManager.h"
 #include "BsRenderBeast.h"
 
 namespace bs { namespace ct 
@@ -23,7 +22,6 @@ namespace bs { namespace ct
 		VolumeInfo info;
 		info.volume = volume;
 		info.isDirty = true;
-		info.lastUpdatedProbe = 0;
 
 		mVolumes.push_back(info);
 		volume->setRendererId(handle);
@@ -33,19 +31,14 @@ namespace bs { namespace ct
 
 	void LightProbes::notifyDirty(const SPtr<LightProbeVolume>& volume)
 	{
-		volume->prune(mEmptyEntries);
-		
 		UINT32 handle = volume->getRendererId();
 		mVolumes[handle].isDirty = true;
-		mVolumes[handle].lastUpdatedProbe = 0;
 
 		mTetrahedronVolumeDirty = true;
 	}
 
 	void LightProbes::notifyRemoved(const SPtr<LightProbeVolume>& volume)
 	{
-		volume->prune(mEmptyEntries, true);
-
 		UINT32 handle = volume->getRendererId();
 
 		LightProbeVolume* lastVolume = mVolumes.back().volume.get();
@@ -64,7 +57,7 @@ namespace bs { namespace ct
 		mTetrahedronVolumeDirty = true;
 	}
 
-	void LightProbes::updateProbes(const FrameInfo& frameInfo, UINT32 maxProbes)
+	void LightProbes::updateProbes()
 	{
 		if(mTetrahedronVolumeDirty)
 		{
@@ -104,64 +97,6 @@ namespace bs { namespace ct
 			mTempTetrahedronPositions.clear();
 			mTetrahedronVolumeDirty = false;
 		}
-
-		// Render dirty probes
-		UINT32 numProbeUpdates = 0;
-		for(auto& entry : mVolumes)
-		{
-			if (!entry.isDirty)
-				continue;
-
-			Vector<LightProbeInfo>& probes = entry.volume->getLightProbeInfos();
-			const Vector<Vector3>& probePositions = entry.volume->getLightProbePositions();
-			for (; entry.lastUpdatedProbe < (UINT32)probes.size(); ++entry.lastUpdatedProbe)
-			{
-				LightProbeInfo& probeInfo = probes[entry.lastUpdatedProbe];
-
-				// Assign buffer idx, if not assigned
-				if(probeInfo.bufferIdx == -1)
-				{
-					if(!mEmptyEntries.empty())
-					{
-						probeInfo.bufferIdx = mEmptyEntries.back();
-						mEmptyEntries.erase(mEmptyEntries.end() - 1);
-					}
-					else
-					{
-						if(mNumUsedEntries >= mNumAllocatedEntries)
-							resizeCoefficientBuffer(mNumAllocatedEntries * 2);
-
-						probeInfo.bufferIdx = mNumUsedEntries++;
-					}
-				}
-
-				if(probeInfo.flags == LightProbeFlags::Dirty)
-				{
-					TEXTURE_DESC cubemapDesc;
-					cubemapDesc.type = TEX_TYPE_CUBE_MAP;
-					cubemapDesc.format = PF_FLOAT16_RGB;
-					cubemapDesc.width = IBLUtility::IRRADIANCE_CUBEMAP_SIZE;
-					cubemapDesc.height = IBLUtility::REFLECTION_CUBEMAP_SIZE;
-					cubemapDesc.usage = TU_STATIC | TU_RENDERTARGET;
-
-					SPtr<Texture> cubemap = Texture::create(cubemapDesc);
-
-					RenderBeast& renderer = static_cast<RenderBeast&>(*RendererManager::instance().getActive());
-					renderer.captureSceneCubeMap(cubemap, probePositions[entry.lastUpdatedProbe], true);
-
-					gIBLUtility().filterCubemapForIrradiance(cubemap, mProbeCoefficientsGPU, probeInfo.bufferIdx);
-
-					probeInfo.flags = LightProbeFlags::Clean;
-					numProbeUpdates++;
-				}
-
-				if (maxProbes != 0 && numProbeUpdates >= numProbeUpdates)
-					break;
-			}
-
-			if (entry.lastUpdatedProbe == (UINT32)probes.size())
-				entry.isDirty = false;
-		}
 	}
 
 	void LightProbes::resizeTetrahedronBuffers(VisibleLightProbeData& data, UINT32 count)
@@ -311,7 +246,7 @@ namespace bs { namespace ct
 
 			output.reserve(includeOuterFaces);
 
-			// Insert innert tetrahedrons, generate matrices
+			// Insert inner tetrahedrons, generate matrices
 			for(UINT32 i = 0; i < (UINT32)volume.tetrahedra.size(); ++i)
 			{
 				TetrahedronData entry;

+ 1 - 1
Source/RenderBeast/Source/BsRenderBeast.cpp

@@ -190,7 +190,7 @@ namespace bs { namespace ct
 		assert(false); // TODO
 	}
 
-	void RenderBeast::notifyLightProbeVolumeUpdated(LightProbeVolume* volume)
+	void RenderBeast::notifyLightProbeVolumeUpdated(LightProbeVolume* volume, bool coefficientsUpdated)
 	{
 		assert(false); // TODO
 	}