Camera banking on a curved path

Im learning ThreeJS by extending the roller coaster example to add procedural track generation and random other features I thought would be fun.

Im currently having an issue trying to get the camera to follow the angle of the track as it banks in corners. If you ride the coaster in the demo below you can see how the camera doesnt bank. It stays perfectly horizontal.
Demo: https://il13yl.csb.app/

I have seen examples like these:
https://hofk.de/main/discourse.threejs/2021/MotionAlongCurve/MotionAlongCurve.html
https://hofk.de/main/discourse.threejs/2021/BasisToQuaternion/BasisToQuaternion.html
https://threejs.org/examples/#webgl_geometry_extrude_splines

But those are following the curves, whereas the roller coaster is following the geometry of the track.

I have tried to apply the logic used to build the geometry to the camera but that has some pretty crazy effects

        point.copy(curve.getPointAt(i / divisions));
        up.set(0, 1, 0);
        forward.subVectors(point, prevPoint).normalize();
        right.crossVectors(up, forward).normalize();
        up.crossVectors(forward, right);
        const angle = Math.atan2(forward.x, forward.z);
        quaternion.setFromAxisAngle(up, angle);

Thanks in advance

I’m not sure I’ve grasped your problem.

I assume you want the object to come out of the vertical in the curve like a motorcyclist and take an appropriate lean angle?

CarRacingQuaternionSimple

In code normal.y = 0; causes the road to remain horizontal. Accordingly, you can force other behaviors. For this you can calculate the path curvature.

for ( let j = 0; j < lss; j ++ ) {

    // to the points
    
    tangent = curve.getTangent(  j / ls );
    t.push( tangent.clone( ) );
    
    normal.crossVectors( tangent, binormal );
    
    normal.y = 0; // to prevent lateral slope of the road
    
    normal.normalize( );
    n.push( normal.clone( ) );
    
    binormal.crossVectors( normal, tangent ); // new binormal
    b.push( binormal.clone( ) );    
    
}

Yes the desire is for the camera to twist and turn with the track. But the track and curve seem to have different quaternions. Because of this I dont believe I can strictly use the curve to manage the lateral rotations of the cart/camera. Using something like setFromBasis does create a banking effect, but it does not follow the angle of the track.

Demo: https://u8gsm1.csb.app/

You can see how the camera rotation is sometimes the opposite to the track

How are you creating the geometry for the track here? It seems as though each of the tracks segments or cross beams has the correct orientation, would it be an idea to simply copy the procedure of how each of these “segments” find their respective rotations?

The track geometry code is from a ThreeJS example
https://threejs.org/examples/webxr_vr_rollercoaster.html

The relevant rotation code I think is this:

      const offset = new THREE.Vector3();
      for (let i = 1; i <= divisions; i++) {
        point.copy(curve.getPointAt(i / divisions));
        up.set(0, 1, 0);
        forward.subVectors(point, prevPoint).normalize();
        right.crossVectors(up, forward).normalize();
        up.crossVectors(forward, right);
        const angle = Math.atan2(forward.x, forward.z);
        quaternion.setFromAxisAngle(up, angle);

        if (i % 2 === 0) {
          drawShape(step, color2);
        }

        extrudeShape(tube1, offset.set(0, -0.125, 0), color2);
        extrudeShape(tube2, offset.set(0.2, 0, 0), color1);
        extrudeShape(tube2, offset.set(-0.2, 0, 0), color1);
        prevPoint.copy(point);
        prevQuaternion.copy(quaternion);
      }

From that I can get the line between the left and right of the track (the white rails) and use those values to calculate up

        vector10.copy(offset.set(0.2, 0, 0));
        vector10.applyQuaternion(quaternion);
        vector10.add(p1);

        vector11.copy(offset.set(-0.2, 0, 0));
        vector11.applyQuaternion(quaternion);
        vector11.add(p1);

        dir.subVectors(vector10, vector11).normalize();

        up.set(0, 1, 0);
        forward.subVectors(vector10, vector11).normalize();
        right.crossVectors(up, forward).normalize();
        up.crossVectors(forward, right);

        // debugLine(red, vector10.clone(), vector10.clone().add(up))
        // debugLine(red, vector12.clone(), vector12.clone().add(up))
        debugLine(red, vector10.clone(), vector10.clone().sub(dir))
        debugLine(red, point.clone(), point.clone().add(up))

And that seems to get the correct horizontal rotation, but incorrect vert rotation. For that I use this

      up2.set(0, 1, 0);
      forward2.subVectors(p1, p2).normalize();
      right2.crossVectors(up2, forward2).normalize();
      up2.crossVectors(forward2, righ2t);

      debugLine(blue, p1, p1.clone().add(forward2));
      debugLine(blue, p1, p1.clone().add(right2));
      debugLine(blue, p1, p1.clone().add(up2));

Producing this:

I think I either need to rotate the blue to laterally match the red, or vertically rotate the red to match the blue.

I tried this, but it created some odd results
forward2 from blue and right from red

up3.crossVectors(forward2, right);
debugLine(purple, p1, p1.clone().add(up3));

Example of the above as Im terrible at explaining things :slight_smile:
https://giedqw.csb.app/

Progress?

I figured if I use the track as my “eye” and “target” I could get the correct lateral rotation of the cart carrying the camera. I would just need to rotate the camera to look ahead.

      up.set(0, 1, 0);
      forward.subVectors(this.position, this.nextPosition).normalize();
      right.crossVectors(up, forward).normalize();
      up.crossVectors(forward, right);
      const angle = Math.atan2(forward.x, forward.z);
      quaternion.setFromAxisAngle(up, angle);

      // left side of track
      vector11.copy(offset.set(-0.2, 0, 0));
      vector11.applyQuaternion(quaternion);
      vector11.add(this.position);

      // right side of track
      vector10.copy(offset.set(0.2, 0, 0));
      vector10.applyQuaternion(quaternion);
      vector10.add(this.position);

      this.matrix.lookAt(
        vector11,
        vector10,
        up
      );
      this.quaternion.setFromRotationMatrix(this.matrix);

This works in as much as the train/cart angles correctly with the dips and hills but now banks in the opposite direction on corners

As you can see here. The track is banking left and the train is banking the same angle on the right.

hi @LillyBilly i’ve had a play with your setup, is the following fiddle closer to the result you’re after?

1 Like

Thanks that was a huge help.
Flipping the atan2 to negative seems to have been the missing key. I was trying all sorts of crazy stuff to flip the angle

const angle = Math.atan2(-forward.x, -forward.z);

And your use of a lesser “nextPosition” delta really helped me to figure out how to smooth the track. Feels like a no-brainer post solution

curve.getPointAt(f + 0.001, point1);

Im happy with the final result. Time to test in VR and see if it makes people feel sick :slight_smile:
https://h3tx11.csb.app/

2 Likes

ah yeah, your result is so nice and smooth! i was using a hack around with the lookAt method on the dummy object to “look ahead” which created little quaternion confusion glitches at some points along the track, i’m glad some of the other logic helped out though, nice result!

I really appreciate you and hofk jumping to offer help. Such a great community to learn from here. Hope I can repay with some helpful knowledge in the future.

Also thanks for this

        let ah = new THREE.AxesHelper(5);
        ah.position.copy(p1);
        ah.applyQuaternion(quaternion);
        scene.add(ah);

I had no idea that tool existed

1 Like