You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在保留时间数据的前提下平滑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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 18:32:25