pose_controller_compatibility_test.cpp 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. #include "render/humanoid/humanoid_math.h"
  2. #include "render/humanoid/humanoid_specs.h"
  3. #include "render/humanoid/pose_controller.h"
  4. #include "render/humanoid/rig.h"
  5. #include <QVector3D>
  6. #include <cmath>
  7. #include <gtest/gtest.h>
  8. using namespace Render::GL;
  9. /**
  10. * Compatibility tests to verify that the new HumanoidPoseController
  11. * generates the same poses as the existing direct manipulation approach.
  12. */
  13. class PoseControllerCompatibilityTest : public ::testing::Test {
  14. protected:
  15. void SetUp() override {
  16. using HP = HumanProportions;
  17. // Initialize a default pose
  18. pose = HumanoidPose{};
  19. float const head_center_y = HP::HEAD_CENTER_Y;
  20. float const half_shoulder = 0.5F * HP::SHOULDER_WIDTH;
  21. pose.head_pos = QVector3D(0.0F, head_center_y, 0.0F);
  22. pose.head_r = HP::HEAD_RADIUS;
  23. pose.neck_base = QVector3D(0.0F, HP::NECK_BASE_Y, 0.0F);
  24. pose.shoulder_l = QVector3D(-half_shoulder, HP::SHOULDER_Y, 0.0F);
  25. pose.shoulder_r = QVector3D(half_shoulder, HP::SHOULDER_Y, 0.0F);
  26. pose.pelvis_pos = QVector3D(0.0F, HP::WAIST_Y, 0.0F);
  27. pose.hand_l = QVector3D(-0.05F, HP::SHOULDER_Y + 0.05F, 0.55F);
  28. pose.hand_r = QVector3D(0.15F, HP::SHOULDER_Y + 0.15F, 0.20F);
  29. pose.foot_l = QVector3D(-0.14F, 0.022F, 0.06F);
  30. pose.foot_r = QVector3D(0.14F, 0.022F, -0.06F);
  31. pose.foot_y_offset = 0.022F;
  32. anim_ctx = HumanoidAnimationContext{};
  33. anim_ctx.variation = VariationParams::fromSeed(12345);
  34. }
  35. HumanoidPose pose;
  36. HumanoidAnimationContext anim_ctx;
  37. bool approxEqual(const QVector3D &a, const QVector3D &b,
  38. float epsilon = 0.01F) {
  39. return std::abs(a.x() - b.x()) < epsilon &&
  40. std::abs(a.y() - b.y()) < epsilon &&
  41. std::abs(a.z() - b.z()) < epsilon;
  42. }
  43. };
  44. TEST_F(PoseControllerCompatibilityTest, ElbowIKMatchesLegacyFunction) {
  45. // Test that controller's solveElbowIK produces same result as elbowBendTorso
  46. QVector3D const shoulder(0.21F, 1.45F, 0.0F);
  47. QVector3D const hand(0.35F, 1.15F, 0.75F);
  48. QVector3D const outward_dir(1.0F, 0.0F, 0.0F);
  49. float const along_frac = 0.48F;
  50. float const lateral_offset = 0.12F;
  51. float const y_bias = 0.02F;
  52. float const outward_sign = 1.0F;
  53. // Legacy approach
  54. QVector3D const legacy_elbow =
  55. elbowBendTorso(shoulder, hand, outward_dir, along_frac, lateral_offset,
  56. y_bias, outward_sign);
  57. // New controller approach
  58. HumanoidPoseController controller(pose, anim_ctx);
  59. QVector3D const controller_elbow =
  60. controller.solveElbowIK(false, shoulder, hand, outward_dir, along_frac,
  61. lateral_offset, y_bias, outward_sign);
  62. // Should be identical
  63. EXPECT_TRUE(approxEqual(legacy_elbow, controller_elbow, 0.001F))
  64. << "Legacy: " << legacy_elbow.x() << ", " << legacy_elbow.y() << ", "
  65. << legacy_elbow.z() << "\n"
  66. << "Controller: " << controller_elbow.x() << ", " << controller_elbow.y()
  67. << ", " << controller_elbow.z();
  68. }
  69. TEST_F(PoseControllerCompatibilityTest, PlaceHandAtUsesCorrectElbowIK) {
  70. // Verify that placeHandAt uses the same IK as direct manipulation
  71. // Create a copy for legacy approach
  72. HumanoidPose legacy_pose = pose;
  73. QVector3D const target_hand(0.30F, 1.20F, 0.80F);
  74. // Legacy approach: manual IK
  75. legacy_pose.hand_r = target_hand;
  76. QVector3D right_axis = legacy_pose.shoulder_r - legacy_pose.shoulder_l;
  77. right_axis.setY(0.0F);
  78. right_axis.normalize();
  79. QVector3D const outward_r = right_axis;
  80. legacy_pose.elbow_r = elbowBendTorso(legacy_pose.shoulder_r, target_hand,
  81. outward_r, 0.48F, 0.12F, 0.02F, 1.0F);
  82. // New controller approach
  83. HumanoidPoseController controller(pose, anim_ctx);
  84. controller.placeHandAt(false, target_hand);
  85. // Hand should be at target
  86. EXPECT_TRUE(approxEqual(pose.hand_r, target_hand, 0.001F));
  87. // Elbow should be very similar (minor differences due to internal
  88. // calculations)
  89. EXPECT_TRUE(approxEqual(pose.elbow_r, legacy_pose.elbow_r, 0.05F))
  90. << "Legacy elbow: " << legacy_pose.elbow_r.x() << ", "
  91. << legacy_pose.elbow_r.y() << ", " << legacy_pose.elbow_r.z() << "\n"
  92. << "Controller elbow: " << pose.elbow_r.x() << ", " << pose.elbow_r.y()
  93. << ", " << pose.elbow_r.z();
  94. }
  95. TEST_F(PoseControllerCompatibilityTest, KneeIKHandlesExtremeCases) {
  96. // Test knee IK with extreme cases to verify robustness
  97. HumanoidPoseController controller(pose, anim_ctx);
  98. // Very short distance (hip very close to foot)
  99. QVector3D const hip1(0.0F, 0.50F, 0.0F);
  100. QVector3D const foot1(0.05F, 0.45F, 0.05F);
  101. QVector3D const knee1 = controller.solveKneeIK(true, hip1, foot1, 1.0F);
  102. EXPECT_GE(knee1.y(), HumanProportions::GROUND_Y);
  103. EXPECT_LE(knee1.y(), hip1.y());
  104. // Maximum reach (foot very far from hip)
  105. QVector3D const hip2(0.0F, 1.00F, 0.0F);
  106. QVector3D const foot2(0.80F, 0.0F, 0.80F);
  107. QVector3D const knee2 = controller.solveKneeIK(false, hip2, foot2, 1.0F);
  108. EXPECT_GE(knee2.y(), HumanProportions::GROUND_Y);
  109. EXPECT_LE(knee2.y(), hip2.y());
  110. }
  111. TEST_F(PoseControllerCompatibilityTest,
  112. KneelProducesSimilarPoseToExistingCode) {
  113. // Compare kneel() result with typical hand-coded kneeling pose
  114. using HP = HumanProportions;
  115. // Create a reference pose with manual kneeling (similar to
  116. // archer_renderer.cpp)
  117. HumanoidPose reference_pose = pose;
  118. float const kneel_depth = 0.45F;
  119. float const pelvis_y = HP::WAIST_Y - kneel_depth;
  120. reference_pose.pelvis_pos.setY(pelvis_y);
  121. reference_pose.shoulder_l.setY(HP::SHOULDER_Y - kneel_depth);
  122. reference_pose.shoulder_r.setY(HP::SHOULDER_Y - kneel_depth);
  123. reference_pose.neck_base.setY(HP::NECK_BASE_Y - kneel_depth);
  124. reference_pose.head_pos.setY(HP::HEAD_CENTER_Y - kneel_depth);
  125. // Use controller to kneel
  126. HumanoidPoseController controller(pose, anim_ctx);
  127. controller.kneel(1.0F); // Full kneel
  128. // Should be similar (allowing for controller's specific implementation)
  129. EXPECT_NEAR(pose.pelvis_pos.y(), reference_pose.pelvis_pos.y(), 0.10F);
  130. EXPECT_LT(pose.shoulder_l.y(), HP::SHOULDER_Y); // Shoulders lowered
  131. EXPECT_LT(pose.shoulder_r.y(), HP::SHOULDER_Y);
  132. }
  133. TEST_F(PoseControllerCompatibilityTest,
  134. LeanProducesReasonableUpperBodyDisplacement) {
  135. // Test that lean produces sensible displacement
  136. using HP = HumanProportions;
  137. QVector3D const original_shoulder_l = pose.shoulder_l;
  138. QVector3D const original_shoulder_r = pose.shoulder_r;
  139. QVector3D const original_head = pose.head_pos;
  140. QVector3D const lean_dir(0.0F, 0.0F, 1.0F); // Forward
  141. float const lean_amount = 0.8F;
  142. HumanoidPoseController controller(pose, anim_ctx);
  143. controller.lean(lean_dir, lean_amount);
  144. // Shoulders should move forward
  145. EXPECT_GT(pose.shoulder_l.z(), original_shoulder_l.z());
  146. EXPECT_GT(pose.shoulder_r.z(), original_shoulder_r.z());
  147. // Head should move forward but less than shoulders
  148. EXPECT_GT(pose.head_pos.z(), original_head.z());
  149. float const shoulder_displacement =
  150. pose.shoulder_l.z() - original_shoulder_l.z();
  151. float const head_displacement = pose.head_pos.z() - original_head.z();
  152. EXPECT_LT(head_displacement, shoulder_displacement);
  153. // Displacement should be proportional to lean amount
  154. float const expected_magnitude = 0.12F * lean_amount;
  155. EXPECT_NEAR(shoulder_displacement, expected_magnitude, 0.02F);
  156. }
  157. TEST_F(PoseControllerCompatibilityTest, CanRecreateBowAimingPose) {
  158. // Recreate a typical bow aiming pose using the controller
  159. using HP = HumanProportions;
  160. HumanoidPoseController controller(pose, anim_ctx);
  161. // Archer kneel and aim
  162. controller.kneel(1.0F);
  163. controller.lean(QVector3D(0.0F, 0.0F, 1.0F), 0.2F); // Slight forward lean
  164. // Position hands for bow
  165. float const lowered_shoulder_y = pose.shoulder_l.y();
  166. controller.placeHandAt(true,
  167. QVector3D(-0.15F, lowered_shoulder_y + 0.30F, 0.55F));
  168. controller.placeHandAt(false,
  169. QVector3D(0.12F, pose.shoulder_r.y() + 0.15F, 0.10F));
  170. // Verify pose is in a reasonable configuration
  171. EXPECT_LT(pose.pelvis_pos.y(), HP::WAIST_Y); // Kneeling
  172. EXPECT_GT(pose.hand_l.y(), pose.shoulder_l.y()); // Left hand raised
  173. EXPECT_GT(pose.hand_l.z(), 0.0F); // Left hand forward
  174. EXPECT_LT(pose.hand_r.z(), pose.hand_l.z()); // Right hand back (drawing bow)
  175. }
  176. TEST_F(PoseControllerCompatibilityTest, CanRecreateMeleeAttackPose) {
  177. // Recreate a typical melee attack pose using the controller
  178. using HP = HumanProportions;
  179. HumanoidPoseController controller(pose, anim_ctx);
  180. // Spearman thrust pose
  181. controller.lean(QVector3D(0.0F, 0.0F, 1.0F), 0.5F); // Forward lean
  182. // Thrust position
  183. QVector3D const thrust_hand(0.32F, HP::SHOULDER_Y + 0.10F, 0.90F);
  184. controller.placeHandAt(false, thrust_hand);
  185. // Support hand
  186. controller.placeHandAt(true,
  187. QVector3D(-0.05F, HP::SHOULDER_Y + 0.03F, 0.53F));
  188. // Verify thrust pose characteristics
  189. EXPECT_GT(pose.hand_r.z(), 0.80F); // Hand extended forward
  190. EXPECT_GT(pose.shoulder_l.z(), 0.0F); // Body leaning forward
  191. EXPECT_GT(pose.elbow_r.z(), pose.shoulder_r.z()); // Elbow extended
  192. }