如何以恒定的“下降速率”从预定义的2D(xz)线段中构建3D贝塞尔曲线样条线?

I've been exploring building a small 3D game that uses a spline for randomized, generated tracks. The closest analogue for helping visualize this is something like Impossible Road, though likely with the track being a "path" rather than a collidable physics body - so, in that sense, the gameplay will be more like Audiosurf.

现在,我大约有一个星期的时间,而且在尝试产生明智的样条曲线方面,我仍处于杂草丛生的境地。我从Godot开始,但是最近转到了Three JS,这是因为我对TypeScript的理解比对GDScript的适应要多,这使得对项目的最初部分进行推理变得容易一些(我有很大的机会切换回到Godot之后,我又将其变成了一个“游戏”。)

Godot和Three都具有方便的贝塞尔曲线样条曲线类,并且贝塞尔曲线似乎相当容易推论,因此我开始使用三次贝塞尔曲线构建我的样条曲线。我的想法是定义不同的轨道“预制段”,这些轨道可以随机排序以形成随机生成的轨道,例如“硬左转”,“右U形转弯”,“ 45度左转”等。这就是《不可能的道路》所要做的(在该视频中请注意,检查点始终始终是片段之间的“胶合点”),这对我来说很有意义。

当我住在“ XZ”飞机上并且根本不处理高度时,这已经可以了。我已将细分定义为“扁平”形状:

const prefabs = {
  leftTurn: {
    curve: new CubicBezierCurve3(
      new Vector3(0, 0, 0),
      new Vector3(0, 0, 0),
      new Vector3(0, 0, -1),
      new Vector3(-1, 0, -1)
    )
  }
};

Notice it's flat along the Y axis, so this is just a 90 degree turn in 2D.

This made it pretty easy to glue pieces together. If I defined a path as [leftTurn, rightTurn, leftTurn], when I generate my curve from those segments, I'd just keep track of the tangent line at each exit, then rotate the piece around its origin to match the "yaw" represented by the tangent (that is, its rotation around the y axis/on the xz plane):

/**
 * Transform a list of prefab names ("leftTurn", "rightTurn") to a series of
 * Bezier curves.
 */
export function convertPiecesToSplineSegments(
  pieces: string[]
): SplineSegment[] {
  let enterHeading = new Vector3(0, 0, -1).normalize();
  let enterPoint = new Vector3(0, 0, 0);

  return pieces.map((piece) => {
    const prefab = prefabs[piece];

    // get the angle between (0, 0, -1) and the current heading to figure out
    // how much to rotate the piece by.
    //
    // via https://stackoverflow.com/a/33920320
    const yaw = Math.atan2(
      enterHeading
        .clone()
        .cross(new Vector3(0, 0, -1))
        // a lil weirded out i had to use the negative y axis here, not sure what's
        // going on w that...
        .dot(new Vector3(0, -1, 0)),
      new Vector3(0, 0, -1).dot(enterHeading)
    );

    const transform = (v: Vector3): Vector3 => {
      return v
        .clone()
        .applyAxisAngle(new Vector3(0, 1, 0), yaw)
        .add(enterPoint);
    };

    const a = transform(prefab.curve.v0);
    const b = transform(prefab.curve.v1);
    const c = transform(prefab.curve.v2);
    const d = transform(prefab.curve.v3);
    const curve = new CubicBezierCurve3(a, b, c, d);

    enterHeading = d.clone().sub(c).normalize();
    enterPoint = d;

    return {
      curve,
    }
  }
}

这真的很好!最后,我添加了一些其他逻辑,以便能够沿着预制件的曲线定义“滚动”,这样您就可以设置角度,并使用一些东西生成法线,根据角度“倾斜”一个转弯(我认为您可以称为“绕切线局部空间的z轴旋转”?)。

您可以看到我到这里的演示:

https://disco.zone/splines/1

您可以使用WASD和mouselook环顾四周。在我看来,似乎可以工作!

然后我尝试增加一些高度。一切都变得非常糟糕。

我唯一的目标是使样条曲线始终以相同的速度下降-也就是说,xz平面上每距离的y轴上的位移相同。最终弄清楚如何随机改变每条曲线下降的量可能会很好,但是就目前而言,我认为保持事物恒定更简单。即使这样,我在数学上也遇到了麻烦。

首先,我天真地想出了一个方法,就像我按照当前偏航方向旋转每个零件一样,我可以用“俯仰”来做同样的事情,就像将每个点相对于曲线原点向下旋转15度一样。掉头会立即引起问题:

当您仅选取一条平坦曲线并在任一轴上“旋转”它时,它将整个曲线作为一个单元旋转。在只有90度曲线的世界中,这实际上可以正常工作,但在有180度曲线的世界中却没有那么好。

So, clearly, rotation isn't going to be what I want; I need to the curve points add additional y for the descent. And this is where things get tricky.

据我所知,贝塞尔曲线样条线的作用是,如果您希望它们具有连续性-即没有任何尖点-曲线n的t = 0处的切线必须与曲线n处的切线相同曲线n-1的t = 1(我通常不理解的数学解释中将其称为“ C1”连续性)。这对我来说很有意义,在“ 2D”世界中很容易做到:我只是在旋转新线段以匹配上一个线段的正切线,而且由于它是平坦的,我们只需要担心“偏航”角度。

我对如何从高处得到这种行为一无所知。凭直觉,我想:“哦,也许它们都可以具有线性下降率”,但我不知道该怎么计算。如果这只是一系列由点定义的线段:

a=(0, 0, 0)
b=(0, 0, -1/3)
c=(0, 0, -2/3)
d=(0, 0, -1)

Then it would be easy to apply a constant rate of descent: just add a Y value of -1/3, -2/3, and -1 to b, c, and d. Both b-a and and d-c would be (0, 0, -1/3), so the tangents would be equal all the way down.

In practice, these are, well, curves, so it's not so simple. I think you'd need to calculate the XZ distance of b and c from a, and scale the y appropriately, but I'm not sure if this is actually a rasonable approach in any way. I have tried a bunch of random "throwing code at the wall" to see if I could come up with anything that resembled what I wanted, but so far nothing has seemed to work.

我已经使用有限的数学知识尽了最大的努力来尝试使用Google谷歌搜索,但是结果却很短。尽管有很多有关创建和渲染样条线的资料,但我还没有看到很多关于平滑生成曲线的资料,我认为这些资料可以涵盖这样的内容。

另外,我想知道是否可能是因为尝试使用贝塞尔曲线样条线而在错误的树上吠叫-B样条曲线或Catmull-Rom样条曲线会更容易制作连续路径吗?我知道它们从最字面意义上来说是可以的,但是我不确定我是否会用这些样条线来定义我的“段”。

My code so far, in full, is here. While I hope you don't need to read it to understand the problem, it may help in providing solutions: https://github.com/thomasboyt/rascal