3 minutes
NURBS, Roll, Physics in Action
Parallel to writing this series about building a roller coaster simulator in the browser, I am also implementing and testing everything I write about in a real project.
At this point, the simulator already supports NURBS tracks, roll interpolation, and the full physics simulation. Most of the ideas in the articles are no longer just sketches. They are running code.
A large part of the work recently was, understanding how NoLimits Roller Coaster 2 handles track interpolation and roll. I spent quite a few hours digging into this, because, well, I more or less reimplemented its curve generation logic.
Why do that?
First, it is a genuinely challenging problem. Second, it is a great way to understand how a real coaster simulator works without having to build a full editor and toolchain around it. You can test ideas quickly, break things, and learn where your assumptions were wrong.
I plan to write a separate article that focuses only on how NoLimits Roller Coaster 2 style interpolation and track generation works. There is a lot going on there, and it deserves its own chapter. Stay tuned for that.
One thing I have not fully figured out yet is how to properly close NURBS curves.
And no, this is not about not knowing how to compute knot vectors for closed NURBS. The tricky bit is how the curve parameter space maps to actual arc length once the curve is closed. For clamped curves, this is straightforward. For closed curves, I have not figured out the arc length mapping yet.
I will figure it out eventually. Probably.
Interactive demo
Below is an interactive demo of the current state of the simulator. You can play around with it and see how the track interpolation, roll, and physics behave together.
As always, the full code is available below.
Code
If you want to run the code locally, clone this
repository,
run npm install and npm run dev-scripts,
then open this
link
in your browser.
import React from 'react';
import { useControls } from 'leva';
import { Vector3 } from 'three';
import { toLocalTransformed } from '../../../../maths/curve';
import { fromURL } from '../../../../helper/nl2park/nl2park';
import { curveFromCustomTrack } from '../../../../helper/nolimits';
import { useColors } from '../../../../hooks/useColors';
import { CurveWireframe } from '../../../../components/curve/CurveWireframe';
import { Ground } from '../../../../components/Ground';
import { PerspectiveScene } from '../../../../components/scenes/PerspectiveScene';
import { TrainWithPhysics } from '../../../../components/TrainWithPhysics';
// @ts-ignore
import Park from './Hybris.nl2park';
const exampleCoaster = (await fromURL(Park)).coaster[0];
const exampleTrack = exampleCoaster?.tracks[0];
const exampleTrackCurve = toLocalTransformed(
curveFromCustomTrack(exampleTrack),
new Vector3(0, -1.1, 0),
);
export const NurbsRollPhysicsInActionDemoScene = () => {
const colors = useColors();
const { pov } = useControls({
pov: true,
});
return (
<>
<PerspectiveScene cameraControlsActive={!pov}>
<Ground position={new Vector3(0, -7, 0)} />
<CurveWireframe
color={colors.secondary}
curve={exampleTrackCurve}
/>
<TrainWithPhysics
curve={exampleTrackCurve}
activateCamera={pov}
init={{
velocity: 7,
distanceTraveled: 186,
friction: 0.026,
airResistance: 2e-5,
}}
/>
</PerspectiveScene>
</>
);
};