How do we determine the acceleration of the coaster?

To simplify things, we ignore air resistance, rolling friction and any other additional forces. We also assume Earth’s gravity is 9.81 m/s². This allows us to focus entirely on the fundamental idea:

Gravity is the primary source of acceleration.

On Earth, gravity provides a constant acceleration of roughly 9.81 m/s².

What does this number actually mean?

Every second, an object’s speed increases by 9.81 m/s.

Some quick examples:

  • after 1 second → 9.81 m/s
  • after 2 seconds → 19.62 m/s
  • after 3 seconds → 29.43 m/s

In short: acceleration continuously increases an object’s speed.


Acceleration on slopes

Gravity is always pointing straight down.
But on a slope, only a part of that force helps the coaster move along the track.

To compute that portion, we use the downhill-slope acceleration:

$$acceleration = gravity \cdot sin(angle)$$

Where:

  • gravity: Earth’s gravity (9.81 m/s²)
  • angle: the tilt of the track
  • sin(angle): how much of gravity points along the track

But why?

For simplification, we only look at downward slopes at this point.
Of course, in a real simulation we also have negative angles when the coaster travels uphill, but we will get to that later.

For these downhill angles, the relevant sine values lie between 0 and 1:

  • sin = 0 → no gravity along the track
  • sin = 1 → full gravity along the track

Examples

Vertical drop (90°)

Full gravity acts along the track, essentially free fall.

  • sin(90°) = 1 (100% of gravity)
  • acceleration = 9.81 m/s²

45° slope

You probably expected to get half of gravity here, right? Nope, it’s not linear at all :D It’s a sine function.

  • sin(45°) ≈ 0.707 (≈ 70% of gravity)
  • acceleration ≈ 6.93 m/s²

Flat track (0°)

No slope → no downhill force.

  • sin(0°) = 0 (0% of gravity)
  • acceleration = 0 m/s²

Why this matters for simulation

As you can see, to simulate our coaster we only need to know:

the angle of the track at each point.

  • 0° → 0 m/s², no acceleration
  • 45° → 6.93 m/s², about 70% of gravity
  • 90° → 9.81 m/s², full gravity (free fall)

And the crucial insight is:

This relationship is not linear, it’s governed by sine.

This is why 45° does not give half of Earth’s gravity, but roughly 70%.

Interactive Example

When it comes to calculating the acceleration component along the slope, the formula ultimately reduces to:

const acceleration = gravity * Math.sin(slope);

Keep in mind that Math.sin accepts radians, not degrees.

A radian is simply another way of measuring angles, where angles are expressed as multiples of π.

The range from −π to π corresponds to −180° to 180°.

In code, we almost always work with radians, because most math functions expect them. Degrees are typically used only for user input, since they are more intuitive.

When converting between the two, use:

$$\text{radians} = \text{degrees} \cdot \frac{\pi}{180}$$

$$\text{degrees} = \text{radians} \cdot \frac{180}{\pi}$$

You can play around with different slopes and see how they affect the percentage of gravity applied to the coaster. Having a visual representation of written concepts really helps me reach that I got it moment, so it might help you as well.

What comes next?

In the next chapter, we’ll add motion evaluation and move a simple object along a linear plane.

Demo code

Click here to view the code on GitHub (opens in a new tab)

import React, { useEffect } from 'react';
import { useControls } from 'leva';
import { MathUtils, Vector3 } from 'three';

import { Arrow } from '../../components/Arrow';
import Line from '../../components/Line';
import useColors from '../../hooks/useColors';
import OrthographicScene from '../../scenes/OrthographicScene';

const Gravity = () => {
  const colors = useColors();
  const lineLength = 8;

  const [{ slope, gravity }, setState] = useControls(() => ({
    slope: {
      value: 0,
      step: 5,
      min: -180,
      max: 180,
    },
    gravity: {
      value: 9.81665,
      pad: 5,
    },
    sinSlope: {
      disabled: true,
      label: 'sin(slope)',
      pad: 5,
      value: 0,
    },
    acceleration: {
      disabled: true,
      value: 0,
      pad: 5,
    },
  }));

  useEffect(() => {
    const sinSlope = Math.sin(MathUtils.degToRad(slope));
    const acceleration = gravity * sinSlope;

    setState({ acceleration, sinSlope });
  }, [slope, gravity, setState]);

  return (
    <>
      <group position={[-5, 0, 0]} rotation={[0, 0, MathUtils.degToRad(slope)]}>
        <Arrow
          position={[-lineLength / 2, 0, 0]}
          rotation={[0, 0, Math.PI / 2]}
          color={colors.secondary}
        />

        <Line
          points={[new Vector3(-lineLength / 2, 0, 0), new Vector3(lineLength / 2, 0, 0)]}
          color={colors.secondary}
        />
      </group>
    </>
  );
};

export const GravityScene = () => {
  return (
    <OrthographicScene>
      <Gravity />
    </OrthographicScene>
  );
};