16 KiB
16 KiB
动画检测集成方案
一、现状分析
1.1 当前动画系统架构
核心组件
- PathAnimationManager: 动画管理器,负责动画帧预计算和播放
- AnimationControlViewModel: 动画控制视图模型,处理UI交互
- PathCurveEngine: 路径曲线化引擎,已实现圆弧过渡功能
当前路径插值机制
// PathAnimationManager.cs:1253
private Point3D InterpolatePosition(double progress)
- 基于原始控制点(_pathPoints)进行线性插值
- 按路径段距离比例计算位置
- 问题: 未使用PathRoute.Edges,动画沿直线段运动
当前朝向计算机制
// PathAnimationManager.cs:600
private double ComputeYawFromPath(int frameIndex, List<Point3D> framePositions)
- 基于前后帧位置计算方向向量
- 使用atan2计算偏航角
- 问题: 在直线段上方向准确,但在圆弧处需要改进
1.2 路径数据结构
PathRoute模型
// 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类型
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数据
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 核心目标
- 动画帧预计算使用路径: 替代原有的直线插值
- 圆弧处方向计算准确: 确保车辆在圆弧段正确朝向
- 性能优化: 避免重复计算采样点
2.2 数据流设计
PathRoute (控制点)
↓
PathCurveEngine.ApplyCurvatureToRoute()
↓
PathRoute.Edges (包含直线段和圆弧段)
↓
PathAnimationManager.PrecomputeAnimationFrames()
↓
使用PathEdge.SampledPoints生成动画帧
↓
动画播放
2.3 关键修改点
修改点1: PathAnimationManager.SetupAnimation()
当前实现:
// PathAnimationManager.cs:407
public void SetupAnimation(ModelItem animatedObject, List<Point3D> pathPoints, double durationSeconds = 10.0)
{
// 直接使用pathPoints作为路径
_pathPoints = new List<Point3D>(pathPoints);
PrecomputeAnimationFrames();
}
修改方案:
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()
当前实现:
// 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);
// ...
}
}
修改方案:
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: 曲线边内插值方法
/// <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: 路径朝向计算(核心)
/// <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()
当前实现:
// AnimationControlViewModel.cs:815
private void ExecuteGenerateAnimation()
{
// 获取当前路径的控制点
var pathPoints = CurrentPathRoute.Points.Select(p => p.Position).ToList();
// 调用SetupAnimation
_pathAnimationManager.SetupAnimation(
_animatedObject,
pathPoints,
_animationDuration
);
}
修改方案:
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中添加:
// 路径相关
private PathRoute _route = null; // 路径引用
三、实现步骤
阶段1: 数据结构准备
- ✅ PathRoute、PathEdge、ArcTrajectory已存在
- ✅ PathCurveEngine已实现曲线化算法
- ⏳ 在PathAnimationManager中添加_route字段
阶段2: 核心算法实现
- ⏳ 修改SetupAnimation()方法,必需传入route参数
- ⏳ 重写PrecomputeAnimationFrames()方法,使用路径
- ⏳ 实现曲线边插值方法(InterpolateOnStraightEdge, InterpolateOnArcEdge)
- ⏳ 实现FindEdgeForDistance()方法
- ⏳ 实现ComputeYawOnPath()方法(核心)
- ⏳ 实现GetArcRotationAxis()辅助方法
阶段3: UI集成
- ⏳ 修改AnimationControlViewModel.ExecuteGenerateAnimation()
- ⏳ 添加路径验证和边数据生成
- ⏳ 更新日志输出
阶段4: 测试验证
- ⏳ 单元测试:曲线边插值准确性
- ⏳ 单元测试:圆弧朝向计算准确性
- ⏳ 集成测试:完整动画播放流程
- ⏳ 性能测试:预计算时间
四、关键技术点
4.1 圆弧朝向计算原理
方法1: 采样点差分法(简单但精度较低)
// 使用前后采样点计算方向
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: 圆弧切线法(精确)
// 计算圆弧切线方向
Vector3D radiusVec = (currentPoint - arcCenter).Normalize();
Vector3D rotationAxis = GetArcRotationAxis(trajectory);
Vector3D tangentVec = Vector3D.CrossProduct(rotationAxis, radiusVec);
double yaw = Math.Atan2(tangentVec.Y, tangentVec.X);
推荐使用方法2,因为:
- 数学上精确,不依赖采样密度
- 在圆弧段上方向连续平滑
- 性能更好,无需查找相邻采样点
4.2 采样点密度控制
# default_config.toml
[path_editing]
# 圆弧采样步长(米) - 推荐值:0.02-0.1
arc_sampling_step = 0.05
- 步长越小:动画越平滑,但内存占用越大
- 步长越大:内存占用小,但可能不够平滑
- 推荐值:0.05米(平衡精度和性能)
4.3 边界情况处理
- 单边路径:只有一条直线边
- 全圆弧路径:所有边都是圆弧
- 混合路径:直线段和圆弧段交替
- 零长度边:跳过或合并
- 采样点不足:自动补充或调整步长
4.4 性能优化
- 采样点缓存:PathEdge.SampledPoints只生成一次
- 边长度缓存:edgeLengths只计算一次
- 二分查找优化:FindEdgeForDistance可使用二分查找
- 并行计算:碰撞检测可并行化(考虑线程安全)
五、后续优化方向
- 贝塞尔曲线支持: 扩展PathCurveEngine支持三次贝塞尔曲线
- 速度曲线: 支持在路径上设置变速点
- 碰撞精度自适应: 根据曲率动态调整检测密度
- GPU加速: 使用CUDA加速大量碰撞检测计算
文档版本: 1.0 创建日期: 2026-01-05 作者: iFlow CLI 状态: 待评审