如何在保留时间数据的前提下平滑F1赛车GPS坐标插值动画?
解决F1 GPS赛车位置动画卡顿问题
问题背景
现有F1赛事GPS数据,包含时间戳与三维坐标,时间戳更新间隔不稳定(100-400ms),示例数据如下:
timestamp x y z 2023-03-19 18:23:39.562 -1396 503 118 2023-03-19 18:23:39.842 -1443 630 118 2023-03-19 18:23:40.142 -1531 868 117 ...
当前使用Unity协程基于Time.deltaTime进行线性插值,但因时间间隔不均导致动画卡顿,协程代码如下:
private IEnumerator AnimationRoutine() { if (alreadyAnimating) yield break; alreadyAnimating = true; var lastSample = _samples[0]; Car.transform.position = lastSample.Position; yield return null; for (var i = 1; i < _samples.Count; i++) { var lastPosition = lastSample.Position; var currentSample = _samples[i]; var targetPosition = currentSample.Position; var duration = currentSample.TimeDelta; var timePassed = 0f; while (timePassed < duration) { var factor = timePassed / duration; Car.transform.position = Vector3.Lerp(lastPosition, targetPosition, factor); yield return null; timePassed += Time.deltaTime; } Car.transform.position = targetPosition; lastSample = currentSample; } alreadyAnimating = false; }
需求:保留原始时间数据的前提下,让点之间的过渡动画更平滑。
解决方案
1. 基于全局时间轴的插值计算
原代码按单段时间差计算进度,易受帧率波动和间隔突变影响。改为基于全局时间轴定位采样区间,保证动画严格贴合原始时间数据:
private IEnumerator SmoothAnimationRoutine() { if (alreadyAnimating) yield break; alreadyAnimating = true; // 预计算每个采样点的全局时间(相对于第一个点的累计秒数) var globalTimestamps = new List<float>(); float totalElapsed = 0f; globalTimestamps.Add(0f); for (int i = 1; i < _samples.Count; i++) { totalElapsed += _samples[i].TimeDelta; globalTimestamps.Add(totalElapsed); } float animationTimer = 0f; int currentSegmentIndex = 0; while (currentSegmentIndex < _samples.Count - 1) { // 定位当前动画时间对应的采样区间 while (currentSegmentIndex < _samples.Count - 1 && animationTimer >= globalTimestamps[currentSegmentIndex + 1]) { currentSegmentIndex++; } if (currentSegmentIndex >= _samples.Count - 1) break; var startSample = _samples[currentSegmentIndex]; var endSample = _samples[currentSegmentIndex + 1]; float segmentStartTime = globalTimestamps[currentSegmentIndex]; float segmentEndTime = globalTimestamps[currentSegmentIndex + 1]; // 计算当前区间内的插值因子(严格匹配原始时间比例) float segmentElapsed = animationTimer - segmentStartTime; float segmentDuration = segmentEndTime - segmentStartTime; float factor = Mathf.Clamp01(segmentElapsed / segmentDuration); // 基础线性插值,如需更平滑可替换为缓动或Hermite插值 Car.transform.position = Vector3.Lerp(startSample.Position, endSample.Position, factor); yield return null; animationTimer += Time.unscaledDeltaTime; // 不受时间缩放影响 } Car.transform.position = _samples[_samples.Count - 1].Position; alreadyAnimating = false; }
2. 加入速度连续性的平滑插值
采样间隔不稳定会导致赛车速度跳变,使用三次Hermite插值可保证位置和速度的连续过渡:
// 预计算每个采样点的瞬时速度 private List<Vector3> CalculateSampleVelocities() { var velocities = new List<Vector3>(); velocities.Add(Vector3.zero); // 第一个点初始速度设为0 for (int i = 1; i < _samples.Count; i++) { Vector3 posDelta = _samples[i].Position - _samples[i-1].Position; velocities.Add(posDelta / _samples[i].TimeDelta); } return velocities; } // 三次Hermite插值,保证位置和速度平滑过渡 private Vector3 HermiteLerp(Vector3 startPos, Vector3 endPos, Vector3 startVel, Vector3 endVel, float t) { float t2 = t * t; float t3 = t2 * t; float a = 2 * t3 - 3 * t2 + 1; float b = t3 - 2 * t2 + t; float c = -2 * t3 + 3 * t2; float d = t3 - t2; return a * startPos + b * startVel + c * endPos + d * endVel; }
在协程中调用HermiteLerp替代Vector3.Lerp:
// 在SmoothAnimationRoutine中添加 var sampleVelocities = CalculateSampleVelocities(); // 替换插值部分 Car.transform.position = HermiteLerp( startSample.Position, endSample.Position, sampleVelocities[currentSegmentIndex], sampleVelocities[currentSegmentIndex + 1], factor );
3. 帧率补偿处理
针对帧率波动,一次性处理累积时间差,避免动画滞后:
// 在SmoothAnimationRoutine的循环内修改时间累加逻辑 float frameDelta = Time.unscaledDeltaTime; // 处理超过当前区间的累积时间 while (animationTimer + frameDelta > segmentEndTime && currentSegmentIndex < _samples.Count - 2) { frameDelta -= segmentEndTime - animationTimer; animationTimer = segmentEndTime; currentSegmentIndex++; startSample = _samples[currentSegmentIndex]; endSample = _samples[currentSegmentIndex + 1]; segmentStartTime = globalTimestamps[currentSegmentIndex]; segmentEndTime = globalTimestamps[currentSegmentIndex + 1]; segmentDuration = segmentEndTime - segmentStartTime; } animationTimer += frameDelta; segmentElapsed = animationTimer - segmentStartTime; factor = segmentElapsed / segmentDuration;
内容的提问来源于stack exchange,提问作者dave
相关产品推荐
相关产品推荐

