七彩极 发表于 2022-3-29 11:59

Unity3D 参数曲线 实现曲线上的匀速运动

环境:Unity2021.1.14 语言:C#
总起

本文源代码可以在https://github.com/anguangzhihen/TestOdinInspector中的TestCurve场景中找到。

Bezier曲线和Catmull-Rom曲线是工程中常见的曲线实现方式,他们本身原理十分简单,只是个多项式方程组,拿到公式带入就能实现。
Bezier曲线和Catmull-Rom曲线之间可以相互转换,所以本篇内容只针对Bezier曲线进行说明,Catmull-Rom本质上是一样的。

最近在工作中需要使用Bezier曲线来控制镜头运动,实现后在跟美术反复沟通后发现,他们想要时间曲线直接控制镜头的运动速度,但是现在Bezier的实现中传入参数t在曲线上运动是有快有慢的,这会造成美术在控制时间曲线时非常不直观。
因此保证传入参数使得能在曲线上匀速运动是非常重要的。
后面查了很多资料后发现了一种工业界常用的方案能解决该问题——参数曲线。
Bezier曲线

一般开发中最常用的Bezier曲线为三次方公式:


具体实现也非常简单:
public static Vector3 GetBezierPoint(Vector3[] points, float t)
{
        Vector3 p0 = points;
        Vector3 p1 = points;
        Vector3 p2 = points;
        Vector3 p3 = points;

        float rest = (1f - t);
        Vector3 newPos = Vector3.zero;
        newPos += p0 * rest * rest * rest;
        newPos += p1 * t * 3f * rest * rest;
        newPos += p2 * 3f * t * t * rest;
        newPos += p3 * t * t * t;
        return newPos;
}
实现效果:


可以看到采样小球并不是均匀分布的。
而我们的目标就是获得一个函数t=f(s),使得传入的s保证是匀速运动的,s具体来说应该是曲线长度的百分比,最终目标就变成了获取弧长。
参数曲线

包括Bezier曲线的很多曲线都能表示为以下形式:


使用Bezier曲线公式进行推导就能获得具体的A、B、C、D:
public static ParametricCurve CreateByBezier(Vector3[] points)
{
        ParametricCurve curve = new ParametricCurve();
        curve.points = points;
        curve.A = -1 * points + 3 * points - 3 * points + points;
        curve.B = 3 * points - 6 * points + 3 * points;
        curve.C = -3 * points + 3 * points;
        curve.D = points;
        return curve;
}

然后对该公式求导获取绝对值后,求0到t的积分就能获得从0到t处的弧长:


积分的求解非常复杂,《数值分析》中对这块内容有详细介绍,我只是简单的学习了一下就不班门弄斧了。不过具体应用起来还是很方便的,我们这边采用Gauss-Legendre求积公式:


public float GetArcLength(float t)
{
        var halfT = t / 2f;

        float sum = 0f;
        foreach (var wx in gaussWX)
        {
                var w = wx;
                var x = wx;
      // 权值w乘以公式带入特定值x
                sum += w * GetPointDer(halfT * x + halfT).magnitude;
        }
        sum *= halfT;
        return sum;
}
获得了该公式后,我们实际已知的是s,而需要求t,这个时候使用牛顿迭代法迭代3、4次就可以快速的获得精确的t:


其中t0是初始迭代值,t1是下一个迭代值,这边我们t0就直接选用s了:
public float S2T(float s)
{
        const int NEWTON_SEGMENT = 4;

        s = Mathf.Clamp01(s);
        float t = s;
        // 牛顿迭代法
        for (int i = 0; i < NEWTON_SEGMENT; i++)
        {
                t = t - (T2S(t) - s) / T2SDer(t);
        }
        return t;
}
结果:


参考

《微积分的本质》https://www.bilibili.com/video/BV1qW411N7FU
《数值分析》第2版 Timothy Sauer
https://www.zhihu.com/question/27715729/answer/310580409
页: [1]
查看完整版本: Unity3D 参数曲线 实现曲线上的匀速运动