# 动画检测集成方案 ## 一、现状分析 ### 1.1 当前动画系统架构 #### 核心组件 - **PathAnimationManager**: 动画管理器,负责动画帧预计算和播放 - **AnimationControlViewModel**: 动画控制视图模型,处理UI交互 - **PathCurveEngine**: 路径曲线化引擎,已实现圆弧过渡功能 #### 当前路径插值机制 ```csharp // PathAnimationManager.cs:1253 private Point3D InterpolatePosition(double progress) ``` - 基于原始控制点(_pathPoints)进行线性插值 - 按路径段距离比例计算位置 - **问题**: 未使用PathRoute.Edges,动画沿直线段运动 #### 当前朝向计算机制 ```csharp // PathAnimationManager.cs:600 private double ComputeYawFromPath(int frameIndex, List framePositions) ``` - 基于前后帧位置计算方向向量 - 使用atan2计算偏航角 - **问题**: 在直线段上方向准确,但在圆弧处需要改进 ### 1.2 路径数据结构 #### PathRoute模型 ```csharp // PathPlanningModels.cs public class PathRoute { public List Points { get; set; } // 控制点 public List Edges { get; set; } // 物理路径段 public bool IsCurved { get; set; } // 是否已曲线化 public double TurnRadius { get; set; } // 转向半径 } ``` #### PathEdge类型 ```csharp public enum PathSegmentType { Straight, // 直线段 Arc // 圆弧段 } public class PathEdge { public PathSegmentType SegmentType { get; set; } public ArcTrajectory Trajectory { get; set; } // 圆弧轨迹数据 public List SampledPoints { get; set; } // 采样点序列 public double PhysicalLength { get; set; } // 物理长度 } ``` #### ArcTrajectory数据 ```csharp public class ArcTrajectory { public Point3D Ts { get; set; } // 进入切点 public Point3D Te { get; set; } // 退出切点 public Point3D ArcCenter { get; set; } // 圆心 public double ActualRadius { get; set; } // 实际半径 public double DeflectionAngle { get; set; } // 偏转角(弧度) public double ArcLength { get; set; } // 圆弧长度 } ``` ### 1.3 PathCurveEngine已实现功能 - **CalculateFillet**: 计算圆弧切点和轨迹参数 - **SampleArc**: 圆弧采样为离散点序列 - **ApplyCurvatureToRoute**: 对路径应用曲线化处理 - **GenerateSampledPoints**: 生成采样点序列 ## 二、集成方案设计 ### 2.1 核心目标 1. **动画帧预计算使用路径**: 替代原有的直线插值 2. **圆弧处方向计算准确**: 确保物体在圆弧段正确朝向 3. **性能优化**: 避免重复计算采样点 ### 2.2 数据流设计 ``` PathRoute (控制点) ↓ PathCurveEngine.ApplyCurvatureToRoute() ↓ PathRoute.Edges (包含直线段和圆弧段) ↓ PathAnimationManager.PrecomputeAnimationFrames() ↓ 使用PathEdge.SampledPoints生成动画帧 ↓ 动画播放 ``` ### 2.3 关键修改点 #### 修改点1: PathAnimationManager.SetupAnimation() **当前实现**: ```csharp // PathAnimationManager.cs:407 public void SetupAnimation(ModelItem animatedObject, List pathPoints, double durationSeconds = 10.0) { // 直接使用pathPoints作为路径 _pathPoints = new List(pathPoints); PrecomputeAnimationFrames(); } ``` **修改方案**: ```csharp public void SetupAnimation( ModelItem animatedObject, double durationSeconds, PathRoute route) { if (route == null) throw new ArgumentNullException(nameof(route), "必须提供路径"); _animatedObject = animatedObject; _animationDuration = durationSeconds; _route = route; LogManager.Info($"使用路径,边数:{route.Edges.Count},总长度:{route.TotalLength:F2}米,动画时长:{durationSeconds:F1}秒"); PrecomputeAnimationFrames(); } ``` #### 修改点2: PathAnimationManager.PrecomputeAnimationFrames() **当前实现**: ```csharp // PathAnimationManager.cs:468 private void PrecomputeAnimationFrames() { // 第一遍:收集所有帧位置(使用直线插值) var framePositions = new List(); for (int i = 0; i < totalFrames; i++) { double progress = (double)i / (totalFrames - 1); var framePosition = InterpolatePosition(progress); // 直线插值 framePositions.Add(framePosition); } // 第二遍:计算朝向并预计算每一帧 for (int i = 0; i < totalFrames; i++) { double yawRadians = ComputeYawFromPath(i, framePositions); // ... } } ``` **修改方案**: ```csharp private void PrecomputeAnimationFrames() { LogManager.Info("=== 使用路径预计算动画帧 ==="); int totalFrames = (int)(_animationDuration * _animationFrameRate); // 1. 构建完整的采样点序列(按边顺序拼接) var allSampledPoints = new List(); var edgeLengths = new List(); // 每条边的长度 var edgeStartIndices = new List(); // 每条边在采样序列中的起始索引 foreach (var edge in _route.Edges) { if (edge.SampledPoints == null || edge.SampledPoints.Count == 0) { // 延迟加载采样点 var samplingStep = ConfigManager.Instance.Current.PathEditing.ArcSamplingStep; edge.SampledPoints = PathCurveEngine.GenerateSampledPoints(edge, samplingStep); } edgeStartIndices.Add(allSampledPoints.Count); allSampledPoints.AddRange(edge.SampledPoints); edgeLengths.Add(edge.PhysicalLength); } double totalLength = _route.TotalLength; LogManager.Info($"路径总长度: {totalLength:F2}米, 采样点数: {allSampledPoints.Count}"); // 2. 按帧数采样生成动画帧 _animationFrames = new List(); for (int i = 0; i < totalFrames; i++) { double progress = (double)i / (totalFrames - 1); double targetDistance = totalLength * progress; // 3. 找到当前在哪条边上 int edgeIndex = FindEdgeForDistance(targetDistance, edgeLengths); if (edgeIndex < 0 || edgeIndex >= _route.Edges.Count) { LogManager.Warning($"无法找到边,目标距离:{targetDistance:F2}米"); continue; } var edge = _route.Edges[edgeIndex]; double accumulatedLength = edgeLengths.Take(edgeIndex).Sum(); double edgeProgress = (targetDistance - accumulatedLength) / edge.PhysicalLength; // 4. 在边内插值位置 Point3D framePosition; if (edge.SegmentType == PathSegmentType.Straight) { framePosition = InterpolateOnStraightEdge(edge, edgeProgress); } else // Arc { framePosition = InterpolateOnArcEdge(edge, edgeProgress); } // 5. 计算朝向(关键改进) double yawRadians = ComputeYawOnPath(i, allSampledPoints, edgeIndex, edgeProgress); // 6. 创建帧并检测碰撞 var frame = new AnimationFrame { Index = i, Progress = progress, Position = framePosition, YawRadians = yawRadians, Collisions = new List() }; // ... 碰撞检测逻辑保持不变 ... _animationFrames.Add(frame); } } ``` #### 修改点3: 曲线边内插值方法 ```csharp /// /// 在直线边上插值位置 /// private Point3D InterpolateOnStraightEdge(PathEdge edge, double progress) { if (edge.SampledPoints == null || edge.SampledPoints.Count < 2) return edge.Trajectory?.Ts ?? new Point3D(); int pointIndex = (int)(progress * (edge.SampledPoints.Count - 1)); pointIndex = Math.Max(0, Math.Min(pointIndex, edge.SampledPoints.Count - 1)); return edge.SampledPoints[pointIndex]; } /// /// 在圆弧边上插值位置 /// private Point3D InterpolateOnArcEdge(PathEdge edge, double progress) { if (edge.Trajectory == null || edge.SampledPoints == null || edge.SampledPoints.Count < 2) return edge.Trajectory?.Ts ?? new Point3D(); // 使用采样点插值(更精确) int pointIndex = (int)(progress * (edge.SampledPoints.Count - 1)); pointIndex = Math.Max(0, Math.Min(pointIndex, edge.SampledPoints.Count - 1)); return edge.SampledPoints[pointIndex]; } /// /// 查找指定距离对应的边索引 /// private int FindEdgeForDistance(double targetDistance, List edgeLengths) { double accumulatedLength = 0.0; for (int i = 0; i < edgeLengths.Count; i++) { if (accumulatedLength + edgeLengths[i] >= targetDistance) { return i; } accumulatedLength += edgeLengths[i]; } return edgeLengths.Count - 1; // 返回最后一条边 } ``` #### 修改点4: 路径朝向计算(核心) ```csharp /// /// 计算路径上的朝向(改进版) /// private double ComputeYawOnPath( int frameIndex, List allSampledPoints, int currentEdgeIndex, double edgeProgress) { Point3D currentPos = allSampledPoints.Count > 0 ? allSampledPoints[Math.Min(frameIndex, allSampledPoints.Count - 1)] : new Point3D(); Point3D nextPos; var edge = _route.Edges[currentEdgeIndex]; if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null) { // 圆弧段: 计算切线方向 // 使用圆弧参数方程计算精确方向 // 1. 获取当前点在圆弧上的角度 Vector3D startVec = (edge.Trajectory.Ts - edge.Trajectory.ArcCenter).Normalize(); double signedAngle = edge.Trajectory.DeflectionAngle; // 总偏转角 double currentAngle = edgeProgress * signedAngle; // 2. 旋转起始向量得到当前方向向量 Point3D rotatedPoint = RotatePointAroundAxis( edge.Trajectory.Ts, edge.Trajectory.ArcCenter, GetArcRotationAxis(edge.Trajectory), currentAngle ); // 3. 计算切线方向(沿圆弧切线) Vector3D radiusVec = (rotatedPoint - edge.Trajectory.ArcCenter).Normalize(); Vector3D tangentVec = Vector3D.CrossProduct( GetArcRotationAxis(edge.Trajectory), radiusVec ); // 4. 使用atan2计算yaw return Math.Atan2(tangentVec.Y, tangentVec.X); } else { // 直线段: 查找下一个采样点 int nextIndex = Math.Min(frameIndex + 1, allSampledPoints.Count - 1); nextPos = allSampledPoints[nextIndex]; double dx = nextPos.X - currentPos.X; double dy = nextPos.Y - currentPos.Y; double length = Math.Sqrt(dx * dx + dy * dy); if (length < 1e-6) { // 如果距离太小,尝试查找更远的点 for (int offset = 2; offset < 10 && frameIndex + offset < allSampledPoints.Count; offset++) { nextPos = allSampledPoints[frameIndex + offset]; dx = nextPos.X - currentPos.X; dy = nextPos.Y - currentPos.Y; length = Math.Sqrt(dx * dx + dy * dy); if (length > 1e-6) break; } } return length > 1e-6 ? Math.Atan2(dy, dx) : 0.0; } } /// /// 获取圆弧旋转轴 /// private Vector3D GetArcRotationAxis(ArcTrajectory trajectory) { Vector3D startVec = (trajectory.Ts - trajectory.ArcCenter).Normalize(); Vector3D endVec = (trajectory.Te - trajectory.ArcCenter).Normalize(); Point3D cross = GeometryHelper.CrossProduct( new Point3D(startVec.X, startVec.Y, startVec.Z), new Point3D(endVec.X, endVec.Y, endVec.Z) ); return new Vector3D(cross.X, cross.Y, cross.Z).Normalize(); } ``` ### 2.4 UI集成修改 #### 修改点2: AnimationControlViewModel.ExecuteGenerateAnimation() **当前实现**: ```csharp // AnimationControlViewModel.cs:815 private void ExecuteGenerateAnimation() { // 获取当前路径的控制点 var pathPoints = CurrentPathRoute.Points.Select(p => p.Position).ToList(); // 调用SetupAnimation _pathAnimationManager.SetupAnimation( _animatedObject, pathPoints, _animationDuration ); } ``` **修改方案**: ```csharp private void ExecuteGenerateAnimation() { if (CurrentPathRoute == null) throw new InvalidOperationException("未选择路径"); LogManager.Info($"使用路径生成动画,边数:{CurrentPathRoute.Edges.Count},总长度:{CurrentPathRoute.TotalLength:F2}米"); // 调用SetupAnimation,传入路径和用户配置的动画时长 _pathAnimationManager.SetupAnimation( _animatedObject, _animationDuration, CurrentPathRoute ); SetState(AnimationState.Ready); UpdateMainStatus($"动画已生成,时长:{_animationDuration}秒,帧率:{_animationFrameRate}fps"); } ``` ### 2.4 新增字段 在PathAnimationManager中添加: ```csharp // 路径相关 private PathRoute _route = null; // 路径引用 ``` ## 三、实现步骤 ### 阶段1: 数据结构准备 1. ✅ PathRoute、PathEdge、ArcTrajectory已存在 2. ✅ PathCurveEngine已实现曲线化算法 3. ⏳ 在PathAnimationManager中添加_route字段 ### 阶段2: 核心算法实现 1. ⏳ 修改SetupAnimation()方法,必需传入route参数 2. ⏳ 重写PrecomputeAnimationFrames()方法,使用路径 3. ⏳ 实现曲线边插值方法(InterpolateOnStraightEdge, InterpolateOnArcEdge) 4. ⏳ 实现FindEdgeForDistance()方法 5. ⏳ 实现ComputeYawOnPath()方法(核心) 6. ⏳ 实现GetArcRotationAxis()辅助方法 ### 阶段3: UI集成 1. ⏳ 修改AnimationControlViewModel.ExecuteGenerateAnimation() 2. ⏳ 添加路径验证和边数据生成 3. ⏳ 更新日志输出 ### 阶段4: 测试验证 1. ⏳ 单元测试:曲线边插值准确性 2. ⏳ 单元测试:圆弧朝向计算准确性 3. ⏳ 集成测试:完整动画播放流程 4. ⏳ 性能测试:预计算时间 ## 四、关键技术点 ### 4.1 圆弧朝向计算原理 #### 方法1: 采样点差分法(简单但精度较低) ```csharp // 使用前后采样点计算方向 Point3D prevPoint = allSampledPoints[Math.Max(0, frameIndex - 1)]; Point3D nextPoint = allSampledPoints[Math.Min(frameIndex + 1, allSampledPoints.Count - 1)]; Vector3D direction = (nextPoint - prevPoint).Normalize(); double yaw = Math.Atan2(direction.Y, direction.X); ``` #### 方法2: 圆弧切线法(精确) ```csharp // 计算圆弧切线方向 Vector3D radiusVec = (currentPoint - arcCenter).Normalize(); Vector3D rotationAxis = GetArcRotationAxis(trajectory); Vector3D tangentVec = Vector3D.CrossProduct(rotationAxis, radiusVec); double yaw = Math.Atan2(tangentVec.Y, tangentVec.X); ``` **推荐使用方法2**,因为: 1. 数学上精确,不依赖采样密度 2. 在圆弧段上方向连续平滑 3. 性能更好,无需查找相邻采样点 ### 4.2 采样点密度控制 ```toml # default_config.toml [path_editing] # 圆弧采样步长(米) - 推荐值:0.02-0.1 arc_sampling_step = 0.05 ``` - 步长越小:动画越平滑,但内存占用越大 - 步长越大:内存占用小,但可能不够平滑 - **推荐值**:0.05米(平衡精度和性能) ### 4.3 边界情况处理 1. **单边路径**:只有一条直线边 2. **全圆弧路径**:所有边都是圆弧 3. **混合路径**:直线段和圆弧段交替 4. **零长度边**:跳过或合并 5. **采样点不足**:自动补充或调整步长 ### 4.4 性能优化 1. **采样点缓存**:PathEdge.SampledPoints只生成一次 2. **边长度缓存**:edgeLengths只计算一次 3. **二分查找优化**:FindEdgeForDistance可使用二分查找 4. **并行计算**:碰撞检测可并行化(考虑线程安全) ## 五、后续优化方向 1. **贝塞尔曲线支持**: 扩展PathCurveEngine支持三次贝塞尔曲线 2. **速度曲线**: 支持在路径上设置变速点 3. **碰撞精度自适应**: 根据曲率动态调整检测密度 4. **GPU加速**: 使用CUDA加速大量碰撞检测计算 --- **文档版本**: 1.0 **创建日期**: 2026-01-05 **作者**: iFlow CLI **状态**: 待评审