pose_controller_test.cpp 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. #include "render/humanoid/humanoid_specs.h"
  2. #include "render/humanoid/pose_controller.h"
  3. #include "render/humanoid/rig.h"
  4. #include <QVector3D>
  5. #include <cmath>
  6. #include <gtest/gtest.h>
  7. using namespace Render::GL;
  8. class HumanoidPoseControllerTest : public ::testing::Test {
  9. protected:
  10. void SetUp() override {
  11. using HP = HumanProportions;
  12. // Initialize a default pose with basic standing configuration
  13. pose = HumanoidPose{};
  14. float const head_center_y = 0.5F * (HP::HEAD_TOP_Y + HP::CHIN_Y);
  15. float const half_shoulder = 0.5F * HP::SHOULDER_WIDTH;
  16. pose.headPos = QVector3D(0.0F, head_center_y, 0.0F);
  17. pose.headR = HP::HEAD_RADIUS;
  18. pose.neck_base = QVector3D(0.0F, HP::NECK_BASE_Y, 0.0F);
  19. pose.shoulderL = QVector3D(-half_shoulder, HP::SHOULDER_Y, 0.0F);
  20. pose.shoulderR = QVector3D(half_shoulder, HP::SHOULDER_Y, 0.0F);
  21. pose.pelvisPos = QVector3D(0.0F, HP::WAIST_Y, 0.0F);
  22. pose.handL = QVector3D(-0.05F, HP::SHOULDER_Y + 0.05F, 0.55F);
  23. pose.hand_r = QVector3D(0.15F, HP::SHOULDER_Y + 0.15F, 0.20F);
  24. pose.elbowL = QVector3D(-0.15F, HP::SHOULDER_Y - 0.15F, 0.25F);
  25. pose.elbowR = QVector3D(0.25F, HP::SHOULDER_Y - 0.10F, 0.10F);
  26. pose.knee_l = QVector3D(-0.10F, HP::KNEE_Y, 0.05F);
  27. pose.knee_r = QVector3D(0.10F, HP::KNEE_Y, -0.05F);
  28. pose.footL = QVector3D(-0.14F, 0.022F, 0.06F);
  29. pose.foot_r = QVector3D(0.14F, 0.022F, -0.06F);
  30. pose.footYOffset = 0.022F;
  31. // Initialize animation context with default idle state
  32. anim_ctx = HumanoidAnimationContext{};
  33. anim_ctx.inputs.time = 0.0F;
  34. anim_ctx.inputs.isMoving = false;
  35. anim_ctx.inputs.is_attacking = false;
  36. anim_ctx.variation = VariationParams::fromSeed(12345);
  37. anim_ctx.gait.state = HumanoidMotionState::Idle;
  38. }
  39. HumanoidPose pose;
  40. HumanoidAnimationContext anim_ctx;
  41. // Helper to check if a position is approximately equal
  42. bool approxEqual(const QVector3D &a, const QVector3D &b,
  43. float epsilon = 0.01F) {
  44. return std::abs(a.x() - b.x()) < epsilon &&
  45. std::abs(a.y() - b.y()) < epsilon &&
  46. std::abs(a.z() - b.z()) < epsilon;
  47. }
  48. };
  49. TEST_F(HumanoidPoseControllerTest, ConstructorInitializesCorrectly) {
  50. HumanoidPoseController controller(pose, anim_ctx);
  51. // Constructor should not modify the pose
  52. EXPECT_FLOAT_EQ(pose.headPos.y(), 0.5F * (HumanProportions::HEAD_TOP_Y +
  53. HumanProportions::CHIN_Y));
  54. EXPECT_FLOAT_EQ(pose.pelvisPos.y(), HumanProportions::WAIST_Y);
  55. }
  56. TEST_F(HumanoidPoseControllerTest, StandIdleDoesNotModifyPose) {
  57. HumanoidPoseController controller(pose, anim_ctx);
  58. QVector3D const original_pelvis = pose.pelvisPos;
  59. QVector3D const original_shoulder_l = pose.shoulderL;
  60. controller.standIdle();
  61. // standIdle should be a no-op, keeping pose unchanged
  62. EXPECT_TRUE(approxEqual(pose.pelvisPos, original_pelvis));
  63. EXPECT_TRUE(approxEqual(pose.shoulderL, original_shoulder_l));
  64. }
  65. TEST_F(HumanoidPoseControllerTest, KneelLowersPelvis) {
  66. HumanoidPoseController controller(pose, anim_ctx);
  67. float const original_pelvis_y = pose.pelvisPos.y();
  68. controller.kneel(0.5F);
  69. // Kneeling should lower the pelvis
  70. EXPECT_LT(pose.pelvisPos.y(), original_pelvis_y);
  71. // Pelvis should be lowered by approximately depth * 0.40F
  72. float const expected_offset = 0.5F * 0.40F;
  73. EXPECT_NEAR(pose.pelvisPos.y(), HumanProportions::WAIST_Y - expected_offset,
  74. 0.05F);
  75. }
  76. TEST_F(HumanoidPoseControllerTest, KneelFullDepthTouchesGroundWithKnee) {
  77. HumanoidPoseController controller(pose, anim_ctx);
  78. controller.kneel(1.0F);
  79. // At full kneel, left knee should be very close to ground
  80. EXPECT_NEAR(pose.knee_l.y(), HumanProportions::GROUND_Y + 0.07F, 0.02F);
  81. // Pelvis should be lowered significantly
  82. EXPECT_LT(pose.pelvisPos.y(), HumanProportions::WAIST_Y - 0.35F);
  83. }
  84. TEST_F(HumanoidPoseControllerTest, KneelZeroDepthKeepsStanding) {
  85. HumanoidPoseController controller(pose, anim_ctx);
  86. float const original_pelvis_y = pose.pelvisPos.y();
  87. controller.kneel(0.0F);
  88. // Zero depth should keep pelvis at original height
  89. EXPECT_NEAR(pose.pelvisPos.y(), original_pelvis_y, 0.01F);
  90. }
  91. TEST_F(HumanoidPoseControllerTest, LeanMovesUpperBody) {
  92. HumanoidPoseController controller(pose, anim_ctx);
  93. QVector3D const original_shoulder_l = pose.shoulderL;
  94. QVector3D const original_shoulder_r = pose.shoulderR;
  95. QVector3D const lean_direction(0.0F, 0.0F, 1.0F); // Forward
  96. controller.lean(lean_direction, 0.5F);
  97. // Shoulders should move forward when leaning forward
  98. EXPECT_GT(pose.shoulderL.z(), original_shoulder_l.z());
  99. EXPECT_GT(pose.shoulderR.z(), original_shoulder_r.z());
  100. }
  101. TEST_F(HumanoidPoseControllerTest, LeanZeroAmountNoChange) {
  102. HumanoidPoseController controller(pose, anim_ctx);
  103. QVector3D const original_shoulder_l = pose.shoulderL;
  104. QVector3D const lean_direction(1.0F, 0.0F, 0.0F); // Right
  105. controller.lean(lean_direction, 0.0F);
  106. // Zero amount should keep shoulders unchanged
  107. EXPECT_TRUE(approxEqual(pose.shoulderL, original_shoulder_l));
  108. }
  109. TEST_F(HumanoidPoseControllerTest, PlaceHandAtSetsHandPosition) {
  110. HumanoidPoseController controller(pose, anim_ctx);
  111. QVector3D const target_position(0.30F, 1.20F, 0.80F);
  112. controller.placeHandAt(false, target_position); // Right hand
  113. // Hand should be at target position
  114. EXPECT_TRUE(approxEqual(pose.hand_r, target_position));
  115. }
  116. TEST_F(HumanoidPoseControllerTest, PlaceHandAtComputesElbow) {
  117. HumanoidPoseController controller(pose, anim_ctx);
  118. QVector3D const target_position(0.30F, 1.20F, 0.80F);
  119. QVector3D const original_elbow = pose.elbowR;
  120. controller.placeHandAt(false, target_position); // Right hand
  121. // Elbow should be recomputed (different from original)
  122. EXPECT_FALSE(approxEqual(pose.elbowR, original_elbow));
  123. // Elbow should be between shoulder and hand
  124. float const shoulder_to_elbow_dist = (pose.elbowR - pose.shoulderR).length();
  125. float const elbow_to_hand_dist = (target_position - pose.elbowR).length();
  126. EXPECT_GT(shoulder_to_elbow_dist, 0.0F);
  127. EXPECT_GT(elbow_to_hand_dist, 0.0F);
  128. }
  129. TEST_F(HumanoidPoseControllerTest, SolveElbowIKReturnsValidPosition) {
  130. HumanoidPoseController controller(pose, anim_ctx);
  131. QVector3D const shoulder = pose.shoulderR;
  132. QVector3D const hand(0.35F, 1.15F, 0.75F);
  133. QVector3D const outward_dir(1.0F, 0.0F, 0.0F);
  134. QVector3D const elbow = controller.solveElbowIK(
  135. false, shoulder, hand, outward_dir, 0.45F, 0.15F, 0.0F, 1.0F);
  136. // Elbow should be somewhere between shoulder and hand
  137. EXPECT_GT(elbow.length(), 0.0F);
  138. // Distance from shoulder to elbow should be reasonable
  139. float const shoulder_elbow_dist = (elbow - shoulder).length();
  140. EXPECT_GT(shoulder_elbow_dist, 0.05F);
  141. EXPECT_LT(shoulder_elbow_dist, 0.50F);
  142. }
  143. TEST_F(HumanoidPoseControllerTest, SolveKneeIKReturnsValidPosition) {
  144. HumanoidPoseController controller(pose, anim_ctx);
  145. QVector3D const hip(0.10F, 0.93F, 0.0F);
  146. QVector3D const foot(0.10F, 0.0F, 0.05F);
  147. float const height_scale = 1.0F;
  148. QVector3D const knee = controller.solveKneeIK(false, hip, foot, height_scale);
  149. // Knee should be between hip and foot (in Y)
  150. EXPECT_LT(knee.y(), hip.y());
  151. EXPECT_GT(knee.y(), foot.y());
  152. // Knee should not be below ground
  153. EXPECT_GE(knee.y(), HumanProportions::GROUND_Y);
  154. }
  155. TEST_F(HumanoidPoseControllerTest, SolveKneeIKPreventsGroundPenetration) {
  156. HumanoidPoseController controller(pose, anim_ctx);
  157. // Set up a scenario where IK would put knee below ground
  158. QVector3D const hip(0.0F, 0.30F, 0.0F); // Very low hip
  159. QVector3D const foot(0.50F, 0.0F, 0.50F); // Far foot
  160. float const height_scale = 1.0F;
  161. QVector3D const knee = controller.solveKneeIK(true, hip, foot, height_scale);
  162. // Knee should be at or above the floor threshold
  163. float const min_knee_y = HumanProportions::GROUND_Y + pose.footYOffset * 0.5F;
  164. EXPECT_GE(knee.y(), min_knee_y - 0.001F); // Small epsilon for floating point
  165. }
  166. TEST_F(HumanoidPoseControllerTest, PlaceHandAtLeftHandWorks) {
  167. HumanoidPoseController controller(pose, anim_ctx);
  168. QVector3D const target_position(-0.40F, 1.30F, 0.60F);
  169. controller.placeHandAt(true, target_position); // Left hand
  170. // Left hand should be at target position
  171. EXPECT_TRUE(approxEqual(pose.handL, target_position));
  172. // Left elbow should be computed
  173. EXPECT_GT((pose.elbowL - pose.shoulderL).length(), 0.0F);
  174. }
  175. TEST_F(HumanoidPoseControllerTest, KneelClampsBounds) {
  176. HumanoidPoseController controller(pose, anim_ctx);
  177. // Test clamping of depth > 1.0
  178. controller.kneel(1.5F);
  179. float const max_kneel_pelvis_y = pose.pelvisPos.y();
  180. // Reset pose
  181. SetUp();
  182. HumanoidPoseController controller2(pose, anim_ctx);
  183. // Test depth = 1.0
  184. controller2.kneel(1.0F);
  185. // Should be same as clamped 1.5F
  186. EXPECT_NEAR(pose.pelvisPos.y(), max_kneel_pelvis_y, 0.001F);
  187. }
  188. TEST_F(HumanoidPoseControllerTest, LeanClampsBounds) {
  189. HumanoidPoseController controller(pose, anim_ctx);
  190. QVector3D const lean_direction(0.0F, 0.0F, 1.0F);
  191. // Test clamping of amount > 1.0
  192. controller.lean(lean_direction, 1.5F);
  193. float const max_lean_z = pose.shoulderL.z();
  194. // Reset pose
  195. SetUp();
  196. HumanoidPoseController controller2(pose, anim_ctx);
  197. // Test amount = 1.0
  198. controller2.lean(lean_direction, 1.0F);
  199. // Should be same as clamped 1.5F
  200. EXPECT_NEAR(pose.shoulderL.z(), max_lean_z, 0.001F);
  201. }