HeightFieldShapeTests.cpp 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  2. // SPDX-License-Identifier: MIT
  3. #include "UnitTestFramework.h"
  4. #include "PhysicsTestContext.h"
  5. #include <Jolt/Physics/Collision/RayCast.h>
  6. #include <Jolt/Physics/Collision/CastResult.h>
  7. #include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
  8. #include <Jolt/Physics/Collision/PhysicsMaterialSimple.h>
  9. TEST_SUITE("HeightFieldShapeTests")
  10. {
  11. static void sRandomizeMaterials(HeightFieldShapeSettings &ioSettings, uint inMaxMaterials)
  12. {
  13. // Create materials
  14. for (uint i = 0; i < inMaxMaterials; ++i)
  15. ioSettings.mMaterials.push_back(new PhysicsMaterialSimple("Material " + ConvertToString(i), Color::sGetDistinctColor(i)));
  16. if (inMaxMaterials > 1)
  17. {
  18. // Make random material indices
  19. UnitTestRandom random;
  20. uniform_int_distribution<uint> index_distribution(0, inMaxMaterials - 1);
  21. ioSettings.mMaterialIndices.resize(Square(ioSettings.mSampleCount - 1));
  22. for (uint y = 0; y < ioSettings.mSampleCount - 1; ++y)
  23. for (uint x = 0; x < ioSettings.mSampleCount - 1; ++x)
  24. ioSettings.mMaterialIndices[y * (ioSettings.mSampleCount - 1) + x] = uint8(index_distribution(random));
  25. }
  26. }
  27. static Ref<HeightFieldShape> sValidateGetPosition(const HeightFieldShapeSettings &inSettings, float inMaxError)
  28. {
  29. // Create shape
  30. Ref<HeightFieldShape> shape = static_cast<HeightFieldShape *>(inSettings.Create().Get().GetPtr());
  31. // Validate it
  32. float max_diff = -1.0f;
  33. for (uint y = 0; y < inSettings.mSampleCount; ++y)
  34. for (uint x = 0; x < inSettings.mSampleCount; ++x)
  35. {
  36. // Perform a raycast from above the height field on this location
  37. RayCast ray { inSettings.mOffset + inSettings.mScale * Vec3((float)x, 100.0f, (float)y), inSettings.mScale.GetY() * Vec3(0, -200, 0) };
  38. RayCastResult hit;
  39. shape->CastRay(ray, SubShapeIDCreator(), hit);
  40. // Get original (unscaled) height
  41. float height = inSettings.mHeightSamples[y * inSettings.mSampleCount + x];
  42. if (height != HeightFieldShapeConstants::cNoCollisionValue)
  43. {
  44. // Check there is collision
  45. CHECK(!shape->IsNoCollision(x, y));
  46. // Calculate position
  47. Vec3 original_pos = inSettings.mOffset + inSettings.mScale * Vec3((float)x, height, (float)y);
  48. // Calculate position from the shape
  49. Vec3 shape_pos = shape->GetPosition(x, y);
  50. // Calculate delta
  51. float diff = (original_pos - shape_pos).Length();
  52. max_diff = max(max_diff, diff);
  53. // Materials are defined on the triangle, not on the sample points
  54. if (x < inSettings.mSampleCount - 1 && y < inSettings.mSampleCount - 1)
  55. {
  56. const PhysicsMaterial *m1 = PhysicsMaterial::sDefault;
  57. if (!inSettings.mMaterialIndices.empty())
  58. m1 = inSettings.mMaterials[inSettings.mMaterialIndices[y * (inSettings.mSampleCount - 1) + x]];
  59. else if (!inSettings.mMaterials.empty())
  60. m1 = inSettings.mMaterials.front();
  61. const PhysicsMaterial *m2 = shape->GetMaterial(x, y);
  62. CHECK(m1 == m2);
  63. }
  64. // Don't test borders, the ray may or may not hit
  65. if (x > 0 && y > 0 && x < inSettings.mSampleCount - 1 && y < inSettings.mSampleCount - 1)
  66. {
  67. // Check that the ray hit the height field
  68. Vec3 hit_pos = ray.mOrigin + ray.mDirection * hit.mFraction;
  69. CHECK_APPROX_EQUAL(hit_pos, shape_pos, 1.0e-3f);
  70. }
  71. }
  72. else
  73. {
  74. // Should be no collision here
  75. CHECK(shape->IsNoCollision(x, y));
  76. // Ray should not have given a hit
  77. CHECK(hit.mFraction > 1.0f);
  78. }
  79. }
  80. // Check error
  81. CHECK(max_diff <= inMaxError);
  82. return shape;
  83. }
  84. TEST_CASE("TestPlane")
  85. {
  86. // Create flat plane with offset and scale
  87. HeightFieldShapeSettings settings;
  88. settings.mOffset = Vec3(3, 5, 7);
  89. settings.mScale = Vec3(9, 13, 17);
  90. settings.mSampleCount = 32;
  91. settings.mBitsPerSample = 1;
  92. settings.mBlockSize = 4;
  93. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  94. for (float &h : settings.mHeightSamples)
  95. h = 1.0f;
  96. // Make some random holes
  97. UnitTestRandom random;
  98. uniform_int_distribution<uint> index_distribution(0, (uint)settings.mHeightSamples.size() - 1);
  99. for (int i = 0; i < 10; ++i)
  100. settings.mHeightSamples[index_distribution(random)] = HeightFieldShapeConstants::cNoCollisionValue;
  101. // We should be able to encode a flat plane in 1 bit
  102. CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
  103. sRandomizeMaterials(settings, 256);
  104. sValidateGetPosition(settings, 0.0f);
  105. }
  106. TEST_CASE("TestPlaneCloseToOrigin")
  107. {
  108. // Create flat plane very close to origin, this tests that we don't introduce a quantization error on a flat plane
  109. HeightFieldShapeSettings settings;
  110. settings.mSampleCount = 32;
  111. settings.mBitsPerSample = 1;
  112. settings.mBlockSize = 4;
  113. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  114. for (float &h : settings.mHeightSamples)
  115. h = 1.0e-6f;
  116. // We should be able to encode a flat plane in 1 bit
  117. CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
  118. sRandomizeMaterials(settings, 50);
  119. sValidateGetPosition(settings, 0.0f);
  120. }
  121. TEST_CASE("TestRandomHeightField")
  122. {
  123. const float cMinHeight = -5.0f;
  124. const float cMaxHeight = 10.0f;
  125. UnitTestRandom random;
  126. uniform_real_distribution<float> height_distribution(cMinHeight, cMaxHeight);
  127. // Create height field with random samples
  128. HeightFieldShapeSettings settings;
  129. settings.mOffset = Vec3(0.3f, 0.5f, 0.7f);
  130. settings.mScale = Vec3(1.1f, 1.2f, 1.3f);
  131. settings.mSampleCount = 32;
  132. settings.mBitsPerSample = 8;
  133. settings.mBlockSize = 4;
  134. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  135. for (float &h : settings.mHeightSamples)
  136. h = height_distribution(random);
  137. // Check if bits per sample is ok
  138. for (uint32 bits_per_sample = 1; bits_per_sample <= 8; ++bits_per_sample)
  139. {
  140. // Calculate maximum error you can get if you quantize using bits_per_sample.
  141. // We ignore the fact that we have range blocks that give much better compression, although
  142. // with random input data there shouldn't be much benefit of that.
  143. float max_error = 0.5f * (cMaxHeight - cMinHeight) / ((1 << bits_per_sample) - 1);
  144. uint32 calculated_bits_per_sample = settings.CalculateBitsPerSampleForError(max_error);
  145. CHECK(calculated_bits_per_sample <= bits_per_sample);
  146. }
  147. sRandomizeMaterials(settings, 1);
  148. sValidateGetPosition(settings, settings.mScale.GetY() * (cMaxHeight - cMinHeight) / ((1 << settings.mBitsPerSample) - 1));
  149. }
  150. TEST_CASE("TestEmptyHeightField")
  151. {
  152. // Create height field with no collision
  153. HeightFieldShapeSettings settings;
  154. settings.mSampleCount = 32;
  155. settings.mHeightSamples.resize(Square(settings.mSampleCount));
  156. for (float &h : settings.mHeightSamples)
  157. h = HeightFieldShapeConstants::cNoCollisionValue;
  158. // This should use the minimum amount of bits
  159. CHECK(settings.CalculateBitsPerSampleForError(0.0f) == 1);
  160. sRandomizeMaterials(settings, 50);
  161. Ref<HeightFieldShape> shape = sValidateGetPosition(settings, 0.0f);
  162. // Check that we allocated the minimum amount of memory
  163. Shape::Stats stats = shape->GetStats();
  164. CHECK(stats.mNumTriangles == 0);
  165. CHECK(stats.mSizeBytes == sizeof(HeightFieldShape));
  166. }
  167. }