Browse Source

Added creation of relevant buffers and mesh for light probe tetrahedron rendering

BearishSun 8 years ago
parent
commit
5fb9e312d6

+ 14 - 2
Source/BansheeCore/Include/BsLightProbeVolume.h

@@ -220,11 +220,23 @@ namespace bs
 		/**	Retrieves an ID that can be used for uniquely identifying this object by the renderer. */
 		UINT32 getRendererId() const { return mRendererId; }
 
-		/** Returns a list of positions for all light probes. */
-		Vector<Vector3>& getLightProbePositions() { return mProbePositions; }
+		/** Returns the number of light probes that are active. */
+		UINT32 getNumActiveProbes() const { return (UINT32)mProbeMap.size(); }
+
+		/** Returns a list of positions for all light probes. Only the first getNumActiveProbes() entries are active. */
+		const Vector<Vector3>& getLightProbePositions() const { return mProbePositions; }
+
+		/** 
+		 * Returns non-positional information about all light probes. Only the first getNumActiveProbes() entries are 
+		 * active. 
+		 */
+		const Vector<LightProbeInfo>& getLightProbeInfos() const { return mProbeInfos; }
 
 		/** Populates the vector with SH coefficients for each light probe. Involves reading the GPU buffer. */
 		void getProbeCoefficients(Vector<LightProbeCoefficientInfo>& output) const;
+
+		/** Returns the GPU buffer containing SH coefficients. */
+		SPtr<GpuBuffer> getCoefficientsBuffer() const { return mCoefficients; }
 	protected:
 		friend class bs::LightProbeVolume;
 

+ 2 - 3
Source/BansheeCore/Include/BsPixelData.h

@@ -80,13 +80,12 @@ namespace bs
 		/** Depth format, 16bits. */
 		PF_D16					BS_SCRIPT_EXPORT(n:D16) = 32,
 		/** 
-		 * 32-bit float format, 11 bits (float) for red, 11 bits (float) for green, 10 bits (float) for blue. Framebuffer 
-		 * only format, not for CPU use. 
+		 * 32-bit float format, 11 bits (float) for red, 11 bits (float) for green, 10 bits (float) for blue.
 		 */
 		PF_FLOAT_R11G11B10		BS_SCRIPT_EXPORT(ex:true) = 33,
 		/** 
 		 * 32-bit unsigned normalized format, 10 bits (float) for red, 10 bits (float) for green, 10 bits (float) for blue, 
-		 * and two bits for alpha. Framebuffer only format, not for CPU use.
+		 * and two bits for alpha.
 		 */
 		PF_UNORM_R10G10B10A2	BS_SCRIPT_EXPORT(ex:true) = 34,
 		/** Number of pixel formats currently defined. */

+ 16 - 28
Source/RenderBeast/Include/BsLightProbes.h

@@ -11,7 +11,6 @@ namespace bs { namespace ct
 {
 	struct FrameInfo;
 	class LightProbeVolume;
-	struct VisibleLightProbeData;
 
 	/** @addtogroup RenderBeast
 	 *  @{
@@ -54,21 +53,25 @@ namespace bs { namespace ct
 		/** 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);
-
 	private:
 		/**
 		 * Perform tetrahedrization of the provided point list, and outputs a list of tetrahedrons and outer faces of the
 		 * volume. Each entry contains connections to nearby tetrahedrons/faces, as well as a matrix that can be used for
 		 * calculating barycentric coordinates within the tetrahedron (or projected triangle barycentric coordinates for
 		 * faces). 
+		 * 
+		 * @param[in,out]	positions					A set of positions to generate the tetrahedra from. If 
+		 *												@p generateExtrapolationVolume is enabled then this array will be
+		 *												appended with new vertices forming that volume.
+		 * @param[out]		output						A list of generated tetrahedra and relevant data.
+		 * @param[in]		generateExtrapolationVolume	If true, the tetrahedron volume will be surrounded with points
+		 *												at "infinity" (technically just far away).
 		 */
-		void generateTetrahedronData(const Vector<Vector3>& positions, Vector<TetrahedronData>& output, 
-			bool includeOuterFaces = false);
+		void generateTetrahedronData(Vector<Vector3>& positions, Vector<TetrahedronData>& output, 
+			bool generateExtrapolationVolume = false);
 
-		/** Resizes the GPU buffers used for holding tetrahedron data, to the specified size (in number of tetraheda). */
-		void resizeTetrahedronBuffers(VisibleLightProbeData& data, UINT32 count);
+		/** Resizes the GPU buffer used for holding tetrahedron data, to the specified size (in number of tetraheda). */
+		void resizeTetrahedronBuffer(UINT32 count);
 
 		/** 
 		 * Resized the GPU buffer that stores light probe SH coefficients, to the specified size (in the number of probes). 
@@ -78,17 +81,18 @@ namespace bs { namespace ct
 		Vector<VolumeInfo> mVolumes;
 		bool mTetrahedronVolumeDirty;
 
-		UINT32 mNumAllocatedEntries;
-		UINT32 mNumUsedEntries;
+		UINT32 mMaxCoefficients;
+		UINT32 mMaxTetrahedra;
 
-		Vector<AABox> mTetrahedronBounds;
 		Vector<TetrahedronData> mTetrahedronInfos;
 
 		SPtr<GpuBuffer> mProbeCoefficientsGPU;
+		SPtr<GpuBuffer> mTetrahedronInfosGPU;
+		SPtr<Mesh> mVolumeMesh;
 
 		// Temporary buffers
 		Vector<Vector3> mTempTetrahedronPositions;
-		Vector<UINT32> mTempTetrahedronVisibility;
+		Vector<UINT32> mTempTetrahedronBufferIndices;
 	};
 
 	/** Storage of tetrahedron AA box, for use on the GPU. */
@@ -105,21 +109,5 @@ namespace bs { namespace ct
 		Matrix3x4 transform;
 	};
 
-	/** Contains information about light probes visible from a particular RendererView. */
-	struct VisibleLightProbeData
-	{
-		/** Current number of visible tetrahedrons in the GPU buffers. */
-		UINT32 numEntries;
-
-		/** Maximum number of tetrahedrons that fit in the GPU buffers, before the buffers need to be resized. */
-		UINT32 maxNumEntries;
-		
-		/** GPU buffer containing tetrahedron bounds in form of TetrahedronBoundsGPU structure. */
-		SPtr<GpuBuffer> tetrahedronBounds;
-
-		/** GPU buffer containing tetrahedron information in form of TetrahedronDataGPU structure. */
-		SPtr<GpuBuffer> tetrahedronInfos;
-	};
-
 	/** @} */
 }}

+ 312 - 363
Source/RenderBeast/Source/BsLightProbes.cpp

@@ -6,11 +6,13 @@
 #include "BsRendererView.h"
 #include "BsRenderBeastIBLUtility.h"
 #include "BsRenderBeast.h"
+#include "BsMesh.h"
+#include "BsVertexDataDesc.h"
 
 namespace bs { namespace ct 
 {
 	LightProbes::LightProbes()
-		:mTetrahedronVolumeDirty(false), mNumAllocatedEntries(0), mNumUsedEntries(0)
+		:mTetrahedronVolumeDirty(false), mMaxCoefficients(0), mMaxTetrahedra(0)
 	{
 		resizeCoefficientBuffer(512);
 	}
@@ -61,162 +63,187 @@ namespace bs { namespace ct
 	{
 		if(mTetrahedronVolumeDirty)
 		{
+			// Move all coefficients into the global buffer
+			UINT32 numCoeffs = 0;
+			for(auto& entry : mVolumes)
+			{
+				UINT32 numProbes = (UINT32)entry.volume->getLightProbePositions().size();
+				numCoeffs += numProbes;
+			}
+
+			if(numCoeffs > mMaxCoefficients)
+			{
+				UINT32 newSize = Math::divideAndRoundUp(numCoeffs, 32U) * 32U;
+				resizeCoefficientBuffer(newSize);
+			}
+
+			UINT8* dest = (UINT8*)mProbeCoefficientsGPU->lock(0, mProbeCoefficientsGPU->getSize(), GBL_WRITE_ONLY_DISCARD);
+			for(auto& entry : mVolumes)
+			{
+				UINT32 numProbes = (UINT32)entry.volume->getLightProbePositions().size();
+				SPtr<GpuBuffer> localBuffer = entry.volume->getCoefficientsBuffer();
+				
+				// Note: Some of the coefficients might still be dirty (unrendered). Check for this and write them as black?
+				UINT32 size = numProbes * sizeof(LightProbeSHCoefficients);
+				UINT8* src = (UINT8*)localBuffer->lock(0, size, GBL_READ_ONLY);
+				memcpy(dest, src, size);
+
+				localBuffer->unlock();
+				dest += size;
+			}
+			mProbeCoefficientsGPU->unlock();
+
 			// Gather all positions
+			UINT32 bufferOffset = 0;
 			for(auto& entry : mVolumes)
 			{
+				const Vector<LightProbeInfo>& infos = entry.volume->getLightProbeInfos();
 				const Vector<Vector3>& positions = entry.volume->getLightProbePositions();
+				UINT32 numProbes = entry.volume->getNumActiveProbes();
 				
+				if (numProbes == 0)
+					continue;
+
 				Vector3 offset = entry.volume->getPosition();
 				Quaternion rotation = entry.volume->getRotation();
-				for(auto& localPos : positions)
+				for(UINT32 i = 0; i < numProbes; i++)
 				{
+					Vector3 localPos = positions[i];
 					Vector3 transformedPos = rotation.rotate(localPos) + offset;
 					mTempTetrahedronPositions.push_back(transformedPos);
+					mTempTetrahedronBufferIndices.push_back(bufferOffset + infos[i].bufferIdx);
 				}
+
+				bufferOffset += (UINT32)positions.size();
 			}
 
 			mTetrahedronInfos.clear();
-			mTetrahedronBounds.clear();
 
+			UINT32 innerVertexCount = (UINT32)mTempTetrahedronPositions.size();
 			generateTetrahedronData(mTempTetrahedronPositions, mTetrahedronInfos, false);
 
-			// Generate bounds
-			for(auto& entry : mTetrahedronInfos)
-			{
-				// Skipping outer faces
-				if (entry.volume.neighbors[3] < 0)
-					continue;
+			// Generate a mesh out of all the tetrahedron triangles
+			// Note: Currently the entire volume is rendered as a single large mesh, which will isn't optimal as we can't
+			// perform frustum culling. A better option would be to split the mesh into multiple smaller volumes, do
+			// frustum culling and possibly even sort by distance from camera.
+			UINT32 numTetrahedra = (UINT32)mTetrahedronInfos.size();
 
-				AABox aabox = AABox(Vector3::INF, -Vector3::INF);
-				for (int i = 0; i < 4; ++i)
-					aabox.merge(mTempTetrahedronPositions[entry.volume.neighbors[i]]);
+			UINT32 numVertices = numTetrahedra * 4 * 3;
+			UINT32 numIndices = numTetrahedra * 4 * 3;
 
-				mTetrahedronBounds.push_back(aabox);
-			}
+			SPtr<VertexDataDesc> vertexDesc = bs_shared_ptr_new<VertexDataDesc>();
+			vertexDesc->addVertElem(VET_FLOAT3, VES_POSITION);
+			vertexDesc->addVertElem(VET_UINT1, VES_TEXCOORD);
 
-			mTempTetrahedronPositions.clear();
-			mTetrahedronVolumeDirty = false;
-		}
-	}
+			SPtr<MeshData> meshData = MeshData::create(numVertices, numIndices, vertexDesc);
+			auto posIter = meshData->getVec3DataIter(VES_POSITION);
+			auto idIter = meshData->getDWORDDataIter(VES_TEXCOORD);
 
-	void LightProbes::resizeTetrahedronBuffers(VisibleLightProbeData& data, UINT32 count)
-	{
-		{
-			GPU_BUFFER_DESC desc;
-			desc.type = GBT_STRUCTURED;
-			desc.elementSize = sizeof(TetrahedronBoundsGPU);
-			desc.elementCount = count;
-			desc.usage = GBU_STATIC;
-			desc.format = BF_UNKNOWN;
-
-			SPtr<GpuBuffer> newBuffer = GpuBuffer::create(desc);
-			if (data.tetrahedronBounds)
-				newBuffer->copyData(*data.tetrahedronBounds, 0, 0, data.tetrahedronBounds->getSize(), true);
-
-			data.tetrahedronBounds = newBuffer;
-		}
-
-		{
-			GPU_BUFFER_DESC desc;
-			desc.type = GBT_STRUCTURED;
-			desc.elementSize = sizeof(TetrahedronDataGPU);
-			desc.elementCount = count;
-			desc.usage = GBU_STATIC;
-			desc.format = BF_UNKNOWN;
-
-			SPtr<GpuBuffer> newBuffer = GpuBuffer::create(desc);
-			if (data.tetrahedronInfos)
-				newBuffer->copyData(*data.tetrahedronInfos, 0, 0, data.tetrahedronInfos->getSize(), true);
-
-			data.tetrahedronInfos = newBuffer;
-		}
+			UINT32 tetIdx = 0;
+			for(auto& entry : mTetrahedronInfos)
+			{
+				const Tetrahedron& volume = entry.volume;
 
-		data.maxNumEntries = count;
-	}
+				Vector3 center(BsZero);
+				for(UINT32 i = 0; i < 4; i++)
+					center += mTempTetrahedronPositions[volume.vertices[i]];
 
-	void LightProbes::resizeCoefficientBuffer(UINT32 count)
-	{
-		GPU_BUFFER_DESC desc;
-		desc.type = GBT_STRUCTURED;
-		desc.elementSize = sizeof(SHVector3RGB);
-		desc.elementCount = count;
-		desc.usage = GBU_STATIC;
-		desc.format = BF_UNKNOWN;
+				center /= 4.0f;
 
-		SPtr<GpuBuffer> newBuffer = GpuBuffer::create(desc);
-		if (mProbeCoefficientsGPU)
-			newBuffer->copyData(*mProbeCoefficientsGPU, 0, 0, mProbeCoefficientsGPU->getSize(), true);
+				static const UINT32 Permutations[4][3] = 
+				{
+					{ 0, 1, 2 },
+					{ 0, 1, 3 },
+					{ 0, 2, 3 },
+					{ 1, 2, 3 }
+				};
 
-		mProbeCoefficientsGPU = newBuffer;
-		mNumAllocatedEntries = count;
-	}
+				for(UINT32 i = 0; i < 4; i++)
+				{
+					Vector3 A = mTempTetrahedronPositions[volume.vertices[Permutations[i][0]]];
+					Vector3 B = mTempTetrahedronPositions[volume.vertices[Permutations[i][1]]];
+					Vector3 C = mTempTetrahedronPositions[volume.vertices[Permutations[i][2]]];
 
-	void LightProbes::updateVisibleProbes(const RendererView& view, VisibleLightProbeData& output)
-	{
-		// Ignore all probes past this point
-		static const float MAX_PROBE_DISTANCE = 100.0f;
+					// Make sure the triangle is clockwise
+					Vector3 e0 = A - C;
+					Vector3 e1 = B - C;
 
-		const RendererViewProperties& viewProps = view.getProperties();
-		const ConvexVolume& worldFrustum = viewProps.cullFrustum;
+					Vector3 normal = e0.cross(e1);
+					if (normal.dot(A - center) < 0.0f)
+						std::swap(B, C);
 
-		const float maxProbeDistance2 = MAX_PROBE_DISTANCE * MAX_PROBE_DISTANCE;
-		for (UINT32 i = 0; i < (UINT32)mTetrahedronBounds.size(); i++)
-		{
-			float distance2 = viewProps.viewOrigin.squaredDistance(mTetrahedronBounds[i].getCenter());
-			if (distance2 > maxProbeDistance2)
-				continue;
+					posIter.addValue(A);
+					posIter.addValue(B);
+					posIter.addValue(C);
 
-			if (worldFrustum.intersects(mTetrahedronBounds[i]))
-				mTempTetrahedronVisibility.push_back(i);
-		}
+					idIter.addValue(tetIdx);
+					idIter.addValue(tetIdx);
+					idIter.addValue(tetIdx);
+				}
 
-		UINT32 numVisibleTets = (UINT32)mTempTetrahedronVisibility.size();
-		if (numVisibleTets > output.maxNumEntries)
-		{
-			UINT32 newBufferSize = 256;
-			if(output.maxNumEntries > 0)
-				newBufferSize = Math::divideAndRoundUp(numVisibleTets, output.maxNumEntries) * output.maxNumEntries;
+				tetIdx++;
+			}
 
-			resizeTetrahedronBuffers(output, newBufferSize);
-		}
+			mVolumeMesh = Mesh::create(meshData);
 
-		// Write bounds
-		{
-			TetrahedronBoundsGPU* dst = (TetrahedronBoundsGPU*)output.tetrahedronBounds->lock(0, 
-				output.tetrahedronBounds->getSize(), GBL_WRITE_ONLY_DISCARD);
+			// Map vertices to actual SH coefficient indices, and write GPU buffer with tetrahedron information
+			if (numTetrahedra > mMaxTetrahedra)
+			{
+				UINT32 newSize = Math::divideAndRoundUp(numTetrahedra, 64U) * 64U;
+				resizeTetrahedronBuffer(newSize);
+			}
 
-			for (auto& entry : mTempTetrahedronVisibility)
+			TetrahedronDataGPU* dst = (TetrahedronDataGPU*)mTetrahedronInfosGPU->lock(0, mTetrahedronInfosGPU->getSize(), 
+				GBL_WRITE_ONLY_DISCARD);
+			for (auto& entry : mTetrahedronInfos)
 			{
-				const AABox& aabox = mTetrahedronBounds[entry];
+				for(UINT32 i = 0; i < 4; ++i)
+				{
+					// Check for outer vertices, which have no SH data associated with them
+					if (entry.volume.vertices[i] >= innerVertexCount)
+						entry.volume.vertices[i] = -1;
+					else
+						entry.volume.vertices[i] = mTempTetrahedronBufferIndices[i];
+				}
 
-				dst->center = aabox.getCenter();
-				dst->extents = aabox.getHalfSize();
+				memcpy(dst->indices, entry.volume.vertices, sizeof(UINT32) * 4);
+				memcpy(&dst->transform, &entry.transform, sizeof(float) * 12);
 
 				dst++;
 			}
 
-			output.tetrahedronBounds->unlock();
-		}
-
-		// Write other information
-		{
-			TetrahedronDataGPU* dst = (TetrahedronDataGPU*)output.tetrahedronInfos->lock(0, 
-				output.tetrahedronInfos->getSize(), GBL_WRITE_ONLY_DISCARD);
+			mTetrahedronInfosGPU->unlock();
 
-			for (auto& entry : mTempTetrahedronVisibility)
-			{
-				const TetrahedronData& data = mTetrahedronInfos[entry];
+			mTempTetrahedronPositions.clear();
+			mTempTetrahedronBufferIndices.clear();
+			mTetrahedronVolumeDirty = false;
+		}
+	}
 
-				memcpy(dst->indices, data.volume.vertices, sizeof(UINT32) * 4);
-				memcpy(&dst->transform, &data.transform, sizeof(float) * 12);
+	void LightProbes::resizeTetrahedronBuffer(UINT32 count)
+	{
+		GPU_BUFFER_DESC desc;
+		desc.type = GBT_STRUCTURED;
+		desc.elementSize = sizeof(TetrahedronDataGPU);
+		desc.elementCount = count;
+		desc.usage = GBU_STATIC;
+		desc.format = BF_UNKNOWN;
 
-				dst++;
-			}
+		mTetrahedronInfosGPU = GpuBuffer::create(desc);
+		mMaxTetrahedra = count;
+	}
 
-			output.tetrahedronBounds->unlock();
-		}
+	void LightProbes::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;
 
-		mTempTetrahedronVisibility.clear();
+		mProbeCoefficientsGPU = GpuBuffer::create(desc);
+		mMaxCoefficients = count;
 	}
 
 	/** Hash value generator for std::pair<INT32, INT32>. */
@@ -232,99 +259,24 @@ namespace bs { namespace ct
 		}
 	};
 
-	void LightProbes::generateTetrahedronData(const Vector<Vector3>& positions, Vector<TetrahedronData>& output, 
-		bool includeOuterFaces)
+	void LightProbes::generateTetrahedronData(Vector<Vector3>& positions, Vector<TetrahedronData>& output, 
+		bool generateExtrapolationVolume)
 	{
 		bs_frame_mark();
 		{
 			TetrahedronVolume volume = Triangulation::tetrahedralize(positions);
 
-			// Generate matrices
-			UINT32 numOutputTets = (UINT32)volume.tetrahedra.size();
-			if (includeOuterFaces)
-				numOutputTets += (UINT32)volume.outerFaces.size();
-
-			output.reserve(includeOuterFaces);
-
-			// Insert inner tetrahedrons, generate matrices
-			for(UINT32 i = 0; i < (UINT32)volume.tetrahedra.size(); ++i)
+			if (generateExtrapolationVolume)
 			{
-				TetrahedronData entry;
-				entry.volume = volume.tetrahedra[i];
-
-				// Generate a matrix that can be used for calculating barycentric coordinates
-				// To determine a point within a tetrahedron, using barycentric coordinates, we use:
-				// P = (P1 - P4) * a + (P2 - P4) * b + (P3 - P4) * c + P4
-				//
-				// Where P1, P2, P3, P4 are the corners of the tetrahedron.
-				//
-				// Expanded for each coordinate this is:
-				// x = (x1 - x4) * a + (x2 - x4) * b + (x3 - x4) * c + x4
-				// y = (y1 - y4) * a + (y2 - y4) * b + (y3 - y4) * c + y4
-				// z = (z1 - z4) * a + (z2 - z4) * b + (z3 - z4) * c + z4
-				//
-				// In matrix form this is:
-				//                                      a
-				// P = [P1 - P4, P2 - P4, P3 - P4, P4] [b]
-				//                                      c
-				//                                      1
-				//
-				// Solved for barycentric coordinates:
-				//  a
-				// [b] = Minv * P 
-				//  c
-				//  1
-				//
-				// Where Minv is the inverse of the matrix above.
-
-				const Vector3& P1 = positions[volume.tetrahedra[i].vertices[0]];
-				const Vector3& P2 = positions[volume.tetrahedra[i].vertices[1]];
-				const Vector3& P3 = positions[volume.tetrahedra[i].vertices[2]];
-				const Vector3& P4 = positions[volume.tetrahedra[i].vertices[3]];
-
-				Matrix4 mat;
-				mat.setColumn(0, Vector4(P1 - P4, 0.0f));
-				mat.setColumn(1, Vector4(P2 - P4, 0.0f));
-				mat.setColumn(2, Vector4(P3 - P4, 0.0f));
-				mat.setColumn(3, Vector4(P4, 1.0f));
-
-				entry.transform = mat.inverse();
-
-				output.push_back(entry);
-			}
-
-			if (includeOuterFaces)
-			{
-				// Put outer faces into the Tetrahedron structure, for convenience
-				UINT32 outerFaceOffset = (UINT32)volume.tetrahedra.size();
-				FrameVector<Tetrahedron> outerTetrahedrons;
-
-				outerTetrahedrons.resize(volume.outerFaces.size());
-
-				for (UINT32 i = 0; i < (UINT32)volume.outerFaces.size(); ++i)
-				{
-					Tetrahedron outerTetrahedron;
-					memcpy(outerTetrahedron.vertices, volume.outerFaces[i].vertices, sizeof(INT32) * 3);
-					memset(outerTetrahedron.neighbors, -1, sizeof(INT32) * 3);
-
-					outerTetrahedron.vertices[4] = -1; // Marks the tetrahedron as an outer face
-					outerTetrahedron.neighbors[4] = volume.outerFaces[i].tetrahedron;
-
-					outerTetrahedrons[i] = outerTetrahedron;
-				}
-
-				// Connect boundary tetrahedrons with these new outer tetrahedrons
-				for (UINT32 i = 0; i < (UINT32)volume.outerFaces.size(); ++i)
-				{
-					Tetrahedron& tet = volume.tetrahedra[volume.outerFaces[i].tetrahedron];
-					for (UINT32 j = 0; j < 4; j++)
-					{
-						if (tet.neighbors[j] == -1)
-							tet.neighbors[j] = outerFaceOffset + i;
-					}
-				}
-
-				// Make a map between outer edges and faces, used in the following algorithms
+				// We don't want ot handle the case where the user looks up a position and it falls outside of the 
+				// tetrahedron volume, as the math for projecting the point onto the volume might be too slow for the 
+				// shader (which we need in order not to have a sharp cutoff in lighting where the volume ends). Therefore
+				// we extend the tetrahedron volume to "infinity" (technically to some far away distance, but we treat it as
+				// infinity when calculating barycentric coordinates) by adding new points along the outer face normals.
+				UINT32 numOuterFaces = (UINT32)volume.outerFaces.size();
+
+				// Calculate face normals for outer faces
+				//// Make an edge map
 				struct Edge
 				{
 					INT32 faces[2];
@@ -332,7 +284,7 @@ namespace bs { namespace ct
 				};
 
 				FrameUnorderedMap<std::pair<INT32, INT32>, Edge, pair_hash> edgeMap;
-				for (UINT32 i = 0; i < (UINT32)volume.outerFaces.size(); ++i)
+				for (UINT32 i = 0; i < numOuterFaces; ++i)
 				{
 					for (UINT32 j = 0; j < 3; ++j)
 					{
@@ -360,19 +312,7 @@ namespace bs { namespace ct
 					}
 				}
 
-				// Form connections between outer tetrahedrons
-				for (auto& entry : edgeMap)
-				{
-					const Edge& edge = entry.second;
-
-					Tetrahedron& tet0 = outerTetrahedrons[outerFaceOffset + edge.faces[0]];
-					tet0.neighbors[edge.oppositeVerts[0]] = outerFaceOffset + edge.faces[1];
-
-					Tetrahedron& tet1 = outerTetrahedrons[outerFaceOffset + edge.faces[1]];
-					tet1.neighbors[edge.oppositeVerts[1]] = outerFaceOffset + edge.faces[0];
-				}
-
-				// Generate face normals
+				//// Generate face normals
 				FrameVector<Vector3> faceNormals(volume.outerFaces.size());
 				for (UINT32 i = 0; i < (UINT32)volume.outerFaces.size(); ++i)
 				{
@@ -386,23 +326,23 @@ namespace bs { namespace ct
 					faceNormals[i] = Vector3::normalize(e1.cross(e0));
 				}
 
-				// Generate vertex normals
-				struct VertexAccum
+				//// Generate vertex normals
+				struct FaceVertex
 				{
-					Vector3 sum;
-					float weight;
+					Vector3 normal = Vector3::ZERO;
+					UINT32 outerIdx = -1;
 				};
 
-				FrameUnorderedMap<INT32, Vector3> vertexNormals;
+				FrameUnorderedMap<INT32, FaceVertex> faceVertices;
 				for (auto& entry : edgeMap)
 				{
 					const Edge& edge = entry.second;
 
 					auto accumulateNormalForEdgeVertex = [&](UINT32 v0Idx, UINT32 v1Idx)
 					{
-						auto iter = vertexNormals.insert(std::make_pair(v0Idx, Vector3(BsZero)));
+						auto iter = faceVertices.insert(std::make_pair(v0Idx, FaceVertex()));
 
-						Vector3& accum = iter.first->second;
+						FaceVertex& accum = iter.first->second;
 						const Vector3& v0 = positions[v0Idx];
 
 						auto accumulateNormalForFace = [&](INT32 faceIdx, INT32 v2LocIdx)
@@ -418,7 +358,7 @@ namespace bs { namespace ct
 							Vector3 e1 = Vector3::normalize(v2 - v0);
 
 							float weight = acos(e0.dot(e1));
-							accum += weight * faceNormals[faceIdx];
+							accum.normal += weight * faceNormals[faceIdx];
 						};
 
 						accumulateNormalForFace(edge.faces[0], entry.second.oppositeVerts[0]);
@@ -429,150 +369,159 @@ namespace bs { namespace ct
 					accumulateNormalForEdgeVertex(entry.first.second, entry.first.first);
 				}
 
-				for (auto& entry : vertexNormals)
-					entry.second.normalize();
+				for (auto& entry : faceVertices)
+					entry.second.normal.normalize();
 
-				// Insert outer tetrahedrons, generate matrices
-				for(UINT32 i = 0; i < (UINT32)outerTetrahedrons.size(); ++i)
+				// For each face vertex, generate an outer vertex along its normal
+				static const float ExtrapolationDistance = 1000.0f;
+				for(auto& entry : faceVertices)
 				{
-					TetrahedronData entry;
-					entry.volume = outerTetrahedrons[i];
-
-					// We need a way to project a point outside the tetrahedron volume onto an outer face, then calculate
-					// triangle's barycentric coordinates. Use use the per-vertex normals to extrude the triangle face into
-					// infinity.
-
-					// Our point can be represented as:
-					// p == a (p0 + t*v0) + b (p1 + t*v1) + c (p2 + t*v2)
-					//
-					// where a, b and c are barycentric coordinates,
-					// p0, p1, p2 are the corners of the face
-					// v0, v1, v2 are the vertex normals, per corner
-					// t is the distance from the triangle to the point
-					//
-					// Essentially we're calculating the corners of a bigger triangle that's "t" units away from the
-					// face, and its corners lie along the per-vertex normals. Point "p" will lie on that triangle, for which
-					// we can then calculate barycentric coordinates normally.
-					//
-					// First we substitute: c = 1 - a - b
-					// p == a (p0 + t v0) + b (p1 + t v1) + (1 - a - b) (p2 + t v2)
-					// p == a (p0 + t v0) + b (p1 + t v1) + (p2 + t v2) - a (p2 + t v2) - b (p2 + t v2)
-					// p == a (p0 - p2 + t v0 - t v2) + b (p1 - p2 + t v1 - t v2) + (p2 + t v2)
-					//
-					// And move everything to one side:
-					// p - p2 - t v2 == a (p0 - p2 + t ( v0 - v2)) + b (p1 - p2 + t ( v1 - v2))
-					// a (p0 - p2 + t ( v0 - v2)) + b (p1 - p2 + t ( v1 - v2)) - (p - p2 - t v2) == 0
-					//
-					// We rewrite it using:
-					// Ap = p0 - p2
-					// Av = v0 - v2
-					// Bp = p1 - p2
-					// Bv = v1 - v2
-					// Cp = p - p2
-					// Cv = -v2
-					//
-					// Which yields:
-					// a (Ap + t Av) + b (Bp + t Bv) - (Cp + t Cv) == 0
-					//
-					// Which can be written in matrix form:
-					//
-					// M = {Ap + t Av, Bp + t Bv, Cp + t Cv}
-					//       a      0
-					// M * [ b ] = [0]
-					//      -1      0
-					//
-					// From that we can tell that matrix M cannot be inverted, because if we multiply the zero vector with the
-					// inverted matrix the result would be zero, and not [a, b, -1]. Since the matrix cannot be inverted
-					// det(M) == 0.
-					//
-					// We can use that fact to calculate "t". After we have "t" we can calculate barycentric coordinates
-					// normally.
-					//
-					// Solving equation det(M) == 0 yields a cubic in form:
-					// p t^3 + q t^2 + r t + s = 0
-					//
-					// We'll convert this to monic form, by dividing by p:
-					// t^3 + q/p t^2 + r/p t + s/p = 0
-					//
-					// Or if p ends up being zero, we end up with a quadratic instead:
-					// q t^2 + r t + s = 0
-					// 
-					// We want to create a matrix that when multiplied with the position, yields us the three coefficients,
-					// which we can then use to solve for "t". For this we create a 4x3 matrix, where each row represents
-					// a solution for one of the coefficients. We factor contributons to each coefficient whether they depend on
-					// position x, y, z, or don't depend on position (row columns, in that order respectively).
-
-					const Vector3& p0 = positions[entry.volume.vertices[0]];
-					const Vector3& p1 = positions[entry.volume.vertices[1]];
-					const Vector3& p2 = positions[entry.volume.vertices[2]];
-
-					const Vector3& v0 = vertexNormals[entry.volume.vertices[0]];
-					const Vector3& v1 = vertexNormals[entry.volume.vertices[1]];
-					const Vector3& v2 = vertexNormals[entry.volume.vertices[2]];
-
-					float p =
-						v2.x * v1.y * v0.z -
-						v1.x * v2.y * v0.z -
-						v2.x * v0.y * v1.z +
-						v0.x * v2.y * v1.z +
-						v1.x * v0.y * v2.z -
-						v0.x * v1.y * v2.z;
-					
-					float qx = -v1.y * v0.z + v2.y * v0.z + v0.y * v1.z - v2.y * v1.z - v0.y * v2.z + v1.y * v2.z;
-					float qy = v1.x * v0.z - v2.x * v0.z - v0.x * v1.z + v2.x * v1.z + v0.x * v2.z - v1.x * v2.z;
-					float qz = -v1.x * v0.y + v2.x * v0.y + v0.x * v1.y - v2.x * v1.y - v0.x * v2.y + v1.x * v2.y;
-					float qw = v2.y * v1.z * p0.x - v1.y * v2.z * p0.x - v2.y * v0.z * p1.x + v0.y * v2.z * p1.x + 
-						v1.y * v0.z * p2.x - v0.y * v1.z * p2.x - v2.x * v1.z * p0.y + v1.x * v2.z * p0.y + 
-						v2.x * v0.z * p1.y - v0.x * v2.z * p1.y - v1.x * v0.z * p2.y + v0.x * v1.z * p2.y + 
-						v2.x * v1.y * p0.z - v1.x * v2.y * p0.z - v2.x * v0.y * p1.z + v0.x * v2.y * p1.z + 
-						v1.x * v0.y * p2.z - v0.x * v1.y * p2.z;
-
-					float rx = v1.z * p0.y - v2.z * p0.y - v0.z * p1.y + v2.z * p1.y + v0.z * p2.y - v1.z * p2.y -
-						v1.y * p0.z + v2.y * p0.z + v0.y * p1.z - v2.y * p1.z - v0.y * p2.z + v1.y * p2.z;
-					float ry = -v1.z * p0.x + v2.z * p0.x + v0.z * p1.x - v2.z * p1.x - v0.z * p2.x + v1.z * p2.x +
-						v1.x * p0.z - v2.x * p0.z - v0.x * p1.z + v2.x * p1.z + v0.x * p2.z - v1.x * p2.z;
-					float rz = v1.y * p0.x - v2.y * p0.x - v0.y * p1.x + v2.y * p1.x + v0.y * p2.x - v1.y * p2.x -
-						v1.x * p0.y + v2.x * p0.y + v0.x * p1.y - v2.x * p1.y - v0.x * p2.y + v1.x * p2.y;
-					float rw = v2.z * p1.x * p0.y - v1.z * p2.x * p0.y - v2.z * p0.x * p1.y + v0.z * p2.x * p1.y +
-						v1.z * p0.x * p2.y - v0.z * p1.x * p2.y - v2.y * p1.x * p0.z + v1.y * p2.x * p0.z +
-						v2.x * p1.y * p0.z - v1.x * p2.y * p0.z + v2.y * p0.x * p1.z - v0.y * p2.x * p1.z -
-						v2.x * p0.y * p1.z + v0.x * p2.y * p1.z - v1.y * p0.x * p2.z + v0.y * p1.x * p2.z +
-						v1.x * p0.y * p2.z - v0.x * p1.y * p2.z;
-
-					float sx = -p1.y * p0.z + p2.y * p0.z + p0.y * p1.z - p2.y * p1.z - p0.y * p2.z + p1.y * p2.z;
-					float sy = p1.x * p0.z - p2.x * p0.z - p0.x * p1.z + p2.x * p1.z + p0.x * p2.z - p1.x * p2.z;
-					float sz = -p1.x * p0.y + p2.x * p0.y + p0.x * p1.y - p2.x * p1.y - p0.x * p2.y + p1.x * p2.y;
-					float sw = p2.x * p1.y * p0.z - p1.x * p2.y * p0.z - p2.x * p0.y * p1.z + 
-						p0.x * p2.y * p1.z + p1.x * p0.y * p2.z - p0.x * p1.y * p2.z;
-
-					entry.transform[0][0] = qx;
-					entry.transform[0][1] = qy;
-					entry.transform[0][2] = qz;
-					entry.transform[0][3] = qw;
-
-					entry.transform[1][0] = rx;
-					entry.transform[1][1] = ry;
-					entry.transform[1][2] = rz;
-					entry.transform[1][3] = rw;
-
-					entry.transform[2][0] = sx;
-					entry.transform[2][1] = sy;
-					entry.transform[2][2] = sz;
-					entry.transform[2][3] = sw;
-
-					// Unused
-					entry.transform[3][0] = 0.0f;
-					entry.transform[3][1] = 0.0f;
-					entry.transform[3][2] = 0.0f;
-					entry.transform[3][3] = 0.0f;
-
-					if (fabs(p) > 0.00001f)
-						entry.transform = entry.transform * (1.0f / p);
-					else // Quadratic
-						entry.volume.neighbors[3] = -2;
-
-					output.push_back(entry);
+					entry.second.outerIdx = (UINT32)positions.size();
+
+					Vector3 outerPos = positions[entry.first] + entry.second.normal * ExtrapolationDistance;
+					positions.push_back(outerPos);
 				}
+
+				// For each face, generate outer tetrahedrons
+				Vector<Vector3> outerVolumeVerts;
+				FrameVector<TetrahedronVolume> outerTetrahedra;
+				for (UINT32 i = 0; i < numOuterFaces; ++i)
+				{
+					const TetrahedronFace& face = volume.outerFaces[i];
+					UINT32 originalIndices[6];
+					for (UINT32 j = 0; j < 3; j++)
+					{
+						const FaceVertex& faceVertex = faceVertices[face.vertices[j]];
+
+						outerVolumeVerts.push_back(positions[face.vertices[j]]);
+						outerVolumeVerts.push_back(positions[faceVertex.outerIdx]);
+
+						originalIndices[j * 2 + 0] = face.vertices[j];
+						originalIndices[j * 2 + 1] = faceVertex.outerIdx;
+					}
+
+					TetrahedronVolume outerVolume = Triangulation::tetrahedralize(outerVolumeVerts);
+
+					UINT32 tetStartIdx = (UINT32)volume.tetrahedra.size();
+					for (auto& entry : outerVolume.tetrahedra)
+					{
+						// Remap vertices back to global array
+						for (UINT32 j = 0; j < 4; j++)
+							entry.vertices[j] = originalIndices[entry.vertices[j]];
+
+						// Remap neighbors to global array
+						for (UINT32 j = 0; j < 4; j++)
+						{
+							if (entry.neighbors[j] != -1)
+							{
+								// Valid neighbor, map to global array
+								entry.neighbors[j] += tetStartIdx;
+							}
+						}
+					}
+
+					// Connect the new volume to the original face
+					for (auto& entry : outerVolume.outerFaces)
+					{
+						bool isValid = true;
+						for (UINT32 j = 0; j < 3; j++)
+						{
+							if (entry.vertices[j] % 2 == 1)
+							{
+								isValid = false;
+								break;
+							}
+						}
+
+						if (!isValid)
+							continue;
+
+						Tetrahedron& outerTet = outerVolume.tetrahedra[entry.tetrahedron];
+
+						UINT32 oppositeVert = -1;
+						for (UINT32 j = 0; j < 4; j++)
+						{
+							if(outerTet.vertices[j] != entry.vertices[0] &&
+								outerTet.vertices[j] != entry.vertices[1] &&
+								outerTet.vertices[j] != entry.vertices[2])
+							{
+								oppositeVert = j;
+								break;
+							}
+						}
+
+						assert(outerTet.neighbors[oppositeVert] == -1);
+						outerTet.neighbors[oppositeVert] = face.tetrahedron;
+
+						Tetrahedron& innerTet = volume.tetrahedra[face.tetrahedron];
+						for(UINT32 j = 0; j < 4; j++)
+						{
+							if (innerTet.neighbors[j] == -1)
+								innerTet.neighbors[j] = entry.tetrahedron;
+						}
+					}
+
+					outerTetrahedra.push_back(outerVolume);
+					outerVolumeVerts.clear();
+
+					// Add to global tetrahedra array
+					for (auto& entry : outerVolume.tetrahedra)
+						volume.tetrahedra.push_back(entry);
+				}
+
+				// Note: Not forming neighbor connections between outer tetrahedrons. Since we generate them separately
+				// we can't guarantee they even exist.
+			}
+
+			// Generate matrices
+			UINT32 numOutputTets = (UINT32)volume.tetrahedra.size();
+			output.reserve(numOutputTets);
+
+			// Insert tetrahedrons, generate matrices
+			for(UINT32 i = 0; i < (UINT32)volume.tetrahedra.size(); ++i)
+			{
+				TetrahedronData entry;
+				entry.volume = volume.tetrahedra[i];
+
+				// Generate a matrix that can be used for calculating barycentric coordinates
+				// To determine a point within a tetrahedron, using barycentric coordinates, we use:
+				// P = (P1 - P4) * a + (P2 - P4) * b + (P3 - P4) * c + P4
+				//
+				// Where P1, P2, P3, P4 are the corners of the tetrahedron.
+				//
+				// Expanded for each coordinate this is:
+				// x = (x1 - x4) * a + (x2 - x4) * b + (x3 - x4) * c + x4
+				// y = (y1 - y4) * a + (y2 - y4) * b + (y3 - y4) * c + y4
+				// z = (z1 - z4) * a + (z2 - z4) * b + (z3 - z4) * c + z4
+				//
+				// In matrix form this is:
+				//                                      a
+				// P = [P1 - P4, P2 - P4, P3 - P4, P4] [b]
+				//                                      c
+				//                                      1
+				//
+				// Solved for barycentric coordinates:
+				//  a
+				// [b] = Minv * P 
+				//  c
+				//  1
+				//
+				// Where Minv is the inverse of the matrix above.
+
+				const Vector3& P1 = positions[volume.tetrahedra[i].vertices[0]];
+				const Vector3& P2 = positions[volume.tetrahedra[i].vertices[1]];
+				const Vector3& P3 = positions[volume.tetrahedra[i].vertices[2]];
+				const Vector3& P4 = positions[volume.tetrahedra[i].vertices[3]];
+
+				Matrix4 mat;
+				mat.setColumn(0, Vector4(P1 - P4, 0.0f));
+				mat.setColumn(1, Vector4(P2 - P4, 0.0f));
+				mat.setColumn(2, Vector4(P3 - P4, 0.0f));
+				mat.setColumn(3, Vector4(P4, 1.0f));
+
+				entry.transform = mat.inverse();
+
+				output.push_back(entry);
 			}
 		}
 		bs_frame_clear();