曲线路径恒速和终点

Curved Path Constant Speed and End Points

我正在尝试在给定时间内以恒定速度在弯曲路径上移动。我通过在曲线上的不同点取导数并将它们取平均来计算出在曲线上行驶所需的平均速度。然后我将路径的位置 (t) 乘以平均导数与曲线当前位置的导数之比。这种设置恒速的方法很管用

我遇到的问题发生在多个控制点(3 个或更多)被放置在同一位置时。那么此时的速度(或导数)为0,用平均速度除以速度0显然会导致计算出现问题。

BSpline 需要在两端放置三个控制点,以使曲线真正到达终点的起点和终点。如果我只在末端放置 1 或 2 个控制点,则路径从第一个控制点之后开始,到最后一个控制点之前结束。对于我的应用程序,运动到达终点很重要,因为我将把多个 BSplines 连接在一起,并且它们正确排列并且它们之间没有任何时间间隔很重要。

我尝试了几次不同的修复尝试,但 none 成功了。

这是我的示例代码,我已经包含了注释以指出问题所在。

注意: 我在示例中使用 CatmullRomSpline 而不是 BSpline 只是因为我在 BSpline 的导数方法中发现了一个错误,该错误已修复但未修复然而在 LibGDX.

的稳定版本中

Test.java

public class Test extends Game {
    private Stage stage;
    private MyPath path;

    @Override
    public void create () {
        Gdx.graphics.setDisplayMode(1000, 1000, false);
        stage = new Stage();
        stage.setViewport(new ScreenViewport(stage.getViewport().getCamera()));
        Gdx.input.setInputProcessor(stage);
        path = new MyPath(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        stage.addActor(path);
    }
    @Override
    public void render () {
        Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        stage.act(Gdx.graphics.getDeltaTime());
        stage.draw();
    }
    @Override
    public void dispose(){
        path.dispose();
        stage.dispose();
        super.dispose();
    }
}

MyPath.java

public class MyPath extends WidgetGroup implements Disposable {
    private Path<Vector2> path;
    private Vector2 result=new Vector2(), derivative=new Vector2();
    private float time, t, tPrev, dt, tConst, tConstPrev, derivativeAverage;
    private Array<Texture> textures = new Array<Texture>(Texture.class);
    private Array<Image> points = new Array<Image>(Image.class);
    private Image dot;

    private final float CYCLE = 4;  // path cycle time (in seconds)

    private Vector2[] pointsData = {
            new Vector2(100, 100),
            new Vector2(100, 100),
//          new Vector2(100, 100),  // << UN-COMMENT TO PRODUCE BUG

            new Vector2(350, 800),
            new Vector2(550, 200),
            new Vector2(650, 400),
            new Vector2(900, 100),
            new Vector2(900, 100)
    };

    public MyPath(int width, int height){
        this.setSize(width, height);        
        path = new CatmullRomSpline<Vector2>(pointsData, false);
        // create and add images
        createImages();
        for (int i=0; i<points.size; i++){
            points.items[i].setPosition(pointsData[i].x - points.items[i].getWidth()/2, pointsData[i].y - points.items[i].getHeight()/2);
            addActor(points.items[i]);
        }
        addActor(dot);

        // calculate derivative average
        derivativeAverage();
    }

    @Override
    public void act(float delta){
        result = getValue(delta);
        dot.setPosition(result.x - dot.getWidth()/2, result.y - dot.getHeight()/2);     
    }
    private Vector2 getValue(float delta){
        // set t in the range [0,1] for path
        time += delta;
        if (time > CYCLE){
            time = tPrev = dt = tConst = tConstPrev = 0;
        }
        t = time / CYCLE;
        dt = t - tPrev;
        tPrev = t;

        // constant speed (tConst)
        path.derivativeAt(derivative, tConstPrev);
        tConst += dt * (derivativeAverage / derivative.len());  // << ERROR when derivative.len() is 0
        tConstPrev = tConst;

        path.valueAt(result, tConst);

        return result;
    }

    private void derivativeAverage(){
        float segmentCount = 20000;
        derivativeAverage = 0;      
        for (float i=0; i<=1; i+=1.0/segmentCount) {    
            path.derivativeAt(result, i);
            derivativeAverage += result.len();          
        }
        derivativeAverage /= segmentCount;
        if (derivativeAverage==0){ throw new GdxRuntimeException("ERROR: derivative average is zero"); }
    }

    private void createImages(){
        dot = getImage(Color.GREEN, true);
        for (int i=0; i<pointsData.length; i++){
            points.add(getImage(Color.WHITE, false));
        }
    }
    private Image getImage(Color color, boolean fillCircle){
        Pixmap pixmap = new Pixmap(50, 50, Pixmap.Format.RGBA8888);
        pixmap.setColor(color);
        if (fillCircle){
            pixmap.fillCircle(pixmap.getWidth()/2, pixmap.getHeight()/2, pixmap.getWidth()/2-1);
        } else {
            pixmap.drawCircle(pixmap.getWidth()/2, pixmap.getHeight()/2, pixmap.getWidth()/2-1);
        }
        textures.add(new Texture(pixmap));
        pixmap.dispose();
        return new Image(textures.peek());
    }
    @Override
    public void dispose(){
        while (textures.size > 0){
            textures.pop().dispose();
        }
    }
}

============================================= ======================

编辑

============================================= ======================

这是我最近尝试增加 t 直到点移动。

此方法偶尔适用于某些帧(平滑地移动过零导数)。但其他时候,当点到达零导数时,它会在曲线的起点重新开始,或者延伸到曲线的终点之外,向不同的方向移动或完全消失(因为位置被设置为负值)。

所以看起来这个方法真的很接近,因为它偶尔会在某些帧上工作,但它会出现故障并在其他帧上做一些奇怪的事情。

Vector2 lastPoint = new Vector2();
float minSpeed = 1;
float minDerivative = 1;
float temp;

...

private Vector2 getValue(float delta){      
    // set t in the range [0,1] for path
    time += delta;
    if (time > CYCLE){
        time = tPrev = dt = tConst = tConstPrev = 0;
    }
    t = time / CYCLE;

    // CONSTANT SPEED
    dt = t - tPrev;
    path.derivativeAt(derivative, tConstPrev);
    temp = dt * (derivativeAverage / derivative.len());
    path.valueAt(result, tConst + temp);

    //**************************************
    //  FIX FOR ZERO SPEED
    //  increase t in loop until speed > 0
    //**************************************
    while (result.dst(lastPoint)<minSpeed || derivative.len()<minDerivative){
        // set t in the range [0,1] for path
        time += delta;
        if (time > CYCLE){
            time = tPrev = dt = tConst = tConstPrev = 0;
            lastPoint.set(0,0);         
        }
        t = time / CYCLE;

        // CONSTANT SPEED
        dt = t - tPrev;
        // new derivative
        path.valueAt(derivative, t);
        derivative.sub(lastPoint);

        temp = dt * (speedAverage / derivative.len());
        path.valueAt(result, tConst + temp);
    }

    tConst += temp;

    lastPoint.set(result);
    tPrev = t;
    tConstPrev = tConst;

    return result;
}

我在计算平均速度时也做了类似的事情,以防止零导数影响它。我还尝试在计算平均值时使用带有 "addedSegmentCount" 变量的注释部分,但由于某种原因实际上导致了更多故障......尽管理论上这似乎是计算平均值的 "correct" 方法因为如果距离太小,有些段不会被添加。

private void pathLength_SpeedAverage(){
    float segmentCount = 20000;
//  float addedSegmentCount = 0;
    pathLength = 0;

    path.valueAt(lastPoint, 0);
    for (float i=0; i<=1; i+=1.0/segmentCount){
        path.valueAt(result, i);
        if (result.dst(lastPoint) >= minSpeed){
            path.derivativeAt(result, i);
            if (result.len() >= minDerivative){
                pathLength += result.len();

                lastPoint.set(result);
//              ++addedSegmentCount;
            }
        }
    }
    speedAverage = pathLength / segmentCount;
//  speedAverage = pathLength / addedSegmentCount;
    lastPoint.set(0,0);
}

如果控制点可能重合,则无法完全避免一阶导数为零。所以,我的建议是根本不要使用一阶导数。您的目的是以恒定速度遍历路径,这相当于沿路径具有相等弧长的采样点。解决这个问题的理论方法涉及微积分以数字方式计算弧长,但我们可以采用如下所示的近似方法:

假设你想遍历 N 步路径,
1)在参数域(即t=0.0、0.1、0.2、0.3、....)中沿路径均匀采样M点,其中M优选大于N。将这些点表示为P0、P1、P2, ....
2) 计算 P0P1, P1P2, P2P3,....
之间的距离 3) 编译从参数 t(i) 映射到累积弦长 |P0P1|+|P1P2|+.....+|P(i-1)P(i) 的查找 table |。最后,您还将获得路径的总长度,记为L。
4) 现在,对于 kL/N 的每个值(其中 k=0 到 N),您可以通过对两者进行线性插值从查找 table 中计算相应的 t 值kL/N落在的参数值。