pose_controller_test.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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 = HP::HEAD_CENTER_Y;
  15. float const half_shoulder = 0.5F * HP::SHOULDER_WIDTH;
  16. pose.head_pos = QVector3D(0.0F, head_center_y, 0.0F);
  17. pose.head_r = HP::HEAD_RADIUS;
  18. pose.neck_base = QVector3D(0.0F, HP::NECK_BASE_Y, 0.0F);
  19. pose.shoulder_l = QVector3D(-half_shoulder, HP::SHOULDER_Y, 0.0F);
  20. pose.shoulder_r = QVector3D(half_shoulder, HP::SHOULDER_Y, 0.0F);
  21. pose.pelvis_pos = QVector3D(0.0F, HP::WAIST_Y, 0.0F);
  22. pose.hand_l = 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.elbow_l = QVector3D(-0.15F, HP::SHOULDER_Y - 0.15F, 0.25F);
  25. pose.elbow_r = 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.foot_l = QVector3D(-0.14F, 0.022F, 0.06F);
  29. pose.foot_r = QVector3D(0.14F, 0.022F, -0.06F);
  30. pose.foot_y_offset = 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.is_moving = 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.head_pos.y(), HumanProportions::HEAD_CENTER_Y);
  53. EXPECT_FLOAT_EQ(pose.pelvis_pos.y(), HumanProportions::WAIST_Y);
  54. }
  55. TEST_F(HumanoidPoseControllerTest, StandIdleDoesNotModifyPose) {
  56. HumanoidPoseController controller(pose, anim_ctx);
  57. QVector3D const original_pelvis = pose.pelvis_pos;
  58. QVector3D const original_shoulder_l = pose.shoulder_l;
  59. controller.standIdle();
  60. // standIdle should be a no-op, keeping pose unchanged
  61. EXPECT_TRUE(approxEqual(pose.pelvis_pos, original_pelvis));
  62. EXPECT_TRUE(approxEqual(pose.shoulder_l, original_shoulder_l));
  63. }
  64. TEST_F(HumanoidPoseControllerTest, KneelLowersPelvis) {
  65. HumanoidPoseController controller(pose, anim_ctx);
  66. float const original_pelvis_y = pose.pelvis_pos.y();
  67. controller.kneel(0.5F);
  68. // Kneeling should lower the pelvis
  69. EXPECT_LT(pose.pelvis_pos.y(), original_pelvis_y);
  70. // Pelvis should be lowered by approximately depth * 0.40F
  71. float const expected_offset = 0.5F * 0.40F;
  72. EXPECT_NEAR(pose.pelvis_pos.y(), HumanProportions::WAIST_Y - expected_offset,
  73. 0.05F);
  74. }
  75. TEST_F(HumanoidPoseControllerTest, KneelFullDepthTouchesGroundWithKnee) {
  76. HumanoidPoseController controller(pose, anim_ctx);
  77. controller.kneel(1.0F);
  78. // At full kneel, left knee should be very close to ground
  79. EXPECT_NEAR(pose.knee_l.y(), HumanProportions::GROUND_Y + 0.07F, 0.02F);
  80. // Pelvis should be lowered significantly
  81. EXPECT_LT(pose.pelvis_pos.y(), HumanProportions::WAIST_Y - 0.35F);
  82. }
  83. TEST_F(HumanoidPoseControllerTest, KneelZeroDepthKeepsStanding) {
  84. HumanoidPoseController controller(pose, anim_ctx);
  85. float const original_pelvis_y = pose.pelvis_pos.y();
  86. controller.kneel(0.0F);
  87. // Zero depth should keep pelvis at original height
  88. EXPECT_NEAR(pose.pelvis_pos.y(), original_pelvis_y, 0.01F);
  89. }
  90. TEST_F(HumanoidPoseControllerTest, LeanMovesUpperBody) {
  91. HumanoidPoseController controller(pose, anim_ctx);
  92. QVector3D const original_shoulder_l = pose.shoulder_l;
  93. QVector3D const original_shoulder_r = pose.shoulder_r;
  94. QVector3D const lean_direction(0.0F, 0.0F, 1.0F); // Forward
  95. controller.lean(lean_direction, 0.5F);
  96. // Shoulders should move forward when leaning forward
  97. EXPECT_GT(pose.shoulder_l.z(), original_shoulder_l.z());
  98. EXPECT_GT(pose.shoulder_r.z(), original_shoulder_r.z());
  99. }
  100. TEST_F(HumanoidPoseControllerTest, LeanZeroAmountNoChange) {
  101. HumanoidPoseController controller(pose, anim_ctx);
  102. QVector3D const original_shoulder_l = pose.shoulder_l;
  103. QVector3D const lean_direction(1.0F, 0.0F, 0.0F); // Right
  104. controller.lean(lean_direction, 0.0F);
  105. // Zero amount should keep shoulders unchanged
  106. EXPECT_TRUE(approxEqual(pose.shoulder_l, original_shoulder_l));
  107. }
  108. TEST_F(HumanoidPoseControllerTest, PlaceHandAtSetsHandPosition) {
  109. HumanoidPoseController controller(pose, anim_ctx);
  110. QVector3D const target_position(0.30F, 1.20F, 0.80F);
  111. controller.placeHandAt(false, target_position); // Right hand
  112. // Hand should be at target position
  113. EXPECT_TRUE(approxEqual(pose.hand_r, target_position));
  114. }
  115. TEST_F(HumanoidPoseControllerTest, PlaceHandAtComputesElbow) {
  116. HumanoidPoseController controller(pose, anim_ctx);
  117. QVector3D const target_position(0.30F, 1.20F, 0.80F);
  118. QVector3D const original_elbow = pose.elbow_r;
  119. controller.placeHandAt(false, target_position); // Right hand
  120. // Elbow should be recomputed (different from original)
  121. EXPECT_FALSE(approxEqual(pose.elbow_r, original_elbow));
  122. // Elbow should be between shoulder and hand
  123. float const shoulder_to_elbow_dist =
  124. (pose.elbow_r - pose.shoulder_r).length();
  125. float const elbow_to_hand_dist = (target_position - pose.elbow_r).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.shoulder_r;
  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 =
  164. HumanProportions::GROUND_Y + pose.foot_y_offset * 0.5F;
  165. EXPECT_GE(knee.y(), min_knee_y - 0.001F); // Small epsilon for floating point
  166. }
  167. TEST_F(HumanoidPoseControllerTest, PlaceHandAtLeftHandWorks) {
  168. HumanoidPoseController controller(pose, anim_ctx);
  169. QVector3D const target_position(-0.40F, 1.30F, 0.60F);
  170. controller.placeHandAt(true, target_position); // Left hand
  171. // Left hand should be at target position
  172. EXPECT_TRUE(approxEqual(pose.hand_l, target_position));
  173. // Left elbow should be computed
  174. EXPECT_GT((pose.elbow_l - pose.shoulder_l).length(), 0.0F);
  175. }
  176. TEST_F(HumanoidPoseControllerTest, KneelClampsBounds) {
  177. HumanoidPoseController controller(pose, anim_ctx);
  178. // Test clamping of depth > 1.0
  179. controller.kneel(1.5F);
  180. float const max_kneel_pelvis_y = pose.pelvis_pos.y();
  181. // Reset pose
  182. SetUp();
  183. HumanoidPoseController controller2(pose, anim_ctx);
  184. // Test depth = 1.0
  185. controller2.kneel(1.0F);
  186. // Should be same as clamped 1.5F
  187. EXPECT_NEAR(pose.pelvis_pos.y(), max_kneel_pelvis_y, 0.001F);
  188. }
  189. TEST_F(HumanoidPoseControllerTest, LeanClampsBounds) {
  190. HumanoidPoseController controller(pose, anim_ctx);
  191. QVector3D const lean_direction(0.0F, 0.0F, 1.0F);
  192. // Test clamping of amount > 1.0
  193. controller.lean(lean_direction, 1.5F);
  194. float const max_lean_z = pose.shoulder_l.z();
  195. // Reset pose
  196. SetUp();
  197. HumanoidPoseController controller2(pose, anim_ctx);
  198. // Test amount = 1.0
  199. controller2.lean(lean_direction, 1.0F);
  200. // Should be same as clamped 1.5F
  201. EXPECT_NEAR(pose.shoulder_l.z(), max_lean_z, 0.001F);
  202. }
  203. TEST_F(HumanoidPoseControllerTest, HoldSwordAndShieldPositionsHandsCorrectly) {
  204. HumanoidPoseController controller(pose, anim_ctx);
  205. controller.hold_sword_and_shield();
  206. // Right hand (sword hand) should be positioned for sword holding
  207. EXPECT_GT(pose.hand_r.x(), 0.0F); // To the right
  208. EXPECT_GT(pose.hand_r.z(), 0.0F); // In front
  209. // Left hand (shield hand) should be positioned for shield holding
  210. EXPECT_LT(pose.hand_l.x(), 0.0F); // To the left
  211. EXPECT_GT(pose.hand_l.z(), 0.0F); // In front
  212. // Both elbows should be computed
  213. EXPECT_GT((pose.elbow_r - pose.shoulder_r).length(), 0.0F);
  214. EXPECT_GT((pose.elbow_l - pose.shoulder_l).length(), 0.0F);
  215. }
  216. TEST_F(HumanoidPoseControllerTest, LookAtMovesHeadTowardTarget) {
  217. HumanoidPoseController controller(pose, anim_ctx);
  218. QVector3D const original_head_pos = pose.head_pos;
  219. QVector3D const target(0.5F, pose.head_pos.y(),
  220. 2.0F); // Target in front and to the right
  221. controller.look_at(target);
  222. // Head should move toward target (right and forward)
  223. EXPECT_GT(pose.head_pos.x(), original_head_pos.x());
  224. EXPECT_GT(pose.head_pos.z(), original_head_pos.z());
  225. }
  226. TEST_F(HumanoidPoseControllerTest, LookAtWithSamePositionDoesNothing) {
  227. HumanoidPoseController controller(pose, anim_ctx);
  228. QVector3D const original_head_pos = pose.head_pos;
  229. controller.look_at(pose.head_pos); // Look at current position
  230. // Head should remain unchanged
  231. EXPECT_TRUE(approxEqual(pose.head_pos, original_head_pos));
  232. }
  233. TEST_F(HumanoidPoseControllerTest, GetShoulderYReturnsCorrectValues) {
  234. HumanoidPoseController controller(pose, anim_ctx);
  235. float const left_y = controller.get_shoulder_y(true);
  236. float const right_y = controller.get_shoulder_y(false);
  237. EXPECT_FLOAT_EQ(left_y, pose.shoulder_l.y());
  238. EXPECT_FLOAT_EQ(right_y, pose.shoulder_r.y());
  239. }
  240. TEST_F(HumanoidPoseControllerTest, GetPelvisYReturnsCorrectValue) {
  241. HumanoidPoseController controller(pose, anim_ctx);
  242. float const pelvis_y = controller.get_pelvis_y();
  243. EXPECT_FLOAT_EQ(pelvis_y, pose.pelvis_pos.y());
  244. }
  245. TEST_F(HumanoidPoseControllerTest, GetShoulderYReflectsKneeling) {
  246. HumanoidPoseController controller(pose, anim_ctx);
  247. float const original_shoulder_y = controller.get_shoulder_y(true);
  248. controller.kneel(0.5F);
  249. float const kneeling_shoulder_y = controller.get_shoulder_y(true);
  250. // After kneeling, shoulder should be lower
  251. EXPECT_LT(kneeling_shoulder_y, original_shoulder_y);
  252. }