553 lines
16 KiB
Markdown
553 lines
16 KiB
Markdown
# 动画检测集成方案
|
|
|
|
## 一、现状分析
|
|
|
|
### 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<Point3D> framePositions)
|
|
```
|
|
|
|
- 基于前后帧位置计算方向向量
|
|
- 使用atan2计算偏航角
|
|
- **问题**: 在直线段上方向准确,但在圆弧处需要改进
|
|
|
|
### 1.2 路径数据结构
|
|
|
|
#### PathRoute模型
|
|
|
|
```csharp
|
|
// PathPlanningModels.cs
|
|
public class PathRoute
|
|
{
|
|
public List<PathPoint> Points { get; set; } // 控制点
|
|
public List<PathEdge> 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<Point3D> 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<Point3D> pathPoints, double durationSeconds = 10.0)
|
|
{
|
|
// 直接使用pathPoints作为路径
|
|
_pathPoints = new List<Point3D>(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<Point3D>();
|
|
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<Point3D>();
|
|
var edgeLengths = new List<double>(); // 每条边的长度
|
|
var edgeStartIndices = new List<int>(); // 每条边在采样序列中的起始索引
|
|
|
|
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<AnimationFrame>();
|
|
|
|
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<CollisionResult>()
|
|
};
|
|
|
|
// ... 碰撞检测逻辑保持不变 ...
|
|
|
|
_animationFrames.Add(frame);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 修改点3: 曲线边内插值方法
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// 在直线边上插值位置
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 在圆弧边上插值位置
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 查找指定距离对应的边索引
|
|
/// </summary>
|
|
private int FindEdgeForDistance(double targetDistance, List<double> 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
|
|
/// <summary>
|
|
/// 计算路径上的朝向(改进版)
|
|
/// </summary>
|
|
private double ComputeYawOnPath(
|
|
int frameIndex,
|
|
List<Point3D> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取圆弧旋转轴
|
|
/// </summary>
|
|
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
|
|
**状态**: 待评审
|