Переглянути джерело

Heightfield size optimizations (#10)

* Ability to specify block size to allow reducing the tree structure size
* Ability to specify the amount of bits per height sample
* Reduced quantization error at lower bits per sample
* Ability to draw the original terrain
* Added two more test cases: a flat terrain and a terrain that has no collision
* Added function to estimate amounts of bits needed to get to a certain precision
* Fixed active edges test
- The center of mass offset was not taken into account so that numerical inaccuracy could lead to a collision being discarded as back facing
- Changed from box to capsule shape because a box vs a parallel edge does not have a well defined contact point (it's a point on a line segment)
* Added unit tests for heightfield shapes
jrouwe 3 роки тому
батько
коміт
9e37417156

Різницю між файлами не показано, бо вона завелика
+ 412 - 247
Jolt/Physics/Collision/Shape/HeightFieldShape.cpp


+ 47 - 18
Jolt/Physics/Collision/Shape/HeightFieldShape.h

@@ -20,9 +20,6 @@ namespace HeightFieldShapeConstants
 	/// Value used to create gaps in the height field
 	constexpr float			cNoCollisionValue = FLT_MAX;
 
-	/// Hierarchical grids stop when cBlockSize x cBlockSize height samples remain
-	constexpr uint			cBlockSize = 2;
-
 	/// Stack size to use during WalkHeightField
 	constexpr int			cStackSize = 128;
 
@@ -34,10 +31,6 @@ namespace HeightFieldShapeConstants
 	/// When height samples are converted to 16 bit:
 	constexpr uint16		cNoCollisionValue16 = 0xffff;		///< This is the magic value for 'no collision'
 	constexpr uint16		cMaxHeightValue16 = 0xfffe;			///< This is the maximum allowed height value
-
-	/// When height samples are converted to 8 bit:
-	constexpr uint8			cNoCollisionValue8 = 0xff;			///< This is the magic value for 'no collision'
-	constexpr uint8			cMaxHeightValue8 = 0xfe;			///< This is the maximum allowed height value
 };
 
 /// Class that constructs a HeightFieldShape
@@ -52,7 +45,7 @@ public:
 	/// Create a height field shape of inSampleCount * inSampleCount vertices.
 	/// The height field is a surface defined by: inOffset + inScale * (x, inSamples[y * inSampleCount + x], y).
 	/// where x and y are integers in the range x and y e [0, inSampleCount - 1].
-	/// inSampleCount: Must be a power of 2 and minimally 8.
+	/// inSampleCount: inSampleCount / mBlockSize must be a power of 2 and minimally 2.
 	/// inSamples: inSampleCount^2 vertices.
 	/// inMaterialIndices: (inSampleCount - 1)^2 indices that index into inMaterialList.
 									HeightFieldShapeSettings(const float *inSamples, Vec3Arg inOffset, Vec3Arg inScale, uint32 inSampleCount, const uint8 *inMaterialIndices = nullptr, const PhysicsMaterialList &inMaterialList = PhysicsMaterialList());
@@ -60,12 +53,33 @@ public:
 	// See: ShapeSettings
 	virtual ShapeResult				Create() const override;
 
+	/// Determine the minimal and maximal value of mHeightSamples (will ignore cNoCollisionValue)
+	/// @param outMinValue The minimal value fo mHeightSamples or FLT_MAX if no samples have collision
+	/// @param outMaxValue The maximal value fo mHeightSamples or -FLT_MAX if no samples have collision
+	/// @param outQuantizationScale (value - outMinValue) * outQuantizationScale quantizes a height sample to 16 bits
+	void							DetermineMinAndMaxSample(float &outMinValue, float &outMaxValue, float &outQuantizationScale) const;
+
+	/// Given mBlockSize, mSampleCount and mHeightSamples, calculate the amount of bits needed to stay below absolute error inMaxError
+	/// @param inMaxError Maximum allowed error in mHeightSamples after compression (note that this does not take mScale.Y into account)
+	/// @return Needed bits per sample in the range [1, 8].
+	uint32							CalculateBitsPerSampleForError(float inMaxError) const;
+
 	/// The height field is a surface defined by: mOffset + mScale * (x, mHeightSamples[y * mSampleCount + x], y).
 	/// where x and y are integers in the range x and y e [0, mSampleCount - 1].
 	Vec3							mOffset = Vec3::sZero();
 	Vec3							mScale = Vec3::sReplicate(1.0f);
 	uint32							mSampleCount = 0;
 
+	/// The heightfield is divided in blocks of mBlockSize * mBlockSize * 2 triangles and the acceleration structure culls blocks only, 
+	/// bigger block sizes reduce memory consumption but also reduce query performance. Sensible values are [1, 4], does not need to be
+	/// a power of 2.
+	uint32							mBlockSize = 2;
+
+	/// How many bits per sample to use to compress the height field. Can be in the range [1, 8].
+	/// Note that each sample is compressed relative to the min/max value of its block of mBlockSize * mBlockSize pixels so the effective precision is higher.
+	/// Also note that increasing mBlockSize saves more memory than reducing the amount of bits per sample.
+	uint32							mBitsPerSample = 8;
+
 	vector<float>					mHeightSamples;
 	vector<uint8>					mMaterialIndices;
 
@@ -139,7 +153,7 @@ public:
 
 	/// Get height field position at sampled location (inX, inY).
 	/// where inX and inY are integers in the range inX e [0, mSampleCount - 1] and inY e [0, mSampleCount - 1].
-	const Vec3						GetPosition(uint inX, uint inY) const;
+	Vec3							GetPosition(uint inX, uint inY) const;
 
 	/// Check if height field at sampled location (inX, inY) has collision (has a hole or not)
 	bool							IsNoCollision(uint inX, uint inY) const;
@@ -171,12 +185,24 @@ protected:
 private:	
 	class							DecodingContext;						///< Context class for walking through all nodes of a heightfield
 	struct							HSGetTrianglesContext;					///< Context class for GetTrianglesStart/Next
+
+	/// Calculate commonly used values and store them in the shape
+	void							CacheValues();
+
+	/// Calculate bit mask for all active edges in the heightfield
+	void							CalculateActiveEdges();
+	
+	/// Store material indices in the least amount of bits per index possible
+	void							StoreMaterialIndices(const vector<uint8> &inMaterialIndices);
 	
 	/// For location (inX, inY) get the block that contains this position and get the offset and scale needed to decode a uint8 height sample to a uint16
 	void							GetBlockOffsetAndScale(uint inX, uint inY, float &outBlockOffset, float &outBlockScale) const;
 
+	/// Get the height sample at position (inX, inY)
+	inline uint8					GetHeightSample(uint inX, uint inY) const;
+
 	/// Faster version of GetPosition when block offset and scale are already known
-	const Vec3						GetPosition(uint inX, uint inY, float inBlockOffset, float inBlockScale) const;
+	Vec3							GetPosition(uint inX, uint inY, float inBlockOffset, float inBlockScale) const;
 	
 	/// Determine amount of bits needed to encode sub shape id
 	uint							GetSubShapeIDBits() const;
@@ -190,7 +216,7 @@ private:
 
 	/// Visit the entire height field using a visitor pattern
 	template <class Visitor>
-	void							WalkHeightField(Visitor &ioVisitor) const;
+	JPH_INLINE void					WalkHeightField(Visitor &ioVisitor) const;
 
 	/// A block of 2x2 ranges used to form a hierarchical grid, ordered left top, right top, left bottom, right bottom
 	struct alignas(16) RangeBlock
@@ -207,18 +233,21 @@ private:
 	Vec3							mOffset = Vec3::sZero();
 	Vec3							mScale = Vec3::sReplicate(1.0f);
 
-	/// The materials of square at (x, y) is: mMaterials[mMaterialIndices[x + y * (mSampleCount - 1)]]
-	PhysicsMaterialList				mMaterials;
-
-	// Calculated data
-	uint32							mSampleCount = 0;
+	/// Height data
+	uint32							mSampleCount = 0;					///< See HeightFieldShapeSettings::mSampleCount
+	uint32							mBlockSize = 2;						///< See HeightFieldShapeSettings::mBlockSize
+	uint8							mBitsPerSample = 8;					///< See HeightFieldShapeSettings::mBitsPerSample
+	uint8							mSampleMask = 0xff;					///< All bits set for a sample: (1 << mBitsPerSample) - 1, used to indicate that there's no collision
 	uint16							mMinSample = HeightFieldShapeConstants::cNoCollisionValue16;	///< Min and max value in mHeightSamples quantized to 16 bit, for calculating bounding box
 	uint16							mMaxSample = HeightFieldShapeConstants::cNoCollisionValue16;
 	vector<RangeBlock>				mRangeBlocks;						///< Hierarchical grid of range data describing the height variations within 1 block. The grid for level <level> starts at offset sGridOffsets[<level>]
-	vector<uint8>					mHeightSamples;						///< 8-bit height samples. Value [0, cMaxHeightValue8] maps to highest detail grid in mRangeBlocks [mMin, mMax].
+	vector<uint8>					mHeightSamples;						///< mBitsPerSample-bit height samples. Value [0, mMaxHeightValue] maps to highest detail grid in mRangeBlocks [mMin, mMax]. mNoCollisionValue is reserved to indicate no collision.
 	vector<uint8>					mActiveEdges;						///< (mSampleCount - 1)^2 * 3-bit active edge flags. 
+
+	/// Materials
+	PhysicsMaterialList				mMaterials;							///< The materials of square at (x, y) is: mMaterials[mMaterialIndices[x + y * (mSampleCount - 1)]]
 	vector<uint8>					mMaterialIndices;					///< Compressed to the minimum amount of bits per material index (mSampleCount - 1) * (mSampleCount - 1) * mNumBitsPerMaterialIndex bits of data
-	uint							mNumBitsPerMaterialIndex = 0;		///< Number of bits per material index
+	uint32							mNumBitsPerMaterialIndex = 0;		///< Number of bits per material index
 
 #ifdef JPH_DEBUG_RENDERER
 	/// Temporary rendering data

+ 149 - 71
Samples/Tests/Shapes/HeightFieldShapeTest.cpp

@@ -25,50 +25,11 @@ static int sTerrainType = 0;
 
 static const char *sTerrainTypes[] = {
 	"Procedural Terrain",
-	"Heightfield 1"
+	"Heightfield 1",
+	"Flat",
+	"No Collision"
 };
 
-static void sValidateHeightField(const float *inUncompressedHeights, uint inSize, const HeightFieldShape *inShape)
-{
-	float max_diff = -1.0f;
-	uint max_diff_x = 0, max_diff_y = 0;
-	float min_height = FLT_MAX, max_height = -FLT_MAX;
-	for (uint y = 0; y < inSize; ++y)
-		for (uint x = 0; x < inSize; ++x)
-		{
-			float h1 = inUncompressedHeights[y * inSize + x];
-			if (h1 != HeightFieldShapeConstants::cNoCollisionValue)
-			{
-				if (inShape->IsNoCollision(x, y))
-					FatalError("No collision where there should be");
-				float h2 = inShape->GetPosition(x, y).GetY();
-				float diff = abs(h2 - h1);
-				if (diff > max_diff)
-				{
-					max_diff = diff;
-					max_diff_x = x;
-					max_diff_y = y;
-				}
-				min_height = min(min_height, h1);
-				max_height = max(max_height, h1);
-			}
-			else
-			{
-				if (!inShape->IsNoCollision(x, y))
-					FatalError("Collision where there shouldn't be");
-			}
-		}
-
-	// Calculate relative error compared to 8-bit compression (error should be below 1)
-	float rel_error = 255.0f * max_diff / (max_height - min_height);
-
-	Trace("Min height: %g, max height: %g", (double)min_height, (double)max_height);
-	Trace("Max diff: %g at (%d, %d), relative error: %g", (double)max_diff, max_diff_x, max_diff_y, (double)rel_error);
-
-	if (rel_error > 1.0f)
-		FatalError("Error too big!");
-}
-
 void HeightFieldShapeTest::Initialize()
 {
 	if (sTerrainType == 0)
@@ -78,69 +39,153 @@ void HeightFieldShapeTest::Initialize()
 		const float max_height = 5.0f;
 
 		// Create height samples
-		float heights[n * n];
+		mTerrainSize = n;
+		mTerrain.resize(n * n);
 		for (int y = 0; y < n; ++y)
 			for (int x = 0; x < n; ++x)
-				heights[y * n + x] = max_height * PerlinNoise3(float(x) * 8.0f / n, 0, float(y) * 8.0f / n, 256, 256, 256);
+				mTerrain[y * n + x] = max_height * PerlinNoise3(float(x) * 8.0f / n, 0, float(y) * 8.0f / n, 256, 256, 256);
 
 		// Make some holes
-		heights[2 * n + 2] = HeightFieldShapeConstants::cNoCollisionValue;
+		mTerrain[2 * n + 2] = HeightFieldShapeConstants::cNoCollisionValue;
 		for (int y = 4; y < 33; ++y)
 			for (int x = 4; x < 33; ++x)
-				heights[y * n + x] = HeightFieldShapeConstants::cNoCollisionValue;
+				mTerrain[y * n + x] = HeightFieldShapeConstants::cNoCollisionValue;
 
 		// Make material indices
 		uint8 max_material_index = 0;
-		uint8 material_indices[Square(n - 1)];
+		mMaterialIndices.resize(Square(n - 1));
 		for (int y = 0; y < n - 1; ++y)
 			for (int x = 0; x < n - 1; ++x)
 			{
 				uint8 material_index = uint8(round((Vec3(x * cell_size, 0, y * cell_size) - Vec3(n * cell_size / 2, 0, n * cell_size / 2)).Length() / 10.0f));
 				max_material_index = max(max_material_index, material_index);
-				material_indices[y * (n - 1) + x] = material_index;
+				mMaterialIndices[y * (n - 1) + x] = material_index;
 			}
 
 		// Mark the corners to validate that materials and heights match
-		heights[0] = 0.0f;
-		heights[n - 1] = 10.0f;
-		heights[(n - 1) * n] = 20.0f;
-		heights[n * n - 1] = 30.0f;
-		material_indices[0] = 0;
-		material_indices[n - 2] = 1;
-		material_indices[(n - 2) * (n - 1)] = 2;
-		material_indices[Square(n - 1) - 1] = 3;
+		mTerrain[0] = 0.0f;
+		mTerrain[n - 1] = 10.0f;
+		mTerrain[(n - 1) * n] = 20.0f;
+		mTerrain[n * n - 1] = 30.0f;
+		mMaterialIndices[0] = 0;
+		mMaterialIndices[n - 2] = 1;
+		mMaterialIndices[(n - 2) * (n - 1)] = 2;
+		mMaterialIndices[Square(n - 1) - 1] = 3;
 
 		// Create materials
-		PhysicsMaterialList materials;
 		for (uint8 i = 0; i <= max_material_index; ++i)
-			materials.push_back(new PhysicsMaterialSimple("Material " + ConvertToString(uint(i)), Color::sGetDistinctColor(i)));
+			mMaterials.push_back(new PhysicsMaterialSimple("Material " + ConvertToString(uint(i)), Color::sGetDistinctColor(i)));
 
-		// Create height field
-		mHeightField = static_cast<const HeightFieldShape *>(HeightFieldShapeSettings(heights, Vec3(-0.5f * cell_size * n, 0.0f, -0.5f * cell_size * n), Vec3(cell_size, 1.0f, cell_size), n, material_indices, materials).Create().Get().GetPtr());
-		Body &terrain = *mBodyInterface->CreateBody(BodyCreationSettings(mHeightField, Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
-		mBodyInterface->AddBody(terrain.GetID(), EActivation::DontActivate);
-
-		sValidateHeightField(heights, n, mHeightField);
+		// Determine scale and offset (deliberately apply extra offset and scale in Y direction)
+		mTerrainOffset = Vec3(-0.5f * cell_size * n, -2.0f, -0.5f * cell_size * n);
+		mTerrainScale = Vec3(cell_size, 1.5f, cell_size);
 	}
 	else if (sTerrainType == 1)
 	{
 		const int n = 1024;
 		const float cell_size = 0.5f;
-
+		
 		// Get height samples
 		vector<uint8> data = ReadData("Assets/heightfield1.bin");
 		if (data.size() != sizeof(float) * n * n)
 			FatalError("Invalid file size");
-		const float *heights = (float *)&data[0];
+		mTerrainSize = n;
+		mTerrain.resize(n * n);
+		memcpy(mTerrain.data(), data.data(), n * n * sizeof(float));
+
+		// Determine scale and offset
+		mTerrainOffset = Vec3(-0.5f * cell_size * n, 0.0f, -0.5f * cell_size * n);
+		mTerrainScale = Vec3(cell_size, 1.0f, cell_size);
+	}
+	else if (sTerrainType == 2)
+	{
+		const int n = 128;
+		const float cell_size = 1.0f;
+		const float height = JPH_PI;
+
+		// Determine scale and offset
+		mTerrainOffset = Vec3(-0.5f * cell_size * n, 0.0f, -0.5f * cell_size * n);
+		mTerrainScale = Vec3(cell_size, 1.0f, cell_size);
+
+		// Mark the entire terrain as no collision
+		mTerrainSize = n;
+		mTerrain.resize(n * n);
+		for (float &v : mTerrain)
+			v = height;
+	}
+	else if (sTerrainType == 3)
+	{
+		const int n = 128;
+		const float cell_size = 1.0f;
 
-		// Create height field
-		mHeightField = static_cast<const HeightFieldShape *>(HeightFieldShapeSettings(heights, Vec3(-0.5f * cell_size * n, 0.0f, -0.5f * cell_size * n), Vec3(cell_size, 1.0f, cell_size), n).Create().Get().GetPtr());
-		Body &terrain = *mBodyInterface->CreateBody(BodyCreationSettings(mHeightField, Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
-		mBodyInterface->AddBody(terrain.GetID(), EActivation::DontActivate);
+		// Determine scale and offset
+		mTerrainOffset = Vec3(-0.5f * cell_size * n, 0.0f, -0.5f * cell_size * n);
+		mTerrainScale = Vec3(cell_size, 1.0f, cell_size);
 
-		sValidateHeightField(heights, n, mHeightField);
+		// Mark the entire terrain as no collision
+		mTerrainSize = n;
+		mTerrain.resize(n * n);
+		for (float &v : mTerrain)
+			v = HeightFieldShapeConstants::cNoCollisionValue;
 	}
 
+	// Create height field
+	HeightFieldShapeSettings settings(mTerrain.data(), mTerrainOffset, mTerrainScale, mTerrainSize, mMaterialIndices.data(), mMaterials);
+	settings.mBlockSize = 1 << sBlockSizeShift;
+	settings.mBitsPerSample = sBitsPerSample;
+	mHeightField = static_cast<const HeightFieldShape *>(settings.Create().Get().GetPtr());
+	Body &terrain = *mBodyInterface->CreateBody(BodyCreationSettings(mHeightField, Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, Layers::NON_MOVING));
+	mBodyInterface->AddBody(terrain.GetID(), EActivation::DontActivate);
+
+	// Validate it
+	float max_diff = -1.0f;
+	uint max_diff_x = 0, max_diff_y = 0;
+	float min_height = FLT_MAX, max_height = -FLT_MAX, avg_diff = 0.0f;
+	for (uint y = 0; y < mTerrainSize; ++y)
+		for (uint x = 0; x < mTerrainSize; ++x)
+		{
+			float h1 = mTerrain[y * mTerrainSize + x];
+			if (h1 != HeightFieldShapeConstants::cNoCollisionValue)
+			{
+				h1 = mTerrainOffset.GetY() + mTerrainScale.GetY() * h1;
+				if (mHeightField->IsNoCollision(x, y))
+					FatalError("No collision where there should be");
+				float h2 = mHeightField->GetPosition(x, y).GetY();
+				float diff = abs(h2 - h1);
+				if (diff > max_diff)
+				{
+					max_diff = diff;
+					max_diff_x = x;
+					max_diff_y = y;
+				}
+				min_height = min(min_height, h1);
+				max_height = max(max_height, h1);
+				avg_diff += diff;
+			}
+			else
+			{
+				if (!mHeightField->IsNoCollision(x, y))
+					FatalError("Collision where there shouldn't be");
+			}
+		}
+
+	// Calculate relative error
+	float rel_error = min_height < max_height? 100.0f * max_diff / (max_height - min_height) : 0.0f;
+
+	// Max error we expect given sBitsPerSample (normally the error should be much lower because we quantize relative to the block rather than the full height)
+	float max_error = 0.5f * 100.0f / ((1 << sBitsPerSample) - 1);
+
+	// Calculate average
+	avg_diff /= mTerrainSize * mTerrainSize;
+
+	// Calculate amount of memory used
+	Shape::Stats stats = mHeightField->GetStats();
+
+	// Trace stats
+	Trace("Block size: %d, bits per sample: %d, min height: %g, max height: %g, avg diff: %g, max diff: %g at (%d, %d), relative error: %g%%, size: %u bytes", 1 << sBlockSizeShift, sBitsPerSample, (double)min_height, (double)max_height, (double)avg_diff, (double)max_diff, max_diff_x, max_diff_y, (double)rel_error, stats.mSizeBytes);
+	if (rel_error > max_error)
+		FatalError("Error too big!");
+
 	// Determine terrain height
 	RayCastResult result;
 	Vec3 start(0, 1000, 0);
@@ -165,6 +210,29 @@ void HeightFieldShapeTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		mDebugRenderer->DrawMarker(surface_pos, Color::sWhite, 1.0f);
 		mDebugRenderer->DrawArrow(surface_pos, surface_pos + surface_normal, Color::sRed, 0.1f);
 	}
+
+	// Draw the original uncompressed terrain
+	if (sShowOriginalTerrain)
+		for (uint y = 0; y < mTerrainSize; ++y)
+			for (uint x = 0; x < mTerrainSize; ++x)
+			{
+				// Get original height
+				float h = mTerrain[y * mTerrainSize + x];
+				if (h == HeightFieldShapeConstants::cNoCollisionValue)
+					continue;
+
+				// Get original position
+				Vec3 original = mTerrainOffset + mTerrainScale * Vec3(float(x), h, float(y));
+
+				// Get compressed position
+				Vec3 compressed = mHeightField->GetPosition(x, y);
+
+				// Draw marker that is red when error is too big and green when not
+				const float cMaxError = 0.1f;
+				float error = (original - compressed).Length();
+				uint8 c = uint8(round(255.0f * min(error / cMaxError, 1.0f)));
+				mDebugRenderer->DrawMarker(original, Color(c, 255 - c, 0, 255), 0.1f);
+			}
 }
 
 void HeightFieldShapeTest::GetInitialCamera(CameraState &ioState) const
@@ -181,5 +249,15 @@ void HeightFieldShapeTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMen
 			inUI->CreateTextButton(terrain_name, sTerrainTypes[i], [this, i]() { sTerrainType = i; RestartTest(); });
 		inUI->ShowMenu(terrain_name);
 	});
+
+	inUI->CreateTextButton(inSubMenu, "Configuration Settings", [this, inUI]() { 
+		UIElement *terrain_settings = inUI->CreateMenu();
+		inUI->CreateComboBox(terrain_settings, "Block Size", { "1", "2", "4" }, sBlockSizeShift, [=](int inItem) { sBlockSizeShift = inItem; });
+		inUI->CreateSlider(terrain_settings, "Bits Per Sample", (float)sBitsPerSample, 1.0f, 8.0f, 1.0f, [=](float inValue) { sBitsPerSample = (int)inValue; });
+		inUI->CreateTextButton(terrain_settings, "Accept", [this]() { RestartTest(); });
+		inUI->ShowMenu(terrain_settings);
+	});
+	
+	inUI->CreateCheckBox(inSubMenu, "Show Original Terrain", sShowOriginalTerrain, [](UICheckBox::EState inState) { sShowOriginalTerrain = inState == UICheckBox::STATE_CHECKED; });
 }
 

+ 24 - 6
Samples/Tests/Shapes/HeightFieldShapeTest.h

@@ -12,18 +12,36 @@ public:
 	JPH_DECLARE_RTTI_VIRTUAL(HeightFieldShapeTest)
 
 	// Initialize the test
-	virtual void	Initialize() override;
+	virtual void		Initialize() override;
 
 	// Update the test, called before the physics update
-	virtual void	PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+	virtual void		PrePhysicsUpdate(const PreUpdateParams &inParams) override;
 
 	// Override to specify the initial camera state (local to GetCameraPivot)
-	virtual void	GetInitialCamera(CameraState &ioState) const override;
+	virtual void		GetInitialCamera(CameraState &ioState) const override;
 
 	// Optional settings menu
-	virtual bool	HasSettingsMenu() const	override							{ return true; }
-	virtual void	CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
+	virtual bool		HasSettingsMenu() const	override							{ return true; }
+	virtual void		CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu) override;
+
+	// Original (uncompressed) terrain
+	vector<float>		mTerrain;
+	PhysicsMaterialList mMaterials;
+	vector<uint8>		mMaterialIndices;
+	uint				mTerrainSize;
+	Vec3				mTerrainOffset;
+	Vec3				mTerrainScale;
+
+	// Block size = 1 << sBlockSizeShift
+	inline static int	sBlockSizeShift = 1;
+
+	// Bits per sample
+	inline static int	sBitsPerSample = 8;
+
+	// Draw the terrain
+	inline static bool	sShowOriginalTerrain = false;
 
 	RefConst<HeightFieldShape> mHeightField;
-	Vec3			mHitPos = Vec3::sZero();
+
+	Vec3				mHitPos = Vec3::sZero();
 };

+ 1 - 1
Samples/Tests/Shapes/MutableCompoundShapeTest.cpp

@@ -94,7 +94,7 @@ void MutableCompoundShapeTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 			if (roll < 0.001f && count > 1)
 			{
 				// Remove a random shape
-				uniform_int_distribution<uint> index_distribution(0, count = 1);
+				uniform_int_distribution<uint> index_distribution(0, count - 1);
 				shape->RemoveShape(index_distribution(frame_random));
 			}
 			else if (roll < 0.002f && count < 10)

+ 29 - 30
UnitTests/Physics/ActiveEdgesTests.cpp

@@ -5,6 +5,7 @@
 #include "PhysicsTestContext.h"
 #include "Layers.h"
 #include <Physics/Collision/Shape/BoxShape.h>
+#include <Physics/Collision/Shape/CapsuleShape.h>
 #include <Physics/Collision/Shape/RotatedTranslatedShape.h>
 #include <Physics/Collision/Shape/MeshShape.h>
 #include <Physics/Collision/Shape/HeightFieldShape.h>
@@ -16,18 +17,16 @@
 
 TEST_SUITE("ActiveEdgesTest")
 {
-	static const float cBoxSize = 0.1f;
+	static const float cCapsuleProbeOffset = 0.1f; // How much to offset the probe from y = 0 in order to avoid hitting a back instead of a front face
+	static const float cCapsuleRadius = 0.1f;
 
-	// Create a box as our probe
-	static Ref<Shape> sCreateProbeBox()
+	// Create a capsule as our probe
+	static Ref<Shape> sCreateProbeCapsule()
 	{
-		// Ensure box is larger in y direction so that when active edges mode is on, we will always get a horizontal penetration axis rather than a vertical one
-		BoxShapeSettings box(Vec3(cBoxSize, 1.0f, cBoxSize), 0.01f); 
-		box.SetEmbedded();
-
-		// Offset box by 0.1 in y direction so that the when active edges mode is off, the penetration axis is well defined
-		RotatedTranslatedShapeSettings offset_settings(Vec3(0, 0.1f, 0), Quat::sIdentity(), &box);
-		return offset_settings.Create().Get();
+		// Ensure capsule is long enough so that when active edges mode is on, we will always get a horizontal penetration axis rather than a vertical one
+		CapsuleShapeSettings capsule(1.0f, cCapsuleRadius); 
+		capsule.SetEmbedded();
+		return capsule.Create().Get();
 	}
 
 	// Create a flat mesh shape consting of 7 x 7 quads, we know that only the outer edges of this shape are active
@@ -62,7 +61,7 @@ TEST_SUITE("ActiveEdgesTest")
 
 	// Compare expected hits with returned hits
 	template <class ResultType>
-	static void sCheckMatch(const vector<ResultType> &inResult, const vector<ExpectedHit> &inExpectedHits)
+	static void sCheckMatch(const vector<ResultType> &inResult, const vector<ExpectedHit> &inExpectedHits, float inAccuracySq)
 	{
 		CHECK(inResult.size() == inExpectedHits.size());
 
@@ -70,8 +69,8 @@ TEST_SUITE("ActiveEdgesTest")
 		{
 			bool found = false;
 			for (const ResultType &result : inResult)
-				if (result.mContactPointOn2.IsClose(hit.mPosition) 
-					&& result.mPenetrationAxis.Normalized().IsClose(hit.mPenetrationAxis))
+				if (result.mContactPointOn2.IsClose(hit.mPosition, inAccuracySq) 
+					&& result.mPenetrationAxis.Normalized().IsClose(hit.mPenetrationAxis, inAccuracySq))
 				{
 					found = true;
 					break;
@@ -86,7 +85,7 @@ TEST_SUITE("ActiveEdgesTest")
 		AllHitCollisionCollector<CollideShapeCollector> collector;
 		CollisionDispatch::sCollideShapeVsShape(inProbeShape, inTestShape, Vec3::sReplicate(1.0f), inTestShapeScale, Mat44::sTranslation(inProbeShapePos), Mat44::sIdentity(), SubShapeIDCreator(), SubShapeIDCreator(), inSettings, collector);
 
-		sCheckMatch(collector.mHits, inExpectedHits);
+		sCheckMatch(collector.mHits, inExpectedHits, 1.0e-8f);
 	}
 
 	// Collide a probe shape against our test shape in various locations to verify active edge behavior
@@ -96,20 +95,20 @@ TEST_SUITE("ActiveEdgesTest")
 		settings.mActiveEdgeMode = inActiveEdgesOnly? EActiveEdgeMode::CollideOnlyWithActive : EActiveEdgeMode::CollideWithAll;
 
 		Ref<Shape> test_shape = inTestShape->Create().Get();
-		Ref<Shape> box = sCreateProbeBox();
+		Ref<Shape> capsule = sCreateProbeCapsule();
 
 		// Test hitting all active edges
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(-3.5f, 0, 0), { { Vec3(-3.5f, 0, 0), Vec3(1, 0, 0) } });
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(3.5f, 0, 0), { { Vec3(3.5f, 0, 0), Vec3(-1, 0, 0) } });
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(0, 0, -3.5f), { { Vec3(0, 0, -3.5f), Vec3(0, 0, 1) } });
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(0, 0, 3.5f), { { Vec3(0, 0, 3.5f), Vec3(0, 0, -1) } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(-3.5f, cCapsuleProbeOffset, 0), { { Vec3(-3.5f, 0, 0), Vec3(1, 0, 0) } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(3.5f, cCapsuleProbeOffset, 0), { { Vec3(3.5f, 0, 0), Vec3(-1, 0, 0) } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(0, cCapsuleProbeOffset, -3.5f), { { Vec3(0, 0, -3.5f), Vec3(0, 0, 1) } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(0, cCapsuleProbeOffset, 3.5f), { { Vec3(0, 0, 3.5f), Vec3(0, 0, -1) } });
 
 		// Test hitting internal edges, this should return two hits
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(-2.5f, 0, 0), { { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(-1, 0, 0) }, { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(1, 0, 0) } });
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(0, 0, -2.5f), { { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) }, { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(-2.5f, cCapsuleProbeOffset, 0), { { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(-1, 0, 0) }, { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(1, 0, 0) } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(0, cCapsuleProbeOffset, -2.5f), { { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) }, { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) } });
 
 		// Test hitting an interior diagonal, this should return two hits
-		sTestCollideShape(box, test_shape, inTestShapeScale, settings, Vec3(-3.0f, 0, 0), { { Vec3(-3.0f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : (inTestShapeScale * Vec3(1, 0, -1)).Normalized() }, { Vec3(-3.0f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : (inTestShapeScale * Vec3(-1, 0, 1)).Normalized() } });
+		sTestCollideShape(capsule, test_shape, inTestShapeScale, settings, Vec3(-3.0f, cCapsuleProbeOffset, 0), { { Vec3(-3.0f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : (inTestShapeScale * Vec3(1, 0, -1)).Normalized() }, { Vec3(-3.0f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : (inTestShapeScale * Vec3(-1, 0, 1)).Normalized() } });
 	}
 
 	TEST_CASE("CollideShapeMesh")
@@ -145,7 +144,7 @@ TEST_SUITE("ActiveEdgesTest")
 		ShapeCast shape_cast(inProbeShape, Vec3::sReplicate(1.0f), Mat44::sTranslation(inProbeShapePos), inProbeShapeDirection);
 		CollisionDispatch::sCastShapeVsShape(shape_cast, inSettings, inTestShape, inTestShapeScale, ShapeFilter(), Mat44::sIdentity(), SubShapeIDCreator(), SubShapeIDCreator(), collector);
 
-		sCheckMatch(collector.mHits, inExpectedHits);
+		sCheckMatch(collector.mHits, inExpectedHits, 1.0e-6f);
 	}
 
 	// Cast a probe shape against our test shape in various locations to verify active edge behavior
@@ -156,17 +155,17 @@ TEST_SUITE("ActiveEdgesTest")
 		settings.mReturnDeepestPoint = true;
 
 		Ref<Shape> test_shape = inTestShape->Create().Get();
-		Ref<Shape> box = sCreateProbeBox();
+		Ref<Shape> capsule = sCreateProbeCapsule();
 
 		// Test hitting all active edges
-		sTestCastShape(box, test_shape, inTestShapeScale, settings, Vec3(-4, 0, 0), Vec3(0.5f, 0, 0), { { Vec3(-3.5f, 0, 0), Vec3(1, 0, 0) } });
-		sTestCastShape(box, test_shape, inTestShapeScale, settings, Vec3(4, 0, 0), Vec3(-0.5f, 0, 0), { { Vec3(3.5f, 0, 0), Vec3(-1, 0, 0) } });
-		sTestCastShape(box, test_shape, inTestShapeScale, settings, Vec3(0, 0, -4), Vec3(0, 0, 0.5f), { { Vec3(0, 0, -3.5f), Vec3(0, 0, 1) } });
-		sTestCastShape(box, test_shape, inTestShapeScale, settings, Vec3(0, 0, 4), Vec3(0, 0, -0.5f), { { Vec3(0, 0, 3.5f), Vec3(0, 0, -1) } });
+		sTestCastShape(capsule, test_shape, inTestShapeScale, settings, Vec3(-4, cCapsuleProbeOffset, 0), Vec3(0.5f, 0, 0), { { Vec3(-3.5f, 0, 0), Vec3(1, 0, 0) } });
+		sTestCastShape(capsule, test_shape, inTestShapeScale, settings, Vec3(4, cCapsuleProbeOffset, 0), Vec3(-0.5f, 0, 0), { { Vec3(3.5f, 0, 0), Vec3(-1, 0, 0) } });
+		sTestCastShape(capsule, test_shape, inTestShapeScale, settings, Vec3(0, cCapsuleProbeOffset, -4), Vec3(0, 0, 0.5f), { { Vec3(0, 0, -3.5f), Vec3(0, 0, 1) } });
+		sTestCastShape(capsule, test_shape, inTestShapeScale, settings, Vec3(0, cCapsuleProbeOffset, 4), Vec3(0, 0, -0.5f), { { Vec3(0, 0, 3.5f), Vec3(0, 0, -1) } });
 
 		// Test hitting internal edges, this should return two hits
-		sTestCastShape(box, test_shape, inTestShapeScale, settings, Vec3(-2.5f - 1.1f * cBoxSize, 0, 0), Vec3(0.2f * cBoxSize, 0, 0), { { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(-1, 0, 0) }, { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(1, 0, 0) } });
-		sTestCastShape(box, test_shape, inTestShapeScale, settings, Vec3(0, 0, -2.5f - 1.1f * cBoxSize), Vec3(0, 0, 0.2f * cBoxSize), { { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) }, { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) } });
+		sTestCastShape(capsule, test_shape, inTestShapeScale, settings, Vec3(-2.5f - 1.1f * cCapsuleRadius, cCapsuleProbeOffset, 0), Vec3(0.2f * cCapsuleRadius, 0, 0), { { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(-1, 0, 0) }, { Vec3(-2.5f, 0, 0), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(1, 0, 0) } });
+		sTestCastShape(capsule, test_shape, inTestShapeScale, settings, Vec3(0, cCapsuleProbeOffset, -2.5f - 1.1f * cCapsuleRadius), Vec3(0, 0, 0.2f * cCapsuleRadius), { { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) }, { Vec3(0, 0, -2.5f), inActiveEdgesOnly? Vec3(0, -1, 0) : Vec3(0, 0, -1) } });
 	}
 
 	TEST_CASE("CastShapeMesh")

+ 175 - 0
UnitTests/Physics/HeightFieldShapeTests.cpp

@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include "UnitTestFramework.h"
+#include "PhysicsTestContext.h"
+#include <Physics/Collision/RayCast.h>
+#include <Physics/Collision/CastResult.h>
+#include <Physics/Collision/Shape/HeightFieldShape.h>
+#include <Physics/Collision/PhysicsMaterialSimple.h>
+
+TEST_SUITE("HeightFieldShapeTests")
+{
+	static void sRandomizeMaterials(HeightFieldShapeSettings &ioSettings, uint inMaxMaterials)
+	{
+		// Create materials
+		for (uint i = 0; i < inMaxMaterials; ++i)
+			ioSettings.mMaterials.push_back(new PhysicsMaterialSimple("Material " + ConvertToString(i), Color::sGetDistinctColor(i)));
+		
+		if (inMaxMaterials > 1)
+		{
+			// Make random material indices
+			UnitTestRandom random;
+			uniform_int_distribution<uint> index_distribution(0, inMaxMaterials - 1);
+			ioSettings.mMaterialIndices.resize(Square(ioSettings.mSampleCount - 1));
+			for (uint y = 0; y < ioSettings.mSampleCount - 1; ++y)
+				for (uint x = 0; x < ioSettings.mSampleCount - 1; ++x)
+					ioSettings.mMaterialIndices[y * (ioSettings.mSampleCount - 1) + x] = uint8(index_distribution(random));
+		}
+	}
+
+	static void sValidateGetPosition(const HeightFieldShapeSettings &inSettings, float inMaxError)
+	{
+		// Create shape
+		Ref<HeightFieldShape> shape = static_cast<HeightFieldShape *>(inSettings.Create().Get().GetPtr());
+
+		// Validate it
+		float max_diff = -1.0f;
+		for (uint y = 0; y < inSettings.mSampleCount; ++y)
+			for (uint x = 0; x < inSettings.mSampleCount; ++x)
+			{
+				// Perform a raycast from above the terrain on this location
+				RayCast ray { inSettings.mOffset + inSettings.mScale * Vec3((float)x, 100.0f, (float)y), inSettings.mScale.GetY() * Vec3(0, -200, 0) };
+				RayCastResult hit;
+				shape->CastRay(ray, SubShapeIDCreator(), hit);
+
+				// Get original (unscaled) height
+				float height = inSettings.mHeightSamples[y * inSettings.mSampleCount + x];
+				if (height != HeightFieldShapeConstants::cNoCollisionValue)
+				{
+					// Check there is collision
+					CHECK(!shape->IsNoCollision(x, y));
+
+					// Calculate position
+					Vec3 original_pos = inSettings.mOffset + inSettings.mScale * Vec3((float)x, height, (float)y);
+
+					// Calculate position from the shape
+					Vec3 shape_pos = shape->GetPosition(x, y);
+
+					// Calculate delta
+					float diff = (original_pos - shape_pos).Length();
+					max_diff = max(max_diff, diff);
+
+					// Materials are defined on the triangle, not on the sample points
+					if (x < inSettings.mSampleCount - 1 && y < inSettings.mSampleCount - 1)
+					{
+						const PhysicsMaterial *m1 = PhysicsMaterial::sDefault;
+						if (!inSettings.mMaterialIndices.empty())
+							m1 = inSettings.mMaterials[inSettings.mMaterialIndices[y * (inSettings.mSampleCount - 1) + x]];
+						else if (!inSettings.mMaterials.empty())
+							m1 = inSettings.mMaterials.front();
+
+						const PhysicsMaterial *m2 = shape->GetMaterial(x, y);
+						CHECK(m1 == m2);
+					}
+
+					// Don't test borders, the ray may or may not hit
+					if (x > 0 && y > 0 && x < inSettings.mSampleCount - 1 && y < inSettings.mSampleCount - 1)
+					{
+						// Check that the ray hit the terrain
+						Vec3 hit_pos = ray.mOrigin + ray.mDirection * hit.mFraction;
+						CHECK_APPROX_EQUAL(hit_pos, shape_pos, 1.0e-3f);
+					}
+				}
+				else
+				{
+					// Should be no collision here
+					CHECK(shape->IsNoCollision(x, y));
+
+					// Ray should not have given a hit
+					CHECK(hit.mFraction > 1.0f);
+				}
+			}
+
+		// Check error
+		CHECK(max_diff <= inMaxError);
+	}
+
+	TEST_CASE("TestPlane")
+	{
+		// Create flat plane with offset and scale
+		HeightFieldShapeSettings settings;
+		settings.mOffset = Vec3(3, 5, 7);
+		settings.mScale = Vec3(9, 13, 17);
+		settings.mSampleCount = 32;
+		settings.mBitsPerSample = 1;
+		settings.mBlockSize = 4;
+		settings.mHeightSamples.resize(Square(settings.mSampleCount));
+		for (float &h : settings.mHeightSamples)
+			h = 1.0f;
+
+		// Make some random holes
+		UnitTestRandom random;
+		uniform_int_distribution<uint> index_distribution(0, (uint)settings.mHeightSamples.size() - 1);
+		for (int i = 0; i < 10; ++i)
+			settings.mHeightSamples[index_distribution(random)] = HeightFieldShapeConstants::cNoCollisionValue;
+
+		// We should be able to encode a flat plane in 1 bit
+		CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
+
+		sRandomizeMaterials(settings, 256);
+		sValidateGetPosition(settings, 0.0f);
+	}
+	
+	TEST_CASE("TestPlaneCloseToOrigin")
+	{
+		// Create flat plane very close to origin, this tests that we don't introduce a quantization error on a flat plane
+		HeightFieldShapeSettings settings;
+		settings.mSampleCount = 32;
+		settings.mBitsPerSample = 1;
+		settings.mBlockSize = 4;
+		settings.mHeightSamples.resize(Square(settings.mSampleCount));
+		for (float &h : settings.mHeightSamples)
+			h = 1.0e-6f;
+
+		// We should be able to encode a flat plane in 1 bit
+		CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
+
+		sRandomizeMaterials(settings, 50);
+		sValidateGetPosition(settings, 0.0f);
+	}
+
+	TEST_CASE("TestRandomTerrain")
+	{
+		const float cMinHeight = -5.0f;
+		const float cMaxHeight = 10.0f;
+
+		UnitTestRandom random;
+		uniform_real_distribution<float> height_distribution(cMinHeight, cMaxHeight);
+
+		// Create terrain with random samples
+		HeightFieldShapeSettings settings;
+		settings.mOffset = Vec3(0.3f, 0.5f, 0.7f);
+		settings.mScale = Vec3(1.1f, 1.2f, 1.3f);
+		settings.mSampleCount = 32;
+		settings.mBitsPerSample = 8;
+		settings.mBlockSize = 4;
+		settings.mHeightSamples.resize(Square(settings.mSampleCount));
+		for (float &h : settings.mHeightSamples)
+			h = height_distribution(random);
+
+		// Check if bits per sample is ok
+		for (uint32 bits_per_sample = 1; bits_per_sample <= 8; ++bits_per_sample)
+		{
+			// Calculate maximum error you can get if you quantize using bits_per_sample.
+			// We ignore the fact that we have range blocks that give much better compression, although
+			// with random input data there shouldn't be much benefit of that.
+			float max_error = 0.5f * (cMaxHeight - cMinHeight) / ((1 << bits_per_sample) - 1);
+			uint32 calculated_bits_per_sample = settings.CalculateBitsPerSampleForError(max_error);
+			CHECK(calculated_bits_per_sample <= bits_per_sample);
+		}
+
+		sRandomizeMaterials(settings, 1);
+		sValidateGetPosition(settings, settings.mScale.GetY() * (cMaxHeight - cMinHeight) / ((1 << settings.mBitsPerSample) - 1));
+	}
+}

+ 12 - 11
UnitTests/UnitTests.cmake

@@ -6,6 +6,16 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Core/FPFlushDenormalsTest.cpp
 	${UNIT_TESTS_ROOT}/Core/JobSystemTest.cpp
 	${UNIT_TESTS_ROOT}/Core/LinearCurveTest.cpp
+	${UNIT_TESTS_ROOT}/doctest.h
+	${UNIT_TESTS_ROOT}/Geometry/ConvexHullBuilderTest.cpp
+	${UNIT_TESTS_ROOT}/Geometry/EllipseTest.cpp
+	${UNIT_TESTS_ROOT}/Geometry/EPATests.cpp
+	${UNIT_TESTS_ROOT}/Geometry/GJKTests.cpp
+	${UNIT_TESTS_ROOT}/Geometry/PlaneTests.cpp
+	${UNIT_TESTS_ROOT}/Geometry/RayAABoxTests.cpp
+	${UNIT_TESTS_ROOT}/Layers.h
+	${UNIT_TESTS_ROOT}/LoggingBodyActivationListener.h
+	${UNIT_TESTS_ROOT}/LoggingContactListener.h
 	${UNIT_TESTS_ROOT}/Math/DVec3Tests.cpp
 	${UNIT_TESTS_ROOT}/Math/HalfFloatTests.cpp
 	${UNIT_TESTS_ROOT}/Math/Mat44Tests.cpp
@@ -16,12 +26,6 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Math/Vec3Tests.cpp
 	${UNIT_TESTS_ROOT}/Math/Vec4Tests.cpp
 	${UNIT_TESTS_ROOT}/Math/VectorTests.cpp
-	${UNIT_TESTS_ROOT}/Geometry/ConvexHullBuilderTest.cpp
-	${UNIT_TESTS_ROOT}/Geometry/EllipseTest.cpp
-	${UNIT_TESTS_ROOT}/Geometry/EPATests.cpp
-	${UNIT_TESTS_ROOT}/Geometry/GJKTests.cpp
-	${UNIT_TESTS_ROOT}/Geometry/PlaneTests.cpp
-	${UNIT_TESTS_ROOT}/Geometry/RayAABoxTests.cpp
 	${UNIT_TESTS_ROOT}/ObjectStream/ObjectStreamTest.cpp
 	${UNIT_TESTS_ROOT}/Physics/ActiveEdgesTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/BroadPhaseTests.cpp
@@ -29,11 +33,12 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Physics/CollideShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/CollisionGroupTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/ContactListenerTests.cpp
+	${UNIT_TESTS_ROOT}/Physics/HeightFieldShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/MotionQualityLinearCastTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PathConstraintTests.cpp
-	${UNIT_TESTS_ROOT}/Physics/PhysicsTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PhysicsDeterminismTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/PhysicsStepListenerTests.cpp
+	${UNIT_TESTS_ROOT}/Physics/PhysicsTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/RayShapeTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/SensorTests.cpp
 	${UNIT_TESTS_ROOT}/Physics/ShapeTests.cpp
@@ -42,10 +47,6 @@ set(UNIT_TESTS_SRC_FILES
 	${UNIT_TESTS_ROOT}/Physics/TransformedShapeTests.cpp
 	${UNIT_TESTS_ROOT}/PhysicsTestContext.cpp
 	${UNIT_TESTS_ROOT}/PhysicsTestContext.h
-	${UNIT_TESTS_ROOT}/doctest.h
-	${UNIT_TESTS_ROOT}/Layers.h
-	${UNIT_TESTS_ROOT}/LoggingBodyActivationListener.h
-	${UNIT_TESTS_ROOT}/LoggingContactListener.h
 	${UNIT_TESTS_ROOT}/UnitTestFramework.cpp
 	${UNIT_TESTS_ROOT}/UnitTestFramework.h
 	${UNIT_TESTS_ROOT}/UnitTests.cmake

Деякі файли не було показано, через те що забагато файлів було змінено