HeightFieldShapeTests.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
  2. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  3. // SPDX-License-Identifier: MIT
  4. #include "UnitTestFramework.h"
  5. #include "PhysicsTestContext.h"
  6. #include <Jolt/Physics/Collision/RayCast.h>
  7. #include <Jolt/Physics/Collision/CastResult.h>
  8. #include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
  9. #include <Jolt/Physics/Collision/PhysicsMaterialSimple.h>
  10. TEST_SUITE("HeightFieldShapeTests")
  11. {
  12. static void sRandomizeMaterials(HeightFieldShapeSettings &ioSettings, uint inMaxMaterials)
  13. {
  14. // Create materials
  15. for (uint i = 0; i < inMaxMaterials; ++i)
  16. ioSettings.mMaterials.push_back(new PhysicsMaterialSimple("Material " + ConvertToString(i), Color::sGetDistinctColor(i)));
  17. if (inMaxMaterials > 1)
  18. {
  19. // Make random material indices
  20. UnitTestRandom random;
  21. uniform_int_distribution<uint> index_distribution(0, inMaxMaterials - 1);
  22. ioSettings.mMaterialIndices.resize(Square(ioSettings.mSampleCount - 1));
  23. for (uint y = 0; y < ioSettings.mSampleCount - 1; ++y)
  24. for (uint x = 0; x < ioSettings.mSampleCount - 1; ++x)
  25. ioSettings.mMaterialIndices[y * (ioSettings.mSampleCount - 1) + x] = uint8(index_distribution(random));
  26. }
  27. }
  28. static Ref<HeightFieldShape> sValidateGetPosition(const HeightFieldShapeSettings &inSettings, float inMaxError)
  29. {
  30. // Create shape
  31. Ref<HeightFieldShape> shape = static_cast<HeightFieldShape *>(inSettings.Create().Get().GetPtr());
  32. // Validate it
  33. float max_diff = -1.0f;
  34. for (uint y = 0; y < inSettings.mSampleCount; ++y)
  35. for (uint x = 0; x < inSettings.mSampleCount; ++x)
  36. {
  37. // Perform a raycast from above the height field on this location
  38. RayCast ray { inSettings.mOffset + inSettings.mScale * Vec3((float)x, 100.0f, (float)y), inSettings.mScale.GetY() * Vec3(0, -200, 0) };
  39. RayCastResult hit;
  40. shape->CastRay(ray, SubShapeIDCreator(), hit);
  41. // Get original (unscaled) height
  42. float height = inSettings.mHeightSamples[y * inSettings.mSampleCount + x];
  43. if (height != HeightFieldShapeConstants::cNoCollisionValue)
  44. {
  45. // Check there is collision
  46. CHECK(!shape->IsNoCollision(x, y));
  47. // Calculate position
  48. Vec3 original_pos = inSettings.mOffset + inSettings.mScale * Vec3((float)x, height, (float)y);
  49. // Calculate position from the shape
  50. Vec3 shape_pos = shape->GetPosition(x, y);
  51. // Calculate delta
  52. float diff = (original_pos - shape_pos).Length();
  53. max_diff = max(max_diff, diff);
  54. // Materials are defined on the triangle, not on the sample points
  55. if (x < inSettings.mSampleCount - 1 && y < inSettings.mSampleCount - 1)
  56. {
  57. const PhysicsMaterial *m1 = PhysicsMaterial::sDefault;
  58. if (!inSettings.mMaterialIndices.empty())
  59. m1 = inSettings.mMaterials[inSettings.mMaterialIndices[y * (inSettings.mSampleCount - 1) + x]];
  60. else if (!inSettings.mMaterials.empty())
  61. m1 = inSettings.mMaterials.front();
  62. const PhysicsMaterial *m2 = shape->GetMaterial(x, y);
  63. CHECK(m1 == m2);
  64. }
  65. // Don't test borders, the ray may or may not hit
  66. if (x > 0 && y > 0 && x < inSettings.mSampleCount - 1 && y < inSettings.mSampleCount - 1)
  67. {
  68. // Check that the ray hit the height field
  69. Vec3 hit_pos = ray.GetPointOnRay(hit.mFraction);
  70. CHECK_APPROX_EQUAL(hit_pos, shape_pos, 1.0e-3f);
  71. }
  72. }
  73. else
  74. {
  75. // Should be no collision here
  76. CHECK(shape->IsNoCollision(x, y));
  77. // Ray should not have given a hit
  78. CHECK(hit.mFraction > 1.0f);
  79. }
  80. }
  81. // Check error
  82. CHECK(max_diff <= inMaxError);
  83. return shape;
  84. }
  85. TEST_CASE("TestPlane")
  86. {
  87. // Create flat plane with offset and scale
  88. HeightFieldShapeSettings settings;
  89. settings.mOffset = Vec3(3, 5, 7);
  90. settings.mScale = Vec3(9, 13, 17);
  91. settings.mSampleCount = 32;
  92. settings.mBitsPerSample = 1;
  93. settings.mBlockSize = 4;
  94. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  95. for (float &h : settings.mHeightSamples)
  96. h = 1.0f;
  97. // Make some random holes
  98. UnitTestRandom random;
  99. uniform_int_distribution<uint> index_distribution(0, (uint)settings.mHeightSamples.size() - 1);
  100. for (int i = 0; i < 10; ++i)
  101. settings.mHeightSamples[index_distribution(random)] = HeightFieldShapeConstants::cNoCollisionValue;
  102. // We should be able to encode a flat plane in 1 bit
  103. CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
  104. sRandomizeMaterials(settings, 256);
  105. sValidateGetPosition(settings, 0.0f);
  106. }
  107. TEST_CASE("TestPlaneCloseToOrigin")
  108. {
  109. // Create flat plane very close to origin, this tests that we don't introduce a quantization error on a flat plane
  110. HeightFieldShapeSettings settings;
  111. settings.mSampleCount = 32;
  112. settings.mBitsPerSample = 1;
  113. settings.mBlockSize = 4;
  114. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  115. for (float &h : settings.mHeightSamples)
  116. h = 1.0e-6f;
  117. // We should be able to encode a flat plane in 1 bit
  118. CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
  119. sRandomizeMaterials(settings, 50);
  120. sValidateGetPosition(settings, 0.0f);
  121. }
  122. TEST_CASE("TestRandomHeightField")
  123. {
  124. const float cMinHeight = -5.0f;
  125. const float cMaxHeight = 10.0f;
  126. UnitTestRandom random;
  127. uniform_real_distribution<float> height_distribution(cMinHeight, cMaxHeight);
  128. // Create height field with random samples
  129. HeightFieldShapeSettings settings;
  130. settings.mOffset = Vec3(0.3f, 0.5f, 0.7f);
  131. settings.mScale = Vec3(1.1f, 1.2f, 1.3f);
  132. settings.mSampleCount = 32;
  133. settings.mBitsPerSample = 8;
  134. settings.mBlockSize = 4;
  135. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  136. for (float &h : settings.mHeightSamples)
  137. h = height_distribution(random);
  138. // Check if bits per sample is ok
  139. for (uint32 bits_per_sample = 1; bits_per_sample <= 8; ++bits_per_sample)
  140. {
  141. // Calculate maximum error you can get if you quantize using bits_per_sample.
  142. // We ignore the fact that we have range blocks that give much better compression, although
  143. // with random input data there shouldn't be much benefit of that.
  144. float max_error = 0.5f * (cMaxHeight - cMinHeight) / ((1 << bits_per_sample) - 1);
  145. uint32 calculated_bits_per_sample = settings.CalculateBitsPerSampleForError(max_error);
  146. CHECK(calculated_bits_per_sample <= bits_per_sample);
  147. }
  148. sRandomizeMaterials(settings, 1);
  149. sValidateGetPosition(settings, settings.mScale.GetY() * (cMaxHeight - cMinHeight) / ((1 << settings.mBitsPerSample) - 1));
  150. }
  151. TEST_CASE("TestEmptyHeightField")
  152. {
  153. // Create height field with no collision
  154. HeightFieldShapeSettings settings;
  155. settings.mSampleCount = 32;
  156. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  157. for (float &h : settings.mHeightSamples)
  158. h = HeightFieldShapeConstants::cNoCollisionValue;
  159. // This should use the minimum amount of bits
  160. CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
  161. sRandomizeMaterials(settings, 50);
  162. Ref<HeightFieldShape> shape = sValidateGetPosition(settings, 0.0f);
  163. // Check that we allocated the minimum amount of memory
  164. Shape::Stats stats = shape->GetStats();
  165. CHECK(stats.mNumTriangles == 0);
  166. CHECK(stats.mSizeBytes == sizeof(HeightFieldShape));
  167. }
  168. TEST_CASE("TestGetHeights")
  169. {
  170. const float cMinHeight = -5.0f;
  171. const float cMaxHeight = 10.0f;
  172. const uint cSampleCount = 32;
  173. const uint cNoCollisionIndex = 10;
  174. UnitTestRandom random;
  175. uniform_real_distribution<float> height_distribution(cMinHeight, cMaxHeight);
  176. // Create height field with random samples
  177. HeightFieldShapeSettings settings;
  178. settings.mOffset = Vec3(0.3f, 0.5f, 0.7f);
  179. settings.mScale = Vec3(1.1f, 1.2f, 1.3f);
  180. settings.mSampleCount = cSampleCount;
  181. settings.mBitsPerSample = 8;
  182. settings.mBlockSize = 4;
  183. settings.mHeightSamples.resize(Square(cSampleCount));
  184. for (float &h : settings.mHeightSamples)
  185. h = height_distribution(random);
  186. // Add 1 sample that has no collision
  187. settings.mHeightSamples[cNoCollisionIndex] = HeightFieldShapeConstants::cNoCollisionValue;
  188. // Create shape
  189. ShapeRefC shape = settings.Create().Get();
  190. const HeightFieldShape *height_field = static_cast<const HeightFieldShape *>(shape.GetPtr());
  191. {
  192. // Check that the GetHeights function returns the same values as the original height samples
  193. Array<float> sampled_heights;
  194. sampled_heights.resize(Square(cSampleCount));
  195. height_field->GetHeights(0, 0, cSampleCount, cSampleCount, sampled_heights.data(), cSampleCount);
  196. for (uint i = 0; i < Square(cSampleCount); ++i)
  197. if (i == cNoCollisionIndex)
  198. CHECK(sampled_heights[i] == HeightFieldShapeConstants::cNoCollisionValue);
  199. else
  200. CHECK_APPROX_EQUAL(sampled_heights[i], settings.mOffset.GetY() + settings.mScale.GetY() * settings.mHeightSamples[i], 0.05f);
  201. }
  202. {
  203. // With a random height field the max error is going to be limited by the amount of bits we have per sample as we will not get any benefit from a reduced range per block
  204. float tolerance = (cMaxHeight - cMinHeight) / ((1 << settings.mBitsPerSample) - 2);
  205. // Check a sub rect of the height field
  206. uint sx = 4, sy = 8, cx = 16, cy = 8;
  207. Array<float> sampled_heights;
  208. sampled_heights.resize(cx * cy);
  209. height_field->GetHeights(sx, sy, cx, cy, sampled_heights.data(), cx);
  210. for (uint y = 0; y < cy; ++y)
  211. for (uint x = 0; x < cx; ++x)
  212. CHECK_APPROX_EQUAL(sampled_heights[y * cx + x], settings.mOffset.GetY() + settings.mScale.GetY() * settings.mHeightSamples[(sy + y) * cSampleCount + sx + x], tolerance);
  213. }
  214. }
  215. TEST_CASE("TestSetHeights")
  216. {
  217. const float cMinHeight = -5.0f;
  218. const float cMaxHeight = 10.0f;
  219. const uint cSampleCount = 32;
  220. UnitTestRandom random;
  221. uniform_real_distribution<float> height_distribution(cMinHeight, cMaxHeight);
  222. // Create height field with random samples
  223. HeightFieldShapeSettings settings;
  224. settings.mOffset = Vec3(0.3f, 0.5f, 0.7f);
  225. settings.mScale = Vec3(1.1f, 1.2f, 1.3f);
  226. settings.mSampleCount = cSampleCount;
  227. settings.mBitsPerSample = 8;
  228. settings.mBlockSize = 4;
  229. settings.mHeightSamples.resize(Square(cSampleCount));
  230. settings.mMinHeightValue = cMinHeight;
  231. settings.mMaxHeightValue = cMaxHeight;
  232. for (float &h : settings.mHeightSamples)
  233. h = height_distribution(random);
  234. // Create shape
  235. Ref<Shape> shape = settings.Create().Get();
  236. HeightFieldShape *height_field = static_cast<HeightFieldShape *>(shape.GetPtr());
  237. // Get the original (quantized) heights
  238. Array<float> original_heights;
  239. original_heights.resize(Square(cSampleCount));
  240. height_field->GetHeights(0, 0, cSampleCount, cSampleCount, original_heights.data(), cSampleCount);
  241. // Create new data for height field
  242. Array<float> patched_heights;
  243. uint sx = 4, sy = 16, cx = 16, cy = 8;
  244. patched_heights.resize(cx * cy);
  245. for (uint y = 0; y < cy; ++y)
  246. for (uint x = 0; x < cx; ++x)
  247. patched_heights[y * cx + x] = height_distribution(random);
  248. // Add 1 sample that has no collision
  249. uint no_collision_idx = (sy + 1) * cSampleCount + sx + 2;
  250. patched_heights[1 * cx + 2] = HeightFieldShapeConstants::cNoCollisionValue;
  251. // Update the height field
  252. TempAllocatorMalloc temp_allocator;
  253. height_field->SetHeights(sx, sy, cx, cy, patched_heights.data(), cx, temp_allocator);
  254. // With a random height field the max error is going to be limited by the amount of bits we have per sample as we will not get any benefit from a reduced range per block
  255. float tolerance = (cMaxHeight - cMinHeight) / ((1 << settings.mBitsPerSample) - 2);
  256. // Check a sub rect of the height field
  257. Array<float> verify_heights;
  258. verify_heights.resize(cSampleCount * cSampleCount);
  259. height_field->GetHeights(0, 0, cSampleCount, cSampleCount, verify_heights.data(), cSampleCount);
  260. for (uint y = 0; y < cSampleCount; ++y)
  261. for (uint x = 0; x < cSampleCount; ++x)
  262. {
  263. uint idx = y * cSampleCount + x;
  264. if (idx == no_collision_idx)
  265. CHECK(verify_heights[idx] == HeightFieldShapeConstants::cNoCollisionValue);
  266. else if (x >= sx && x < sx + cx && y >= sy && y < sy + cy)
  267. CHECK_APPROX_EQUAL(verify_heights[y * cSampleCount + x], patched_heights[(y - sy) * cx + x - sx], tolerance);
  268. else if (x >= sx - settings.mBlockSize && x < sx + cx && y >= sy - settings.mBlockSize && y < sy + cy)
  269. CHECK_APPROX_EQUAL(verify_heights[idx], original_heights[idx], tolerance); // We didn't modify this but it has been quantized again
  270. else
  271. CHECK(verify_heights[idx] == original_heights[idx]); // We didn't modify this and it is outside of the affected range
  272. }
  273. }
  274. }