pose_controller_compatibility_test.cpp 8.7 KB

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