Selaa lähdekoodia

fix issue of face clipping trough helmet on mounted units

djeada 4 viikkoa sitten
vanhempi
sitoutus
52d166e3c2

+ 1 - 1
assets/maps/map_rivers.json

@@ -475,4 +475,4 @@
       "no_key_structures"
     ]
   }
-}
+}

+ 12 - 1
render/entity/horse_archer_renderer_base.cpp

@@ -142,7 +142,18 @@ void HorseArcherRendererBase::draw_helmet(const DrawContext &ctx,
       registry.get(EquipmentCategory::Helmet, m_config.helmet_equipment_id);
   if (helmet) {
     HumanoidAnimationContext anim_ctx{};
-    helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
+    BodyFrames frames = pose.body_frames;
+    if (ctx.entity != nullptr) {
+      auto *move = ctx.entity->getComponent<Engine::Core::MovementComponent>();
+      if (move != nullptr) {
+        float speed_sq = move->vx * move->vx + move->vz * move->vz;
+        if (speed_sq > 0.0001F && m_config.helmet_offset_moving > 0.0F) {
+          frames.head.origin +=
+              frames.head.forward * m_config.helmet_offset_moving;
+        }
+      }
+    }
+    helmet->render(ctx, frames, v.palette, anim_ctx, out);
   }
 }
 

+ 1 - 0
render/entity/horse_archer_renderer_base.h

@@ -22,6 +22,7 @@ struct HorseArcherRendererConfig {
   float mount_scale = 0.75F;
   bool has_bow = true;
   bool has_quiver = true;
+  float helmet_offset_moving = 0.0F;
   std::vector<std::shared_ptr<IHorseEquipmentRenderer>> horse_attachments;
 };
 

+ 12 - 2
render/entity/horse_spearman_renderer_base.cpp

@@ -88,7 +88,6 @@ void HorseSpearmanRendererBase::apply_riding_animation(
       mounted_controller.ridingCharging(mount, 1.0F);
       mounted_controller.holdSpearMounted(mount, SpearGrip::COUCHED);
 
-      pose.head_pos -= mount.seat_forward * 0.04F;
       pose.neck_base -= mount.seat_forward * 0.03F;
     } else {
       float const attack_phase =
@@ -156,7 +155,18 @@ void HorseSpearmanRendererBase::draw_helmet(const DrawContext &ctx,
       registry.get(EquipmentCategory::Helmet, m_config.helmet_equipment_id);
   if (helmet) {
     HumanoidAnimationContext anim_ctx{};
-    helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
+    BodyFrames frames = pose.body_frames;
+    if (ctx.entity != nullptr) {
+      auto *move = ctx.entity->getComponent<Engine::Core::MovementComponent>();
+      if (move != nullptr) {
+        float speed_sq = move->vx * move->vx + move->vz * move->vz;
+        if (speed_sq > 0.0001F && m_config.helmet_offset_moving > 0.0F) {
+          frames.head.origin +=
+              frames.head.forward * m_config.helmet_offset_moving;
+        }
+      }
+    }
+    helmet->render(ctx, frames, v.palette, anim_ctx, out);
   }
 }
 

+ 1 - 0
render/entity/horse_spearman_renderer_base.h

@@ -21,6 +21,7 @@ struct HorseSpearmanRendererConfig {
   float mount_scale = 0.75F;
   bool has_spear = true;
   bool has_shield = false;
+  float helmet_offset_moving = 0.0F;
   std::vector<std::shared_ptr<IHorseEquipmentRenderer>> horse_attachments;
 };
 

+ 6 - 0
render/entity/mounted_humanoid_renderer_base.cpp

@@ -79,11 +79,17 @@ void MountedHumanoidRendererBase::customize_pose(
                          reins);
 
   applyMountedKnightLowerBody(dims, mount, anim_ctx, pose);
+
+  mounted_controller.finalizeHeadSync(mount, "customize_pose_final_sync");
 }
 
 void MountedHumanoidRendererBase::addAttachments(
     const DrawContext &ctx, const HumanoidVariant &v, const HumanoidPose &pose,
     const HumanoidAnimationContext &anim_ctx, ISubmitter &out) const {
+  static uint64_t s_mounted_frame_counter = 0;
+  ++s_mounted_frame_counter;
+  uint64_t frame_id = s_mounted_frame_counter;
+
   uint32_t horse_seed = 0U;
   if (ctx.entity != nullptr) {
     horse_seed = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(ctx.entity) &

+ 15 - 6
render/entity/mounted_knight_renderer_base.cpp

@@ -80,8 +80,7 @@ void MountedKnightRendererBase::apply_riding_animation(
   (void)pose;
   const AnimationInputs &anim = anim_ctx.inputs;
   float const speed_norm = anim_ctx.locomotion_normalized_speed();
-  float const speed_lean = std::clamp(
-      anim_ctx.locomotion_speed() * 0.10F + speed_norm * 0.05F, 0.0F, 0.22F);
+  float const speed_lean = 0.0F;
   float const forward_lean =
       (dims.seatForwardOffset * 0.08F + speed_lean) / 0.15F;
 
@@ -99,9 +98,8 @@ void MountedKnightRendererBase::apply_riding_animation(
   pose_request.seatPose = (speed_norm > 0.55F)
                               ? MountedPoseController::MountedSeatPose::Forward
                               : MountedPoseController::MountedSeatPose::Neutral;
-  pose_request.torsoCompression = std::clamp(
-      0.18F + speed_norm * 0.28F + anim_ctx.variation.posture_slump * 0.9F,
-      0.0F, 0.55F);
+  pose_request.torsoCompression =
+      std::clamp(0.18F + anim_ctx.variation.posture_slump * 0.9F, 0.0F, 0.55F);
   pose_request.torsoTwist = anim_ctx.variation.shoulder_tilt * 3.0F;
   pose_request.shoulderDip =
       std::clamp(anim_ctx.variation.shoulder_tilt * 0.6F +
@@ -186,7 +184,18 @@ void MountedKnightRendererBase::draw_helmet(const DrawContext &ctx,
       registry.get(EquipmentCategory::Helmet, m_config.helmet_equipment_id);
   if (helmet) {
     HumanoidAnimationContext anim_ctx{};
-    helmet->render(ctx, pose.body_frames, v.palette, anim_ctx, out);
+    BodyFrames frames = pose.body_frames;
+    if (ctx.entity != nullptr) {
+      auto *move = ctx.entity->getComponent<Engine::Core::MovementComponent>();
+      if (move != nullptr) {
+        float speed_sq = move->vx * move->vx + move->vz * move->vz;
+        if (speed_sq > 0.0001F && m_config.helmet_offset_moving > 0.0F) {
+          frames.head.origin +=
+              frames.head.forward * m_config.helmet_offset_moving;
+        }
+      }
+    }
+    helmet->render(ctx, frames, v.palette, anim_ctx, out);
   }
 }
 

+ 1 - 0
render/entity/mounted_knight_renderer_base.h

@@ -21,6 +21,7 @@ struct MountedKnightRendererConfig {
   float mount_scale = 0.75F;
   bool has_sword = true;
   bool has_cavalry_shield = true;
+  float helmet_offset_moving = 0.0F;
   std::vector<std::shared_ptr<IHorseEquipmentRenderer>> horse_attachments;
 };
 

+ 1 - 0
render/entity/nations/carthage/horse_archer_renderer.cpp

@@ -19,6 +19,7 @@ auto make_horse_archer_config() -> HorseArcherRendererConfig {
   config.quiver_equipment_id = "quiver";
   config.helmet_equipment_id = "carthage_light";
   config.armor_equipment_id = "armor_light_carthage";
+  config.helmet_offset_moving = 0.035F;
   config.fletching_color = {0.85F, 0.40F, 0.40F};
   config.horse_attachments.emplace_back(
       std::make_shared<CarthageSaddleRenderer>());

+ 1 - 0
render/entity/nations/carthage/horse_spearman_renderer.cpp

@@ -18,6 +18,7 @@ auto make_horse_spearman_config() -> HorseSpearmanRendererConfig {
   config.spear_equipment_id = "spear";
   config.helmet_equipment_id = "carthage_heavy";
   config.armor_equipment_id = "armor_heavy_carthage";
+  config.helmet_offset_moving = 0.04F;
   config.horse_attachments.emplace_back(
       std::make_shared<CarthageSaddleRenderer>());
   config.horse_attachments.emplace_back(std::make_shared<ReinsRenderer>());

+ 1 - 0
render/entity/nations/carthage/horse_swordsman_renderer.cpp

@@ -19,6 +19,7 @@ auto makeMountedKnightConfig() -> MountedKnightRendererConfig {
   config.shield_equipment_id = "shield_carthage_cavalry";
   config.helmet_equipment_id = "carthage_heavy";
   config.armor_equipment_id = "armor_heavy_carthage";
+  config.helmet_offset_moving = 0.03F;
   config.horse_attachments.emplace_back(
       std::make_shared<CarthageSaddleRenderer>());
   config.horse_attachments.emplace_back(std::make_shared<ReinsRenderer>());

+ 1 - 0
render/entity/nations/kingdom/horse_archer_renderer.cpp

@@ -19,6 +19,7 @@ auto make_horse_archer_config() -> HorseArcherRendererConfig {
   config.quiver_equipment_id = "quiver";
   config.helmet_equipment_id = "kingdom_light";
   config.armor_equipment_id = "kingdom_light_armor";
+  config.helmet_offset_moving = 0.035F;
   config.fletching_color = {0.85F, 0.40F, 0.40F};
   config.horse_attachments.emplace_back(
       std::make_shared<LightCavalrySaddleRenderer>());

+ 1 - 0
render/entity/nations/kingdom/horse_spearman_renderer.cpp

@@ -18,6 +18,7 @@ auto make_horse_spearman_config() -> HorseSpearmanRendererConfig {
   config.spear_equipment_id = "spear";
   config.helmet_equipment_id = "kingdom_heavy";
   config.armor_equipment_id = "kingdom_heavy_armor";
+  config.helmet_offset_moving = 0.04F;
   config.horse_attachments.emplace_back(
       std::make_shared<LightCavalrySaddleRenderer>());
   config.horse_attachments.emplace_back(std::make_shared<ReinsRenderer>());

+ 1 - 0
render/entity/nations/kingdom/horse_swordsman_renderer.cpp

@@ -19,6 +19,7 @@ auto makeMountedKnightConfig() -> MountedKnightRendererConfig {
   config.shield_equipment_id = "shield_kingdom";
   config.helmet_equipment_id = "kingdom_heavy";
   config.armor_equipment_id = "kingdom_heavy_armor";
+  config.helmet_offset_moving = 0.03F;
   config.horse_attachments.emplace_back(
       std::make_shared<LightCavalrySaddleRenderer>());
   config.horse_attachments.emplace_back(std::make_shared<ReinsRenderer>());

+ 1 - 0
render/entity/nations/roman/horse_archer_renderer.cpp

@@ -19,6 +19,7 @@ auto make_horse_archer_config() -> HorseArcherRendererConfig {
   config.quiver_equipment_id = "quiver";
   config.helmet_equipment_id = "roman_light";
   config.armor_equipment_id = "roman_light_armor";
+  config.helmet_offset_moving = 0.04F;
   config.fletching_color = {0.85F, 0.40F, 0.40F};
   config.horse_attachments.emplace_back(
       std::make_shared<RomanSaddleRenderer>());

+ 1 - 0
render/entity/nations/roman/horse_spearman_renderer.cpp

@@ -18,6 +18,7 @@ auto make_horse_spearman_config() -> HorseSpearmanRendererConfig {
   config.spear_equipment_id = "spear";
   config.helmet_equipment_id = "roman_heavy";
   config.armor_equipment_id = "roman_heavy_armor";
+  config.helmet_offset_moving = 0.06F;
   config.horse_attachments.emplace_back(
       std::make_shared<RomanSaddleRenderer>());
   config.horse_attachments.emplace_back(std::make_shared<ReinsRenderer>());

+ 1 - 0
render/entity/nations/roman/horse_swordsman_renderer.cpp

@@ -19,6 +19,7 @@ auto makeMountedKnightConfig() -> MountedKnightRendererConfig {
   config.shield_equipment_id = "shield_roman";
   config.helmet_equipment_id = "roman_heavy";
   config.armor_equipment_id = "roman_heavy_armor";
+  config.helmet_offset_moving = 0.035F;
   config.horse_attachments.emplace_back(
       std::make_shared<RomanSaddleRenderer>());
   config.horse_attachments.emplace_back(std::make_shared<ReinsRenderer>());

+ 2 - 1
render/equipment/helmets/roman_heavy_helmet.cpp

@@ -7,6 +7,7 @@
 #include "../../submitter.h"
 #include <QMatrix4x4>
 #include <QVector3D>
+#include <cmath>
 
 namespace Render::GL {
 
@@ -55,7 +56,7 @@ void RomanHeavyHelmetRenderer::render(const DrawContext &ctx,
                                       ISubmitter &submitter) {
   (void)anim;
 
-  const AttachmentFrame &head = frames.head;
+  AttachmentFrame head = frames.head;
   float head_r = head.radius;
   if (head_r <= 0.0f) {
     return;

+ 2 - 1
render/equipment/helmets/roman_light_helmet.cpp

@@ -7,6 +7,7 @@
 #include "../../submitter.h"
 #include <QMatrix4x4>
 #include <QVector3D>
+#include <cmath>
 
 namespace Render::GL {
 
@@ -22,7 +23,7 @@ void RomanLightHelmetRenderer::render(const DrawContext &ctx,
                                       ISubmitter &submitter) {
   (void)anim;
 
-  const AttachmentFrame &head = frames.head;
+  AttachmentFrame head = frames.head;
   float const head_r = head.radius;
   if (head_r <= 0.0F) {
     return;

+ 7 - 0
render/equipment/i_equipment_renderer.h

@@ -3,6 +3,8 @@
 #include "../humanoid/rig.h"
 #include "../palette.h"
 #include "../submitter.h"
+#include <atomic>
+#include <cstdint>
 
 namespace Render::GL {
 
@@ -16,6 +18,11 @@ public:
                       const HumanoidPalette &palette,
                       const HumanoidAnimationContext &anim,
                       ISubmitter &submitter) = 0;
+
+  static auto nextRenderId() -> uint64_t {
+    static std::atomic<uint64_t> counter{0};
+    return ++counter;
+  }
 };
 
 } // namespace Render::GL

+ 126 - 7
render/humanoid/mounted_pose_controller.cpp

@@ -3,6 +3,8 @@
 #include "humanoid_math.h"
 #include "humanoid_specs.h"
 #include "pose_controller.h"
+#include <QDebug>
+#include <QString>
 #include <QVector3D>
 #include <algorithm>
 #include <cmath>
@@ -45,6 +47,10 @@ void MountedPoseController::dismount() {
 }
 
 void MountedPoseController::ridingIdle(const MountedAttachmentFrame &mount) {
+  static int idle_log_counter = 0;
+  if (idle_log_counter++ % 300 == 0) {
+    qDebug() << "MountedPoseController::ridingIdle called";
+  }
   mountOnHorse(mount);
 
   QVector3D const left_hand_rest = seatRelative(mount, 0.12F, -0.14F, -0.05F);
@@ -59,6 +65,8 @@ void MountedPoseController::ridingIdle(const MountedAttachmentFrame &mount) {
                                 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);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "ridingIdle");
 }
 
 void MountedPoseController::ridingLeaning(const MountedAttachmentFrame &mount,
@@ -77,13 +85,13 @@ void MountedPoseController::ridingCharging(const MountedAttachmentFrame &mount,
   m_pose.shoulder_l += charge_lean;
   m_pose.shoulder_r += charge_lean;
   m_pose.neck_base += charge_lean * 0.85F;
-  m_pose.head_pos += charge_lean * 0.85F;
 
   float const crouch = 0.08F * intensity;
   m_pose.shoulder_l.setY(m_pose.shoulder_l.y() - crouch);
   m_pose.shoulder_r.setY(m_pose.shoulder_r.y() - crouch);
   m_pose.neck_base.setY(m_pose.neck_base.y() - crouch * 0.8F);
-  m_pose.head_pos.setY(m_pose.head_pos.y() - crouch * 0.8F);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "ridingCharging");
 
   holdReins(mount, 0.2F, 0.2F, 0.85F, 0.85F);
 }
@@ -114,7 +122,8 @@ void MountedPoseController::ridingReining(const MountedAttachmentFrame &mount,
   m_pose.shoulder_l += lean_back;
   m_pose.shoulder_r += lean_back;
   m_pose.neck_base += lean_back * 0.9F;
-  m_pose.head_pos += lean_back * 0.9F;
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "ridingReining");
 }
 
 void MountedPoseController::ridingMeleeStrike(
@@ -221,6 +230,8 @@ void MountedPoseController::applyPose(const MountedAttachmentFrame &mount,
   applySaddleClearance(mount, request.dims, request.clearanceForward,
                        request.clearanceUp);
 
+  stabilizeUpperBody(mount, request.dims);
+
   float forward = request.forwardBias;
   switch (request.seatPose) {
   case MountedSeatPose::Forward:
@@ -234,9 +245,41 @@ void MountedPoseController::applyPose(const MountedAttachmentFrame &mount,
     break;
   }
   applyLean(mount, forward, request.sideBias);
+
+  static int debug_log_count = 0;
+  static int frame_counter = 0;
+  frame_counter++;
+
+  bool should_log = (frame_counter % 60 == 0);
+
+  if (should_log) {
+    qDebug() << "Frame" << frame_counter
+             << "After Lean - HeadPos:" << m_pose.head_pos
+             << "FrameOrigin:" << m_pose.head_frame.origin << "Diff:"
+             << (m_pose.head_pos - m_pose.head_frame.origin).length();
+  }
+
   applyTorsoSculpt(mount, request.torsoCompression, request.torsoTwist,
                    request.shoulderDip);
-  stabilizeUpperBody(mount, request.dims);
+
+  if (should_log) {
+    qDebug() << "Frame" << frame_counter
+             << "After Sculpt - HeadPos:" << m_pose.head_pos
+             << "FrameOrigin:" << m_pose.head_frame.origin << "Diff:"
+             << (m_pose.head_pos - m_pose.head_frame.origin).length();
+  }
+
+  float const clamped_forward = std::clamp(forward, -1.0F, 1.0F);
+  float const clamped_side = std::clamp(request.sideBias, -1.0F, 1.0F);
+  updateHeadHierarchy(mount, clamped_forward * 0.4F, clamped_side * 0.4F,
+                      "applyPose_fixup");
+
+  if (should_log) {
+    qDebug() << "Frame" << frame_counter
+             << "After Fix - HeadPos:" << m_pose.head_pos
+             << "FrameOrigin:" << m_pose.head_frame.origin << "Diff:"
+             << (m_pose.head_pos - m_pose.head_frame.origin).length();
+  }
 
   const bool needs_weapon_right = request.weaponPose != MountedWeaponPose::None;
   const bool needs_weapon_left =
@@ -306,7 +349,9 @@ void MountedPoseController::applyLean(const MountedAttachmentFrame &mount,
   m_pose.shoulder_l += lean_offset;
   m_pose.shoulder_r += lean_offset;
   m_pose.neck_base += lean_offset * 0.9F;
-  m_pose.head_pos += lean_offset * 0.9F;
+
+  updateHeadHierarchy(mount, clamped_forward * 0.4F, clamped_side * 0.4F,
+                      "applyLean");
 }
 
 void MountedPoseController::applyShieldDefense(
@@ -326,6 +371,8 @@ void MountedPoseController::applyShieldDefense(
                                 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);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "shield_defense");
 }
 
 void MountedPoseController::applyShieldStowed(
@@ -337,6 +384,8 @@ void MountedPoseController::applyShieldStowed(
   const QVector3D left_outward = computeOutwardDir(true);
   getElbow(true) = solveElbowIK(true, getShoulder(true), rest, left_outward,
                                 0.42F, 0.12F, -0.05F, 1.0F);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "shield_stowed");
 }
 
 void MountedPoseController::applySwordIdlePose(
@@ -348,6 +397,8 @@ void MountedPoseController::applySwordIdlePose(
   const QVector3D right_outward = computeOutwardDir(false);
   getElbow(false) = solveElbowIK(false, getShoulder(false), sword_anchor,
                                  right_outward, 0.42F, 0.10F, -0.06F, 1.0F);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "sword_idle");
 }
 
 void MountedPoseController::applySwordStrike(
@@ -367,6 +418,7 @@ void MountedPoseController::applySwordStrike(
     float t = attack_phase / 0.30F;
     t = t * t;
     hand_r_target = rest_pos * (1.0F - t) + raised_pos * t;
+    updateHeadHierarchy(mount, 0.0F, 0.0F, "sword_raise");
   } else if (attack_phase < 0.50F) {
     float t = (attack_phase - 0.30F) / 0.20F;
     t = t * t * t;
@@ -377,11 +429,13 @@ void MountedPoseController::applySwordStrike(
     m_pose.shoulder_l += lean;
     m_pose.shoulder_r += lean;
     m_pose.neck_base += lean * 0.9F;
-    m_pose.head_pos += lean * 0.9F;
+
+    updateHeadHierarchy(mount, 0.3F * t, 0.0F, "sword_strike");
   } 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;
+    updateHeadHierarchy(mount, 0.0F, 0.0F, "sword_recover");
   }
 
   getHand(false) = hand_r_target;
@@ -413,6 +467,7 @@ void MountedPoseController::applySpearThrust(
   if (attack_phase < 0.25F) {
     hand_r_target = guard_pos;
     hand_l_target = guard_pos - mount.seat_right * 0.25F;
+    updateHeadHierarchy(mount, 0.0F, 0.0F, "spear_guard_hold");
   } else if (attack_phase < 0.45F) {
     float t = (attack_phase - 0.25F) / 0.20F;
     t = t * t * t;
@@ -424,13 +479,15 @@ void MountedPoseController::applySpearThrust(
     m_pose.shoulder_l += lean;
     m_pose.shoulder_r += lean;
     m_pose.neck_base += lean * 0.9F;
-    m_pose.head_pos += lean * 0.9F;
+
+    updateHeadHierarchy(mount, 0.5F * t, 0.0F, "spear_thrust");
   } 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;
+    updateHeadHierarchy(mount, 0.0F, 0.0F, "spear_recover");
   }
 
   getHand(false) = hand_r_target;
@@ -473,6 +530,8 @@ void MountedPoseController::applySpearGuard(const MountedAttachmentFrame &mount,
                                 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);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "spear_guard_pose");
 }
 
 void MountedPoseController::applyBowDraw(const MountedAttachmentFrame &mount,
@@ -508,6 +567,8 @@ void MountedPoseController::applyBowDraw(const MountedAttachmentFrame &mount,
                                 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);
+
+  updateHeadHierarchy(mount, 0.0F, 0.0F, "bow_draw");
 }
 
 void MountedPoseController::applyTorsoSculpt(
@@ -717,4 +778,62 @@ auto MountedPoseController::computeOutwardDir(bool is_left) const -> QVector3D {
   return is_left ? -right_axis : right_axis;
 }
 
+void MountedPoseController::applyFixedHeadFrame(
+    const MountedAttachmentFrame &mount, std::string_view debug_label) {
+  using HP = HumanProportions;
+  float const h_scale = m_anim_ctx.variation.height_scale;
+  float const neck_len = (HP::HEAD_HEIGHT * 0.5F + 0.045F) * h_scale;
+
+  QVector3D up_dir = mount.seat_up;
+  if (up_dir.lengthSquared() < 1e-6F) {
+    up_dir = QVector3D(0.0F, 1.0F, 0.0F);
+  } else {
+    up_dir.normalize();
+  }
+
+  QVector3D fwd_dir = mount.seat_forward;
+  if (fwd_dir.lengthSquared() < 1e-6F) {
+    fwd_dir = QVector3D(0.0F, 0.0F, 1.0F);
+  } else {
+    fwd_dir.normalize();
+  }
+
+  QVector3D right_dir = QVector3D::crossProduct(fwd_dir, up_dir);
+  if (right_dir.lengthSquared() < 1e-6F) {
+    right_dir = QVector3D(1.0F, 0.0F, 0.0F);
+  } else {
+    right_dir.normalize();
+  }
+  fwd_dir = QVector3D::crossProduct(up_dir, right_dir).normalized();
+
+  m_pose.head_pos = m_pose.neck_base + up_dir * neck_len;
+
+  QVector3D prev_origin = m_pose.head_frame.origin;
+  QVector3D prev_up = m_pose.head_frame.up;
+  QVector3D prev_forward = m_pose.head_frame.forward;
+
+  m_pose.head_frame.origin = m_pose.head_pos;
+  m_pose.head_frame.up = up_dir;
+  m_pose.head_frame.right = right_dir;
+  m_pose.head_frame.forward = fwd_dir;
+  if (m_pose.head_r < 0.01F) {
+    m_pose.head_r = 0.12F;
+  }
+  m_pose.head_frame.radius = m_pose.head_r;
+  m_pose.body_frames.head = m_pose.head_frame;
+}
+
+void MountedPoseController::updateHeadHierarchy(
+    const MountedAttachmentFrame &mount, float extra_forward_tilt,
+    float extra_side_tilt, std::string_view debug_label) {
+  (void)extra_forward_tilt;
+  (void)extra_side_tilt;
+  applyFixedHeadFrame(mount, debug_label);
+}
+
+void MountedPoseController::finalizeHeadSync(
+    const MountedAttachmentFrame &mount, std::string_view debug_label) {
+  applyFixedHeadFrame(mount, debug_label);
+}
+
 } // namespace Render::GL

+ 10 - 0
render/humanoid/mounted_pose_controller.h

@@ -3,6 +3,7 @@
 #include "../horse/rig.h"
 #include "rig.h"
 #include <QVector3D>
+#include <string_view>
 
 namespace Render::GL {
 
@@ -75,6 +76,9 @@ public:
   void applyPose(const MountedAttachmentFrame &mount,
                  const MountedRiderPoseRequest &request);
 
+  void finalizeHeadSync(const MountedAttachmentFrame &mount,
+                        std::string_view debug_label = "final_head_sync");
+
 private:
   HumanoidPose &m_pose;
   const HumanoidAnimationContext &m_anim_ctx;
@@ -119,9 +123,15 @@ private:
                           const HorseDimensions &dims);
   void applyTorsoSculpt(const MountedAttachmentFrame &mount, float compression,
                         float twist, float shoulderDip);
+  void updateHeadHierarchy(const MountedAttachmentFrame &mount,
+                           float extra_forward_tilt, float extra_side_tilt,
+                           std::string_view debug_label = "head_sync");
   void holdReinsImpl(const MountedAttachmentFrame &mount, float left_slack,
                      float right_slack, float left_tension, float right_tension,
                      bool apply_left, bool apply_right);
+
+  void applyFixedHeadFrame(const MountedAttachmentFrame &mount,
+                           std::string_view debug_label);
 };
 
 } // namespace Render::GL

+ 79 - 28
render/humanoid/rig.cpp

@@ -327,7 +327,7 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
 
   const float torso_depth_factor =
       std::clamp(0.55F + (depth_scale - 1.0F) * 0.20F, 0.40F, 0.85F);
-  const float torso_depth = torso_r * torso_depth_factor;
+  float torso_depth = torso_r * torso_depth_factor;
 
   const float y_top_cover = std::max(y_shoulder + 0.04F, y_neck + 0.00F);
 
@@ -356,37 +356,47 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
 
   float const head_r = pose.head_r;
 
-  QVector3D head_up = pose.head_pos - pose.neck_base;
-  if (head_up.lengthSquared() < 1e-8F) {
-    head_up = up_axis;
+  QVector3D head_up;
+  QVector3D head_right;
+  QVector3D head_forward;
+
+  if (pose.head_frame.radius > 0.001F) {
+    head_up = pose.head_frame.up;
+    head_right = pose.head_frame.right;
+    head_forward = pose.head_frame.forward;
   } else {
-    head_up.normalize();
-  }
+    head_up = pose.head_pos - pose.neck_base;
+    if (head_up.lengthSquared() < 1e-8F) {
+      head_up = up_axis;
+    } else {
+      head_up.normalize();
+    }
 
-  QVector3D head_right =
-      right_axis - head_up * QVector3D::dotProduct(right_axis, head_up);
-  if (head_right.lengthSquared() < 1e-8F) {
-    head_right = QVector3D::crossProduct(head_up, forward_axis);
+    head_right =
+        right_axis - head_up * QVector3D::dotProduct(right_axis, head_up);
     if (head_right.lengthSquared() < 1e-8F) {
-      head_right = QVector3D(1.0F, 0.0F, 0.0F);
+      head_right = QVector3D::crossProduct(head_up, forward_axis);
+      if (head_right.lengthSquared() < 1e-8F) {
+        head_right = QVector3D(1.0F, 0.0F, 0.0F);
+      }
     }
-  }
-  head_right.normalize();
+    head_right.normalize();
 
-  if (QVector3D::dotProduct(head_right, right_axis) < 0.0F) {
-    head_right = -head_right;
-  }
+    if (QVector3D::dotProduct(head_right, right_axis) < 0.0F) {
+      head_right = -head_right;
+    }
 
-  QVector3D head_forward = QVector3D::crossProduct(head_right, head_up);
-  if (head_forward.lengthSquared() < 1e-8F) {
-    head_forward = forward_axis;
-  } else {
-    head_forward.normalize();
-  }
+    head_forward = QVector3D::crossProduct(head_right, head_up);
+    if (head_forward.lengthSquared() < 1e-8F) {
+      head_forward = forward_axis;
+    } else {
+      head_forward.normalize();
+    }
 
-  if (QVector3D::dotProduct(head_forward, forward_axis) < 0.0F) {
-    head_right = -head_right;
-    head_forward = -head_forward;
+    if (QVector3D::dotProduct(head_forward, forward_axis) < 0.0F) {
+      head_right = -head_right;
+      head_forward = -head_forward;
+    }
   }
 
   QVector3D const chin_pos = pose.head_pos - head_up * head_r;
@@ -406,6 +416,7 @@ void HumanoidRendererBase::drawCommonBody(const DrawContext &ctx,
   head_transform = head_transform * head_rot;
   head_transform.scale(head_r);
   head_transform.scale(width_scale, 1.0F, depth_scale);
+
   out.mesh(getUnitSphere(), head_transform, v.palette.skin, nullptr, 1.0F);
 
   pose.head_frame.origin = pose.head_pos;
@@ -987,6 +998,15 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
       formation.max_per_row;
   const int cols = formation.max_per_row;
 
+  bool is_mounted_spawn = false;
+  if (unit_comp != nullptr) {
+    using Game::Units::SpawnType;
+    auto const st = unit_comp->spawn_type;
+    is_mounted_spawn =
+        (st == SpawnType::MountedKnight || st == SpawnType::HorseArcher ||
+         st == SpawnType::HorseSpearman);
+  }
+
   int visible_count = rows * cols;
   if (unit_comp != nullptr) {
     int const mh = std::max(1, unit_comp->max_health);
@@ -1030,19 +1050,21 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
 
     offset_x += pos_jitter_x;
     offset_z += pos_jitter_z;
+    float applied_vertical_jitter = vertical_jitter;
+    float applied_yaw_offset = yaw_offset;
 
     QMatrix4x4 inst_model;
     float applied_yaw = yaw_offset;
 
     if (transform_comp != nullptr) {
-      applied_yaw = transform_comp->rotation.y + yaw_offset;
+      applied_yaw = transform_comp->rotation.y + applied_yaw_offset;
       QMatrix4x4 m = k_identity_matrix;
       m.translate(transform_comp->position.x, transform_comp->position.y,
                   transform_comp->position.z);
       m.rotate(applied_yaw, 0.0F, 1.0F, 0.0F);
       m.scale(transform_comp->scale.x, transform_comp->scale.y,
               transform_comp->scale.z);
-      m.translate(offset_x, vertical_jitter, offset_z);
+      m.translate(offset_x, applied_vertical_jitter, offset_z);
       if (entity_ground_offset != 0.0F) {
         m.translate(0.0F, -entity_ground_offset, 0.0F);
       }
@@ -1050,7 +1072,7 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
     } else {
       inst_model = ctx.model;
       inst_model.rotate(applied_yaw, 0.0F, 1.0F, 0.0F);
-      inst_model.translate(offset_x, vertical_jitter, offset_z);
+      inst_model.translate(offset_x, applied_vertical_jitter, offset_z);
       if (entity_ground_offset != 0.0F) {
         inst_model.translate(0.0F, -entity_ground_offset, 0.0F);
       }
@@ -1180,6 +1202,35 @@ void HumanoidRendererBase::render(const DrawContext &ctx,
       pose.head_pos.setZ(pose.head_pos.z() + 0.04F);
       pose.shoulder_l.setY(pose.shoulder_l.y() - 0.02F);
       pose.shoulder_r.setY(pose.shoulder_r.y() - 0.02F);
+
+      if (pose.head_frame.radius > 0.001F) {
+        QVector3D head_up = pose.head_pos - pose.neck_base;
+        if (head_up.lengthSquared() < 1e-8F) {
+          head_up = pose.head_frame.up;
+        } else {
+          head_up.normalize();
+        }
+
+        QVector3D head_right =
+            pose.head_frame.right -
+            head_up * QVector3D::dotProduct(pose.head_frame.right, head_up);
+        if (head_right.lengthSquared() < 1e-8F) {
+          head_right =
+              QVector3D::crossProduct(head_up, anim_ctx.entity_forward);
+          if (head_right.lengthSquared() < 1e-8F) {
+            head_right = QVector3D(1.0F, 0.0F, 0.0F);
+          }
+        }
+        head_right.normalize();
+        QVector3D head_forward =
+            QVector3D::crossProduct(head_right, head_up).normalized();
+
+        pose.head_frame.origin = pose.head_pos;
+        pose.head_frame.up = head_up;
+        pose.head_frame.right = head_right;
+        pose.head_frame.forward = head_forward;
+        pose.body_frames.head = pose.head_frame;
+      }
     }
 
     drawCommonBody(inst_ctx, variant, pose, out);