简体   繁体   中英

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

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 .

Right now, I'm about a week in, and I'm still pretty far in the weeds on trying to generate a sensible spline. I started in Godot but recently moved to Three JS, just because I'm a bit more comfortable with TypeScript than GDScript and it has made it a bit easier to reason about this initial part of the project (there's a good chance I'll switch back to Godot once I get back to making this more of a "game").

Godot and Three both have handy bezier spline classes, and beziers seemed fairly easy to reason about, so I started building my spline using cubic beziers. My idea is to define different "prefab segments" of track that can be randomly sequenced to form a randomly generated - eg, a "hard left turn," a "right U-turn," a "45 degree left turn," etc. This is what Impossible Road appears to do (notice in that video the checkpoints always are clearly the "glue points" between segments), and made sense to me.

This has gone okay while I've lived in the "XZ" plane and not dealt with height at all. I've defined my segments as "flat" shapes:

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

This worked really well, I ended up adding some additional logic for being able to define "roll" along a prefab's curve, such that you could set an angle and, using some stuff to generate normals? "bank" a turn based on the angle (I think you could call this "rotating around the z-axis of the local space of the tangent line".).

You can see a demo of where I got to here:

https://disco.zone/splines/1

You can use WASD and mouselook to look around. It seems, to my eye, to work okay!

Then I tried to add some height. And everything went very bad.

My only goal is to have the spline always descend at the same speed - that is, the same displacement over the y axis per distance on the xz plane. It might be nice to eventually figure out how to randomly vary the amount that each curve descends, but for now, I figure it's simpler to keep things constant. Even so, I'm having trouble with the math required for this.

I first naively figured that, just like how I rotated each piece by the current yaw heading, I could just do the same with a "pitch," like rotate each point down 15 degrees relative to the curve origin. The problem with that is immediately obvious with a U-turn:

When you just take a flat curve and "rotate it" on any one axis, it rotates the whole curve as a single unit. Which would actually work fine in a world of just 90 degree curves, but not so much in a world of 180 degree curves.

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.

The thing with bezier splines, as I understand it, is that if you want them to have continuitiy - that is, to not have any sharp points - the tangent line at t=0 of curve n has to be the same as the tangent at t=1 of curve n-1 (I see this referred to as "C1" continuity in math-y explanations I mostly don't understand). This makes sense to me, and it was easy to do in a "2D" world: I was literally just rotating new segment to match the exact tangent line of the previous one, and because it's flat, we only need to worry about that "yaw" angle in doing so.

I'm a bit clueless on how, exactly, I would get this behavior from the height. Intuitively, I thought, "oh, maybe they could just all have a linear rate of descent," but I can't figure out how I'd calculate this. If this were just a series of line segments, defined by points:

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 ba and and dc 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.

I've tried Googling this as best I can with my admittedly limited math knowledge, but have come up short. While there's a lot of material about creating and rendering splines, I haven't seen a lot of material about smooth generation of curves that I imagine would cover something like this.

Additionally, I wonder if maybe I'm barking down the wrong tree trying to use a Bezier spline for this - would a B-Spline or Catmull-Rom Spline make it much easier to make a continuous path? I know they would in the most literal sense, but I'm not quite sure I'd define my "segments" in terms that those splines would be able to use.

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

I ended up solving this in a way close to what @Spektre suggested:

Rather than attempt to find the perfect control points for constant slope on aa Bezier spline, I just generated my spline as a "2D" spline on the XZ plane. Then, I just linearly added height to the generated points when my spline was actually rendered/calculated.

This was, in retrospect, obvious, but I got too stuck on the idea of generating this the "right way" with Bezier control points. It does appear this is possible with Bezier curves - a friend linked me this article about drawing helices that I believe covers this, though the math is beyond me.

Adding non-linear height displacement - that is, randomly generated heights for each segment - wasn't too bad with this method, either. I first generated a bunch of random heights, then interpolated over them with a 2D Catmull-Rom Spline with x=t and y=height at each point. This appears to solve any problems with discontinuity.

The result is here, and looks rather pleasing to my eye: https://disco.zone/splines/3/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM