如何使用常量 "rate of descent" 从预定义的 2D (xz) 线段构建 3D 贝塞尔曲线?

How can I build a 3D bezier spline out of predefined 2D (xz) segments with a constant "rate of descent"?

我一直在探索构建一个小型 3D 游戏,该游戏使用样条随机生成轨迹。帮助形象化的最接近的类似物是 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,一旦我回到 "game")。

Godot 和 Three 都有方便的贝塞尔样条曲线 类,而且贝塞尔曲线似乎很容易推理,所以我开始使用三次贝塞尔曲线构建我的样条曲线。我的想法是定义不同的 "prefab segments" 轨道,可以随机排序以形成随机生成的 - 例如,一个 "hard left turn," 一个 "right U-turn," 一个“45 度左转”等。这是Impossible Road 似乎做了什么(请注意该视频中的检查点显然总是在路段之间 "glue points"),并且对我来说很有意义。

当我住在 "XZ" 飞机上并且根本没有处理高度问题时,这已经很好了。我已将我的段定义为 "flat" 形状:

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)
    )
  }
};

注意它在 Y 轴上是平的,所以这只是一个二维的 90 degree turn

这使得将各个部分粘合在一起变得非常容易。如果我将路径定义为 [leftTurn, rightTurn, leftTurn],当我从这些线段生成曲线时,我只需跟踪每个出口处的切线,然后围绕其原点旋转该线段以匹配 "yaw"表示为切线(即它围绕y axis/on xz平面的旋转):

/**
 * 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 
    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,
    }
  }
}

效果非常好!我最后添加了一些额外的逻辑,以便能够沿预制件的曲线定义 "roll",这样你就可以设置一个角度,并使用一些东西生成法线,"bank" 基于角度的转弯(我想你可以称之为 "rotating around the z-axis of the local space of the tangent line"?).

您可以在此处查看我到达的位置的演示:

https://disco.zone/splines/1

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

然后我试着增加了一些高度。一切都很糟糕。

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

我首先天真地认为,就像我如何通过当前偏航方向旋转每个片段一样,我可以用 "pitch," 做同样的事情,比如将每个点相对于曲线原点向下旋转 15 度。掉头后问题就很明显了:

当您只采用一条平坦曲线并在任何一个轴上 "rotate it" 时,它会将 整条曲线 作为一个整体旋转。这在只有 90 度曲线的世界中实际上可以很好地工作,但在 180 度曲线的世界中就没那么好了。

所以,显然,旋转不是我想要的;我需要曲线点为下降添加额外的 y。这就是事情变得棘手的地方。

据我所知,贝塞尔样条曲线的问题是,如果您希望它们具有连续性 - 也就是说,没有任何尖点 - 曲线 n 的 t=0 处的切线必须是与曲线 n-1 的 t=1 处的切线相同(我看到这在数学解释中被称为 "C1" 连续性,我大多不明白)。这对我来说很有意义,而且在“2D”世界中很容易做到:我实际上只是旋转新线段以匹配前一条线段的切线,因为它是平的,我们只需要担心 "yaw"这样做的角度。

我有点不知道如何从高处获得这种行为。凭直觉,我想,"oh, maybe they could just all have a linear rate of descent," 但我无法弄清楚如何计算它。如果这只是一系列由点定义的线段:

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

那么应用恒定下降率就很容易了:只需在 b 上加上 -1/3-2/3-1 的 Y 值,c,以及 db-ad-c 都将是 (0, 0, -1/3),因此切线将一直相等。

实际上,这些是 曲线 ,所以没那么简单。我 认为 你需要计算 bca 的 XZ 距离,并适当地缩放 y,但我不确定这是否真的是一种合理的方法。我尝试了一堆随机 "throwing code at the wall" 看看我是否能想出任何与我想要的相似的东西,但到目前为止似乎没有任何效果。

我已经尝试用我公认的有限的数学知识尽我所能地使用谷歌搜索,但结果很短。虽然有很多关于创建和渲染样条曲线的 material,但我还没有看到很多关于平滑生成曲线的 material 我认为会涵盖这样的内容。

此外,我想知道是否我在尝试为此使用贝塞尔样条时树错了 - B 样条或 Catmull-Rom 样条是否可以更容易地创建连续路径?我知道他们会在最字面上的意义上,但我不太确定我会根据这些样条可以使用的术语来定义我的 "segments"。

到目前为止,我的完整代码在这里。虽然我希望您不需要阅读它来理解问题,但它可能有助于提供解决方案:https://github.com/thomasboyt/rascal

我最终以接近@Spektre 建议的方式解决了这个问题:

我没有尝试在贝塞尔样条曲线上找到恒定斜率的完美控制点,而是将我的样条曲线生成为 XZ 平面上的“2D”样条曲线。然后,当我的样条实际上是 rendered/calculated.

时,我只是将高度线性添加到 生成的点

回想起来,这是显而易见的,但我太坚持用贝塞尔曲线控制点生成 "right way" 的想法了。看起来这确实是 可能 与贝塞尔曲线 - 一个朋友联系我 this article 关于绘制螺旋 我相信涵盖了这一点,虽然数学超出了我的范围。

添加非线性高度位移——即为每个段随机生成的高度——用这种方法也不错。我首先生成了一堆随机高度,然后用 2D Catmull-Rom 样条插值它们,每个点的 x=t 和 y=height。这似乎可以解决任何不连续性问题。

结果来了,还挺顺眼的:https://disco.zone/splines/3/