Browse Source

fix horse rider double rendering

djeada 1 month ago
parent
commit
af166f624d

+ 1 - 0
render/CMakeLists.txt

@@ -70,6 +70,7 @@ add_library(render_gl STATIC
     humanoid/rig.cpp
     humanoid/rig.cpp
     humanoid/style_palette.cpp
     humanoid/style_palette.cpp
     humanoid/pose_controller.cpp
     humanoid/pose_controller.cpp
+    humanoid/mounted_pose_controller.cpp
     equipment/equipment_registry.cpp
     equipment/equipment_registry.cpp
     equipment/register_equipment.cpp
     equipment/register_equipment.cpp
     equipment/armor/tunic_renderer.cpp
     equipment/armor/tunic_renderer.cpp

+ 63 - 133
render/entity/nations/carthage/horse_swordsman_renderer.cpp

@@ -12,6 +12,7 @@
 #include "../../../gl/shader.h"
 #include "../../../gl/shader.h"
 #include "../../../humanoid/humanoid_math.h"
 #include "../../../humanoid/humanoid_math.h"
 #include "../../../humanoid/humanoid_specs.h"
 #include "../../../humanoid/humanoid_specs.h"
+#include "../../../humanoid/mounted_pose_controller.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/rig.h"
 #include "../../../humanoid/rig.h"
 #include "../../../palette.h"
 #include "../../../palette.h"
@@ -54,12 +55,36 @@ struct MountedKnightExtras {
 class MountedKnightRenderer : public HumanoidRendererBase {
 class MountedKnightRenderer : public HumanoidRendererBase {
 public:
 public:
   auto get_proportion_scaling() const -> QVector3D override {
   auto get_proportion_scaling() const -> QVector3D override {
-    return {1.40F, 1.05F, 1.10F};
+
+    return {1.F, 1.7F, 1.F};
+  }
+
+  auto get_mount_scale() const -> float override { return 0.75F; }
+
+  void adjust_variation(const DrawContext &, uint32_t,
+                        VariationParams &variation) const override {
+    variation.height_scale = 0.92F;
+    variation.bulkScale = 0.80F;
+    variation.stanceWidth = 0.65F;
+    variation.armSwingAmp = 0.55F;
+    variation.walkSpeedMult = 1.0F;
+    variation.postureSlump = 0.0F;
+    variation.shoulderTilt = 0.0F;
   }
   }
 
 
 private:
 private:
   mutable std::unordered_map<uint32_t, MountedKnightExtras> m_extrasCache;
   mutable std::unordered_map<uint32_t, MountedKnightExtras> m_extrasCache;
   HorseRenderer m_horseRenderer;
   HorseRenderer m_horseRenderer;
+  mutable const HumanoidPose *m_lastPose = nullptr;
+  mutable MountedAttachmentFrame m_lastMount{};
+  mutable ReinState m_lastReinState{};
+  mutable bool m_hasLastReins = false;
+
+  auto getScaledHorseDimensions(uint32_t seed) const -> HorseDimensions {
+    HorseDimensions dims = makeHorseDimensions(seed);
+    scaleHorseDimensions(dims, get_mount_scale());
+    return dims;
+  }
 
 
 public:
 public:
   void get_variant(const DrawContext &ctx, uint32_t seed,
   void get_variant(const DrawContext &ctx, uint32_t seed,
@@ -89,150 +114,45 @@ public:
 
 
     const AnimationInputs &anim = anim_ctx.inputs;
     const AnimationInputs &anim = anim_ctx.inputs;
 
 
-    const float arm_height_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.03F;
-    const float arm_asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.04F;
-
     uint32_t horse_seed = seed;
     uint32_t horse_seed = seed;
     if (ctx.entity != nullptr) {
     if (ctx.entity != nullptr) {
       horse_seed = static_cast<uint32_t>(
       horse_seed = static_cast<uint32_t>(
           reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
           reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
     }
     }
 
 
-    const HorseDimensions dims = makeHorseDimensions(horse_seed);
+    HorseDimensions dims = getScaledHorseDimensions(horse_seed);
     HorseProfile mount_profile{};
     HorseProfile mount_profile{};
     mount_profile.dims = dims;
     mount_profile.dims = dims;
-    const HorseMountFrame mount = compute_mount_frame(mount_profile);
+    MountedAttachmentFrame mount = compute_mount_frame(mount_profile);
+    HorseMotionSample const motion =
+        evaluate_horse_motion(mount_profile, anim, anim_ctx);
+    apply_mount_vertical_offset(mount, motion.bob);
+
+    m_lastPose = &pose;
+    m_lastMount = mount;
 
 
-    const float saddle_height = mount.seat_position.y();
-    const float offset_y = saddle_height - pose.pelvisPos.y();
+    ReinState const reins = compute_rein_state(horse_seed, anim_ctx);
+    m_lastReinState = reins;
+    m_hasLastReins = true;
 
 
-    pose.pelvisPos.setY(pose.pelvisPos.y() + offset_y);
-    pose.headPos.setY(pose.headPos.y() + offset_y);
-    pose.neck_base.setY(pose.neck_base.y() + offset_y);
-    pose.shoulderL.setY(pose.shoulderL.y() + offset_y);
-    pose.shoulderR.setY(pose.shoulderR.y() + offset_y);
+    MountedPoseController mounted_controller(pose, anim_ctx);
+    mounted_controller.mountOnHorse(mount);
 
 
     float const speed_norm = anim_ctx.locomotion_normalized_speed();
     float const speed_norm = anim_ctx.locomotion_normalized_speed();
     float const speed_lean = std::clamp(
     float const speed_lean = std::clamp(
         anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
         anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
-    const float lean_forward = dims.seatForwardOffset * 0.08F + speed_lean;
-    pose.shoulderL.setZ(pose.shoulderL.z() + lean_forward);
-    pose.shoulderR.setZ(pose.shoulderR.z() + lean_forward);
-
-    pose.footYOffset = 0.0F;
-    pose.footL = mount.stirrup_bottom_left;
-    pose.foot_r = mount.stirrup_bottom_right;
-
-    const float knee_y =
-        mount.stirrup_bottom_left.y() +
-        (saddle_height - mount.stirrup_bottom_left.y()) * 0.62F;
-    const float knee_z = mount.stirrup_bottom_left.z() * 0.60F + 0.06F;
-
-    QVector3D knee_left = mount.stirrup_attach_left;
-    knee_left.setY(knee_y);
-    knee_left.setZ(knee_z);
-    pose.knee_l = knee_left;
-
-    QVector3D knee_right = mount.stirrup_attach_right;
-    knee_right.setY(knee_y);
-    knee_right.setZ(knee_z);
-    pose.knee_r = knee_right;
-
-    float const shoulder_height = pose.shoulderL.y();
-    float const rein_extension = std::clamp(
-        speed_norm * 0.14F + anim_ctx.locomotion_speed() * 0.015F, 0.0F, 0.12F);
-    float const rein_drop = std::clamp(
-        speed_norm * 0.06F + anim_ctx.locomotion_speed() * 0.008F, 0.0F, 0.04F);
-
-    QVector3D forward = anim_ctx.heading_forward();
-    QVector3D right = anim_ctx.heading_right();
-    QVector3D up = anim_ctx.heading_up();
-    float const rein_spread =
-        std::abs(mount.rein_attach_right.x() - mount.rein_attach_left.x()) *
-        0.5F;
-
-    QVector3D rest_hand_r = mount.rein_attach_right;
-    rest_hand_r += forward * (0.08F + rein_extension);
-    rest_hand_r -= right * (0.10F - arm_asymmetry * 0.05F);
-    rest_hand_r += up * (0.05F + arm_height_jitter * 0.6F - rein_drop);
-
-    QVector3D rest_hand_l = mount.rein_attach_left;
-    rest_hand_l += forward * (0.05F + rein_extension * 0.6F);
-    rest_hand_l += right * (0.08F + arm_asymmetry * 0.04F);
-    rest_hand_l += up * (0.04F - arm_height_jitter * 0.5F - rein_drop * 0.6F);
-
-    float const rein_forward = rest_hand_r.z();
-
-    HumanoidPoseController controller(pose, anim_ctx);
-
-    controller.placeHandAt(false, rest_hand_r);
-    controller.placeHandAt(true, rest_hand_l);
-
-    pose.elbowL =
-        QVector3D(pose.shoulderL.x() * 0.4F + rest_hand_l.x() * 0.6F,
-                  (pose.shoulderL.y() + rest_hand_l.y()) * 0.5F - 0.08F,
-                  (pose.shoulderL.z() + rest_hand_l.z()) * 0.5F);
-    pose.elbowR =
-        QVector3D(pose.shoulderR.x() * 0.4F + rest_hand_r.x() * 0.6F,
-                  (pose.shoulderR.y() + rest_hand_r.y()) * 0.5F - 0.08F,
-                  (pose.shoulderR.z() + rest_hand_r.z()) * 0.5F);
+    float const forward_lean =
+        (dims.seatForwardOffset * 0.08F + speed_lean) / 0.15F;
+    mounted_controller.ridingLeaning(mount, forward_lean, 0.0F);
 
 
     if (anim.is_attacking && anim.isMelee) {
     if (anim.is_attacking && anim.isMelee) {
+
       float const attack_phase =
       float const attack_phase =
           std::fmod(anim.time * MOUNTED_KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
           std::fmod(anim.time * MOUNTED_KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
-
-      QVector3D const rest_pos = rest_hand_r;
-      QVector3D const windup_pos =
-          QVector3D(rest_hand_r.x() + 0.32F, shoulder_height + 0.15F,
-                    rein_forward - 0.35F);
-      QVector3D const raised_pos = QVector3D(
-          rein_spread + 0.38F, shoulder_height + 0.28F, rein_forward - 0.25F);
-      QVector3D const slash_pos = QVector3D(
-          -rein_spread * 0.65F, shoulder_height - 0.08F, rein_forward + 0.85F);
-      QVector3D const follow_through = QVector3D(
-          -rein_spread * 0.85F, shoulder_height - 0.15F, rein_forward + 0.60F);
-      QVector3D const recover_pos = QVector3D(
-          rein_spread * 0.45F, shoulder_height - 0.05F, rein_forward + 0.25F);
-
-      QVector3D hand_r_target;
-
-      if (attack_phase < 0.18F) {
-        float const t = easeInOutCubic(attack_phase / 0.18F);
-        hand_r_target = rest_pos * (1.0F - t) + windup_pos * t;
-      } else if (attack_phase < 0.30F) {
-        float const t = easeInOutCubic((attack_phase - 0.18F) / 0.12F);
-        hand_r_target = windup_pos * (1.0F - t) + raised_pos * t;
-      } else if (attack_phase < 0.48F) {
-        float t = (attack_phase - 0.30F) / 0.18F;
-        t = t * t * t;
-        hand_r_target = raised_pos * (1.0F - t) + slash_pos * t;
-      } else if (attack_phase < 0.62F) {
-        float const t = easeInOutCubic((attack_phase - 0.48F) / 0.14F);
-        hand_r_target = slash_pos * (1.0F - t) + follow_through * t;
-      } else if (attack_phase < 0.80F) {
-        float const t = easeInOutCubic((attack_phase - 0.62F) / 0.18F);
-        hand_r_target = follow_through * (1.0F - t) + recover_pos * t;
-      } else {
-        float const t = smoothstep(0.80F, 1.0F, attack_phase);
-        hand_r_target = recover_pos * (1.0F - t) + rest_pos * t;
-      }
-
-      float const rein_tension = clamp01((attack_phase - 0.10F) * 2.2F);
-      QVector3D const hand_l_target =
-          rest_hand_l +
-          QVector3D(0.0F, -0.015F * rein_tension, 0.10F * rein_tension);
-
-      controller.placeHandAt(false, hand_r_target);
-      controller.placeHandAt(true, hand_l_target);
-
-      pose.elbowR =
-          QVector3D(pose.shoulderR.x() * 0.3F + pose.hand_r.x() * 0.7F,
-                    (pose.shoulderR.y() + pose.hand_r.y()) * 0.5F - 0.12F,
-                    (pose.shoulderR.z() + pose.hand_r.z()) * 0.5F);
-      pose.elbowL =
-          QVector3D(pose.shoulderL.x() * 0.4F + pose.handL.x() * 0.6F,
-                    (pose.shoulderL.y() + pose.handL.y()) * 0.5F - 0.08F,
-                    (pose.shoulderL.z() + pose.handL.z()) * 0.5F);
+      mounted_controller.ridingMeleeStrike(mount, attack_phase);
+    } else {
+      mounted_controller.holdReins(mount, reins.slack, reins.slack,
+                                   reins.tension, reins.tension);
     }
     }
   }
   }
 
 
@@ -252,7 +172,8 @@ public:
     if (it != m_extrasCache.end()) {
     if (it != m_extrasCache.end()) {
       extras = it->second;
       extras = it->second;
     } else {
     } else {
-      extras = computeMountedKnightExtras(horse_seed, v);
+      HorseDimensions dims = getScaledHorseDimensions(horse_seed);
+      extras = computeMountedKnightExtras(horse_seed, v, dims);
       m_extrasCache[horse_seed] = extras;
       m_extrasCache[horse_seed] = extras;
 
 
       if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
       if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
@@ -260,7 +181,15 @@ public:
       }
       }
     }
     }
 
 
-    m_horseRenderer.render(ctx, anim, anim_ctx, extras.horseProfile, out);
+    const bool is_current_pose = (m_lastPose == &pose);
+    const MountedAttachmentFrame *mount_ptr =
+        (is_current_pose) ? &m_lastMount : nullptr;
+    const ReinState *rein_ptr =
+        (is_current_pose && m_hasLastReins) ? &m_lastReinState : nullptr;
+    m_horseRenderer.render(ctx, anim, anim_ctx, extras.horseProfile, mount_ptr,
+                           rein_ptr, out);
+    m_lastPose = nullptr;
+    m_hasLastReins = false;
 
 
     bool const is_attacking = anim.is_attacking && anim.isMelee;
     bool const is_attacking = anim.is_attacking && anim.isMelee;
 
 
@@ -320,15 +249,17 @@ public:
   }
   }
 
 
 private:
 private:
-  static auto
-  computeMountedKnightExtras(uint32_t seed,
-                             const HumanoidVariant &v) -> MountedKnightExtras {
+  static auto computeMountedKnightExtras(
+      uint32_t seed, const HumanoidVariant &v,
+      const HorseDimensions &dims) -> MountedKnightExtras {
     MountedKnightExtras e;
     MountedKnightExtras e;
 
 
     e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
     e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
 
 
     e.horseProfile = makeHorseProfile(seed, v.palette.leather, v.palette.cloth);
     e.horseProfile = makeHorseProfile(seed, v.palette.leather, v.palette.cloth);
 
 
+    e.horseProfile.dims = dims;
+
     e.swordLength = 0.82F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.12F;
     e.swordLength = 0.82F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.12F;
     e.swordWidth = 0.042F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.008F;
     e.swordWidth = 0.042F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.008F;
 
 
@@ -340,7 +271,6 @@ private:
 };
 };
 void registerMountedKnightRenderer(
 void registerMountedKnightRenderer(
     Render::GL::EntityRendererRegistry &registry) {
     Render::GL::EntityRendererRegistry &registry) {
-  static MountedKnightRenderer const renderer;
   registry.register_renderer(
   registry.register_renderer(
       "troops/carthage/horse_swordsman",
       "troops/carthage/horse_swordsman",
       [](const DrawContext &ctx, ISubmitter &out) {
       [](const DrawContext &ctx, ISubmitter &out) {

+ 63 - 132
render/entity/nations/kingdom/horse_swordsman_renderer.cpp

@@ -12,6 +12,7 @@
 #include "../../../gl/shader.h"
 #include "../../../gl/shader.h"
 #include "../../../humanoid/humanoid_math.h"
 #include "../../../humanoid/humanoid_math.h"
 #include "../../../humanoid/humanoid_specs.h"
 #include "../../../humanoid/humanoid_specs.h"
+#include "../../../humanoid/mounted_pose_controller.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/rig.h"
 #include "../../../humanoid/rig.h"
 #include "../../../palette.h"
 #include "../../../palette.h"
@@ -55,12 +56,36 @@ struct MountedKnightExtras {
 class MountedKnightRenderer : public HumanoidRendererBase {
 class MountedKnightRenderer : public HumanoidRendererBase {
 public:
 public:
   auto get_proportion_scaling() const -> QVector3D override {
   auto get_proportion_scaling() const -> QVector3D override {
-    return {1.40F, 1.05F, 1.10F};
+
+    return {0.2F, 0.667F, 0.2F};
+  }
+
+  auto get_mount_scale() const -> float override { return 0.75F; }
+
+  void adjust_variation(const DrawContext &, uint32_t,
+                        VariationParams &variation) const override {
+    variation.height_scale = 0.92F;
+    variation.bulkScale = 0.80F;
+    variation.stanceWidth = 0.65F;
+    variation.armSwingAmp = 0.55F;
+    variation.walkSpeedMult = 1.0F;
+    variation.postureSlump = 0.0F;
+    variation.shoulderTilt = 0.0F;
   }
   }
 
 
 private:
 private:
   mutable std::unordered_map<uint32_t, MountedKnightExtras> m_extrasCache;
   mutable std::unordered_map<uint32_t, MountedKnightExtras> m_extrasCache;
   HorseRenderer m_horseRenderer;
   HorseRenderer m_horseRenderer;
+  mutable const HumanoidPose *m_lastPose = nullptr;
+  mutable MountedAttachmentFrame m_lastMount{};
+  mutable ReinState m_lastReinState{};
+  mutable bool m_hasLastReins = false;
+
+  auto getScaledHorseDimensions(uint32_t seed) const -> HorseDimensions {
+    HorseDimensions dims = makeHorseDimensions(seed);
+    scaleHorseDimensions(dims, get_mount_scale());
+    return dims;
+  }
 
 
 public:
 public:
   void get_variant(const DrawContext &ctx, uint32_t seed,
   void get_variant(const DrawContext &ctx, uint32_t seed,
@@ -90,150 +115,46 @@ public:
 
 
     const AnimationInputs &anim = anim_ctx.inputs;
     const AnimationInputs &anim = anim_ctx.inputs;
 
 
-    const float arm_height_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.03F;
-    const float arm_asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.04F;
-
     uint32_t horse_seed = seed;
     uint32_t horse_seed = seed;
     if (ctx.entity != nullptr) {
     if (ctx.entity != nullptr) {
       horse_seed = static_cast<uint32_t>(
       horse_seed = static_cast<uint32_t>(
           reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
           reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
     }
     }
 
 
-    const HorseDimensions dims = makeHorseDimensions(horse_seed);
+    HorseDimensions dims = getScaledHorseDimensions(horse_seed);
     HorseProfile mount_profile{};
     HorseProfile mount_profile{};
     mount_profile.dims = dims;
     mount_profile.dims = dims;
-    const HorseMountFrame mount = compute_mount_frame(mount_profile);
+    MountedAttachmentFrame mount = compute_mount_frame(mount_profile);
+    HorseMotionSample const motion =
+        evaluate_horse_motion(mount_profile, anim, anim_ctx);
+    apply_mount_vertical_offset(mount, motion.bob);
+
+    m_lastPose = &pose;
+    m_lastMount = mount;
 
 
-    const float saddle_height = mount.seat_position.y();
-    const float offset_y = saddle_height - pose.pelvisPos.y();
+    ReinState const reins = compute_rein_state(horse_seed, anim_ctx);
+    m_lastReinState = reins;
+    m_hasLastReins = true;
 
 
-    pose.pelvisPos.setY(pose.pelvisPos.y() + offset_y);
-    pose.headPos.setY(pose.headPos.y() + offset_y);
-    pose.neck_base.setY(pose.neck_base.y() + offset_y);
-    pose.shoulderL.setY(pose.shoulderL.y() + offset_y);
-    pose.shoulderR.setY(pose.shoulderR.y() + offset_y);
+    MountedPoseController mounted_controller(pose, anim_ctx);
+    mounted_controller.mountOnHorse(mount);
 
 
     float const speed_norm = anim_ctx.locomotion_normalized_speed();
     float const speed_norm = anim_ctx.locomotion_normalized_speed();
     float const speed_lean = std::clamp(
     float const speed_lean = std::clamp(
         anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
         anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
-    const float lean_forward = dims.seatForwardOffset * 0.08F + speed_lean;
-    pose.shoulderL.setZ(pose.shoulderL.z() + lean_forward);
-    pose.shoulderR.setZ(pose.shoulderR.z() + lean_forward);
-
-    pose.footYOffset = 0.0F;
-    pose.footL = mount.stirrup_bottom_left;
-    pose.foot_r = mount.stirrup_bottom_right;
-
-    const float knee_y =
-        mount.stirrup_bottom_left.y() +
-        (saddle_height - mount.stirrup_bottom_left.y()) * 0.62F;
-    const float knee_z = mount.stirrup_bottom_left.z() * 0.60F + 0.06F;
-
-    QVector3D knee_left = mount.stirrup_attach_left;
-    knee_left.setY(knee_y);
-    knee_left.setZ(knee_z);
-    pose.knee_l = knee_left;
-
-    QVector3D knee_right = mount.stirrup_attach_right;
-    knee_right.setY(knee_y);
-    knee_right.setZ(knee_z);
-    pose.knee_r = knee_right;
-
-    float const shoulder_height = pose.shoulderL.y();
-    float const rein_extension = std::clamp(
-        speed_norm * 0.14F + anim_ctx.locomotion_speed() * 0.015F, 0.0F, 0.12F);
-    float const rein_drop = std::clamp(
-        speed_norm * 0.06F + anim_ctx.locomotion_speed() * 0.008F, 0.0F, 0.04F);
-
-    QVector3D forward = anim_ctx.heading_forward();
-    QVector3D right = anim_ctx.heading_right();
-    QVector3D up = anim_ctx.heading_up();
-    float const rein_spread =
-        std::abs(mount.rein_attach_right.x() - mount.rein_attach_left.x()) *
-        0.5F;
-
-    QVector3D rest_hand_r = mount.rein_attach_right;
-    rest_hand_r += forward * (0.08F + rein_extension);
-    rest_hand_r -= right * (0.10F - arm_asymmetry * 0.05F);
-    rest_hand_r += up * (0.05F + arm_height_jitter * 0.6F - rein_drop);
-
-    QVector3D rest_hand_l = mount.rein_attach_left;
-    rest_hand_l += forward * (0.05F + rein_extension * 0.6F);
-    rest_hand_l += right * (0.08F + arm_asymmetry * 0.04F);
-    rest_hand_l += up * (0.04F - arm_height_jitter * 0.5F - rein_drop * 0.6F);
-
-    float const rein_forward = rest_hand_r.z();
-
-    HumanoidPoseController controller(pose, anim_ctx);
-
-    controller.placeHandAt(false, rest_hand_r);
-    controller.placeHandAt(true, rest_hand_l);
-
-    pose.elbowL =
-        QVector3D(pose.shoulderL.x() * 0.4F + rest_hand_l.x() * 0.6F,
-                  (pose.shoulderL.y() + rest_hand_l.y()) * 0.5F - 0.08F,
-                  (pose.shoulderL.z() + rest_hand_l.z()) * 0.5F);
-    pose.elbowR =
-        QVector3D(pose.shoulderR.x() * 0.4F + rest_hand_r.x() * 0.6F,
-                  (pose.shoulderR.y() + rest_hand_r.y()) * 0.5F - 0.08F,
-                  (pose.shoulderR.z() + rest_hand_r.z()) * 0.5F);
+    float const forward_lean =
+        (dims.seatForwardOffset * 0.08F + speed_lean) / 0.15F;
+    mounted_controller.ridingLeaning(mount, forward_lean, 0.0F);
 
 
     if (anim.is_attacking && anim.isMelee) {
     if (anim.is_attacking && anim.isMelee) {
+
       float const attack_phase =
       float const attack_phase =
           std::fmod(anim.time * MOUNTED_KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
           std::fmod(anim.time * MOUNTED_KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
+      mounted_controller.ridingMeleeStrike(mount, attack_phase);
+    } else {
 
 
-      QVector3D const rest_pos = rest_hand_r;
-      QVector3D const windup_pos =
-          QVector3D(rest_hand_r.x() + 0.32F, shoulder_height + 0.15F,
-                    rein_forward - 0.35F);
-      QVector3D const raised_pos = QVector3D(
-          rein_spread + 0.38F, shoulder_height + 0.28F, rein_forward - 0.25F);
-      QVector3D const slash_pos = QVector3D(
-          -rein_spread * 0.65F, shoulder_height - 0.08F, rein_forward + 0.85F);
-      QVector3D const follow_through = QVector3D(
-          -rein_spread * 0.85F, shoulder_height - 0.15F, rein_forward + 0.60F);
-      QVector3D const recover_pos = QVector3D(
-          rein_spread * 0.45F, shoulder_height - 0.05F, rein_forward + 0.25F);
-
-      QVector3D hand_r_target;
-
-      if (attack_phase < 0.18F) {
-        float const t = easeInOutCubic(attack_phase / 0.18F);
-        hand_r_target = rest_pos * (1.0F - t) + windup_pos * t;
-      } else if (attack_phase < 0.30F) {
-        float const t = easeInOutCubic((attack_phase - 0.18F) / 0.12F);
-        hand_r_target = windup_pos * (1.0F - t) + raised_pos * t;
-      } else if (attack_phase < 0.48F) {
-        float t = (attack_phase - 0.30F) / 0.18F;
-        t = t * t * t;
-        hand_r_target = raised_pos * (1.0F - t) + slash_pos * t;
-      } else if (attack_phase < 0.62F) {
-        float const t = easeInOutCubic((attack_phase - 0.48F) / 0.14F);
-        hand_r_target = slash_pos * (1.0F - t) + follow_through * t;
-      } else if (attack_phase < 0.80F) {
-        float const t = easeInOutCubic((attack_phase - 0.62F) / 0.18F);
-        hand_r_target = follow_through * (1.0F - t) + recover_pos * t;
-      } else {
-        float const t = smoothstep(0.80F, 1.0F, attack_phase);
-        hand_r_target = recover_pos * (1.0F - t) + rest_pos * t;
-      }
-
-      float const rein_tension = clamp01((attack_phase - 0.10F) * 2.2F);
-      QVector3D const hand_l_target =
-          rest_hand_l +
-          QVector3D(0.0F, -0.015F * rein_tension, 0.10F * rein_tension);
-
-      controller.placeHandAt(false, hand_r_target);
-      controller.placeHandAt(true, hand_l_target);
-
-      pose.elbowR =
-          QVector3D(pose.shoulderR.x() * 0.3F + pose.hand_r.x() * 0.7F,
-                    (pose.shoulderR.y() + pose.hand_r.y()) * 0.5F - 0.12F,
-                    (pose.shoulderR.z() + pose.hand_r.z()) * 0.5F);
-      pose.elbowL =
-          QVector3D(pose.shoulderL.x() * 0.4F + pose.handL.x() * 0.6F,
-                    (pose.shoulderL.y() + pose.handL.y()) * 0.5F - 0.08F,
-                    (pose.shoulderL.z() + pose.handL.z()) * 0.5F);
+      mounted_controller.holdReins(mount, reins.slack, reins.slack,
+                                   reins.tension, reins.tension);
     }
     }
   }
   }
 
 
@@ -253,7 +174,8 @@ public:
     if (it != m_extrasCache.end()) {
     if (it != m_extrasCache.end()) {
       extras = it->second;
       extras = it->second;
     } else {
     } else {
-      extras = computeMountedKnightExtras(horse_seed, v);
+      HorseDimensions dims = getScaledHorseDimensions(horse_seed);
+      extras = computeMountedKnightExtras(horse_seed, v, dims);
       m_extrasCache[horse_seed] = extras;
       m_extrasCache[horse_seed] = extras;
 
 
       if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
       if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
@@ -261,7 +183,15 @@ public:
       }
       }
     }
     }
 
 
-    m_horseRenderer.render(ctx, anim, anim_ctx, extras.horseProfile, out);
+    const bool is_current_pose = (m_lastPose == &pose);
+    const MountedAttachmentFrame *mount_ptr =
+        (is_current_pose) ? &m_lastMount : nullptr;
+    const ReinState *rein_ptr =
+        (is_current_pose && m_hasLastReins) ? &m_lastReinState : nullptr;
+    m_horseRenderer.render(ctx, anim, anim_ctx, extras.horseProfile, mount_ptr,
+                           rein_ptr, out);
+    m_lastPose = nullptr;
+    m_hasLastReins = false;
 
 
     bool const is_attacking = anim.is_attacking && anim.isMelee;
     bool const is_attacking = anim.is_attacking && anim.isMelee;
 
 
@@ -321,15 +251,17 @@ public:
   }
   }
 
 
 private:
 private:
-  static auto
-  computeMountedKnightExtras(uint32_t seed,
-                             const HumanoidVariant &v) -> MountedKnightExtras {
+  static auto computeMountedKnightExtras(
+      uint32_t seed, const HumanoidVariant &v,
+      const HorseDimensions &dims) -> MountedKnightExtras {
     MountedKnightExtras e;
     MountedKnightExtras e;
 
 
     e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
     e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
 
 
     e.horseProfile = makeHorseProfile(seed, v.palette.leather, v.palette.cloth);
     e.horseProfile = makeHorseProfile(seed, v.palette.leather, v.palette.cloth);
 
 
+    e.horseProfile.dims = dims;
+
     e.swordLength = 0.82F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.12F;
     e.swordLength = 0.82F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.12F;
     e.swordWidth = 0.042F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.008F;
     e.swordWidth = 0.042F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.008F;
 
 
@@ -342,7 +274,6 @@ private:
 
 
 void registerMountedKnightRenderer(
 void registerMountedKnightRenderer(
     Render::GL::EntityRendererRegistry &registry) {
     Render::GL::EntityRendererRegistry &registry) {
-  static MountedKnightRenderer const renderer;
   registry.register_renderer(
   registry.register_renderer(
       "troops/kingdom/horse_swordsman",
       "troops/kingdom/horse_swordsman",
       [](const DrawContext &ctx, ISubmitter &out) {
       [](const DrawContext &ctx, ISubmitter &out) {

+ 63 - 133
render/entity/nations/roman/horse_swordsman_renderer.cpp

@@ -12,6 +12,7 @@
 #include "../../../gl/shader.h"
 #include "../../../gl/shader.h"
 #include "../../../humanoid/humanoid_math.h"
 #include "../../../humanoid/humanoid_math.h"
 #include "../../../humanoid/humanoid_specs.h"
 #include "../../../humanoid/humanoid_specs.h"
+#include "../../../humanoid/mounted_pose_controller.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/pose_controller.h"
 #include "../../../humanoid/rig.h"
 #include "../../../humanoid/rig.h"
 #include "../../../palette.h"
 #include "../../../palette.h"
@@ -54,12 +55,36 @@ struct MountedKnightExtras {
 class MountedKnightRenderer : public HumanoidRendererBase {
 class MountedKnightRenderer : public HumanoidRendererBase {
 public:
 public:
   auto get_proportion_scaling() const -> QVector3D override {
   auto get_proportion_scaling() const -> QVector3D override {
-    return {1.40F, 1.05F, 1.10F};
+
+    return {0.2F, 0.667F, 0.2F};
+  }
+
+  auto get_mount_scale() const -> float override { return 0.75F; }
+
+  void adjust_variation(const DrawContext &, uint32_t,
+                        VariationParams &variation) const override {
+    variation.height_scale = 0.92F;
+    variation.bulkScale = 0.80F;
+    variation.stanceWidth = 0.65F;
+    variation.armSwingAmp = 0.55F;
+    variation.walkSpeedMult = 1.0F;
+    variation.postureSlump = 0.0F;
+    variation.shoulderTilt = 0.0F;
   }
   }
 
 
 private:
 private:
   mutable std::unordered_map<uint32_t, MountedKnightExtras> m_extrasCache;
   mutable std::unordered_map<uint32_t, MountedKnightExtras> m_extrasCache;
   HorseRenderer m_horseRenderer;
   HorseRenderer m_horseRenderer;
+  mutable const HumanoidPose *m_lastPose = nullptr;
+  mutable MountedAttachmentFrame m_lastMount{};
+  mutable ReinState m_lastReinState{};
+  mutable bool m_hasLastReins = false;
+
+  auto getScaledHorseDimensions(uint32_t seed) const -> HorseDimensions {
+    HorseDimensions dims = makeHorseDimensions(seed);
+    scaleHorseDimensions(dims, get_mount_scale());
+    return dims;
+  }
 
 
 public:
 public:
   void get_variant(const DrawContext &ctx, uint32_t seed,
   void get_variant(const DrawContext &ctx, uint32_t seed,
@@ -89,150 +114,45 @@ public:
 
 
     const AnimationInputs &anim = anim_ctx.inputs;
     const AnimationInputs &anim = anim_ctx.inputs;
 
 
-    const float arm_height_jitter = (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.03F;
-    const float arm_asymmetry = (hash_01(seed ^ 0xDEF0U) - 0.5F) * 0.04F;
-
     uint32_t horse_seed = seed;
     uint32_t horse_seed = seed;
     if (ctx.entity != nullptr) {
     if (ctx.entity != nullptr) {
       horse_seed = static_cast<uint32_t>(
       horse_seed = static_cast<uint32_t>(
           reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
           reinterpret_cast<uintptr_t>(ctx.entity) & 0xFFFFFFFFU);
     }
     }
 
 
-    const HorseDimensions dims = makeHorseDimensions(horse_seed);
+    HorseDimensions dims = getScaledHorseDimensions(horse_seed);
     HorseProfile mount_profile{};
     HorseProfile mount_profile{};
     mount_profile.dims = dims;
     mount_profile.dims = dims;
-    const HorseMountFrame mount = compute_mount_frame(mount_profile);
+    MountedAttachmentFrame mount = compute_mount_frame(mount_profile);
+    HorseMotionSample const motion =
+        evaluate_horse_motion(mount_profile, anim, anim_ctx);
+    apply_mount_vertical_offset(mount, motion.bob);
+
+    m_lastPose = &pose;
+    m_lastMount = mount;
 
 
-    const float saddle_height = mount.seat_position.y();
-    const float offset_y = saddle_height - pose.pelvisPos.y();
+    ReinState const reins = compute_rein_state(horse_seed, anim_ctx);
+    m_lastReinState = reins;
+    m_hasLastReins = true;
 
 
-    pose.pelvisPos.setY(pose.pelvisPos.y() + offset_y);
-    pose.headPos.setY(pose.headPos.y() + offset_y);
-    pose.neck_base.setY(pose.neck_base.y() + offset_y);
-    pose.shoulderL.setY(pose.shoulderL.y() + offset_y);
-    pose.shoulderR.setY(pose.shoulderR.y() + offset_y);
+    MountedPoseController mounted_controller(pose, anim_ctx);
+    mounted_controller.mountOnHorse(mount);
 
 
     float const speed_norm = anim_ctx.locomotion_normalized_speed();
     float const speed_norm = anim_ctx.locomotion_normalized_speed();
     float const speed_lean = std::clamp(
     float const speed_lean = std::clamp(
         anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
         anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
-    const float lean_forward = dims.seatForwardOffset * 0.08F + speed_lean;
-    pose.shoulderL.setZ(pose.shoulderL.z() + lean_forward);
-    pose.shoulderR.setZ(pose.shoulderR.z() + lean_forward);
-
-    pose.footYOffset = 0.0F;
-    pose.footL = mount.stirrup_bottom_left;
-    pose.foot_r = mount.stirrup_bottom_right;
-
-    const float knee_y =
-        mount.stirrup_bottom_left.y() +
-        (saddle_height - mount.stirrup_bottom_left.y()) * 0.62F;
-    const float knee_z = mount.stirrup_bottom_left.z() * 0.60F + 0.06F;
-
-    QVector3D knee_left = mount.stirrup_attach_left;
-    knee_left.setY(knee_y);
-    knee_left.setZ(knee_z);
-    pose.knee_l = knee_left;
-
-    QVector3D knee_right = mount.stirrup_attach_right;
-    knee_right.setY(knee_y);
-    knee_right.setZ(knee_z);
-    pose.knee_r = knee_right;
-
-    float const shoulder_height = pose.shoulderL.y();
-    float const rein_extension = std::clamp(
-        speed_norm * 0.14F + anim_ctx.locomotion_speed() * 0.015F, 0.0F, 0.12F);
-    float const rein_drop = std::clamp(
-        speed_norm * 0.06F + anim_ctx.locomotion_speed() * 0.008F, 0.0F, 0.04F);
-
-    QVector3D forward = anim_ctx.heading_forward();
-    QVector3D right = anim_ctx.heading_right();
-    QVector3D up = anim_ctx.heading_up();
-    float const rein_spread =
-        std::abs(mount.rein_attach_right.x() - mount.rein_attach_left.x()) *
-        0.5F;
-
-    QVector3D rest_hand_r = mount.rein_attach_right;
-    rest_hand_r += forward * (0.08F + rein_extension);
-    rest_hand_r -= right * (0.10F - arm_asymmetry * 0.05F);
-    rest_hand_r += up * (0.05F + arm_height_jitter * 0.6F - rein_drop);
-
-    QVector3D rest_hand_l = mount.rein_attach_left;
-    rest_hand_l += forward * (0.05F + rein_extension * 0.6F);
-    rest_hand_l += right * (0.08F + arm_asymmetry * 0.04F);
-    rest_hand_l += up * (0.04F - arm_height_jitter * 0.5F - rein_drop * 0.6F);
-
-    float const rein_forward = rest_hand_r.z();
-
-    HumanoidPoseController controller(pose, anim_ctx);
-
-    controller.placeHandAt(false, rest_hand_r);
-    controller.placeHandAt(true, rest_hand_l);
-
-    pose.elbowL =
-        QVector3D(pose.shoulderL.x() * 0.4F + rest_hand_l.x() * 0.6F,
-                  (pose.shoulderL.y() + rest_hand_l.y()) * 0.5F - 0.08F,
-                  (pose.shoulderL.z() + rest_hand_l.z()) * 0.5F);
-    pose.elbowR =
-        QVector3D(pose.shoulderR.x() * 0.4F + rest_hand_r.x() * 0.6F,
-                  (pose.shoulderR.y() + rest_hand_r.y()) * 0.5F - 0.08F,
-                  (pose.shoulderR.z() + rest_hand_r.z()) * 0.5F);
+    float const forward_lean =
+        (dims.seatForwardOffset * 0.08F + speed_lean) / 0.15F;
+    mounted_controller.ridingLeaning(mount, forward_lean, 0.0F);
 
 
     if (anim.is_attacking && anim.isMelee) {
     if (anim.is_attacking && anim.isMelee) {
+
       float const attack_phase =
       float const attack_phase =
           std::fmod(anim.time * MOUNTED_KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
           std::fmod(anim.time * MOUNTED_KNIGHT_INV_ATTACK_CYCLE_TIME, 1.0F);
-
-      QVector3D const rest_pos = rest_hand_r;
-      QVector3D const windup_pos =
-          QVector3D(rest_hand_r.x() + 0.32F, shoulder_height + 0.15F,
-                    rein_forward - 0.35F);
-      QVector3D const raised_pos = QVector3D(
-          rein_spread + 0.38F, shoulder_height + 0.28F, rein_forward - 0.25F);
-      QVector3D const slash_pos = QVector3D(
-          -rein_spread * 0.65F, shoulder_height - 0.08F, rein_forward + 0.85F);
-      QVector3D const follow_through = QVector3D(
-          -rein_spread * 0.85F, shoulder_height - 0.15F, rein_forward + 0.60F);
-      QVector3D const recover_pos = QVector3D(
-          rein_spread * 0.45F, shoulder_height - 0.05F, rein_forward + 0.25F);
-
-      QVector3D hand_r_target;
-
-      if (attack_phase < 0.18F) {
-        float const t = easeInOutCubic(attack_phase / 0.18F);
-        hand_r_target = rest_pos * (1.0F - t) + windup_pos * t;
-      } else if (attack_phase < 0.30F) {
-        float const t = easeInOutCubic((attack_phase - 0.18F) / 0.12F);
-        hand_r_target = windup_pos * (1.0F - t) + raised_pos * t;
-      } else if (attack_phase < 0.48F) {
-        float t = (attack_phase - 0.30F) / 0.18F;
-        t = t * t * t;
-        hand_r_target = raised_pos * (1.0F - t) + slash_pos * t;
-      } else if (attack_phase < 0.62F) {
-        float const t = easeInOutCubic((attack_phase - 0.48F) / 0.14F);
-        hand_r_target = slash_pos * (1.0F - t) + follow_through * t;
-      } else if (attack_phase < 0.80F) {
-        float const t = easeInOutCubic((attack_phase - 0.62F) / 0.18F);
-        hand_r_target = follow_through * (1.0F - t) + recover_pos * t;
-      } else {
-        float const t = smoothstep(0.80F, 1.0F, attack_phase);
-        hand_r_target = recover_pos * (1.0F - t) + rest_pos * t;
-      }
-
-      float const rein_tension = clamp01((attack_phase - 0.10F) * 2.2F);
-      QVector3D const hand_l_target =
-          rest_hand_l +
-          QVector3D(0.0F, -0.015F * rein_tension, 0.10F * rein_tension);
-
-      controller.placeHandAt(false, hand_r_target);
-      controller.placeHandAt(true, hand_l_target);
-
-      pose.elbowR =
-          QVector3D(pose.shoulderR.x() * 0.3F + pose.hand_r.x() * 0.7F,
-                    (pose.shoulderR.y() + pose.hand_r.y()) * 0.5F - 0.12F,
-                    (pose.shoulderR.z() + pose.hand_r.z()) * 0.5F);
-      pose.elbowL =
-          QVector3D(pose.shoulderL.x() * 0.4F + pose.handL.x() * 0.6F,
-                    (pose.shoulderL.y() + pose.handL.y()) * 0.5F - 0.08F,
-                    (pose.shoulderL.z() + pose.handL.z()) * 0.5F);
+      mounted_controller.ridingMeleeStrike(mount, attack_phase);
+    } else {
+      mounted_controller.holdReins(mount, reins.slack, reins.slack,
+                                   reins.tension, reins.tension);
     }
     }
   }
   }
 
 
@@ -252,7 +172,8 @@ public:
     if (it != m_extrasCache.end()) {
     if (it != m_extrasCache.end()) {
       extras = it->second;
       extras = it->second;
     } else {
     } else {
-      extras = computeMountedKnightExtras(horse_seed, v);
+      HorseDimensions dims = getScaledHorseDimensions(horse_seed);
+      extras = computeMountedKnightExtras(horse_seed, v, dims);
       m_extrasCache[horse_seed] = extras;
       m_extrasCache[horse_seed] = extras;
 
 
       if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
       if (m_extrasCache.size() > MAX_EXTRAS_CACHE_SIZE) {
@@ -260,7 +181,15 @@ public:
       }
       }
     }
     }
 
 
-    m_horseRenderer.render(ctx, anim, anim_ctx, extras.horseProfile, out);
+    const bool is_current_pose = (m_lastPose == &pose);
+    const MountedAttachmentFrame *mount_ptr =
+        (is_current_pose) ? &m_lastMount : nullptr;
+    const ReinState *rein_ptr =
+        (is_current_pose && m_hasLastReins) ? &m_lastReinState : nullptr;
+    m_horseRenderer.render(ctx, anim, anim_ctx, extras.horseProfile, mount_ptr,
+                           rein_ptr, out);
+    m_lastPose = nullptr;
+    m_hasLastReins = false;
 
 
     bool const is_attacking = anim.is_attacking && anim.isMelee;
     bool const is_attacking = anim.is_attacking && anim.isMelee;
 
 
@@ -320,15 +249,17 @@ public:
   }
   }
 
 
 private:
 private:
-  static auto
-  computeMountedKnightExtras(uint32_t seed,
-                             const HumanoidVariant &v) -> MountedKnightExtras {
+  static auto computeMountedKnightExtras(
+      uint32_t seed, const HumanoidVariant &v,
+      const HorseDimensions &dims) -> MountedKnightExtras {
     MountedKnightExtras e;
     MountedKnightExtras e;
 
 
     e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
     e.metalColor = QVector3D(0.72F, 0.73F, 0.78F);
 
 
     e.horseProfile = makeHorseProfile(seed, v.palette.leather, v.palette.cloth);
     e.horseProfile = makeHorseProfile(seed, v.palette.leather, v.palette.cloth);
 
 
+    e.horseProfile.dims = dims;
+
     e.swordLength = 0.82F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.12F;
     e.swordLength = 0.82F + (hash_01(seed ^ 0xABCDU) - 0.5F) * 0.12F;
     e.swordWidth = 0.042F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.008F;
     e.swordWidth = 0.042F + (hash_01(seed ^ 0x7777U) - 0.5F) * 0.008F;
 
 
@@ -340,7 +271,6 @@ private:
 };
 };
 void registerMountedKnightRenderer(
 void registerMountedKnightRenderer(
     Render::GL::EntityRendererRegistry &registry) {
     Render::GL::EntityRendererRegistry &registry) {
-  static MountedKnightRenderer const renderer;
   registry.register_renderer(
   registry.register_renderer(
       "troops/roman/horse_swordsman",
       "troops/roman/horse_swordsman",
       [](const DrawContext &ctx, ISubmitter &out) {
       [](const DrawContext &ctx, ISubmitter &out) {

+ 18 - 27
render/horse/horse_animation_controller.cpp

@@ -9,7 +9,6 @@ namespace Render::GL {
 namespace {
 namespace {
 constexpr float k_pi = std::numbers::pi_v<float>;
 constexpr float k_pi = std::numbers::pi_v<float>;
 
 
-// Gait parameters based on real horse gaits
 struct GaitParameters {
 struct GaitParameters {
   float cycleTime;
   float cycleTime;
   float frontLegPhase;
   float frontLegPhase;
@@ -41,7 +40,7 @@ HorseAnimationController::HorseAnimationController(
     HorseProfile &profile, const AnimationInputs &anim,
     HorseProfile &profile, const AnimationInputs &anim,
     const HumanoidAnimationContext &rider_ctx)
     const HumanoidAnimationContext &rider_ctx)
     : m_profile(profile), m_anim(anim), m_rider_ctx(rider_ctx) {
     : m_profile(profile), m_anim(anim), m_rider_ctx(rider_ctx) {
-  // Initialize from current profile
+
   m_phase = 0.0F;
   m_phase = 0.0F;
   m_bob = 0.0F;
   m_bob = 0.0F;
   m_rein_slack = 0.05F;
   m_rein_slack = 0.05F;
@@ -62,8 +61,7 @@ HorseAnimationController::HorseAnimationController(
 
 
 void HorseAnimationController::setGait(GaitType gait) {
 void HorseAnimationController::setGait(GaitType gait) {
   m_current_gait = gait;
   m_current_gait = gait;
-  
-  // Update speed based on gait
+
   switch (gait) {
   switch (gait) {
   case GaitType::IDLE:
   case GaitType::IDLE:
     m_speed = 0.0F;
     m_speed = 0.0F;
@@ -81,28 +79,26 @@ void HorseAnimationController::setGait(GaitType gait) {
     m_speed = 10.0F;
     m_speed = 10.0F;
     break;
     break;
   }
   }
-  
+
   updateGaitParameters();
   updateGaitParameters();
 }
 }
 
 
 void HorseAnimationController::idle(float bob_intensity) {
 void HorseAnimationController::idle(float bob_intensity) {
   m_current_gait = GaitType::IDLE;
   m_current_gait = GaitType::IDLE;
   m_speed = 0.0F;
   m_speed = 0.0F;
-  
-  // Calculate idle bob
+
   float const phase = std::fmod(m_anim.time * 0.25F, 1.0F);
   float const phase = std::fmod(m_anim.time * 0.25F, 1.0F);
   m_phase = phase;
   m_phase = phase;
   m_bob = std::sin(phase * 2.0F * k_pi) * m_profile.dims.idleBobAmplitude *
   m_bob = std::sin(phase * 2.0F * k_pi) * m_profile.dims.idleBobAmplitude *
           bob_intensity;
           bob_intensity;
-  
+
   updateGaitParameters();
   updateGaitParameters();
 }
 }
 
 
 void HorseAnimationController::accelerate(float speed_delta) {
 void HorseAnimationController::accelerate(float speed_delta) {
   m_speed += speed_delta;
   m_speed += speed_delta;
   m_speed = std::max(0.0F, m_speed);
   m_speed = std::max(0.0F, m_speed);
-  
-  // Auto-adjust gait based on speed
+
   if (m_speed < 0.5F) {
   if (m_speed < 0.5F) {
     m_current_gait = GaitType::IDLE;
     m_current_gait = GaitType::IDLE;
   } else if (m_speed < 3.0F) {
   } else if (m_speed < 3.0F) {
@@ -114,7 +110,7 @@ void HorseAnimationController::accelerate(float speed_delta) {
   } else {
   } else {
     m_current_gait = GaitType::GALLOP;
     m_current_gait = GaitType::GALLOP;
   }
   }
-  
+
   updateGaitParameters();
   updateGaitParameters();
 }
 }
 
 
@@ -128,7 +124,7 @@ void HorseAnimationController::turn(float yaw_radians, float banking_amount) {
 }
 }
 
 
 void HorseAnimationController::strafeStep(bool left, float distance) {
 void HorseAnimationController::strafeStep(bool left, float distance) {
-  // Strafe step is a lateral movement - could adjust phase slightly
+
   float const direction = left ? -1.0F : 1.0F;
   float const direction = left ? -1.0F : 1.0F;
   m_phase = std::fmod(m_phase + direction * distance * 0.1F, 1.0F);
   m_phase = std::fmod(m_phase + direction * distance * 0.1F, 1.0F);
 }
 }
@@ -168,39 +164,34 @@ auto HorseAnimationController::getStrideCycle() const -> float {
 
 
 void HorseAnimationController::updateGaitParameters() {
 void HorseAnimationController::updateGaitParameters() {
   GaitParameters const params = getGaitParams(m_current_gait);
   GaitParameters const params = getGaitParams(m_current_gait);
-  
-  // Update profile gait parameters
+
   m_profile.gait.cycleTime = params.cycleTime;
   m_profile.gait.cycleTime = params.cycleTime;
   m_profile.gait.frontLegPhase = params.frontLegPhase;
   m_profile.gait.frontLegPhase = params.frontLegPhase;
   m_profile.gait.rearLegPhase = params.rearLegPhase;
   m_profile.gait.rearLegPhase = params.rearLegPhase;
   m_profile.gait.strideSwing = params.strideSwing;
   m_profile.gait.strideSwing = params.strideSwing;
   m_profile.gait.strideLift = params.strideLift;
   m_profile.gait.strideLift = params.strideLift;
-  
-  // Calculate phase and bob based on current gait
+
   bool const is_moving = m_current_gait != GaitType::IDLE;
   bool const is_moving = m_current_gait != GaitType::IDLE;
-  
+
   if (is_moving) {
   if (is_moving) {
-    // Use rider context if available, otherwise compute from time
+
     if (m_rider_ctx.gait.cycle_time > 0.0001F) {
     if (m_rider_ctx.gait.cycle_time > 0.0001F) {
       m_phase = m_rider_ctx.gait.cycle_phase;
       m_phase = m_rider_ctx.gait.cycle_phase;
     } else {
     } else {
       m_phase = std::fmod(m_anim.time / params.cycleTime, 1.0F);
       m_phase = std::fmod(m_anim.time / params.cycleTime, 1.0F);
     }
     }
-    
-    // Calculate bob with intensity from rider
+
     float const rider_intensity = m_rider_ctx.locomotion_normalized_speed();
     float const rider_intensity = m_rider_ctx.locomotion_normalized_speed();
-    float const bob_amp =
-        m_profile.dims.idleBobAmplitude +
-        rider_intensity * (m_profile.dims.moveBobAmplitude -
-                          m_profile.dims.idleBobAmplitude);
+    float const bob_amp = m_profile.dims.idleBobAmplitude +
+                          rider_intensity * (m_profile.dims.moveBobAmplitude -
+                                             m_profile.dims.idleBobAmplitude);
     m_bob = std::sin(m_phase * 2.0F * k_pi) * bob_amp;
     m_bob = std::sin(m_phase * 2.0F * k_pi) * bob_amp;
   } else {
   } else {
-    // Idle animation
+
     m_phase = std::fmod(m_anim.time * 0.25F, 1.0F);
     m_phase = std::fmod(m_anim.time * 0.25F, 1.0F);
     m_bob = std::sin(m_phase * 2.0F * k_pi) * m_profile.dims.idleBobAmplitude;
     m_bob = std::sin(m_phase * 2.0F * k_pi) * m_profile.dims.idleBobAmplitude;
   }
   }
-  
-  // Calculate rein slack based on rider state
+
   float rein_tension = m_rider_ctx.locomotion_normalized_speed();
   float rein_tension = m_rider_ctx.locomotion_normalized_speed();
   if (m_rider_ctx.gait.has_target) {
   if (m_rider_ctx.gait.has_target) {
     rein_tension += 0.25F;
     rein_tension += 0.25F;

+ 5 - 16
render/horse/horse_animation_controller.h

@@ -4,41 +4,30 @@
 
 
 namespace Render::GL {
 namespace Render::GL {
 
 
-enum class GaitType {
-  IDLE,
-  WALK,
-  TROT,
-  CANTER,
-  GALLOP
-};
+enum class GaitType { IDLE, WALK, TROT, CANTER, GALLOP };
 
 
 class HorseAnimationController {
 class HorseAnimationController {
 public:
 public:
   HorseAnimationController(HorseProfile &profile, const AnimationInputs &anim,
   HorseAnimationController(HorseProfile &profile, const AnimationInputs &anim,
-                          const HumanoidAnimationContext &rider_ctx);
+                           const HumanoidAnimationContext &rider_ctx);
 
 
-  // Gait control
-  void setGait(GaitType gait);  // WALK, TROT, CANTER, GALLOP
+  void setGait(GaitType gait);
   void idle(float bob_intensity = 1.0F);
   void idle(float bob_intensity = 1.0F);
   void accelerate(float speed_delta);
   void accelerate(float speed_delta);
   void decelerate(float speed_delta);
   void decelerate(float speed_delta);
 
 
-  // Movement
   void turn(float yaw_radians, float banking_amount);
   void turn(float yaw_radians, float banking_amount);
   void strafeStep(bool left, float distance);
   void strafeStep(bool left, float distance);
 
 
-  // Special animations
-  void rear(float height_factor);  // 0.0 = ground, 1.0 = full rear
+  void rear(float height_factor);
   void kick(bool rear_legs, float power);
   void kick(bool rear_legs, float power);
   void buck(float intensity);
   void buck(float intensity);
   void jumpObstacle(float height, float distance);
   void jumpObstacle(float height, float distance);
 
 
-  // State queries
   auto getCurrentPhase() const -> float;
   auto getCurrentPhase() const -> float;
   auto getCurrentBob() const -> float;
   auto getCurrentBob() const -> float;
   auto getStrideCycle() const -> float;
   auto getStrideCycle() const -> float;
 
 
-  // Apply to profile
   void updateGaitParameters();
   void updateGaitParameters();
 
 
 private:
 private:
@@ -49,7 +38,7 @@ private:
   float m_phase{};
   float m_phase{};
   float m_bob{};
   float m_bob{};
   float m_rein_slack{};
   float m_rein_slack{};
-  
+
   GaitType m_current_gait{GaitType::IDLE};
   GaitType m_current_gait{GaitType::IDLE};
   float m_speed{};
   float m_speed{};
   float m_turn_angle{};
   float m_turn_angle{};

+ 220 - 277
render/horse/rig.cpp

@@ -141,7 +141,6 @@ auto makeHorseDimensions(uint32_t seed) -> HorseDimensions {
   d.bodyLength = randBetween(seed, 0x12U, 0.88F, 0.98F);
   d.bodyLength = randBetween(seed, 0x12U, 0.88F, 0.98F);
   d.bodyWidth = randBetween(seed, 0x34U, 0.18F, 0.22F);
   d.bodyWidth = randBetween(seed, 0x34U, 0.18F, 0.22F);
   d.bodyHeight = randBetween(seed, 0x56U, 0.40F, 0.46F);
   d.bodyHeight = randBetween(seed, 0x56U, 0.40F, 0.46F);
-  d.barrel_centerY = randBetween(seed, 0x78U, 0.05F, 0.09F);
 
 
   d.neckLength = randBetween(seed, 0x9AU, 0.42F, 0.50F);
   d.neckLength = randBetween(seed, 0x9AU, 0.42F, 0.50F);
   d.neckRise = randBetween(seed, 0xBCU, 0.26F, 0.32F);
   d.neckRise = randBetween(seed, 0xBCU, 0.26F, 0.32F);
@@ -163,6 +162,12 @@ auto makeHorseDimensions(uint32_t seed) -> HorseDimensions {
   d.idleBobAmplitude = randBetween(seed, 0xA864U, 0.004F, 0.007F);
   d.idleBobAmplitude = randBetween(seed, 0xA864U, 0.004F, 0.007F);
   d.moveBobAmplitude = randBetween(seed, 0xB975U, 0.024F, 0.032F);
   d.moveBobAmplitude = randBetween(seed, 0xB975U, 0.024F, 0.032F);
 
 
+  float const avg_leg_segment_ratio = 0.59F + 0.30F + 0.12F;
+  float const leg_down_distance =
+      d.legLength * avg_leg_segment_ratio + d.hoofHeight;
+  float const shoulder_to_barrel_offset = d.bodyHeight * 0.05F + 0.05F;
+  d.barrel_centerY = leg_down_distance - shoulder_to_barrel_offset;
+
   d.saddle_height = d.barrel_centerY + d.bodyHeight * 0.55F + d.saddleThickness;
   d.saddle_height = d.barrel_centerY + d.bodyHeight * 0.55F + d.saddleThickness;
 
 
   return d;
   return d;
@@ -232,13 +237,25 @@ auto makeHorseProfile(uint32_t seed, const QVector3D &leatherBase,
   return profile;
   return profile;
 }
 }
 
 
-auto compute_mount_frame(const HorseProfile &profile) -> HorseMountFrame {
+auto MountedAttachmentFrame::stirrup_attach(bool is_left) const
+    -> const QVector3D & {
+  return is_left ? stirrup_attach_left : stirrup_attach_right;
+}
+
+auto MountedAttachmentFrame::stirrup_bottom(bool is_left) const
+    -> const QVector3D & {
+  return is_left ? stirrup_bottom_left : stirrup_bottom_right;
+}
+
+auto compute_mount_frame(const HorseProfile &profile)
+    -> MountedAttachmentFrame {
   const HorseDimensions &d = profile.dims;
   const HorseDimensions &d = profile.dims;
-  HorseMountFrame frame{};
+  MountedAttachmentFrame frame{};
 
 
   frame.seat_forward = QVector3D(0.0F, 0.0F, 1.0F);
   frame.seat_forward = QVector3D(0.0F, 0.0F, 1.0F);
   frame.seat_right = QVector3D(1.0F, 0.0F, 0.0F);
   frame.seat_right = QVector3D(1.0F, 0.0F, 0.0F);
   frame.seat_up = QVector3D(0.0F, 1.0F, 0.0F);
   frame.seat_up = QVector3D(0.0F, 1.0F, 0.0F);
+  frame.ground_offset = QVector3D(0.0F, -d.barrel_centerY, 0.0F);
 
 
   frame.saddle_center =
   frame.saddle_center =
       QVector3D(0.0F, d.saddle_height - d.saddleThickness * 0.35F,
       QVector3D(0.0F, d.saddle_height - d.saddleThickness * 0.35F,
@@ -261,47 +278,78 @@ auto compute_mount_frame(const HorseProfile &profile) -> HorseMountFrame {
   frame.stirrup_bottom_right =
   frame.stirrup_bottom_right =
       frame.stirrup_attach_right + QVector3D(0.0F, -d.stirrupDrop, 0.0F);
       frame.stirrup_attach_right + QVector3D(0.0F, -d.stirrupDrop, 0.0F);
 
 
-  frame.rein_attach_left =
-      frame.saddle_center + QVector3D(-d.bodyWidth * 0.62F,
-                                      -d.saddleThickness * 0.32F,
-                                      d.seatForwardOffset * 0.10F);
-  frame.rein_attach_right =
-      frame.saddle_center + QVector3D(d.bodyWidth * 0.62F,
-                                      -d.saddleThickness * 0.32F,
-                                      d.seatForwardOffset * 0.10F);
-
   QVector3D const neck_top(0.0F,
   QVector3D const neck_top(0.0F,
                            d.barrel_centerY + d.bodyHeight * 0.65F + d.neckRise,
                            d.barrel_centerY + d.bodyHeight * 0.65F + d.neckRise,
                            d.bodyLength * 0.25F);
                            d.bodyLength * 0.25F);
   QVector3D const head_center =
   QVector3D const head_center =
       neck_top + QVector3D(0.0F, d.headHeight * 0.10F, d.headLength * 0.40F);
       neck_top + QVector3D(0.0F, d.headHeight * 0.10F, d.headLength * 0.40F);
-  frame.bridle_base = head_center + QVector3D(0.0F, -d.headHeight * 0.18F,
-                                              d.headLength * 0.58F);
+
+  QVector3D const muzzle_center =
+      head_center +
+      QVector3D(0.0F, -d.headHeight * 0.18F, d.headLength * 0.58F);
+  frame.bridle_base = muzzle_center + QVector3D(0.0F, -d.headHeight * 0.05F,
+                                                d.muzzleLength * 0.20F);
+  frame.rein_bit_left =
+      muzzle_center + QVector3D(d.headWidth * 0.55F, -d.headHeight * 0.08F,
+                                d.muzzleLength * 0.10F);
+  frame.rein_bit_right =
+      muzzle_center + QVector3D(-d.headWidth * 0.55F, -d.headHeight * 0.08F,
+                                d.muzzleLength * 0.10F);
 
 
   return frame;
   return frame;
 }
 }
 
 
-void HorseRendererBase::render(const DrawContext &ctx,
-                               const AnimationInputs &anim,
-                               const HumanoidAnimationContext &rider_ctx,
-                               HorseProfile &profile,
-                               ISubmitter &out) const {
-  const HorseDimensions &d = profile.dims;
-  const HorseVariant &v = profile.variant;
-  const HorseGait &g = profile.gait;
-  HorseMountFrame mount = compute_mount_frame(profile);
+auto compute_rein_state(uint32_t horse_seed,
+                        const HumanoidAnimationContext &rider_ctx)
+    -> ReinState {
+  float const base_slack = hash01(horse_seed ^ 0x707U) * 0.08F + 0.02F;
+  float rein_tension = rider_ctx.locomotion_normalized_speed();
+  if (rider_ctx.gait.has_target) {
+    rein_tension += 0.25F;
+  }
+  if (rider_ctx.is_attacking()) {
+    rein_tension += 0.35F;
+  }
+  rein_tension = std::clamp(rein_tension, 0.0F, 1.0F);
+  float const rein_slack = std::max(0.01F, base_slack * (1.0F - rein_tension));
+  return ReinState{rein_slack, rein_tension};
+}
+
+auto compute_rein_handle(const MountedAttachmentFrame &mount, bool is_left,
+                         float slack, float tension) -> QVector3D {
+  float const clamped_slack = std::clamp(slack, 0.0F, 1.0F);
+  float const clamped_tension = std::clamp(tension, 0.0F, 1.0F);
+
+  QVector3D const &bit = is_left ? mount.rein_bit_left : mount.rein_bit_right;
+
+  QVector3D desired = mount.seat_position;
+  desired += (is_left ? -mount.seat_right : mount.seat_right) * 0.08F;
+  desired += -mount.seat_forward * (0.18F + clamped_tension * 0.18F);
+  desired += mount.seat_up *
+             (-0.10F - clamped_slack * 0.30F + clamped_tension * 0.04F);
+
+  QVector3D dir = desired - bit;
+  if (dir.lengthSquared() < 1e-4F) {
+    dir = -mount.seat_forward;
+  }
+  dir.normalize();
 
 
-  // Use HorseAnimationController to manage animation state
+  constexpr float k_base_length = 0.85F;
+  float const rein_length = k_base_length + clamped_slack * 0.12F;
+  return bit + dir * rein_length;
+}
+
+auto evaluate_horse_motion(HorseProfile &profile, const AnimationInputs &anim,
+                           const HumanoidAnimationContext &rider_ctx)
+    -> HorseMotionSample {
+  HorseMotionSample sample{};
   HorseAnimationController controller(profile, anim, rider_ctx);
   HorseAnimationController controller(profile, anim, rider_ctx);
-  
-  // Determine gait based on rider state
-  const bool rider_has_motion =
+  sample.rider_intensity = rider_ctx.locomotion_normalized_speed();
+  bool const rider_has_motion =
       rider_ctx.is_walking() || rider_ctx.is_running();
       rider_ctx.is_walking() || rider_ctx.is_running();
-  const bool is_moving = rider_has_motion || anim.isMoving;
-  const float rider_intensity = rider_ctx.locomotion_normalized_speed();
-  
-  if (is_moving) {
-    // Auto-adjust gait based on rider speed
+  sample.is_moving = rider_has_motion || anim.isMoving;
+
+  if (sample.is_moving) {
     float const speed = rider_ctx.locomotion_speed();
     float const speed = rider_ctx.locomotion_speed();
     if (speed < 0.5F) {
     if (speed < 0.5F) {
       controller.idle(1.0F);
       controller.idle(1.0F);
@@ -317,13 +365,58 @@ void HorseRendererBase::render(const DrawContext &ctx,
   } else {
   } else {
     controller.idle(1.0F);
     controller.idle(1.0F);
   }
   }
-  
-  // Update gait parameters - this modifies the profile
+
   controller.updateGaitParameters();
   controller.updateGaitParameters();
-  
-  // Get animation state from controller
-  float const phase = controller.getCurrentPhase();
-  float const bob = controller.getCurrentBob();
+  sample.phase = controller.getCurrentPhase();
+  sample.bob = controller.getCurrentBob();
+  return sample;
+}
+
+void apply_mount_vertical_offset(MountedAttachmentFrame &frame, float bob) {
+  QVector3D const offset(0.0F, bob, 0.0F);
+  frame.saddle_center += offset;
+  frame.seat_position += offset;
+  frame.stirrup_attach_left += offset;
+  frame.stirrup_attach_right += offset;
+  frame.stirrup_bottom_left += offset;
+  frame.stirrup_bottom_right += offset;
+  frame.rein_bit_left += offset;
+  frame.rein_bit_right += offset;
+  frame.bridle_base += offset;
+}
+
+void HorseRendererBase::render(const DrawContext &ctx,
+                               const AnimationInputs &anim,
+                               const HumanoidAnimationContext &rider_ctx,
+                               HorseProfile &profile,
+                               const MountedAttachmentFrame *shared_mount,
+                               const ReinState *shared_reins,
+                               ISubmitter &out) const {
+  const HorseDimensions &d = profile.dims;
+  const HorseVariant &v = profile.variant;
+  const HorseGait &g = profile.gait;
+  HorseMotionSample const motion =
+      evaluate_horse_motion(profile, anim, rider_ctx);
+  float const phase = motion.phase;
+  float const bob = motion.bob;
+  const bool is_moving = motion.is_moving;
+  const float rider_intensity = motion.rider_intensity;
+
+  MountedAttachmentFrame mount =
+      shared_mount ? *shared_mount : compute_mount_frame(profile);
+  if (!shared_mount) {
+    apply_mount_vertical_offset(mount, bob);
+  }
+
+  uint32_t horse_seed = 0U;
+  if (ctx.entity != nullptr) {
+    horse_seed = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(ctx.entity) &
+                                       0xFFFFFFFFU);
+  }
+
+  DrawContext horse_ctx = ctx;
+  horse_ctx.model = ctx.model;
+  horse_ctx.model.translate(mount.ground_offset);
 
 
   float const head_nod = is_moving ? std::sin((phase + 0.25F) * 2.0F * k_pi) *
   float const head_nod = is_moving ? std::sin((phase + 0.25F) * 2.0F * k_pi) *
                                          (0.02F + rider_intensity * 0.03F)
                                          (0.02F + rider_intensity * 0.03F)
@@ -335,18 +428,11 @@ void HorseRendererBase::render(const DrawContext &ctx,
   float const sock_chance_rl = hash01(vhash ^ 0x303U);
   float const sock_chance_rl = hash01(vhash ^ 0x303U);
   float const sock_chance_rr = hash01(vhash ^ 0x404U);
   float const sock_chance_rr = hash01(vhash ^ 0x404U);
   bool const has_blaze = hash01(vhash ^ 0x505U) > 0.82F;
   bool const has_blaze = hash01(vhash ^ 0x505U) > 0.82F;
-  
-  // Calculate rein slack using controller's logic
-  float rein_slack = hash01(vhash ^ 0x707U) * 0.08F + 0.02F;
-  float rein_tension = rider_intensity;
-  if (rider_ctx.gait.has_target) {
-    rein_tension += 0.25F;
-  }
-  if (rider_ctx.is_attacking()) {
-    rein_tension += 0.35F;
-  }
-  rein_tension = std::clamp(rein_tension, 0.0F, 1.0F);
-  rein_slack = std::max(0.01F, rein_slack * (1.0F - rein_tension));
+
+  ReinState const rein_state =
+      shared_reins ? *shared_reins : compute_rein_state(horse_seed, rider_ctx);
+  float const rein_slack = rein_state.slack;
+  float const rein_tension = rein_state.tension;
 
 
   const float coat_seed_a = hash01(vhash ^ 0x701U);
   const float coat_seed_a = hash01(vhash ^ 0x701U);
   const float coat_seed_b = hash01(vhash ^ 0x702U);
   const float coat_seed_b = hash01(vhash ^ 0x702U);
@@ -354,6 +440,9 @@ void HorseRendererBase::render(const DrawContext &ctx,
   const float coat_seed_d = hash01(vhash ^ 0x704U);
   const float coat_seed_d = hash01(vhash ^ 0x704U);
 
 
   QVector3D const barrel_center(0.0F, d.barrel_centerY + bob, 0.0F);
   QVector3D const barrel_center(0.0F, d.barrel_centerY + bob, 0.0F);
+
+  float const ground_offset = -d.barrel_centerY - bob;
+
   QVector3D const chest_center =
   QVector3D const chest_center =
       barrel_center +
       barrel_center +
       QVector3D(0.0F, d.bodyHeight * 0.12F, d.bodyLength * 0.34F);
       QVector3D(0.0F, d.bodyHeight * 0.12F, d.bodyLength * 0.34F);
@@ -365,7 +454,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
       QVector3D(0.0F, -d.bodyHeight * 0.35F, -d.bodyLength * 0.05F);
       QVector3D(0.0F, -d.bodyHeight * 0.35F, -d.bodyLength * 0.05F);
 
 
   {
   {
-    QMatrix4x4 chest = ctx.model;
+    QMatrix4x4 chest = horse_ctx.model;
     chest.translate(chest_center);
     chest.translate(chest_center);
     chest.scale(d.bodyWidth * 1.12F, d.bodyHeight * 0.95F,
     chest.scale(d.bodyWidth * 1.12F, d.bodyHeight * 0.95F,
                 d.bodyLength * 0.36F);
                 d.bodyLength * 0.36F);
@@ -375,7 +464,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   }
   }
 
 
   {
   {
-    QMatrix4x4 withers = ctx.model;
+    QMatrix4x4 withers = horse_ctx.model;
     withers.translate(chest_center + QVector3D(0.0F, d.bodyHeight * 0.55F,
     withers.translate(chest_center + QVector3D(0.0F, d.bodyHeight * 0.55F,
                                                -d.bodyLength * 0.03F));
                                                -d.bodyLength * 0.03F));
     withers.scale(d.bodyWidth * 0.75F, d.bodyHeight * 0.35F,
     withers.scale(d.bodyWidth * 0.75F, d.bodyHeight * 0.35F,
@@ -386,7 +475,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   }
   }
 
 
   {
   {
-    QMatrix4x4 belly = ctx.model;
+    QMatrix4x4 belly = horse_ctx.model;
     belly.translate(belly_center);
     belly.translate(belly_center);
     belly.scale(d.bodyWidth * 0.98F, d.bodyHeight * 0.64F,
     belly.scale(d.bodyWidth * 0.98F, d.bodyHeight * 0.64F,
                 d.bodyLength * 0.40F);
                 d.bodyLength * 0.40F);
@@ -397,7 +486,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
 
 
   for (int i = 0; i < 2; ++i) {
   for (int i = 0; i < 2; ++i) {
     float const side = (i == 0) ? 1.0F : -1.0F;
     float const side = (i == 0) ? 1.0F : -1.0F;
-    QMatrix4x4 ribs = ctx.model;
+    QMatrix4x4 ribs = horse_ctx.model;
     ribs.translate(barrel_center + QVector3D(side * d.bodyWidth * 0.90F,
     ribs.translate(barrel_center + QVector3D(side * d.bodyWidth * 0.90F,
                                              -d.bodyHeight * 0.10F,
                                              -d.bodyHeight * 0.10F,
                                              -d.bodyLength * 0.05F));
                                              -d.bodyLength * 0.05F));
@@ -408,7 +497,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   }
   }
 
 
   {
   {
-    QMatrix4x4 rump = ctx.model;
+    QMatrix4x4 rump = horse_ctx.model;
     rump.translate(rump_center);
     rump.translate(rump_center);
     rump.scale(d.bodyWidth * 1.18F, d.bodyHeight * 1.00F, d.bodyLength * 0.36F);
     rump.scale(d.bodyWidth * 1.18F, d.bodyHeight * 1.00F, d.bodyLength * 0.36F);
     QVector3D const rump_color =
     QVector3D const rump_color =
@@ -418,7 +507,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
 
 
   for (int i = 0; i < 2; ++i) {
   for (int i = 0; i < 2; ++i) {
     float const side = (i == 0) ? 1.0F : -1.0F;
     float const side = (i == 0) ? 1.0F : -1.0F;
-    QMatrix4x4 hip = ctx.model;
+    QMatrix4x4 hip = horse_ctx.model;
     hip.translate(rump_center + QVector3D(side * d.bodyWidth * 0.95F,
     hip.translate(rump_center + QVector3D(side * d.bodyWidth * 0.95F,
                                           -d.bodyHeight * 0.10F,
                                           -d.bodyHeight * 0.10F,
                                           -d.bodyLength * 0.08F));
                                           -d.bodyLength * 0.08F));
@@ -427,7 +516,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
         coatGradient(v.coatColor, 0.58F, -0.18F, coat_seed_b + side * 0.06F);
         coatGradient(v.coatColor, 0.58F, -0.18F, coat_seed_b + side * 0.06F);
     out.mesh(getUnitSphere(), hip, hip_color, nullptr, 1.0F);
     out.mesh(getUnitSphere(), hip, hip_color, nullptr, 1.0F);
 
 
-    QMatrix4x4 haunch = ctx.model;
+    QMatrix4x4 haunch = horse_ctx.model;
     haunch.translate(rump_center + QVector3D(side * d.bodyWidth * 0.88F,
     haunch.translate(rump_center + QVector3D(side * d.bodyWidth * 0.88F,
                                              d.bodyHeight * 0.24F,
                                              d.bodyHeight * 0.24F,
                                              -d.bodyLength * 0.20F));
                                              -d.bodyLength * 0.20F));
@@ -445,7 +534,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
                                                  -d.bodyLength * 0.18F);
                                                  -d.bodyLength * 0.18F);
 
 
   {
   {
-    QMatrix4x4 spine = ctx.model;
+    QMatrix4x4 spine = horse_ctx.model;
     spine.translate(lerp(withers_peak, croup_peak, 0.42F));
     spine.translate(lerp(withers_peak, croup_peak, 0.42F));
     spine.scale(QVector3D(d.bodyWidth * 0.50F, d.bodyHeight * 0.14F,
     spine.scale(QVector3D(d.bodyWidth * 0.50F, d.bodyHeight * 0.14F,
                           d.bodyLength * 0.54F));
                           d.bodyLength * 0.54F));
@@ -464,10 +553,10 @@ void HorseRendererBase::render(const DrawContext &ctx,
                                  -d.bodyHeight * 0.02F, d.bodyLength * 0.06F);
                                  -d.bodyHeight * 0.02F, d.bodyLength * 0.06F);
     QVector3D const scapula_mid = lerp(scapula_top, scapula_base, 0.55F);
     QVector3D const scapula_mid = lerp(scapula_top, scapula_base, 0.55F);
     draw_cylinder(
     draw_cylinder(
-        out, ctx.model, scapula_top, scapula_mid, d.bodyWidth * 0.18F,
+        out, horse_ctx.model, scapula_top, scapula_mid, d.bodyWidth * 0.18F,
         coatGradient(v.coatColor, 0.82F, 0.16F, coat_seed_a + side * 0.05F));
         coatGradient(v.coatColor, 0.82F, 0.16F, coat_seed_a + side * 0.05F));
 
 
-    QMatrix4x4 shoulder_cap = ctx.model;
+    QMatrix4x4 shoulder_cap = horse_ctx.model;
     shoulder_cap.translate(scapula_base + QVector3D(0.0F, d.bodyHeight * 0.04F,
     shoulder_cap.translate(scapula_base + QVector3D(0.0F, d.bodyHeight * 0.04F,
                                                     d.bodyLength * 0.02F));
                                                     d.bodyLength * 0.02F));
     shoulder_cap.scale(QVector3D(d.bodyWidth * 0.32F, d.bodyHeight * 0.24F,
     shoulder_cap.scale(QVector3D(d.bodyWidth * 0.32F, d.bodyHeight * 0.24F,
@@ -479,7 +568,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   }
   }
 
 
   {
   {
-    QMatrix4x4 sternum = ctx.model;
+    QMatrix4x4 sternum = horse_ctx.model;
     sternum.translate(barrel_center + QVector3D(0.0F, -d.bodyHeight * 0.40F,
     sternum.translate(barrel_center + QVector3D(0.0F, -d.bodyHeight * 0.40F,
                                                 d.bodyLength * 0.28F));
                                                 d.bodyLength * 0.28F));
     sternum.scale(QVector3D(d.bodyWidth * 0.50F, d.bodyHeight * 0.14F,
     sternum.scale(QVector3D(d.bodyWidth * 0.50F, d.bodyHeight * 0.14F,
@@ -502,11 +591,13 @@ void HorseRendererBase::render(const DrawContext &ctx,
   QVector3D const neck_color_base =
   QVector3D const neck_color_base =
       coatGradient(v.coatColor, 0.78F, 0.12F, coat_seed_c * 0.6F);
       coatGradient(v.coatColor, 0.78F, 0.12F, coat_seed_c * 0.6F);
   out.mesh(getUnitCylinder(),
   out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, neck_base, neck_mid, neck_radius * 1.00F),
+           cylinderBetween(horse_ctx.model, neck_base, neck_mid,
+                           neck_radius * 1.00F),
            neck_color_base, nullptr, 1.0F);
            neck_color_base, nullptr, 1.0F);
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, neck_mid, neck_top, neck_radius * 0.86F),
-           lighten(neck_color_base, 1.03F), nullptr, 1.0F);
+  out.mesh(
+      getUnitCylinder(),
+      cylinderBetween(horse_ctx.model, neck_mid, neck_top, neck_radius * 0.86F),
+      lighten(neck_color_base, 1.03F), nullptr, 1.0F);
 
 
   {
   {
     QVector3D const jugular_start =
     QVector3D const jugular_start =
@@ -516,7 +607,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
     QVector3D const jugular_end =
     QVector3D const jugular_end =
         jugular_start +
         jugular_start +
         QVector3D(0.0F, -d.bodyHeight * 0.24F, d.bodyLength * 0.06F);
         QVector3D(0.0F, -d.bodyHeight * 0.24F, d.bodyLength * 0.06F);
-    draw_cylinder(out, ctx.model, jugular_start, jugular_end,
+    draw_cylinder(out, horse_ctx.model, jugular_start, jugular_end,
                   neck_radius * 0.18F, lighten(neck_color_base, 1.08F), 0.85F);
                   neck_radius * 0.18F, lighten(neck_color_base, 1.08F), 0.85F);
   }
   }
 
 
@@ -531,8 +622,8 @@ void HorseRendererBase::render(const DrawContext &ctx,
     float const length = lerp(0.14F, 0.08F, t) * d.bodyHeight * 1.4F;
     float const length = lerp(0.14F, 0.08F, t) * d.bodyHeight * 1.4F;
     QVector3D const tip =
     QVector3D const tip =
         spine + QVector3D(0.0F, length * 1.2F, 0.02F * length);
         spine + QVector3D(0.0F, length * 1.2F, 0.02F * length);
-    drawCone(out, ctx.model, tip, spine, d.bodyWidth * lerp(0.25F, 0.12F, t),
-             mane_color, 1.0F);
+    drawCone(out, horse_ctx.model, tip, spine,
+             d.bodyWidth * lerp(0.25F, 0.12F, t), mane_color, 1.0F);
   }
   }
 
 
   QVector3D const head_center =
   QVector3D const head_center =
@@ -540,7 +631,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
                            d.headLength * 0.40F);
                            d.headLength * 0.40F);
 
 
   {
   {
-    QMatrix4x4 skull = ctx.model;
+    QMatrix4x4 skull = horse_ctx.model;
     skull.translate(head_center + QVector3D(0.0F, d.headHeight * 0.10F,
     skull.translate(head_center + QVector3D(0.0F, d.headHeight * 0.10F,
                                             -d.headLength * 0.10F));
                                             -d.headLength * 0.10F));
     skull.scale(d.headWidth * 0.95F, d.headHeight * 0.90F,
     skull.scale(d.headWidth * 0.95F, d.headHeight * 0.90F,
@@ -552,7 +643,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
 
 
   for (int i = 0; i < 2; ++i) {
   for (int i = 0; i < 2; ++i) {
     float const side = (i == 0) ? 1.0F : -1.0F;
     float const side = (i == 0) ? 1.0F : -1.0F;
-    QMatrix4x4 cheek = ctx.model;
+    QMatrix4x4 cheek = horse_ctx.model;
     cheek.translate(head_center + QVector3D(side * d.headWidth * 0.55F,
     cheek.translate(head_center + QVector3D(side * d.headWidth * 0.55F,
                                             -d.headHeight * 0.15F, 0.0F));
                                             -d.headHeight * 0.15F, 0.0F));
     cheek.scale(d.headWidth * 0.45F, d.headHeight * 0.50F,
     cheek.scale(d.headWidth * 0.45F, d.headHeight * 0.50F,
@@ -566,7 +657,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
       head_center +
       head_center +
       QVector3D(0.0F, -d.headHeight * 0.18F, d.headLength * 0.58F);
       QVector3D(0.0F, -d.headHeight * 0.18F, d.headLength * 0.58F);
   {
   {
-    QMatrix4x4 muzzle = ctx.model;
+    QMatrix4x4 muzzle = horse_ctx.model;
     muzzle.translate(muzzle_center +
     muzzle.translate(muzzle_center +
                      QVector3D(0.0F, -d.headHeight * 0.05F, 0.0F));
                      QVector3D(0.0F, -d.headHeight * 0.05F, 0.0F));
     muzzle.scale(d.headWidth * 0.68F, d.headHeight * 0.60F,
     muzzle.scale(d.headWidth * 0.68F, d.headHeight * 0.60F,
@@ -585,11 +676,11 @@ void HorseRendererBase::render(const DrawContext &ctx,
     QVector3D const inward =
     QVector3D const inward =
         QVector3D(0.0F, -d.headHeight * 0.02F, d.muzzleLength * -0.30F);
         QVector3D(0.0F, -d.headHeight * 0.02F, d.muzzleLength * -0.30F);
     out.mesh(getUnitCone(),
     out.mesh(getUnitCone(),
-             coneFromTo(ctx.model, left_base + inward, left_base,
+             coneFromTo(horse_ctx.model, left_base + inward, left_base,
                         d.headWidth * 0.11F),
                         d.headWidth * 0.11F),
              darken(v.muzzleColor, 0.6F), nullptr, 1.0F);
              darken(v.muzzleColor, 0.6F), nullptr, 1.0F);
     out.mesh(getUnitCone(),
     out.mesh(getUnitCone(),
-             coneFromTo(ctx.model, right_base + inward, right_base,
+             coneFromTo(horse_ctx.model, right_base + inward, right_base,
                         d.headWidth * 0.11F),
                         d.headWidth * 0.11F),
              darken(v.muzzleColor, 0.6F), nullptr, 1.0F);
              darken(v.muzzleColor, 0.6F), nullptr, 1.0F);
   }
   }
@@ -614,14 +705,14 @@ void HorseRendererBase::render(const DrawContext &ctx,
                               -d.headLength * 0.10F),
                               -d.headLength * 0.10F),
                     ear_flick_r);
                     ear_flick_r);
 
 
-  out.mesh(
-      getUnitCone(),
-      coneFromTo(ctx.model, ear_tip_left, ear_base_left, d.headWidth * 0.11F),
-      v.mane_color, nullptr, 1.0F);
-  out.mesh(
-      getUnitCone(),
-      coneFromTo(ctx.model, ear_tip_right, ear_base_right, d.headWidth * 0.11F),
-      v.mane_color, nullptr, 1.0F);
+  out.mesh(getUnitCone(),
+           coneFromTo(horse_ctx.model, ear_tip_left, ear_base_left,
+                      d.headWidth * 0.11F),
+           v.mane_color, nullptr, 1.0F);
+  out.mesh(getUnitCone(),
+           coneFromTo(horse_ctx.model, ear_tip_right, ear_base_right,
+                      d.headWidth * 0.11F),
+           v.mane_color, nullptr, 1.0F);
 
 
   QVector3D const eye_left =
   QVector3D const eye_left =
       head_center + QVector3D(d.headWidth * 0.48F, d.headHeight * 0.10F,
       head_center + QVector3D(d.headWidth * 0.48F, d.headHeight * 0.10F,
@@ -633,14 +724,14 @@ void HorseRendererBase::render(const DrawContext &ctx,
 
 
   auto draw_eye = [&](const QVector3D &pos) {
   auto draw_eye = [&](const QVector3D &pos) {
     {
     {
-      QMatrix4x4 eye = ctx.model;
+      QMatrix4x4 eye = horse_ctx.model;
       eye.translate(pos);
       eye.translate(pos);
       eye.scale(d.headWidth * 0.14F);
       eye.scale(d.headWidth * 0.14F);
       out.mesh(getUnitSphere(), eye, eye_base_color, nullptr, 1.0F);
       out.mesh(getUnitSphere(), eye, eye_base_color, nullptr, 1.0F);
     }
     }
     {
     {
 
 
-      QMatrix4x4 pupil = ctx.model;
+      QMatrix4x4 pupil = horse_ctx.model;
       pupil.translate(pos + QVector3D(0.0F, 0.0F, d.headWidth * 0.04F));
       pupil.translate(pos + QVector3D(0.0F, 0.0F, d.headWidth * 0.04F));
       pupil.scale(d.headWidth * 0.05F);
       pupil.scale(d.headWidth * 0.05F);
       out.mesh(getUnitSphere(), pupil, QVector3D(0.03F, 0.03F, 0.03F), nullptr,
       out.mesh(getUnitSphere(), pupil, QVector3D(0.03F, 0.03F, 0.03F), nullptr,
@@ -648,7 +739,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
     }
     }
     {
     {
 
 
-      QMatrix4x4 spec = ctx.model;
+      QMatrix4x4 spec = horse_ctx.model;
       spec.translate(pos + QVector3D(d.headWidth * 0.03F, d.headWidth * 0.03F,
       spec.translate(pos + QVector3D(d.headWidth * 0.03F, d.headWidth * 0.03F,
                                      d.headWidth * 0.03F));
                                      d.headWidth * 0.03F));
       spec.scale(d.headWidth * 0.02F);
       spec.scale(d.headWidth * 0.02F);
@@ -660,7 +751,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   draw_eye(eye_right);
   draw_eye(eye_right);
 
 
   if (has_blaze) {
   if (has_blaze) {
-    QMatrix4x4 blaze = ctx.model;
+    QMatrix4x4 blaze = horse_ctx.model;
     blaze.translate(head_center + QVector3D(0.0F, d.headHeight * 0.15F,
     blaze.translate(head_center + QVector3D(0.0F, d.headHeight * 0.15F,
                                             d.headLength * 0.10F));
                                             d.headLength * 0.10F));
     blaze.scale(d.headWidth * 0.22F, d.headHeight * 0.32F,
     blaze.scale(d.headWidth * 0.22F, d.headHeight * 0.32F,
@@ -681,14 +772,14 @@ void HorseRendererBase::render(const DrawContext &ctx,
   QVector3D const brow = head_center + QVector3D(0.0F, d.headHeight * 0.38F,
   QVector3D const brow = head_center + QVector3D(0.0F, d.headHeight * 0.38F,
                                                  -d.headLength * 0.28F);
                                                  -d.headLength * 0.28F);
   QVector3D const tack_color = lighten(v.tack_color, 0.9F);
   QVector3D const tack_color = lighten(v.tack_color, 0.9F);
-  draw_cylinder(out, ctx.model, bridle_base, cheek_anchor_left,
+  draw_cylinder(out, horse_ctx.model, bridle_base, cheek_anchor_left,
                 d.headWidth * 0.07F, tack_color);
                 d.headWidth * 0.07F, tack_color);
-  draw_cylinder(out, ctx.model, bridle_base, cheek_anchor_right,
+  draw_cylinder(out, horse_ctx.model, bridle_base, cheek_anchor_right,
                 d.headWidth * 0.07F, tack_color);
                 d.headWidth * 0.07F, tack_color);
-  draw_cylinder(out, ctx.model, cheek_anchor_left, brow, d.headWidth * 0.05F,
-                tack_color);
-  draw_cylinder(out, ctx.model, cheek_anchor_right, brow, d.headWidth * 0.05F,
-                tack_color);
+  draw_cylinder(out, horse_ctx.model, cheek_anchor_left, brow,
+                d.headWidth * 0.05F, tack_color);
+  draw_cylinder(out, horse_ctx.model, cheek_anchor_right, brow,
+                d.headWidth * 0.05F, tack_color);
 
 
   QVector3D const mane_root =
   QVector3D const mane_root =
       neck_top + QVector3D(0.0F, d.headHeight * 0.20F, -d.headLength * 0.20F);
       neck_top + QVector3D(0.0F, d.headHeight * 0.20F, -d.headLength * 0.20F);
@@ -705,7 +796,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
     QVector3D const seg_end =
     QVector3D const seg_end =
         seg_start + QVector3D(sway, 0.07F - t * 0.05F, -0.05F - t * 0.03F);
         seg_start + QVector3D(sway, 0.07F - t * 0.05F, -0.05F - t * 0.03F);
     out.mesh(getUnitCylinder(),
     out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, seg_start, seg_end,
+             cylinderBetween(horse_ctx.model, seg_start, seg_end,
                              d.headWidth * (0.10F * (1.0F - t * 0.4F))),
                              d.headWidth * (0.10F * (1.0F - t * 0.4F))),
              v.mane_color * (0.98F + t * 0.05F), nullptr, 1.0F);
              v.mane_color * (0.98F + t * 0.05F), nullptr, 1.0F);
   }
   }
@@ -721,8 +812,8 @@ void HorseRendererBase::render(const DrawContext &ctx,
       QVector3D const strand_tip =
       QVector3D const strand_tip =
           strand_base +
           strand_base +
           QVector3D(offset * 0.4F, -d.headHeight * 0.25F, d.headLength * 0.12F);
           QVector3D(offset * 0.4F, -d.headHeight * 0.25F, d.headLength * 0.12F);
-      drawCone(out, ctx.model, strand_tip, strand_base, d.headWidth * 0.10F,
-               v.mane_color * (0.94F + 0.03F * i), 0.96F);
+      drawCone(out, horse_ctx.model, strand_tip, strand_base,
+               d.headWidth * 0.10F, v.mane_color * (0.94F + 0.03F * i), 0.96F);
     }
     }
   }
   }
 
 
@@ -744,12 +835,12 @@ void HorseRendererBase::render(const DrawContext &ctx,
         (0.025F + rider_intensity * 0.020F + 0.015F * (1.0F - t));
         (0.025F + rider_intensity * 0.020F + 0.015F * (1.0F - t));
     p.setX(p.x() + swing);
     p.setX(p.x() + swing);
     float const radius = d.bodyWidth * (0.20F - 0.018F * i);
     float const radius = d.bodyWidth * (0.20F - 0.018F * i);
-    draw_cylinder(out, ctx.model, prev_tail, p, radius, tail_color);
+    draw_cylinder(out, horse_ctx.model, prev_tail, p, radius, tail_color);
     prev_tail = p;
     prev_tail = p;
   }
   }
 
 
   {
   {
-    QMatrix4x4 tail_knot = ctx.model;
+    QMatrix4x4 tail_knot = horse_ctx.model;
     tail_knot.translate(tail_base + QVector3D(0.0F, -d.bodyHeight * 0.06F,
     tail_knot.translate(tail_base + QVector3D(0.0F, -d.bodyHeight * 0.06F,
                                               -d.bodyLength * 0.02F));
                                               -d.bodyLength * 0.02F));
     tail_knot.scale(QVector3D(d.bodyWidth * 0.24F, d.bodyWidth * 0.18F,
     tail_knot.scale(QVector3D(d.bodyWidth * 0.24F, d.bodyWidth * 0.18F,
@@ -766,7 +857,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
     QVector3D const fan_tip =
     QVector3D const fan_tip =
         fan_base +
         fan_base +
         QVector3D(spread, -d.tailLength * 0.32F, -d.tailLength * 0.22F);
         QVector3D(spread, -d.tailLength * 0.32F, -d.tailLength * 0.22F);
-    drawCone(out, ctx.model, fan_tip, fan_base, d.bodyWidth * 0.24F,
+    drawCone(out, horse_ctx.model, fan_tip, fan_base, d.bodyWidth * 0.24F,
              tail_color * (0.96F + 0.02F * i), 0.88F);
              tail_color * (0.96F + 0.02F * i), 0.88F);
   }
   }
 
 
@@ -774,24 +865,25 @@ void HorseRendererBase::render(const DrawContext &ctx,
                          const QVector3D &hoof_bottom, float wallRadius,
                          const QVector3D &hoof_bottom, float wallRadius,
                          const QVector3D &hoof_color, bool is_rear) {
                          const QVector3D &hoof_color, bool is_rear) {
     QVector3D const wall_tint = lighten(hoof_color, is_rear ? 1.04F : 1.0F);
     QVector3D const wall_tint = lighten(hoof_color, is_rear ? 1.04F : 1.0F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, hoof_top, hoof_bottom, wallRadius),
-             wall_tint, nullptr, 1.0F);
+    out.mesh(
+        getUnitCylinder(),
+        cylinderBetween(horse_ctx.model, hoof_top, hoof_bottom, wallRadius),
+        wall_tint, nullptr, 1.0F);
 
 
     QVector3D const toe =
     QVector3D const toe =
         hoof_bottom + QVector3D(0.0F, -d.hoofHeight * 0.14F, 0.0F);
         hoof_bottom + QVector3D(0.0F, -d.hoofHeight * 0.14F, 0.0F);
     out.mesh(getUnitCone(),
     out.mesh(getUnitCone(),
-             coneFromTo(ctx.model, toe, hoof_bottom, wallRadius * 0.90F),
+             coneFromTo(horse_ctx.model, toe, hoof_bottom, wallRadius * 0.90F),
              wall_tint * 0.96F, nullptr, 1.0F);
              wall_tint * 0.96F, nullptr, 1.0F);
 
 
-    QMatrix4x4 sole = ctx.model;
+    QMatrix4x4 sole = horse_ctx.model;
     sole.translate(lerp(hoof_top, hoof_bottom, 0.88F) +
     sole.translate(lerp(hoof_top, hoof_bottom, 0.88F) +
                    QVector3D(0.0F, -d.hoofHeight * 0.05F, 0.0F));
                    QVector3D(0.0F, -d.hoofHeight * 0.05F, 0.0F));
     sole.scale(
     sole.scale(
         QVector3D(wallRadius * 1.08F, wallRadius * 0.28F, wallRadius * 1.02F));
         QVector3D(wallRadius * 1.08F, wallRadius * 0.28F, wallRadius * 1.02F));
     out.mesh(getUnitSphere(), sole, lighten(hoof_color, 1.12F), nullptr, 1.0F);
     out.mesh(getUnitSphere(), sole, lighten(hoof_color, 1.12F), nullptr, 1.0F);
 
 
-    QMatrix4x4 coronet = ctx.model;
+    QMatrix4x4 coronet = horse_ctx.model;
     coronet.translate(lerp(hoof_top, hoof_bottom, 0.12F));
     coronet.translate(lerp(hoof_top, hoof_bottom, 0.12F));
     coronet.scale(
     coronet.scale(
         QVector3D(wallRadius * 1.05F, wallRadius * 0.24F, wallRadius * 1.05F));
         QVector3D(wallRadius * 1.05F, wallRadius * 0.24F, wallRadius * 1.05F));
@@ -861,13 +953,13 @@ void HorseRendererBase::render(const DrawContext &ctx,
         shoulder +
         shoulder +
         QVector3D(0.0F, d.bodyWidth * 0.12F,
         QVector3D(0.0F, d.bodyWidth * 0.12F,
                   is_rear ? -d.bodyLength * 0.03F : d.bodyLength * 0.02F);
                   is_rear ? -d.bodyLength * 0.03F : d.bodyLength * 0.02F);
-    draw_cylinder(out, ctx.model, girdle_top, socket,
+    draw_cylinder(out, horse_ctx.model, girdle_top, socket,
                   d.bodyWidth * (is_rear ? 0.20F : 0.18F),
                   d.bodyWidth * (is_rear ? 0.20F : 0.18F),
                   coatGradient(v.coatColor, is_rear ? 0.70F : 0.80F,
                   coatGradient(v.coatColor, is_rear ? 0.70F : 0.80F,
                                is_rear ? -0.20F : 0.22F,
                                is_rear ? -0.20F : 0.22F,
                                coat_seed_b + lateralSign * 0.03F));
                                coat_seed_b + lateralSign * 0.03F));
 
 
-    QMatrix4x4 socket_cap = ctx.model;
+    QMatrix4x4 socket_cap = horse_ctx.model;
     socket_cap.translate(socket + QVector3D(0.0F, -d.bodyWidth * 0.04F,
     socket_cap.translate(socket + QVector3D(0.0F, -d.bodyWidth * 0.04F,
                                             is_rear ? -d.bodyLength * 0.02F
                                             is_rear ? -d.bodyLength * 0.02F
                                                     : d.bodyLength * 0.03F));
                                                     : d.bodyLength * 0.03F));
@@ -922,11 +1014,11 @@ void HorseRendererBase::render(const DrawContext &ctx,
         v.coatColor, is_rear ? 0.48F : 0.58F, is_rear ? -0.22F : 0.18F,
         v.coatColor, is_rear ? 0.48F : 0.58F, is_rear ? -0.22F : 0.18F,
         coat_seed_a + lateralSign * 0.07F);
         coat_seed_a + lateralSign * 0.07F);
     out.mesh(getUnitCone(),
     out.mesh(getUnitCone(),
-             coneFromTo(ctx.model, thigh_belly, shoulder, thigh_belly_r),
+             coneFromTo(horse_ctx.model, thigh_belly, shoulder, thigh_belly_r),
              thigh_color, nullptr, 1.0F);
              thigh_color, nullptr, 1.0F);
 
 
     {
     {
-      QMatrix4x4 muscle = ctx.model;
+      QMatrix4x4 muscle = horse_ctx.model;
       muscle.translate(thigh_belly +
       muscle.translate(thigh_belly +
                        QVector3D(0.0F, 0.0F, is_rear ? -0.015F : 0.020F));
                        QVector3D(0.0F, 0.0F, is_rear ? -0.015F : 0.020F));
       muscle.scale(thigh_belly_r * QVector3D(1.05F, 0.85F, 0.92F));
       muscle.scale(thigh_belly_r * QVector3D(1.05F, 0.85F, 0.92F));
@@ -935,11 +1027,12 @@ void HorseRendererBase::render(const DrawContext &ctx,
     }
     }
 
 
     QVector3D const knee_color = darken(thigh_color, 0.96F);
     QVector3D const knee_color = darken(thigh_color, 0.96F);
-    out.mesh(getUnitCone(), coneFromTo(ctx.model, knee, thigh_belly, knee_r),
-             knee_color, nullptr, 1.0F);
+    out.mesh(getUnitCone(),
+             coneFromTo(horse_ctx.model, knee, thigh_belly, knee_r), knee_color,
+             nullptr, 1.0F);
 
 
     {
     {
-      QMatrix4x4 joint = ctx.model;
+      QMatrix4x4 joint = horse_ctx.model;
       joint.translate(knee + QVector3D(0.0F, 0.0F, is_rear ? -0.028F : 0.034F));
       joint.translate(knee + QVector3D(0.0F, 0.0F, is_rear ? -0.028F : 0.034F));
       joint.scale(QVector3D(knee_r * 1.18F, knee_r * 1.06F, knee_r * 1.36F));
       joint.scale(QVector3D(knee_r * 1.18F, knee_r * 1.06F, knee_r * 1.36F));
       out.mesh(getUnitSphere(), joint, darken(knee_color, 0.90F), nullptr,
       out.mesh(getUnitSphere(), joint, darken(knee_color, 0.90F), nullptr,
@@ -947,11 +1040,11 @@ void HorseRendererBase::render(const DrawContext &ctx,
     }
     }
 
 
     out.mesh(getUnitCylinder(),
     out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, knee, cannon, cannon_r),
+             cylinderBetween(horse_ctx.model, knee, cannon, cannon_r),
              darken(thigh_color, 0.93F), nullptr, 1.0F);
              darken(thigh_color, 0.93F), nullptr, 1.0F);
 
 
     {
     {
-      QMatrix4x4 tendon = ctx.model;
+      QMatrix4x4 tendon = horse_ctx.model;
       tendon.translate(
       tendon.translate(
           lerp(knee, cannon, 0.55F) +
           lerp(knee, cannon, 0.55F) +
           QVector3D(0.0F, 0.0F,
           QVector3D(0.0F, 0.0F,
@@ -963,7 +1056,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
     }
     }
 
 
     {
     {
-      QMatrix4x4 joint = ctx.model;
+      QMatrix4x4 joint = horse_ctx.model;
       joint.translate(fetlock);
       joint.translate(fetlock);
       joint.scale(
       joint.scale(
           QVector3D(pastern_r * 1.12F, pastern_r * 1.05F, pastern_r * 1.26F));
           QVector3D(pastern_r * 1.12F, pastern_r * 1.05F, pastern_r * 1.26F));
@@ -977,10 +1070,10 @@ void HorseRendererBase::render(const DrawContext &ctx,
         (sock > 0.0F) ? lighten(v.coatColor, 1.18F) : v.coatColor * 0.92F;
         (sock > 0.0F) ? lighten(v.coatColor, 1.18F) : v.coatColor * 0.92F;
     float const t_sock = smoothstep(0.0F, 1.0F, sock);
     float const t_sock = smoothstep(0.0F, 1.0F, sock);
 
 
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, cannon, fetlock, pastern_r * 1.05F),
-             lerp(v.coatColor * 0.94F, distal_color, t_sock * 0.8F), nullptr,
-             1.0F);
+    out.mesh(
+        getUnitCylinder(),
+        cylinderBetween(horse_ctx.model, cannon, fetlock, pastern_r * 1.05F),
+        lerp(v.coatColor * 0.94F, distal_color, t_sock * 0.8F), nullptr, 1.0F);
 
 
     QVector3D const hoof_color = v.hoof_color;
     QVector3D const hoof_color = v.hoof_color;
     render_hoof(hoof_top, hoof_bottom, pastern_r * 0.96F, hoof_color, is_rear);
     render_hoof(hoof_top, hoof_bottom, pastern_r * 0.96F, hoof_color, is_rear);
@@ -988,7 +1081,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
     if (sock > 0.0F) {
     if (sock > 0.0F) {
       QVector3D const feather_tip = lerp(fetlock, hoof_top, 0.35F) +
       QVector3D const feather_tip = lerp(fetlock, hoof_top, 0.35F) +
                                     QVector3D(0.0F, -pastern_r * 0.60F, 0.0F);
                                     QVector3D(0.0F, -pastern_r * 0.60F, 0.0F);
-      drawCone(out, ctx.model, feather_tip, fetlock, pastern_r * 0.85F,
+      drawCone(out, horse_ctx.model, feather_tip, fetlock, pastern_r * 0.85F,
                lerp(distal_color, v.coatColor, 0.25F), 0.85F);
                lerp(distal_color, v.coatColor, 0.25F), 0.85F);
     }
     }
   };
   };
@@ -1009,14 +1102,10 @@ void HorseRendererBase::render(const DrawContext &ctx,
   draw_leg(rear_anchor, -1.0F, -d.bodyLength * 0.28F, g.rearLegPhase + 0.5F,
   draw_leg(rear_anchor, -1.0F, -d.bodyLength * 0.28F, g.rearLegPhase + 0.5F,
            sock_chance_rr);
            sock_chance_rr);
 
 
-  float const saddle_top = d.saddle_height;
-  QVector3D saddle_center(0.0F, saddle_top - d.saddleThickness * 0.35F,
-                          -d.bodyLength * 0.05F + d.seatForwardOffset * 0.25F);
-  mount.saddle_center = saddle_center;
-  mount.seat_position =
-      saddle_center + QVector3D(0.0F, d.saddleThickness * 0.32F, 0.0F);
+  QVector3D const saddle_center = mount.saddle_center;
+  QVector3D const seat_position = mount.seat_position;
   {
   {
-    QMatrix4x4 saddle = ctx.model;
+    QMatrix4x4 saddle = horse_ctx.model;
     saddle.translate(saddle_center);
     saddle.translate(saddle_center);
     saddle.scale(d.bodyWidth * 1.10F, d.saddleThickness * 1.05F,
     saddle.scale(d.bodyWidth * 1.10F, d.saddleThickness * 1.05F,
                  d.bodyLength * 0.34F);
                  d.bodyLength * 0.34F);
@@ -1026,7 +1115,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   QVector3D const blanket_center =
   QVector3D const blanket_center =
       saddle_center + QVector3D(0.0F, -d.saddleThickness, 0.0F);
       saddle_center + QVector3D(0.0F, -d.saddleThickness, 0.0F);
   {
   {
-    QMatrix4x4 blanket = ctx.model;
+    QMatrix4x4 blanket = horse_ctx.model;
     blanket.translate(blanket_center);
     blanket.translate(blanket_center);
     blanket.scale(d.bodyWidth * 1.26F, d.saddleThickness * 0.38F,
     blanket.scale(d.bodyWidth * 1.26F, d.saddleThickness * 0.38F,
                   d.bodyLength * 0.42F);
                   d.bodyLength * 0.42F);
@@ -1034,7 +1123,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   }
   }
 
 
   {
   {
-    QMatrix4x4 cantle = ctx.model;
+    QMatrix4x4 cantle = horse_ctx.model;
     cantle.translate(saddle_center + QVector3D(0.0F, d.saddleThickness * 0.72F,
     cantle.translate(saddle_center + QVector3D(0.0F, d.saddleThickness * 0.72F,
                                                -d.bodyLength * 0.12F));
                                                -d.bodyLength * 0.12F));
     cantle.scale(QVector3D(d.bodyWidth * 0.52F, d.saddleThickness * 0.60F,
     cantle.scale(QVector3D(d.bodyWidth * 0.52F, d.saddleThickness * 0.60F,
@@ -1044,7 +1133,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
   }
   }
 
 
   {
   {
-    QMatrix4x4 pommel = ctx.model;
+    QMatrix4x4 pommel = horse_ctx.model;
     pommel.translate(saddle_center + QVector3D(0.0F, d.saddleThickness * 0.58F,
     pommel.translate(saddle_center + QVector3D(0.0F, d.saddleThickness * 0.58F,
                                                d.bodyLength * 0.16F));
                                                d.bodyLength * 0.16F));
     pommel.scale(QVector3D(d.bodyWidth * 0.40F, d.saddleThickness * 0.48F,
     pommel.scale(QVector3D(d.bodyWidth * 0.40F, d.saddleThickness * 0.48F,
@@ -1055,7 +1144,7 @@ void HorseRendererBase::render(const DrawContext &ctx,
 
 
   for (int i = 0; i < 6; ++i) {
   for (int i = 0; i < 6; ++i) {
     float const t = static_cast<float>(i) / 5.0F;
     float const t = static_cast<float>(i) / 5.0F;
-    QMatrix4x4 stitch = ctx.model;
+    QMatrix4x4 stitch = horse_ctx.model;
     stitch.translate(blanket_center + QVector3D(d.bodyWidth * (t - 0.5F) * 1.1F,
     stitch.translate(blanket_center + QVector3D(d.bodyWidth * (t - 0.5F) * 1.1F,
                                                 -d.saddleThickness * 0.35F,
                                                 -d.saddleThickness * 0.35F,
                                                 d.bodyLength * 0.28F));
                                                 d.bodyLength * 0.28F));
@@ -1064,160 +1153,14 @@ void HorseRendererBase::render(const DrawContext &ctx,
     out.mesh(getUnitSphere(), stitch, v.blanketColor * 0.75F, nullptr, 0.9F);
     out.mesh(getUnitSphere(), stitch, v.blanketColor * 0.75F, nullptr, 0.9F);
   }
   }
 
 
-  for (int i = 0; i < 2; ++i) {
-    float const side = (i == 0) ? 1.0F : -1.0F;
-    QVector3D const strap_top =
-        saddle_center + QVector3D(side * d.bodyWidth * 0.92F,
-                                  d.saddleThickness * 0.32F,
-                                  d.bodyLength * 0.02F);
-    QVector3D const strap_bottom =
-        strap_top +
-        QVector3D(0.0F, -d.bodyHeight * 0.94F, -d.bodyLength * 0.06F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, strap_top, strap_bottom,
-                             d.bodyWidth * 0.065F),
-             v.tack_color * 0.94F, nullptr, 1.0F);
-
-    QMatrix4x4 buckle = ctx.model;
-    buckle.translate(lerp(strap_top, strap_bottom, 0.87F) +
-                     QVector3D(0.0F, 0.0F, -d.bodyLength * 0.02F));
-    buckle.scale(QVector3D(d.bodyWidth * 0.16F, d.bodyWidth * 0.12F,
-                           d.bodyWidth * 0.05F));
-    out.mesh(getUnitSphere(), buckle, QVector3D(0.42F, 0.39F, 0.35F), nullptr,
-             1.0F);
-  }
-
-  for (int i = 0; i < 2; ++i) {
-    float const side = (i == 0) ? 1.0F : -1.0F;
-    QVector3D const breast_anchor =
-        chest_center + QVector3D(side * d.bodyWidth * 0.70F,
-                                 -d.bodyHeight * 0.10F, d.bodyLength * 0.18F);
-    QVector3D const breast_to_saddle =
-        saddle_center + QVector3D(side * d.bodyWidth * 0.48F,
-                                  -d.saddleThickness * 0.20F,
-                                  d.bodyLength * 0.10F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, breast_anchor, breast_to_saddle,
-                             d.bodyWidth * 0.055F),
-             v.tack_color * 0.92F, nullptr, 1.0F);
-  }
-
-  QVector3D stirrup_attach_left =
-      saddle_center + QVector3D(-d.bodyWidth * 0.92F,
-                                -d.saddleThickness * 0.10F,
-                                d.seatForwardOffset * 0.28F);
-  QVector3D stirrup_attach_right =
-      saddle_center + QVector3D(d.bodyWidth * 0.92F, -d.saddleThickness * 0.10F,
-                                d.seatForwardOffset * 0.28F);
-  QVector3D stirrup_bottom_left =
-      stirrup_attach_left + QVector3D(0.0F, -d.stirrupDrop, 0.0F);
-  QVector3D stirrup_bottom_right =
-      stirrup_attach_right + QVector3D(0.0F, -d.stirrupDrop, 0.0F);
-  mount.stirrup_attach_left = stirrup_attach_left;
-  mount.stirrup_attach_right = stirrup_attach_right;
-  mount.stirrup_bottom_left = stirrup_bottom_left;
-  mount.stirrup_bottom_right = stirrup_bottom_right;
-
-  auto draw_stirrup = [&](const QVector3D &attach, const QVector3D &bottom) {
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, attach, bottom, d.bodyWidth * 0.048F),
-             v.tack_color * 0.98F, nullptr, 1.0F);
-
-    QMatrix4x4 leather_loop = ctx.model;
-    leather_loop.translate(lerp(attach, bottom, 0.30F) +
-                           QVector3D(0.0F, 0.0F, d.bodyWidth * 0.02F));
-    leather_loop.scale(QVector3D(d.bodyWidth * 0.18F, d.bodyWidth * 0.05F,
-                                 d.bodyWidth * 0.10F));
-    out.mesh(getUnitSphere(), leather_loop, v.tack_color * 0.92F, nullptr,
-             1.0F);
-
-    QMatrix4x4 stirrup = ctx.model;
-    stirrup.translate(bottom + QVector3D(0.0F, -d.bodyWidth * 0.06F, 0.0F));
-    stirrup.scale(d.bodyWidth * 0.20F, d.bodyWidth * 0.07F,
-                  d.bodyWidth * 0.16F);
-    out.mesh(getUnitSphere(), stirrup, QVector3D(0.66F, 0.65F, 0.62F), nullptr,
-             1.0F);
-  };
-
-  draw_stirrup(stirrup_attach_left, stirrup_bottom_left);
-  draw_stirrup(stirrup_attach_right, stirrup_bottom_right);
-
-  QVector3D const cheek_left_top =
-      head_center + QVector3D(d.headWidth * 0.60F, -d.headHeight * 0.10F,
-                              d.headLength * 0.25F);
-  QVector3D const cheek_left_bottom =
-      cheek_left_top + QVector3D(0.0F, -d.headHeight, -d.headLength * 0.12F);
-  QVector3D const cheek_right_top =
-      head_center + QVector3D(-d.headWidth * 0.60F, -d.headHeight * 0.10F,
-                              d.headLength * 0.25F);
-  QVector3D const cheek_right_bottom =
-      cheek_right_top + QVector3D(0.0F, -d.headHeight, -d.headLength * 0.12F);
-
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, cheek_left_top, cheek_left_bottom,
-                           d.headWidth * 0.08F),
-           v.tack_color, nullptr, 1.0F);
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, cheek_right_top, cheek_right_bottom,
-                           d.headWidth * 0.08F),
-           v.tack_color, nullptr, 1.0F);
-
-  QVector3D const nose_band_front =
-      muzzle_center +
-      QVector3D(0.0F, d.headHeight * 0.02F, d.muzzleLength * 0.35F);
-  QVector3D const nose_band_left =
-      nose_band_front + QVector3D(d.headWidth * 0.55F, 0.0F, 0.0F);
-  QVector3D const nose_band_right =
-      nose_band_front + QVector3D(-d.headWidth * 0.55F, 0.0F, 0.0F);
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, nose_band_left, nose_band_right,
-                           d.headWidth * 0.08F),
-           v.tack_color * 0.92F, nullptr, 1.0F);
-
-  QVector3D const brow_band_front =
-      head_center + QVector3D(0.0F, d.headHeight * 0.28F, d.headLength * 0.15F);
-  QVector3D const brow_band_left =
-      brow_band_front + QVector3D(d.headWidth * 0.58F, 0.0F, 0.0F);
-  QVector3D const brow_band_right =
-      brow_band_front + QVector3D(-d.headWidth * 0.58F, 0.0F, 0.0F);
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, brow_band_left, brow_band_right,
-                           d.headWidth * 0.07F),
-           v.tack_color, nullptr, 1.0F);
-
   QVector3D const bit_left =
   QVector3D const bit_left =
       muzzle_center + QVector3D(d.headWidth * 0.55F, -d.headHeight * 0.08F,
       muzzle_center + QVector3D(d.headWidth * 0.55F, -d.headHeight * 0.08F,
                                 d.muzzleLength * 0.10F);
                                 d.muzzleLength * 0.10F);
   QVector3D const bit_right =
   QVector3D const bit_right =
       muzzle_center + QVector3D(-d.headWidth * 0.55F, -d.headHeight * 0.08F,
       muzzle_center + QVector3D(-d.headWidth * 0.55F, -d.headHeight * 0.08F,
                                 d.muzzleLength * 0.10F);
                                 d.muzzleLength * 0.10F);
-  out.mesh(getUnitCylinder(),
-           cylinderBetween(ctx.model, bit_left, bit_right, d.headWidth * 0.05F),
-           QVector3D(0.55F, 0.55F, 0.55F), nullptr, 1.0F);
-
-  for (int i = 0; i < 2; ++i) {
-    float const side = (i == 0) ? 1.0F : -1.0F;
-    QVector3D const rein_start = (i == 0) ? bit_left : bit_right;
-    QVector3D const rein_end =
-        saddle_center + QVector3D(side * d.bodyWidth * 0.62F,
-                                  -d.saddleThickness * 0.32F,
-                                  d.seatForwardOffset * 0.10F);
-    if (i == 0) {
-      mount.rein_attach_left = rein_end;
-    } else {
-      mount.rein_attach_right = rein_end;
-    }
-
-    QVector3D const mid =
-        lerp(rein_start, rein_end, 0.46F) +
-        QVector3D(0.0F, -d.bodyHeight * (0.08F + rein_slack), 0.0F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, rein_start, mid, d.bodyWidth * 0.02F),
-             v.tack_color * 0.95F, nullptr, 1.0F);
-    out.mesh(getUnitCylinder(),
-             cylinderBetween(ctx.model, mid, rein_end, d.bodyWidth * 0.02F),
-             v.tack_color * 0.95F, nullptr, 1.0F);
-  }
+  mount.rein_bit_left = bit_left;
+  mount.rein_bit_right = bit_right;
 
 
   drawAttachments(ctx, anim, rider_ctx, profile, mount, phase, bob, rein_slack,
   drawAttachments(ctx, anim, rider_ctx, profile, mount, phase, bob, rein_slack,
                   out);
                   out);

+ 56 - 9
render/horse/rig.h

@@ -65,7 +65,7 @@ struct HorseProfile {
   HorseGait gait{};
   HorseGait gait{};
 };
 };
 
 
-struct HorseMountFrame {
+struct MountedAttachmentFrame {
   QVector3D saddle_center;
   QVector3D saddle_center;
   QVector3D seat_position;
   QVector3D seat_position;
   QVector3D seat_forward;
   QVector3D seat_forward;
@@ -77,9 +77,24 @@ struct HorseMountFrame {
   QVector3D stirrup_bottom_left;
   QVector3D stirrup_bottom_left;
   QVector3D stirrup_bottom_right;
   QVector3D stirrup_bottom_right;
 
 
-  QVector3D rein_attach_left;
-  QVector3D rein_attach_right;
+  QVector3D rein_bit_left;
+  QVector3D rein_bit_right;
   QVector3D bridle_base;
   QVector3D bridle_base;
+  QVector3D ground_offset;
+  auto stirrup_attach(bool is_left) const -> const QVector3D &;
+  auto stirrup_bottom(bool is_left) const -> const QVector3D &;
+};
+
+struct ReinState {
+  float slack = 0.0F;
+  float tension = 0.0F;
+};
+
+struct HorseMotionSample {
+  float phase = 0.0F;
+  float bob = 0.0F;
+  bool is_moving = false;
+  float rider_intensity = 0.0F;
 };
 };
 
 
 auto makeHorseDimensions(uint32_t seed) -> HorseDimensions;
 auto makeHorseDimensions(uint32_t seed) -> HorseDimensions;
@@ -87,21 +102,53 @@ auto makeHorseVariant(uint32_t seed, const QVector3D &leatherBase,
                       const QVector3D &clothBase) -> HorseVariant;
                       const QVector3D &clothBase) -> HorseVariant;
 auto makeHorseProfile(uint32_t seed, const QVector3D &leatherBase,
 auto makeHorseProfile(uint32_t seed, const QVector3D &leatherBase,
                       const QVector3D &clothBase) -> HorseProfile;
                       const QVector3D &clothBase) -> HorseProfile;
-auto compute_mount_frame(const HorseProfile &profile) -> HorseMountFrame;
+auto compute_mount_frame(const HorseProfile &profile) -> MountedAttachmentFrame;
+auto compute_rein_state(uint32_t horse_seed,
+                        const HumanoidAnimationContext &rider_ctx) -> ReinState;
+auto compute_rein_handle(const MountedAttachmentFrame &mount, bool is_left,
+                         float slack, float tension) -> QVector3D;
+auto evaluate_horse_motion(HorseProfile &profile, const AnimationInputs &anim,
+                           const HumanoidAnimationContext &rider_ctx)
+    -> HorseMotionSample;
+void apply_mount_vertical_offset(MountedAttachmentFrame &frame, float bob);
+
+inline void scaleHorseDimensions(HorseDimensions &dims, float scale) {
+  dims.bodyLength *= scale;
+  dims.bodyWidth *= scale;
+  dims.bodyHeight *= scale;
+  dims.neckLength *= scale;
+  dims.neckRise *= scale;
+  dims.headLength *= scale;
+  dims.headWidth *= scale;
+  dims.headHeight *= scale;
+  dims.muzzleLength *= scale;
+  dims.legLength *= scale;
+  dims.hoofHeight *= scale;
+  dims.tailLength *= scale;
+  dims.saddleThickness *= scale;
+  dims.seatForwardOffset *= scale;
+  dims.stirrupOut *= scale;
+  dims.stirrupDrop *= scale;
+  dims.barrel_centerY *= scale;
+  dims.saddle_height *= scale;
+  dims.idleBobAmplitude *= scale;
+  dims.moveBobAmplitude *= scale;
+}
 
 
 class HorseRendererBase {
 class HorseRendererBase {
 public:
 public:
   virtual ~HorseRendererBase() = default;
   virtual ~HorseRendererBase() = default;
 
 
   void render(const DrawContext &ctx, const AnimationInputs &anim,
   void render(const DrawContext &ctx, const AnimationInputs &anim,
-              const HumanoidAnimationContext &rider_ctx,
-              HorseProfile &profile, ISubmitter &out) const;
+              const HumanoidAnimationContext &rider_ctx, HorseProfile &profile,
+              const MountedAttachmentFrame *shared_mount,
+              const ReinState *shared_reins, ISubmitter &out) const;
 
 
 protected:
 protected:
   virtual void drawAttachments(const DrawContext &, const AnimationInputs &,
   virtual void drawAttachments(const DrawContext &, const AnimationInputs &,
-                               const HumanoidAnimationContext &,
-                               HorseProfile &, const HorseMountFrame &,
-                               float, float, float, ISubmitter &) const {}
+                               const HumanoidAnimationContext &, HorseProfile &,
+                               const MountedAttachmentFrame &, float, float,
+                               float, ISubmitter &) const {}
 };
 };
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 534 - 0
render/humanoid/mounted_pose_controller.cpp

@@ -0,0 +1,534 @@
+#include "mounted_pose_controller.h"
+#include "../horse/rig.h"
+#include "humanoid_math.h"
+#include "humanoid_specs.h"
+#include "pose_controller.h"
+#include <QVector3D>
+#include <algorithm>
+#include <cmath>
+
+namespace Render::GL {
+
+namespace {
+
+auto seatRelative(const MountedAttachmentFrame &mount, float forward,
+                  float right, float up) -> QVector3D {
+  QVector3D const base = mount.seat_position + mount.ground_offset;
+  return base + mount.seat_forward * forward + mount.seat_right * right +
+         mount.seat_up * up;
+}
+
+auto reinAnchor(const MountedAttachmentFrame &mount, bool is_left, float slack,
+                float tension) -> QVector3D {
+  return compute_rein_handle(mount, is_left, slack, tension) +
+         mount.ground_offset;
+}
+
+} // namespace
+
+MountedPoseController::MountedPoseController(
+    HumanoidPose &pose, const HumanoidAnimationContext &anim_ctx)
+    : m_pose(pose), m_anim_ctx(anim_ctx) {}
+
+void MountedPoseController::mountOnHorse(const MountedAttachmentFrame &mount) {
+  positionPelvisOnSaddle(mount);
+  attachFeetToStirrups(mount);
+  calculateRidingKnees(mount);
+}
+
+void MountedPoseController::dismount() {
+
+  using HP = HumanProportions;
+  m_pose.pelvisPos = QVector3D(0.0F, HP::WAIST_Y, 0.0F);
+  m_pose.footL = QVector3D(-0.14F, HP::GROUND_Y + m_pose.footYOffset, 0.06F);
+  m_pose.foot_r = QVector3D(0.14F, HP::GROUND_Y + m_pose.footYOffset, -0.06F);
+}
+
+void MountedPoseController::ridingIdle(const MountedAttachmentFrame &mount) {
+  mountOnHorse(mount);
+
+  QVector3D const left_hand_rest = seatRelative(mount, 0.12F, -0.14F, -0.05F);
+  QVector3D const right_hand_rest = seatRelative(mount, 0.12F, 0.14F, -0.05F);
+
+  getHand(true) = left_hand_rest;
+  getHand(false) = right_hand_rest;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), left_hand_rest,
+                                left_outward, 0.45F, 0.12F, -0.05F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), right_hand_rest,
+                                 right_outward, 0.45F, 0.12F, -0.05F, 1.0F);
+}
+
+void MountedPoseController::ridingLeaning(const MountedAttachmentFrame &mount,
+                                          float forward_lean, float side_lean) {
+  forward_lean = std::clamp(forward_lean, -1.0F, 1.0F);
+  side_lean = std::clamp(side_lean, -1.0F, 1.0F);
+
+  mountOnHorse(mount);
+
+  QVector3D const lean_offset = mount.seat_forward * (forward_lean * 0.15F) +
+                                mount.seat_right * (side_lean * 0.10F);
+
+  m_pose.shoulderL += lean_offset;
+  m_pose.shoulderR += lean_offset;
+  m_pose.neck_base += lean_offset * 0.9F;
+  m_pose.headPos += lean_offset * 0.85F;
+
+  float const relaxed_slack = 0.35F;
+  float const relaxed_tension = 0.15F;
+  QVector3D const left_hand =
+      reinAnchor(mount, true, relaxed_slack, relaxed_tension) +
+      lean_offset * 0.5F;
+  QVector3D const right_hand =
+      reinAnchor(mount, false, relaxed_slack, relaxed_tension) +
+      lean_offset * 0.5F;
+
+  getHand(true) = left_hand;
+  getHand(false) = right_hand;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), left_hand,
+                                left_outward, 0.48F, 0.10F, -0.08F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), right_hand,
+                                 right_outward, 0.48F, 0.10F, -0.08F, 1.0F);
+}
+
+void MountedPoseController::ridingCharging(const MountedAttachmentFrame &mount,
+                                           float intensity) {
+  intensity = std::clamp(intensity, 0.0F, 1.0F);
+
+  mountOnHorse(mount);
+
+  QVector3D const charge_lean = mount.seat_forward * (0.25F * intensity);
+  m_pose.shoulderL += charge_lean;
+  m_pose.shoulderR += charge_lean;
+  m_pose.neck_base += charge_lean * 0.85F;
+  m_pose.headPos += charge_lean * 0.75F;
+
+  float const crouch = 0.08F * intensity;
+  m_pose.shoulderL.setY(m_pose.shoulderL.y() - crouch);
+  m_pose.shoulderR.setY(m_pose.shoulderR.y() - crouch);
+  m_pose.neck_base.setY(m_pose.neck_base.y() - crouch * 0.8F);
+  m_pose.headPos.setY(m_pose.headPos.y() - crouch * 0.7F);
+
+  holdReins(mount, 0.2F, 0.2F, 0.85F, 0.85F);
+}
+
+void MountedPoseController::ridingReining(const MountedAttachmentFrame &mount,
+                                          float left_tension,
+                                          float right_tension) {
+  left_tension = std::clamp(left_tension, 0.0F, 1.0F);
+  right_tension = std::clamp(right_tension, 0.0F, 1.0F);
+
+  mountOnHorse(mount);
+
+  QVector3D left_rein_pos = reinAnchor(mount, true, 0.15F, left_tension);
+  QVector3D right_rein_pos = reinAnchor(mount, false, 0.15F, right_tension);
+
+  getHand(true) = left_rein_pos;
+  getHand(false) = right_rein_pos;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), left_rein_pos,
+                                left_outward, 0.52F, 0.08F, -0.12F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), right_rein_pos,
+                                 right_outward, 0.52F, 0.08F, -0.12F, 1.0F);
+
+  float const avg_tension = (left_tension + right_tension) * 0.5F;
+  QVector3D const lean_back = mount.seat_forward * (-0.08F * avg_tension);
+  m_pose.shoulderL += lean_back;
+  m_pose.shoulderR += lean_back;
+  m_pose.neck_base += lean_back * 0.9F;
+  m_pose.headPos += lean_back * 0.85F;
+}
+
+void MountedPoseController::ridingMeleeStrike(
+    const MountedAttachmentFrame &mount, float attack_phase) {
+  attack_phase = std::clamp(attack_phase, 0.0F, 1.0F);
+
+  mountOnHorse(mount);
+
+  QVector3D const rest_pos = seatRelative(mount, 0.0F, 0.15F, 0.05F);
+  QVector3D const raised_pos = seatRelative(mount, 0.0F, 0.20F, 0.50F);
+  QVector3D const strike_pos = seatRelative(mount, 0.40F, 0.25F, -0.15F);
+
+  QVector3D hand_r_target;
+  QVector3D hand_l_target =
+      reinAnchor(mount, true, 0.20F, 0.25F) + mount.seat_up * -0.02F;
+
+  if (attack_phase < 0.30F) {
+
+    float t = attack_phase / 0.30F;
+    t = t * t;
+    hand_r_target = rest_pos * (1.0F - t) + raised_pos * t;
+  } else if (attack_phase < 0.50F) {
+
+    float t = (attack_phase - 0.30F) / 0.20F;
+    t = t * t * t;
+    hand_r_target = raised_pos * (1.0F - t) + strike_pos * t;
+    hand_r_target += mount.seat_up * (-0.25F - 0.12F * t);
+
+    QVector3D lean = mount.seat_forward * (0.12F * t);
+    m_pose.shoulderL += lean;
+    m_pose.shoulderR += lean;
+    m_pose.neck_base += lean * 0.9F;
+    m_pose.headPos += lean * 0.85F;
+  } else {
+
+    float t = (attack_phase - 0.50F) / 0.50F;
+    t = 1.0F - (1.0F - t) * (1.0F - t);
+    hand_r_target = strike_pos * (1.0F - t) + rest_pos * t;
+  }
+
+  getHand(false) = hand_r_target;
+  getHand(true) = hand_l_target;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), hand_l_target,
+                                left_outward, 0.45F, 0.12F, -0.08F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), hand_r_target,
+                                 right_outward, 0.42F, 0.15F, 0.0F, 1.0F);
+}
+
+void MountedPoseController::ridingSpearThrust(
+    const MountedAttachmentFrame &mount, float attack_phase) {
+  attack_phase = std::clamp(attack_phase, 0.0F, 1.0F);
+
+  mountOnHorse(mount);
+
+  QVector3D const guard_pos = seatRelative(mount, 0.10F, 0.15F, 0.10F);
+  QVector3D const thrust_pos = seatRelative(mount, 0.85F, 0.10F, 0.15F);
+
+  QVector3D hand_r_target;
+  QVector3D hand_l_target;
+
+  if (attack_phase < 0.25F) {
+
+    float t = attack_phase / 0.25F;
+    hand_r_target = guard_pos;
+    hand_l_target = guard_pos - mount.seat_right * 0.25F;
+  } else if (attack_phase < 0.45F) {
+
+    float t = (attack_phase - 0.25F) / 0.20F;
+    t = t * t * t;
+    hand_r_target = guard_pos * (1.0F - t) + thrust_pos * t;
+    hand_l_target = (guard_pos - mount.seat_right * 0.25F) * (1.0F - t) +
+                    (thrust_pos - mount.seat_right * 0.30F) * t;
+
+    QVector3D lean = mount.seat_forward * (0.18F * t);
+    m_pose.shoulderL += lean;
+    m_pose.shoulderR += lean;
+    m_pose.neck_base += lean * 0.9F;
+    m_pose.headPos += lean * 0.85F;
+  } else {
+
+    float t = (attack_phase - 0.45F) / 0.55F;
+    t = 1.0F - (1.0F - t) * (1.0F - t);
+    hand_r_target = thrust_pos * (1.0F - t) + guard_pos * t;
+    hand_l_target = (thrust_pos - mount.seat_right * 0.30F) * (1.0F - t) +
+                    (guard_pos - mount.seat_right * 0.25F) * t;
+  }
+
+  getHand(false) = hand_r_target;
+  getHand(true) = hand_l_target;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), hand_l_target,
+                                left_outward, 0.48F, 0.10F, -0.06F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), hand_r_target,
+                                 right_outward, 0.48F, 0.10F, -0.04F, 1.0F);
+}
+
+void MountedPoseController::ridingBowShot(const MountedAttachmentFrame &mount,
+                                          float draw_phase) {
+  draw_phase = std::clamp(draw_phase, 0.0F, 1.0F);
+
+  mountOnHorse(mount);
+
+  QVector3D const bow_hold_pos = seatRelative(mount, 0.25F, -0.08F, 0.25F);
+  QVector3D const draw_start_pos =
+      bow_hold_pos + mount.seat_right * 0.08F + QVector3D(0.0F, -0.05F, 0.0F);
+  QVector3D const draw_end_pos = seatRelative(mount, 0.0F, 0.12F, 0.18F);
+
+  QVector3D hand_l_target = bow_hold_pos;
+  QVector3D hand_r_target;
+
+  if (draw_phase < 0.30F) {
+
+    float t = draw_phase / 0.30F;
+    t = t * t;
+    hand_r_target = draw_start_pos * (1.0F - t) + draw_end_pos * t;
+  } else if (draw_phase < 0.65F) {
+
+    hand_r_target = draw_end_pos;
+  } else {
+
+    float t = (draw_phase - 0.65F) / 0.35F;
+    t = t * t * t;
+    hand_r_target = draw_end_pos * (1.0F - t) + draw_start_pos * t;
+  }
+
+  getHand(true) = hand_l_target;
+  getHand(false) = hand_r_target;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), hand_l_target,
+                                left_outward, 0.50F, 0.08F, -0.05F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), hand_r_target,
+                                 right_outward, 0.48F, 0.12F, -0.08F, 1.0F);
+}
+
+void MountedPoseController::ridingShieldDefense(
+    const MountedAttachmentFrame &mount, bool raised) {
+  mountOnHorse(mount);
+
+  QVector3D shield_pos;
+  if (raised) {
+
+    shield_pos = seatRelative(mount, 0.15F, -0.18F, 0.40F);
+  } else {
+
+    shield_pos = seatRelative(mount, 0.0F, -0.15F, 0.08F);
+  }
+
+  float const rein_slack = raised ? 0.15F : 0.30F;
+  float const rein_tension = raised ? 0.45F : 0.25F;
+  QVector3D const rein_pos = reinAnchor(mount, false, rein_slack, rein_tension);
+
+  getHand(true) = shield_pos;
+  getHand(false) = rein_pos;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), shield_pos,
+                                left_outward, 0.45F, 0.15F, -0.10F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), rein_pos,
+                                 right_outward, 0.45F, 0.12F, -0.08F, 1.0F);
+}
+
+void MountedPoseController::holdReins(const MountedAttachmentFrame &mount,
+                                      float left_slack, float right_slack,
+                                      float left_tension, float right_tension) {
+  left_slack = std::clamp(left_slack, 0.0F, 1.0F);
+  right_slack = std::clamp(right_slack, 0.0F, 1.0F);
+  left_tension = std::clamp(left_tension, 0.0F, 1.0F);
+  right_tension = std::clamp(right_tension, 0.0F, 1.0F);
+
+  QVector3D const left_rein_pos =
+      reinAnchor(mount, true, left_slack, left_tension);
+  QVector3D const right_rein_pos =
+      reinAnchor(mount, false, right_slack, right_tension);
+
+  getHand(true) = left_rein_pos;
+  getHand(false) = right_rein_pos;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), left_rein_pos,
+                                left_outward, 0.45F, 0.12F, -0.08F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), right_rein_pos,
+                                 right_outward, 0.45F, 0.12F, -0.08F, 1.0F);
+}
+
+void MountedPoseController::holdSpearMounted(
+    const MountedAttachmentFrame &mount, SpearGrip grip_style) {
+  mountOnHorse(mount);
+
+  QVector3D hand_r_target;
+  QVector3D hand_l_target;
+
+  switch (grip_style) {
+  case SpearGrip::OVERHAND:
+
+    hand_r_target = seatRelative(mount, 0.0F, 0.12F, 0.55F);
+    hand_l_target = reinAnchor(mount, true, 0.30F, 0.30F);
+    break;
+
+  case SpearGrip::COUCHED:
+
+    hand_r_target = seatRelative(mount, -0.15F, 0.08F, 0.08F);
+    hand_l_target = reinAnchor(mount, true, 0.35F, 0.20F);
+    break;
+
+  case SpearGrip::TWO_HANDED:
+
+    hand_r_target = seatRelative(mount, 0.15F, 0.15F, 0.12F);
+    hand_l_target = hand_r_target - mount.seat_right * 0.25F;
+    break;
+  }
+
+  getHand(false) = hand_r_target;
+  getHand(true) = hand_l_target;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), hand_l_target,
+                                left_outward, 0.45F, 0.12F, -0.08F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), hand_r_target,
+                                 right_outward, 0.45F, 0.12F, -0.05F, 1.0F);
+}
+
+void MountedPoseController::holdBowMounted(
+    const MountedAttachmentFrame &mount) {
+  mountOnHorse(mount);
+
+  QVector3D const bow_hold_pos = seatRelative(mount, 0.20F, -0.08F, 0.22F);
+  QVector3D const arrow_nock_pos =
+      bow_hold_pos + mount.seat_right * 0.10F + mount.seat_up * -0.02F;
+
+  getHand(true) = bow_hold_pos;
+  getHand(false) = arrow_nock_pos;
+
+  const QVector3D left_outward = computeOutwardDir(true);
+  const QVector3D right_outward = computeOutwardDir(false);
+  getElbow(true) = solveElbowIK(true, getShoulder(true), bow_hold_pos,
+                                left_outward, 0.48F, 0.10F, -0.05F, 1.0F);
+  getElbow(false) = solveElbowIK(false, getShoulder(false), arrow_nock_pos,
+                                 right_outward, 0.48F, 0.10F, -0.06F, 1.0F);
+}
+
+void MountedPoseController::attachFeetToStirrups(
+    const MountedAttachmentFrame &mount) {
+
+  m_pose.footL = mount.stirrup_bottom_left + mount.ground_offset;
+  m_pose.foot_r = mount.stirrup_bottom_right + mount.ground_offset;
+}
+
+void MountedPoseController::positionPelvisOnSaddle(
+    const MountedAttachmentFrame &mount) {
+  QVector3D const seat_world = mount.seat_position + mount.ground_offset;
+  QVector3D const delta = seat_world - m_pose.pelvisPos;
+  m_pose.pelvisPos = seat_world;
+  translateUpperBody(delta);
+}
+
+void MountedPoseController::translateUpperBody(const QVector3D &delta) {
+  m_pose.shoulderL += delta;
+  m_pose.shoulderR += delta;
+  m_pose.neck_base += delta;
+  m_pose.headPos += delta;
+  m_pose.elbowL += delta;
+  m_pose.elbowR += delta;
+  m_pose.handL += delta;
+  m_pose.hand_r += delta;
+}
+
+void MountedPoseController::calculateRidingKnees(
+    const MountedAttachmentFrame &mount) {
+
+  QVector3D const hip_offset = mount.seat_up * -0.02F;
+  QVector3D const hip_left =
+      m_pose.pelvisPos - mount.seat_right * 0.10F + hip_offset;
+  QVector3D const hip_right =
+      m_pose.pelvisPos + mount.seat_right * 0.10F + hip_offset;
+
+  float const height_scale = m_anim_ctx.variation.height_scale;
+
+  m_pose.knee_l = solveKneeIK(true, hip_left, m_pose.footL, height_scale);
+  m_pose.knee_r = solveKneeIK(false, hip_right, m_pose.foot_r, height_scale);
+}
+
+auto MountedPoseController::solveElbowIK(
+    bool, const QVector3D &shoulder, const QVector3D &hand,
+    const QVector3D &outward_dir, float along_frac, float lateral_offset,
+    float y_bias, float outward_sign) const -> QVector3D {
+  return elbowBendTorso(shoulder, hand, outward_dir, along_frac, lateral_offset,
+                        y_bias, outward_sign);
+}
+
+auto MountedPoseController::solveKneeIK(bool is_left, const QVector3D &hip,
+                                        const QVector3D &foot,
+                                        float height_scale) const -> QVector3D {
+  using HP = HumanProportions;
+
+  QVector3D hip_to_foot = foot - hip;
+  float const distance = hip_to_foot.length();
+  if (distance < 1e-5F) {
+    return hip;
+  }
+
+  float const upper_len = HP::UPPER_LEG_LEN * height_scale;
+  float const lower_len = HP::LOWER_LEG_LEN * height_scale;
+  float const reach = upper_len + lower_len;
+  float const min_reach =
+      std::max(std::abs(upper_len - lower_len) + 1e-4F, 1e-3F);
+  float const max_reach = std::max(reach - 1e-4F, min_reach + 1e-4F);
+  float const clamped_dist = std::clamp(distance, min_reach, max_reach);
+
+  QVector3D const dir = hip_to_foot / distance;
+
+  float cos_theta = (upper_len * upper_len + clamped_dist * clamped_dist -
+                     lower_len * lower_len) /
+                    (2.0F * upper_len * clamped_dist);
+  cos_theta = std::clamp(cos_theta, -1.0F, 1.0F);
+  float const sin_theta =
+      std::sqrt(std::max(0.0F, 1.0F - cos_theta * cos_theta));
+
+  QVector3D bend_pref = is_left ? QVector3D(-0.70F, -0.15F, 0.30F)
+                                : QVector3D(0.70F, -0.15F, 0.30F);
+  bend_pref.normalize();
+
+  QVector3D bend_axis = bend_pref - dir * QVector3D::dotProduct(dir, bend_pref);
+  if (bend_axis.lengthSquared() < 1e-6F) {
+    bend_axis = QVector3D::crossProduct(dir, QVector3D(0.0F, 1.0F, 0.0F));
+    if (bend_axis.lengthSquared() < 1e-6F) {
+      bend_axis = QVector3D::crossProduct(dir, QVector3D(1.0F, 0.0F, 0.0F));
+    }
+  }
+  bend_axis.normalize();
+
+  QVector3D knee =
+      hip + dir * (cos_theta * upper_len) + bend_axis * (sin_theta * upper_len);
+
+  float const knee_floor = HP::GROUND_Y + m_pose.footYOffset * 0.5F;
+  if (knee.y() < knee_floor) {
+    knee.setY(knee_floor);
+  }
+
+  if (knee.y() > hip.y()) {
+    knee.setY(hip.y());
+  }
+
+  return knee;
+}
+
+auto MountedPoseController::getShoulder(bool is_left) const
+    -> const QVector3D & {
+  return is_left ? m_pose.shoulderL : m_pose.shoulderR;
+}
+
+auto MountedPoseController::getHand(bool is_left) -> QVector3D & {
+  return is_left ? m_pose.handL : m_pose.hand_r;
+}
+
+auto MountedPoseController::getHand(bool is_left) const -> const QVector3D & {
+  return is_left ? m_pose.handL : m_pose.hand_r;
+}
+
+auto MountedPoseController::getElbow(bool is_left) -> QVector3D & {
+  return is_left ? m_pose.elbowL : m_pose.elbowR;
+}
+
+auto MountedPoseController::computeRightAxis() const -> QVector3D {
+  QVector3D right_axis = m_pose.shoulderR - m_pose.shoulderL;
+  right_axis.setY(0.0F);
+  if (right_axis.lengthSquared() < 1e-8F) {
+    right_axis = QVector3D(1.0F, 0.0F, 0.0F);
+  }
+  right_axis.normalize();
+  return right_axis;
+}
+
+auto MountedPoseController::computeOutwardDir(bool is_left) const -> QVector3D {
+  QVector3D const right_axis = computeRightAxis();
+  return is_left ? -right_axis : right_axis;
+}
+
+} // namespace Render::GL

+ 67 - 0
render/humanoid/mounted_pose_controller.h

@@ -0,0 +1,67 @@
+#pragma once
+
+#include "rig.h"
+#include <QVector3D>
+
+namespace Render::GL {
+
+class HumanoidPoseController;
+struct MountedAttachmentFrame;
+
+enum class SpearGrip { OVERHAND, COUCHED, TWO_HANDED };
+
+class MountedPoseController {
+public:
+  MountedPoseController(HumanoidPose &pose,
+                        const HumanoidAnimationContext &anim_ctx);
+
+  void mountOnHorse(const MountedAttachmentFrame &mount);
+  void dismount();
+
+  void ridingIdle(const MountedAttachmentFrame &mount);
+  void ridingLeaning(const MountedAttachmentFrame &mount, float forward_lean,
+                     float side_lean);
+  void ridingCharging(const MountedAttachmentFrame &mount, float intensity);
+  void ridingReining(const MountedAttachmentFrame &mount, float left_tension,
+                     float right_tension);
+
+  void ridingMeleeStrike(const MountedAttachmentFrame &mount,
+                         float attack_phase);
+  void ridingSpearThrust(const MountedAttachmentFrame &mount,
+                         float attack_phase);
+  void ridingBowShot(const MountedAttachmentFrame &mount, float draw_phase);
+  void ridingShieldDefense(const MountedAttachmentFrame &mount, bool raised);
+
+  void holdReins(const MountedAttachmentFrame &mount, float left_slack,
+                 float right_slack, float left_tension = 0.0F,
+                 float right_tension = 0.0F);
+  void holdSpearMounted(const MountedAttachmentFrame &mount,
+                        SpearGrip grip_style);
+  void holdBowMounted(const MountedAttachmentFrame &mount);
+
+private:
+  HumanoidPose &m_pose;
+  const HumanoidAnimationContext &m_anim_ctx;
+
+  void attachFeetToStirrups(const MountedAttachmentFrame &mount);
+  void positionPelvisOnSaddle(const MountedAttachmentFrame &mount);
+  void translateUpperBody(const QVector3D &delta);
+  void calculateRidingKnees(const MountedAttachmentFrame &mount);
+
+  auto solveElbowIK(bool is_left, const QVector3D &shoulder,
+                    const QVector3D &hand, const QVector3D &outward_dir,
+                    float along_frac, float lateral_offset, float y_bias,
+                    float outward_sign) const -> QVector3D;
+
+  auto solveKneeIK(bool is_left, const QVector3D &hip, const QVector3D &foot,
+                   float height_scale) const -> QVector3D;
+
+  auto getShoulder(bool is_left) const -> const QVector3D &;
+  auto getHand(bool is_left) -> QVector3D &;
+  auto getHand(bool is_left) const -> const QVector3D &;
+  auto getElbow(bool is_left) -> QVector3D &;
+  auto computeRightAxis() const -> QVector3D;
+  auto computeOutwardDir(bool is_left) const -> QVector3D;
+};
+
+} // namespace Render::GL

+ 40 - 14
render/humanoid/rig.cpp

@@ -427,8 +427,11 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
   const float y_neck = pose.neck_base.y();
   const float y_neck = pose.neck_base.y();
   const float shoulder_half_span =
   const float shoulder_half_span =
       0.5F * std::abs(pose.shoulderR.x() - pose.shoulderL.x());
       0.5F * std::abs(pose.shoulderR.x() - pose.shoulderL.x());
-  const float torso_r =
-      std::max(HP::TORSO_TOP_R * width_scale, shoulder_half_span * 0.95F);
+
+  const float torso_r_base =
+      std::max(HP::TORSO_TOP_R, shoulder_half_span * 0.95F);
+
+  const float torso_r = torso_r_base * width_scale;
 
 
   const float y_top_cover = std::max(y_shoulder + 0.04F, y_neck + 0.00F);
   const float y_top_cover = std::max(y_shoulder + 0.04F, y_neck + 0.00F);
 
 
@@ -447,8 +450,11 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
   QVector3D const tunic_bot{0.0F, pose.pelvisPos.y() + 0.03F, 0.0F};
   QVector3D const tunic_bot{0.0F, pose.pelvisPos.y() + 0.03F, 0.0F};
 
 
   QMatrix4x4 torso_transform =
   QMatrix4x4 torso_transform =
-      cylinderBetween(ctx.model, tunic_top, tunic_bot, torso_r);
-  torso_transform.scale(1.0F, 1.0F, 0.65F);
+      cylinderBetween(ctx.model, tunic_top, tunic_bot, 1.0F);
+  float const depth_scale = scaling.z();
+
+  torso_transform.scale(torso_r_base * width_scale, 1.0F,
+                        torso_r_base * depth_scale * 0.65F);
 
 
   out.mesh(getUnitTorso(), torso_transform, v.palette.cloth, nullptr, 1.0F);
   out.mesh(getUnitTorso(), torso_transform, v.palette.cloth, nullptr, 1.0F);
 
 
@@ -459,8 +465,10 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
            v.palette.skin * 0.9F, nullptr, 1.0F);
            v.palette.skin * 0.9F, nullptr, 1.0F);
 
 
   float const head_r = pose.headR;
   float const head_r = pose.headR;
-  out.mesh(getUnitSphere(), sphereAt(ctx.model, pose.headPos, head_r),
-           v.palette.skin, nullptr, 1.0F);
+
+  QMatrix4x4 head_transform = sphereAt(ctx.model, pose.headPos, head_r);
+  head_transform.scale(width_scale, 1.0F, depth_scale);
+  out.mesh(getUnitSphere(), head_transform, v.palette.skin, nullptr, 1.0F);
 
 
   QVector3D head_up = pose.headPos - pose.neck_base;
   QVector3D head_up = pose.headPos - pose.neck_base;
   if (head_up.lengthSquared() < 1e-8F) {
   if (head_up.lengthSquared() < 1e-8F) {
@@ -685,18 +693,36 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
     }
     }
     foot_forward.normalize();
     foot_forward.normalize();
 
 
-    float const heel_span = foot_radius * 1.35F;
-    float const toe_span = foot_radius * 3.30F;
-    float const sole_radius = foot_radius;
-    float const sole_y = HP::GROUND_Y + 0.004F;
+    float const heel_span = foot_radius * 3.50F;
+    float const toe_span = foot_radius * 5.50F;
+    float const sole_y = HP::GROUND_Y;
+
+    QVector3D ankle_ground = ankle;
+    ankle_ground.setY(sole_y);
 
 
-    QVector3D heel = ankle - foot_forward * heel_span;
-    QVector3D toe = ankle + foot_forward * toe_span;
+    QVector3D heel = ankle_ground - foot_forward * heel_span;
+    QVector3D toe = ankle_ground + foot_forward * toe_span;
     heel.setY(sole_y);
     heel.setY(sole_y);
     toe.setY(sole_y);
     toe.setY(sole_y);
 
 
-    QMatrix4x4 foot_mat = capsuleBetween(ctx.model, heel, toe, sole_radius);
-    foot_mat.scale(1.30F, 0.42F, 0.95F);
+    QMatrix4x4 foot_mat = capsuleBetween(ctx.model, heel, toe, foot_radius);
+
+    float const width_at_heel = 1.2F;
+    float const width_at_toe = 2.5F;
+    float const height_scale = 0.26F;
+    float const depth_scale = 1.0F;
+
+    QMatrix4x4 scale_mat;
+    scale_mat.setToIdentity();
+    scale_mat.scale((width_at_heel + width_at_toe) * 0.5F, height_scale,
+                    depth_scale);
+
+    QMatrix4x4 shear_mat;
+    shear_mat.setToIdentity();
+    shear_mat(0, 2) = (width_at_toe - width_at_heel) * 0.5F;
+
+    foot_mat = foot_mat * scale_mat * shear_mat;
+
     out.mesh(getUnitCapsule(), foot_mat, v.palette.leatherDark * 0.92F, nullptr,
     out.mesh(getUnitCapsule(), foot_mat, v.palette.leatherDark * 0.92F, nullptr,
              1.0F);
              1.0F);
   };
   };

+ 2 - 0
render/humanoid/rig.h

@@ -208,6 +208,8 @@ public:
     return {1.0F, 1.0F, 1.0F};
     return {1.0F, 1.0F, 1.0F};
   }
   }
 
 
+  virtual auto get_mount_scale() const -> float { return 1.0F; }
+
   virtual void adjust_variation(const DrawContext &, uint32_t,
   virtual void adjust_variation(const DrawContext &, uint32_t,
                                 VariationParams &) const {}
                                 VariationParams &) const {}
 
 

+ 1 - 0
tests/CMakeLists.txt

@@ -7,6 +7,7 @@ add_executable(standard_of_iron_tests
     db/save_storage_test.cpp
     db/save_storage_test.cpp
     render/pose_controller_test.cpp
     render/pose_controller_test.cpp
     render/pose_controller_compatibility_test.cpp
     render/pose_controller_compatibility_test.cpp
+    render/mounted_pose_controller_test.cpp
     render/body_frames_test.cpp
     render/body_frames_test.cpp
     render/equipment_registry_test.cpp
     render/equipment_registry_test.cpp
     render/helmet_renderers_test.cpp
     render/helmet_renderers_test.cpp

+ 425 - 0
tests/render/mounted_pose_controller_test.cpp

@@ -0,0 +1,425 @@
+#include "render/horse/rig.h"
+#include "render/humanoid/humanoid_specs.h"
+#include "render/humanoid/mounted_pose_controller.h"
+#include "render/humanoid/rig.h"
+#include <QVector3D>
+#include <cmath>
+#include <gtest/gtest.h>
+
+using namespace Render::GL;
+
+class MountedPoseControllerTest : public ::testing::Test {
+protected:
+  void SetUp() override {
+    using HP = HumanProportions;
+
+    // Initialize a default pose with basic standing configuration
+    pose = HumanoidPose{};
+    float const head_center_y = 0.5F * (HP::HEAD_TOP_Y + HP::CHIN_Y);
+    float const half_shoulder = 0.5F * HP::SHOULDER_WIDTH;
+    pose.headPos = QVector3D(0.0F, head_center_y, 0.0F);
+    pose.headR = HP::HEAD_RADIUS;
+    pose.neck_base = QVector3D(0.0F, HP::NECK_BASE_Y, 0.0F);
+    pose.shoulderL = QVector3D(-half_shoulder, HP::SHOULDER_Y, 0.0F);
+    pose.shoulderR = QVector3D(half_shoulder, HP::SHOULDER_Y, 0.0F);
+    pose.pelvisPos = QVector3D(0.0F, HP::WAIST_Y, 0.0F);
+    pose.handL = QVector3D(-0.05F, HP::SHOULDER_Y + 0.05F, 0.55F);
+    pose.hand_r = QVector3D(0.15F, HP::SHOULDER_Y + 0.15F, 0.20F);
+    pose.elbowL = QVector3D(-0.15F, HP::SHOULDER_Y - 0.15F, 0.25F);
+    pose.elbowR = QVector3D(0.25F, HP::SHOULDER_Y - 0.10F, 0.10F);
+    pose.knee_l = QVector3D(-0.10F, HP::KNEE_Y, 0.05F);
+    pose.knee_r = QVector3D(0.10F, HP::KNEE_Y, -0.05F);
+    pose.footL = QVector3D(-0.14F, 0.022F, 0.06F);
+    pose.foot_r = QVector3D(0.14F, 0.022F, -0.06F);
+    pose.footYOffset = 0.022F;
+
+    // Initialize animation context with default idle state
+    anim_ctx = HumanoidAnimationContext{};
+    anim_ctx.inputs.time = 0.0F;
+    anim_ctx.inputs.isMoving = false;
+    anim_ctx.inputs.is_attacking = false;
+    anim_ctx.variation = VariationParams::fromSeed(12345);
+    anim_ctx.gait.state = HumanoidMotionState::Idle;
+
+    // Initialize a typical horse mount frame
+    mount = MountedAttachmentFrame{};
+    mount.saddle_center = QVector3D(0.0F, 1.20F, 0.0F);
+    mount.seat_position = QVector3D(0.0F, 1.25F, 0.0F);
+    mount.seat_forward = QVector3D(0.0F, 0.0F, 1.0F);
+    mount.seat_right = QVector3D(1.0F, 0.0F, 0.0F);
+    mount.seat_up = QVector3D(0.0F, 1.0F, 0.0F);
+    mount.ground_offset = QVector3D(0.0F, 0.0F, 0.0F);
+
+    mount.stirrup_attach_left = QVector3D(-0.35F, 1.05F, 0.15F);
+    mount.stirrup_attach_right = QVector3D(0.35F, 1.05F, 0.15F);
+    mount.stirrup_bottom_left = QVector3D(-0.40F, 0.75F, 0.20F);
+    mount.stirrup_bottom_right = QVector3D(0.40F, 0.75F, 0.20F);
+
+    mount.rein_bit_left = QVector3D(-0.12F, 1.48F, 0.95F);
+    mount.rein_bit_right = QVector3D(0.12F, 1.48F, 0.95F);
+    mount.bridle_base = QVector3D(0.0F, 1.50F, 0.85F);
+  }
+
+  HumanoidPose pose;
+  HumanoidAnimationContext anim_ctx;
+  MountedAttachmentFrame mount;
+
+  // Helper to check if a position is approximately equal
+  bool approxEqual(const QVector3D &a, const QVector3D &b,
+                   float epsilon = 0.01F) {
+    return std::abs(a.x() - b.x()) < epsilon &&
+           std::abs(a.y() - b.y()) < epsilon &&
+           std::abs(a.z() - b.z()) < epsilon;
+  }
+};
+
+TEST_F(MountedPoseControllerTest, ConstructorInitializesCorrectly) {
+  MountedPoseController controller(pose, anim_ctx);
+  // Constructor should not modify the pose
+  EXPECT_FLOAT_EQ(pose.pelvisPos.y(), HumanProportions::WAIST_Y);
+}
+
+TEST_F(MountedPoseControllerTest, MountOnHorsePositionsPelvisOnSaddle) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.mountOnHorse(mount);
+
+  // Pelvis should be at seat position
+  EXPECT_TRUE(approxEqual(pose.pelvisPos, mount.seat_position));
+}
+
+TEST_F(MountedPoseControllerTest, MountOnHorsePlacesFeetInStirrups) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.mountOnHorse(mount);
+
+  // Feet should be in stirrups
+  EXPECT_TRUE(approxEqual(pose.footL, mount.stirrup_bottom_left));
+  EXPECT_TRUE(approxEqual(pose.foot_r, mount.stirrup_bottom_right));
+}
+
+TEST_F(MountedPoseControllerTest, MountOnHorseLiftsUpperBody) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  float const original_shoulder_y = pose.shoulderL.y();
+
+  controller.mountOnHorse(mount);
+
+  // Shoulders should be lifted when mounted
+  EXPECT_GT(pose.shoulderL.y(), original_shoulder_y);
+  EXPECT_GT(pose.shoulderR.y(), original_shoulder_y);
+}
+
+TEST_F(MountedPoseControllerTest, DismountRestoresStandingPosition) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.mountOnHorse(mount);
+  controller.dismount();
+
+  // Pelvis should be back at standing height
+  EXPECT_NEAR(pose.pelvisPos.y(), HumanProportions::WAIST_Y, 0.01F);
+}
+
+TEST_F(MountedPoseControllerTest, RidingIdleSetsHandsToRestPosition) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+
+  // Hands should be in a resting position near pelvis
+  EXPECT_LT(pose.handL.y(), mount.seat_position.y());
+  EXPECT_LT(pose.hand_r.y(), mount.seat_position.y());
+}
+
+TEST_F(MountedPoseControllerTest, RidingLeaningForwardMovesTorso) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+  QVector3D const original_shoulder_z = pose.shoulderL;
+
+  controller.ridingLeaning(mount, 1.0F, 0.0F); // Full forward lean
+
+  // Shoulders should move forward
+  EXPECT_GT(pose.shoulderL.z(), original_shoulder_z.z());
+  EXPECT_GT(pose.shoulderR.z(), original_shoulder_z.z());
+}
+
+TEST_F(MountedPoseControllerTest, RidingLeaningSidewaysMovesTorso) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+  QVector3D const original_shoulder_x = pose.shoulderL;
+
+  controller.ridingLeaning(mount, 0.0F, 1.0F); // Full right lean
+
+  // Shoulders should move to the right
+  EXPECT_GT(pose.shoulderR.x(), original_shoulder_x.x());
+}
+
+TEST_F(MountedPoseControllerTest, RidingLeaningClampsInputs) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Should not crash with out-of-range inputs
+  EXPECT_NO_THROW(controller.ridingLeaning(mount, 2.0F, -2.0F));
+}
+
+TEST_F(MountedPoseControllerTest, RidingChargingLeansForward) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+  QVector3D const original_shoulder = pose.shoulderL;
+
+  controller.ridingCharging(mount, 1.0F);
+
+  // Should lean forward when charging
+  EXPECT_GT(pose.shoulderL.z(), original_shoulder.z());
+  EXPECT_LT(pose.shoulderL.y(), original_shoulder.y()); // Crouch
+}
+
+TEST_F(MountedPoseControllerTest, RidingReiningPullsHandsBack) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+  float const idle_left_z = pose.handL.z();
+  float const idle_right_z = pose.hand_r.z();
+
+  controller.ridingReining(mount, 1.0F, 1.0F);
+
+  // Hands should be pulled back when reining
+  EXPECT_LT(pose.handL.z(), idle_left_z);
+  EXPECT_LT(pose.hand_r.z(), idle_right_z);
+}
+
+TEST_F(MountedPoseControllerTest, RidingReiningLeansTorsoBack) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+  QVector3D const original_shoulder = pose.shoulderL;
+
+  controller.ridingReining(mount, 1.0F, 1.0F);
+
+  // Should lean back when reining hard
+  EXPECT_LT(pose.shoulderL.z(), original_shoulder.z());
+}
+
+TEST_F(MountedPoseControllerTest, RidingMeleeStrikeAnimatesCorrectly) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Test windup phase
+  controller.ridingMeleeStrike(mount, 0.15F);
+  float const windup_y = pose.hand_r.y();
+
+  // Test strike phase
+  controller.ridingMeleeStrike(mount, 0.40F);
+  float const strike_y = pose.hand_r.y();
+
+  // Hand should be lower during strike than windup
+  EXPECT_LT(strike_y, windup_y);
+}
+
+TEST_F(MountedPoseControllerTest, RidingSpearThrustAnimatesCorrectly) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Test guard phase
+  controller.ridingSpearThrust(mount, 0.10F);
+  float const guard_z = pose.hand_r.z();
+
+  // Test thrust phase
+  controller.ridingSpearThrust(mount, 0.35F);
+  float const thrust_z = pose.hand_r.z();
+
+  // Hand should move forward during thrust
+  EXPECT_GT(thrust_z, guard_z);
+}
+
+TEST_F(MountedPoseControllerTest, RidingBowShotAnimatesCorrectly) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Test initial draw
+  controller.ridingBowShot(mount, 0.10F);
+  QVector3D const draw_start = pose.hand_r;
+
+  // Test full draw
+  controller.ridingBowShot(mount, 0.40F);
+  QVector3D const draw_end = pose.hand_r;
+
+  // Right hand should move back when drawing
+  float const dist_moved = (draw_end - draw_start).length();
+  EXPECT_GT(dist_moved, 0.05F);
+}
+
+TEST_F(MountedPoseControllerTest, RidingShieldDefenseRaisesHand) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingShieldDefense(mount, false);
+  float const lowered_y = pose.handL.y();
+
+  controller.ridingShieldDefense(mount, true);
+  float const raised_y = pose.handL.y();
+
+  // Shield should be higher when raised
+  EXPECT_GT(raised_y, lowered_y);
+}
+
+TEST_F(MountedPoseControllerTest, HoldReinsPositionsHandsCorrectly) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.mountOnHorse(mount);
+  controller.holdReins(mount, 0.5F, 0.5F, 0.3F, 0.3F);
+
+  // Hands should stay near the saddle area with a slight forward bias
+  EXPECT_LT(std::abs(pose.handL.x()), mount.seat_position.x() + 0.30F);
+  EXPECT_LT(std::abs(pose.hand_r.x()), mount.seat_position.x() + 0.30F);
+  EXPECT_LT(pose.handL.y(), mount.seat_position.y());
+  EXPECT_LT(pose.hand_r.y(), mount.seat_position.y());
+}
+
+TEST_F(MountedPoseControllerTest, HoldReinsSlackAffectsHandPosition) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.mountOnHorse(mount);
+  controller.holdReins(mount, 0.0F, 0.0F, 1.0F, 1.0F);
+  QVector3D const tight_left = pose.handL;
+
+  controller.holdReins(mount, 1.0F, 1.0F, 0.0F, 0.0F);
+  QVector3D const slack_left = pose.handL;
+
+  // Slack reins should lower hands
+  EXPECT_LT(slack_left.y(), tight_left.y());
+}
+
+TEST_F(MountedPoseControllerTest, HoldSpearOverhandRaisesHand) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.holdSpearMounted(mount, SpearGrip::OVERHAND);
+
+  // Right hand should be high for overhead grip
+  EXPECT_GT(pose.hand_r.y(), mount.seat_position.y() + 0.40F);
+}
+
+TEST_F(MountedPoseControllerTest, HoldSpearCouchedLowersHand) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.holdSpearMounted(mount, SpearGrip::COUCHED);
+
+  // Right hand should be low for couched grip
+  EXPECT_LT(pose.hand_r.y(), mount.seat_position.y() + 0.20F);
+}
+
+TEST_F(MountedPoseControllerTest, HoldSpearTwoHandedUsesBothHands) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.holdSpearMounted(mount, SpearGrip::TWO_HANDED);
+
+  // Both hands should be on spear shaft
+  float const hand_separation = (pose.hand_r - pose.handL).length();
+  EXPECT_GT(hand_separation, 0.15F);
+  EXPECT_LT(hand_separation, 0.35F);
+}
+
+TEST_F(MountedPoseControllerTest, HoldBowMountedPositionsHandsCorrectly) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.holdBowMounted(mount);
+
+  // Left hand should hold bow forward
+  EXPECT_GT(pose.handL.z(), mount.seat_position.z());
+
+  // Right hand should be near bow for arrow nocking
+  float const hand_separation = (pose.hand_r - pose.handL).length();
+  EXPECT_LT(hand_separation, 0.25F);
+}
+
+TEST_F(MountedPoseControllerTest, KneePositionValidForMountedRiding) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.mountOnHorse(mount);
+
+  // Knees should be between pelvis and feet
+  EXPECT_LT(pose.knee_l.y(), pose.pelvisPos.y());
+  EXPECT_GT(pose.knee_l.y(), pose.footL.y());
+
+  EXPECT_LT(pose.knee_r.y(), pose.pelvisPos.y());
+  EXPECT_GT(pose.knee_r.y(), pose.foot_r.y());
+}
+
+TEST_F(MountedPoseControllerTest, ElbowPositionValidForAllActions) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingIdle(mount);
+
+  // Elbows should be between shoulders and hands
+  float const left_shoulder_elbow = (pose.elbowL - pose.shoulderL).length();
+  float const left_elbow_hand = (pose.handL - pose.elbowL).length();
+
+  EXPECT_GT(left_shoulder_elbow, 0.05F);
+  EXPECT_GT(left_elbow_hand, 0.05F);
+  EXPECT_LT(left_shoulder_elbow, 0.50F);
+  EXPECT_LT(left_elbow_hand, 0.50F);
+}
+
+TEST_F(MountedPoseControllerTest, AllMethodsHandleEdgeCases) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Should not crash with various inputs
+  EXPECT_NO_THROW(controller.mountOnHorse(mount));
+  EXPECT_NO_THROW(controller.dismount());
+  EXPECT_NO_THROW(controller.ridingIdle(mount));
+  EXPECT_NO_THROW(controller.ridingLeaning(mount, 0.0F, 0.0F));
+  EXPECT_NO_THROW(controller.ridingCharging(mount, 0.0F));
+  EXPECT_NO_THROW(controller.ridingReining(mount, 0.0F, 0.0F));
+  EXPECT_NO_THROW(controller.ridingMeleeStrike(mount, 0.5F));
+  EXPECT_NO_THROW(controller.ridingSpearThrust(mount, 0.5F));
+  EXPECT_NO_THROW(controller.ridingBowShot(mount, 0.5F));
+  EXPECT_NO_THROW(controller.ridingShieldDefense(mount, true));
+  EXPECT_NO_THROW(controller.holdReins(mount, 0.5F, 0.5F, 0.4F, 0.4F));
+  EXPECT_NO_THROW(controller.holdSpearMounted(mount, SpearGrip::OVERHAND));
+  EXPECT_NO_THROW(controller.holdBowMounted(mount));
+}
+
+TEST_F(MountedPoseControllerTest, AttackPhaseClamping) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Test clamping of attack phase > 1.0
+  EXPECT_NO_THROW(controller.ridingMeleeStrike(mount, 1.5F));
+  EXPECT_NO_THROW(controller.ridingSpearThrust(mount, 2.0F));
+  EXPECT_NO_THROW(controller.ridingBowShot(mount, -0.5F));
+}
+
+TEST_F(MountedPoseControllerTest, RidingChargingIntensityClamping) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  controller.ridingCharging(mount, 1.5F);
+  QVector3D const max_lean = pose.shoulderL;
+
+  // Reset
+  SetUp();
+  MountedPoseController controller2(pose, anim_ctx);
+  controller2.ridingCharging(mount, 1.0F);
+
+  // Should be same as clamped 1.5F
+  EXPECT_TRUE(approxEqual(pose.shoulderL, max_lean));
+}
+
+TEST_F(MountedPoseControllerTest, FullRidingSequence) {
+  MountedPoseController controller(pose, anim_ctx);
+
+  // Simulate a full riding sequence
+  controller.mountOnHorse(mount);
+  EXPECT_TRUE(approxEqual(pose.pelvisPos, mount.seat_position));
+
+  controller.ridingIdle(mount);
+  QVector3D const idle_hands = pose.handL;
+
+  controller.holdReins(mount, 0.5F, 0.5F, 0.3F, 0.3F);
+  controller.ridingCharging(mount, 1.0F);
+
+  controller.ridingSpearThrust(mount, 0.35F);
+  // Verify animation in progress
+  EXPECT_GT(pose.hand_r.z(), mount.seat_position.z());
+
+  controller.ridingIdle(mount);
+  controller.dismount();
+
+  // Should be back near standing position
+  EXPECT_NEAR(pose.pelvisPos.y(), HumanProportions::WAIST_Y, 0.01F);
+}