The two-body problem

Using the WebGL-based Three.js library, model and visualize the dynamics of the two-body problem, and learn to orbit, pan, and zoom the rendered scene.

The two-body problem

In the two-body problem, our hypothetical universe is composed of only two masses (point masses, to be precise). We provide the masses with some initial conditions and then let the system naturally evolve. Stated another way, the two-body problem is:

Given initial mass, position and velocity values for the two masses, predict their motions as a function of time.

Despite the fact that there is a general closed-form solution to this problem, we numerically approximate its solution in preparation for the three-body problem, which has no general closed-form solution (and therefore must be approximated).

The physics and mathematical details of the two-body problem, abstracted away in 2BodyWorker.js, are discussed in the optional The physics and equations of the two- and three-body problem section. Let's look at the two-body problem code next.

The code

The source code for the two-body problem is available through 2BodyProblem.html and 2BodyWorker.js (right-click to view source). 2BodyProblem.html is similar to 1BodyProblem.html. The only significant differences between the two are as follows:

  • A second bitmap, images/mercury.jpg, was added to the list of images to preload:

    var preloadImagePaths = ["images/jupiter.jpg", "images/mercury.jpg", "images/starField.png", "images/starField_0.15.png"];
    
  • A second fieldset element was added to acquire the second mass's initial conditions:

    <fieldset>
      <legend>Mass 2</legend>
      <table id="mass2">
        <tr>
          <td>mass:</td>
          <td>
            <input type="number" value="1E18" required="required" /></td>
        </tr>
        <tr>
          <td>x-position:</td>
          <td>
            <input type="number" value="200" required="required" /></td>
        </tr>
        <tr>
          <td>y-position:</td>
          <td>
            <input type="number" value="0" required="required" /></td>
        </tr>
        <tr>
          <td>x-velocity:</td>
          <td>
            <input type="number" value="0" required="required" /></td>
        </tr>
        <tr>
          <td>y-velocity:</td>
          <td>
            <input type="number" value="900" required="required" /></td>
        </tr>
        <tr style="display: none;">
          <td>bitmap:</td>
          <td>
            <input type="text" value="images/mercury.jpg" required="required" /></td>
        </tr>
      </table>
    </fieldset>
    
  • Just after the inclusion of three.js, we add another inclusion:

    <script src="https://rawgithub.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
    

    OrbitControls.js is part of the Three.js file set and effortlessly provides orbiting, panning, and zooming functionality by adding a few lines of additional code. In particular, in the one-time initialization code of Simulation, we create a controls object just after the camera setup code:

    gl.camera = new THREE.PerspectiveCamera(gl.cameraSpecs.viewAngle, gl.cameraSpecs.aspectRatio, gl.clippingPlane.near, gl.clippingPlane.far);
    gl.camera.position.set(0, 95, 450); // The camera starts at the origin, so move it to a good position.
    gl.camera.lookAt(gl.scene.position); // Make the camera look at the origin of the xyz-coordinate system.
    
    gl.controls = new THREE.OrbitControls(gl.camera, gl.renderer.domElement); // Allows for orbiting, panning, and zooming via OrbitsControls.js
    

    The first parameter to THREE.OrbitControls is the object to be orbited - in this case we're moving (orbiting) the camera. The second parameter specifies what object to collect mouse events on - in this case it's Three.js's renderer canvas element (previously attached to <div id="WebGLCanvasElementContainer">.

    To enable controls, its update method is repeatedly invoked in requestAnimationFrame's run function:

    var run = function () { // Public method.
      worker.postMessage({
        cmd: 'crunch' // This processing occurs between animation frames and, therefore, is assumed to take a relatively small amount of time.
      }); // worker.postMessage
      gl.controls.update(); // Allows for orbiting, panning, and zooming.
      requestAnimationFrameID = requestAnimationFrame(run); // Allow for the cancellation of this requestAnimationFrame request.
    }; // run()
    that.run = run;
    
  • To get the user's initial conditions for the second mass (m2), we use querySelectorAll again in handleSubmitButton:

    var m1 = InitialCondition(document.getElementById('mass1').querySelectorAll('input')); // A constructor returning an initial condition object.
    var m2 = InitialCondition(document.getElementById('mass2').querySelectorAll('input'));
    
  • We add m2 to the array passed to simulation.init to initialize both mass objects (to the user's entered initial conditions):

    simulation.init([m1, m2]);
    

    Within simulation.init, we transfer the implied xy-plane used in 2BodyWorker.js to the xz-plane in 2BodyProblem.html. We did this for convenience:

    worker.onmessage = function (evt) { // Process the results of the "crunch" command sent to the web worker (via this UI thread).
      for (var i = 0; i < evt.data.length; i++) {
        gl.spheres[i].position.x = evt.data[i].p.x; // Transfer the xy-plane implied in 2BodyWorker.js to the xz-plane in this UI thread.
        gl.spheres[i].position.z = evt.data[i].p.y;
        gl.spheres[i].position.y = 0; // 2BodyWorker.js is 2D (i.e., the physics are constrained to a plane).
        gl.spheres[i].rotation.y += initialConditions[i].rotation; 
      }
      gl.renderer.render(gl.scene, gl.camera);
    }; // worker.onmessage
    

By reviewing their code comments, you can see the remaining minor differences between 2BodyProblem.html and 1BodyProblem.html.

The code differences between 2BodyWorker.js and 1BodyWorker.js are significant. These differences are due to the relatively simplistic physics required for the one-body problem (constant velocity) vs. that of the two-body problem (two mutually gravitating masses). To understand these code differences, you might:

  1. Read and understand The physics and equations of the two- and three-body problem and, as appropriate, review the resources in the Related topics section of this topic.
  2. Review each line of 2BodyWorker.js, including all code comments. Be aware that the code comments within 2BodyWorker.js refer to the equation numbers presented in The physics and equations of the two- and three-body problem.

Once these difference are understood, moving from the two-body problem to the three-body problem is relatively easy, as we'll see in the next section.

Basic 3D graphics using Three.js

The one-body problem

The three-body problem

The physics and equations of the two- and three-body problem