浏览代码

Merge pull request #219 from djeada/copilot/improve-archer-animation

Improve Archer Shooting Animation — Realistic Posture, Draw Tension, and Release Polish
Adam Djellouli 2 月之前
父节点
当前提交
34f9b34486
共有 3 个文件被更改,包括 66 次插入400 次删除
  1. 0 193
      docs/archer_cloth_simulation.md
  2. 0 184
      docs/walking_animation_improvements.md
  3. 66 23
      render/entity/archer_renderer.cpp

+ 0 - 193
docs/archer_cloth_simulation.md

@@ -1,193 +0,0 @@
-# Archer Cloth Simulation
-
-## Overview
-
-The archer mesh has been enhanced with a dynamic tunic skirt that simulates cloth physics in real-time using a procedural rendering approach. This implementation provides believable secondary motion without requiring pre-baked animations or complex GPU compute shaders.
-
-## Architecture
-
-### Procedural Approach
-
-Unlike traditional mesh-based cloth simulation, the archer uses a **procedural rendering system** where geometry is generated at runtime from primitive shapes (cylinders, spheres, cones). The tunic skirt is procedurally generated with the following components:
-
-- **Segments**: 12 radial segments around the waist for smooth appearance
-- **Structure**: Each segment consists of vertical strands from waist to hem, with horizontal connections
-- **Double-layered**: Outer layer for main cloth, inner layer for thickness/depth
-
-### Physics Simulation
-
-The cloth simulation is entirely CPU-based and stateless, calculated per-frame based on:
-
-1. **Animation Time**: Drives periodic motion
-2. **Character Movement**: Influences cloth sway direction
-3. **Wind Effects**: Adds environmental motion
-4. **Collision Detection**: Prevents clipping with legs
-
-## Implementation Details
-
-### Key Parameters
-
-```cpp
-// Geometry
-const float skirtLength = 0.25f;           // Length from waist to hem
-const float waistRadius = HP::TORSO_BOT_R * 1.05f;  // Waist circumference
-const float hemRadius = HP::TORSO_BOT_R * 1.35f;    // Hem circumference (flared)
-const int segments = 12;                   // Radial segments
-
-// Physics
-const float windStrength = 0.18f;          // Wind influence magnitude
-const float swayAmount = 0.05f;            // Maximum sway at hem
-const float stiffness = 4.0f;              // Spring stiffness
-const float damping = 0.4f;                // Damping coefficient
-
-// Collision
-const float legPushRadius = HP::UPPER_LEG_R * 1.6f;  // Leg avoidance radius
-```
-
-### Motion Calculation
-
-#### 1. Wind and Character Velocity
-```cpp
-// Simulated wind direction (varies over time)
-float windX = sin(windPhase) * windStrength;
-float windZ = cos(windPhase * 0.7f) * windStrength;
-
-// Character movement influence (when walking)
-float characterVelX = isMoving ? sin(animTime * 3.0f) * 0.5f : 0.0f;
-float characterVelZ = isMoving ? cos(animTime * 3.0f) * 0.5f : 0.0f;
-
-// Combined swing direction (wind + opposite of movement)
-float swingX = windX - characterVelX * 0.8f;
-float swingZ = windZ - characterVelZ * 0.8f;
-```
-
-#### 2. Spring Physics (Damped Oscillation)
-```cpp
-// Per-vertex random phase offset for variation
-float randomSeed = hash01(seed ^ (i * 7919u));
-float phase = animTime * stiffness + randomSeed * 6.28f;
-
-// Damped sinusoidal response
-float lag = exp(-damping) * sin(phase);
-
-// Additional flutter for detail
-float flutter = sin(animTime * 10.0f + randomSeed * 6.28f) * 0.02f;
-
-// Final offset
-float offsetX = (swingX * swayAmount + flutter) * lag;
-float offsetZ = (swingZ * swayAmount + flutter) * lag;
-```
-
-#### 3. Collision Avoidance
-```cpp
-// Check distance to each foot position
-float distToLeg = sqrt(pow(hem.x - foot.x, 2) + pow(hem.z - foot.z, 2));
-
-// If penetrating, push out radially
-if (distToLeg < legPushRadius) {
-    QVector3D pushDir = (hem - footPos).normalized();
-    hem += pushDir * (legPushRadius - distToLeg);
-}
-```
-
-### Visual Enhancement
-
-#### Fabric Shading (basic.frag)
-
-The shader includes special handling for cloth materials (bright colors):
-
-```glsl
-// Fabric weave pattern
-float weaveX = sin(worldPos.x * 50.0);
-float weaveZ = sin(worldPos.z * 50.0);
-float weavePattern = weaveX * weaveZ * 0.02;
-
-// Subtle noise variation
-float clothNoise = noise(uv * 2.0) * 0.08 - 0.04;
-
-// View-dependent sheen (highlights on edges)
-float viewAngle = abs(dot(normal, viewDir));
-float sheen = pow(1.0 - viewAngle, 3.0) * 0.12;
-
-// Final color
-variation = baseColor * (1.0 + clothNoise + weavePattern) + vec3(sheen);
-```
-
-#### Wrap Diffuse Lighting
-
-Softer lighting terminator for cloth appearance:
-
-```glsl
-float wrapAmount = avgColor > 0.65 ? 0.5 : 0.0;  // Only for bright/cloth colors
-float nDotL = dot(normal, lightDir);
-float diff = max(nDotL * (1.0 - wrapAmount) + wrapAmount, 0.2);
-```
-
-#### Thickness Rendering
-
-Inner layer provides depth perception:
-
-```cpp
-// Inner layer offset inward with slight vertical adjustment
-QVector3D innerPoint = outerPoint - (radialDir * 0.01f) + QVector3D(0, 0.002f, 0);
-
-// Render with darker color and slight transparency
-out.mesh(..., C.tunic * 0.78f, nullptr, 0.85f);
-```
-
-## Performance Considerations
-
-- **CPU-Based**: All calculations done on CPU per frame
-- **Per-Instance**: Each archer individual has independent cloth simulation
-- **Segment Count**: 12 segments provide good quality without excessive geometry
-- **No State**: Stateless simulation requires no memory buffers or GPU compute
-
-### Optimization Opportunities
-
-If performance becomes an issue:
-
-1. **Reduce Segments**: Lower from 12 to 8 for less draw calls
-2. **LOD System**: Simplify or remove cloth for distant archers
-3. **Update Rate**: Update cloth at lower frequency than render rate
-4. **Instancing**: Share cloth simulation across similar movement states
-
-## Usage
-
-The cloth simulation is automatically applied to all archer units. No external configuration is needed.
-
-### Tweaking Parameters
-
-To adjust cloth behavior, modify constants in `archer_renderer.cpp`:
-
-- **More Flutter**: Increase flutter amplitude or frequency
-- **Stiffer Cloth**: Increase `stiffness` value
-- **More Damping**: Increase `damping` for less oscillation
-- **Longer Skirt**: Increase `skirtLength`
-- **Wider Hem**: Increase `hemRadius` multiplier
-
-## Future Enhancements
-
-Potential improvements for more advanced cloth simulation:
-
-1. **GPU Compute Shader**: Move simulation to GPU for better performance
-2. **State Buffers**: Track velocity/position history for true inertia
-3. **More Collision Primitives**: Add capsule collision for thighs
-4. **Vertex Attributes**: If switching to mesh-based rendering
-5. **Wind Zones**: Spatial wind variation across the map
-6. **Material Variants**: Different cloth stiffness per unit type
-
-## Related Files
-
-- `render/entity/archer_renderer.cpp` - Main cloth rendering implementation
-- `assets/shaders/basic.frag` - Fabric material shading
-- `render/geom/transforms.h` - Geometry utility functions
-
-## References
-
-The implementation is inspired by the issue requirements but adapted for the procedural rendering architecture:
-
-- Spring physics simulation (stateless per-vertex)
-- Wind and character velocity influence
-- Basic collision avoidance
-- Fabric material appearance (sheen, wrap diffuse)
-- Thickness rendering via inner layer

+ 0 - 184
docs/walking_animation_improvements.md

@@ -1,184 +0,0 @@
-# Walking Animation Improvements
-
-## Overview
-This document describes the improvements made to the walking animation system to address issues with unnatural posture and movement. The changes primarily affect the archer unit renderer but apply to all humanoid characters.
-
-## Problem Statement
-The original walking animation exhibited several issues:
-- Units appeared to move in a semi-squat position with permanently bent legs
-- Torso had a slight forward hunch, creating a "sneaking" appearance
-- Legs did not straighten during the stride phase
-- Limited hip rotation and weight transfer simulation
-- Overall lack of natural walking fluidity
-
-## Changes Made
-
-### 1. Upright Torso Posture
-**File:** `render/entity/archer_renderer.cpp` (lines 88-89)
-
-**Before:**
-```cpp
-QVector3D shoulderL{-P::TORSO_TOP_R * 0.98f, P::SHOULDER_Y, P::TORSO_TOP_R * 0.05f};
-QVector3D shoulderR{P::TORSO_TOP_R * 0.98f, P::SHOULDER_Y, P::TORSO_TOP_R * 0.05f};
-```
-
-**After:**
-```cpp
-QVector3D shoulderL{-P::TORSO_TOP_R * 0.98f, P::SHOULDER_Y, 0.0f};
-QVector3D shoulderR{P::TORSO_TOP_R * 0.98f, P::SHOULDER_Y, 0.0f};
-```
-
-**Impact:** Removed the forward z-offset (0.05f) from shoulder positions, eliminating the hunched-forward posture. Units now stand upright with vertical torso alignment.
-
-### 2. Neutral Foot Stance
-**File:** `render/entity/archer_renderer.cpp` (lines 96-97)
-
-**Before:**
-```cpp
-QVector3D footL{-P::SHOULDER_WIDTH * 0.58f, P::GROUND_Y + footYOffset, 0.22f};
-QVector3D footR{P::SHOULDER_WIDTH * 0.58f, P::GROUND_Y + footYOffset, -0.18f};
-```
-
-**After:**
-```cpp
-QVector3D footL{-P::SHOULDER_WIDTH * 0.58f, P::GROUND_Y + footYOffset, 0.0f};
-QVector3D footR{P::SHOULDER_WIDTH * 0.58f, P::GROUND_Y + footYOffset, 0.0f};
-```
-
-**Impact:** Changed the default foot placement from a staggered forward-back stance to a side-by-side neutral position. This provides a better starting point for the walking animation and reduces the appearance of constant crouching.
-
-### 3. Reduced Knee Bend
-**File:** `render/entity/archer_renderer.cpp` (lines 511-512)
-
-**Before:**
-```cpp
-const float kneeForward = 0.30f;
-const float kneeDrop = 0.05f * HP::LOWER_LEG_LEN;
-```
-
-**After:**
-```cpp
-const float kneeForward = 0.15f;
-const float kneeDrop = 0.02f * HP::LOWER_LEG_LEN;
-```
-
-**Impact:** 
-- Reduced `kneeForward` by 50% (from 0.30 to 0.15): Knees bend forward less aggressively, allowing for straighter leg extension
-- Reduced `kneeDrop` by 60% (from 0.05 to 0.02): Knees drop less vertically, preventing the semi-squat appearance
-
-These changes allow legs to extend more naturally during the stride phase while maintaining realistic knee articulation during the swing phase.
-
-### 4. Improved Stride Mechanics
-**File:** `render/entity/archer_renderer.cpp` (lines 193-215)
-
-**Before:**
-```cpp
-const float footYOffset = P.footYOffset;
-const float groundY = HP::GROUND_Y;
-
-auto animateFoot = [groundY, footYOffset](QVector3D &foot, float phase) {
-  float lift = std::sin(phase * 2.0f * 3.14159f);
-  if (lift > 0.0f) {
-    foot.setY(groundY + footYOffset + lift * 0.15f);
-  }
-  foot.setZ(foot.z() + std::sin((phase - 0.25f) * 2.0f * 3.14159f) * 0.20f);
-};
-```
-
-**After:**
-```cpp
-const float footYOffset = P.footYOffset;
-const float groundY = HP::GROUND_Y;
-const float strideLength = 0.35f;
-
-auto animateFoot = [groundY, footYOffset, strideLength](QVector3D &foot, float phase) {
-  float lift = std::sin(phase * 2.0f * 3.14159f);
-  if (lift > 0.0f) {
-    foot.setY(groundY + footYOffset + lift * 0.12f);
-  } else {
-    foot.setY(groundY + footYOffset);
-  }
-  foot.setZ(foot.z() + std::sin((phase - 0.25f) * 2.0f * 3.14159f) * strideLength);
-};
-```
-
-**Impact:**
-- Increased stride length from 0.20 to 0.35 (75% increase): Provides more natural, confident stride
-- Reduced foot lift from 0.15 to 0.12 (20% reduction): Creates a more natural, less exaggerated step
-- Added explicit ground contact: Feet now properly contact the ground (else clause) during stance phase
-
-### 5. Hip Sway for Weight Transfer
-**File:** `render/entity/archer_renderer.cpp` (lines 212-214)
-
-**New addition:**
-```cpp
-float hipSway = std::sin(walkPhase * 2.0f * 3.14159f) * 0.02f;
-P.shoulderL.setX(P.shoulderL.x() + hipSway);
-P.shoulderR.setX(P.shoulderR.x() + hipSway);
-```
-
-**Impact:** Added subtle lateral shoulder/torso movement synchronized with the walk cycle. This simulates the natural weight transfer that occurs during walking, where the torso shifts slightly toward the weight-bearing leg. The amplitude (0.02) is intentionally small to maintain realism.
-
-## Technical Details
-
-### Animation Cycle
-The walk cycle operates on a 0.8-second period (`walkCycleTime`):
-- Left and right legs are 180° out of phase (0.5 cycle offset)
-- Each foot goes through: lift → swing forward → plant → stance → repeat
-- Hip sway is synchronized with the overall walk cycle
-
-### Biomechanical Improvements
-1. **Leg Extension**: Legs now straighten during mid-stance phase when body weight passes over the supporting foot
-2. **Heel Strike**: Proper ground contact ensures no floating or sliding
-3. **Weight Distribution**: Hip sway indicates natural center of mass movement
-4. **Posture**: Upright torso with shoulders level and vertical spine alignment
-
-### Performance Impact
-All changes are computational adjustments to existing parameters with no additional rendering overhead. The animation remains as performant as the original implementation.
-
-## Testing and Validation
-
-### Build Verification
-- Code compiles cleanly with no warnings
-- Formatting adheres to project standards (clang-format)
-- No regression in existing functionality
-
-### Visual Validation
-The improved animation should exhibit:
-- Upright posture with vertical torso
-- Natural leg extension during stride
-- Smooth weight transfer with subtle hip movement
-- No crouching or squatting appearance
-- Proper foot-ground contact without sliding
-
-### Animation Blending
-The changes maintain compatibility with:
-- Idle → Walk transitions
-- Walk → Run transitions  
-- Walk → Attack stance transitions
-- Formation movement
-
-## Future Enhancements
-
-Potential future improvements could include:
-1. **Arm Swing**: Natural arm swing counter to leg movement
-2. **Head Bob**: Subtle vertical head movement during walk
-3. **Speed Variation**: Different animation parameters for different movement speeds
-4. **Terrain Response**: Slight adjustments for uphill/downhill movement
-5. **Fatigue System**: Gradual posture degradation during extended movement
-
-## References
-
-### Related Files
-- `render/entity/archer_renderer.cpp` - Main implementation
-- `game/core/component.h` - Movement component definitions
-- `game/systems/movement_system.cpp` - Movement velocity and pathing
-
-### Human Proportions
-The animation uses realistic human proportions defined in `HumanProportions` struct:
-- Total height: 1.80m
-- Upper leg length: 0.35m
-- Lower leg length: 0.35m
-- Ground level: 0.0m
-
-These proportions ensure the animation scales correctly with unit size.

+ 66 - 23
render/entity/archer_renderer.cpp

@@ -168,19 +168,48 @@ static inline ArcherPose makePose(uint32_t seed, float animTime, bool isMoving,
       }
     } else {
 
-      QVector3D restPos(0.15f, HP::SHOULDER_Y + 0.15f, 0.20f);
-      QVector3D drawPos(0.35f, HP::SHOULDER_Y + 0.08f, -0.15f);
+      QVector3D aimPos(0.18f, HP::SHOULDER_Y + 0.18f, 0.35f);
+      QVector3D drawPos(0.22f, HP::SHOULDER_Y + 0.10f, -0.30f);
+      QVector3D releasePos(0.18f, HP::SHOULDER_Y + 0.20f, 0.10f);
 
-      if (attackPhase < 0.3f) {
-        float t = attackPhase / 0.3f;
+      if (attackPhase < 0.20f) {
+        float t = attackPhase / 0.20f;
         t = t * t;
-        P.handR = restPos * (1.0f - t) + drawPos * t;
-      } else if (attackPhase < 0.6f) {
+        P.handR = aimPos * (1.0f - t) + drawPos * t;
+        P.handL = QVector3D(P.bowX - 0.05f, HP::SHOULDER_Y + 0.05f, 0.55f);
+
+        float shoulderTwist = t * 0.08f;
+        P.shoulderR.setY(P.shoulderR.y() + shoulderTwist);
+        P.shoulderL.setY(P.shoulderL.y() - shoulderTwist * 0.5f);
+      } else if (attackPhase < 0.50f) {
         P.handR = drawPos;
+        P.handL = QVector3D(P.bowX - 0.05f, HP::SHOULDER_Y + 0.05f, 0.55f);
+
+        float shoulderTwist = 0.08f;
+        P.shoulderR.setY(P.shoulderR.y() + shoulderTwist);
+        P.shoulderL.setY(P.shoulderL.y() - shoulderTwist * 0.5f);
+      } else if (attackPhase < 0.58f) {
+        float t = (attackPhase - 0.50f) / 0.08f;
+        t = t * t * t;
+        P.handR = drawPos * (1.0f - t) + releasePos * t;
+        P.handL = QVector3D(P.bowX - 0.05f, HP::SHOULDER_Y + 0.05f, 0.55f);
+
+        float shoulderTwist = 0.08f * (1.0f - t * 0.6f);
+        P.shoulderR.setY(P.shoulderR.y() + shoulderTwist);
+        P.shoulderL.setY(P.shoulderL.y() - shoulderTwist * 0.5f);
+
+        P.headPos.setZ(P.headPos.z() - t * 0.04f);
       } else {
-        float t = (attackPhase - 0.6f) / 0.4f;
+        float t = (attackPhase - 0.58f) / 0.42f;
         t = 1.0f - (1.0f - t) * (1.0f - t);
-        P.handR = drawPos * (1.0f - t) + restPos * t;
+        P.handR = releasePos * (1.0f - t) + aimPos * t;
+        P.handL = QVector3D(P.bowX - 0.05f, HP::SHOULDER_Y + 0.05f, 0.55f);
+
+        float shoulderTwist = 0.08f * 0.4f * (1.0f - t);
+        P.shoulderR.setY(P.shoulderR.y() + shoulderTwist);
+        P.shoulderL.setY(P.shoulderL.y() - shoulderTwist * 0.5f);
+
+        P.headPos.setZ(P.headPos.z() - 0.04f * (1.0f - t));
       }
     }
   }
@@ -677,7 +706,8 @@ static inline void drawQuiver(const DrawContext &p, ISubmitter &out,
 }
 
 static inline void drawBowAndArrow(const DrawContext &p, ISubmitter &out,
-                                   const ArcherPose &P, const ArcherColors &C) {
+                                   const ArcherPose &P, const ArcherColors &C,
+                                   bool isAttacking, float attackPhase) {
   const QVector3D up(0.0f, 1.0f, 0.0f);
   const QVector3D forward(0.0f, 0.0f, 1.0f);
 
@@ -716,19 +746,24 @@ static inline void drawBowAndArrow(const DrawContext &p, ISubmitter &out,
   out.mesh(getUnitCylinder(), cylinderBetween(p.model, P.handR, nock, 0.0045f),
            C.stringCol * 0.9f, nullptr, 1.0f);
 
-  QVector3D tail = nock - forward * 0.06f;
-  QVector3D tip = tail + forward * 0.90f;
-  out.mesh(getUnitCylinder(), cylinderBetween(p.model, tail, tip, 0.018f),
-           C.wood, nullptr, 1.0f);
-  QVector3D headBase = tip - forward * 0.10f;
-  out.mesh(getUnitCone(), coneFromTo(p.model, headBase, tip, 0.05f),
-           C.metalHead, nullptr, 1.0f);
-  QVector3D f1b = tail - forward * 0.02f, f1a = f1b - forward * 0.06f;
-  QVector3D f2b = tail + forward * 0.02f, f2a = f2b + forward * 0.06f;
-  out.mesh(getUnitCone(), coneFromTo(p.model, f1b, f1a, 0.04f), C.fletch,
-           nullptr, 1.0f);
-  out.mesh(getUnitCone(), coneFromTo(p.model, f2a, f2b, 0.04f), C.fletch,
-           nullptr, 1.0f);
+  bool showArrow = !isAttacking ||
+                   (isAttacking && attackPhase >= 0.0f && attackPhase < 0.52f);
+
+  if (showArrow) {
+    QVector3D tail = nock - forward * 0.06f;
+    QVector3D tip = tail + forward * 0.90f;
+    out.mesh(getUnitCylinder(), cylinderBetween(p.model, tail, tip, 0.018f),
+             C.wood, nullptr, 1.0f);
+    QVector3D headBase = tip - forward * 0.10f;
+    out.mesh(getUnitCone(), coneFromTo(p.model, headBase, tip, 0.05f),
+             C.metalHead, nullptr, 1.0f);
+    QVector3D f1b = tail - forward * 0.02f, f1a = f1b - forward * 0.06f;
+    QVector3D f2b = tail + forward * 0.02f, f2a = f2b + forward * 0.06f;
+    out.mesh(getUnitCone(), coneFromTo(p.model, f1b, f1a, 0.04f), C.fletch,
+             nullptr, 1.0f);
+    out.mesh(getUnitCone(), coneFromTo(p.model, f2a, f2b, 0.04f), C.fletch,
+             nullptr, 1.0f);
+  }
 }
 
 static inline void drawSelectionFX(const DrawContext &p, ISubmitter &out) {
@@ -911,6 +946,13 @@ void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {
       ArcherPose pose = makePose(instSeed, p.animationTime + phaseOffset,
                                  isMoving, isAttacking, isMelee);
 
+      float attackPhase = 0.0f;
+      if (isAttacking && !isMelee) {
+        float attackCycleTime = 1.2f;
+        attackPhase = fmod(
+            (p.animationTime + phaseOffset) * (1.0f / attackCycleTime), 1.0f);
+      }
+
       drawQuiver(instCtx, out, colors, pose, instSeed);
       drawLegs(instCtx, out, pose, colors);
       drawTorso(instCtx, out, colors, pose);
@@ -918,7 +960,8 @@ void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {
                      isMoving, instSeed);
       drawArms(instCtx, out, pose, colors);
       drawHeadAndNeck(instCtx, out, pose, colors);
-      drawBowAndArrow(instCtx, out, pose, colors);
+      drawBowAndArrow(instCtx, out, pose, colors, isAttacking && !isMelee,
+                      attackPhase);
     }
 
     drawSelectionFX(p, out);