Tracing Earth

We have what we need to build the helical path Sun traces over Earth’s surface.

Tracing Earth

We represent the helical path by a line path that gets extended with new line segments over time, as the earth spins and orbits. With every frame refresh — each time the model has a chance to update — a new ‘marker’ point p is computed in earth’s local coordinate system, and the current path’s last endpoint is connected to p with a line segment. We calculate successive marker points using the markPath function of the previous program.

The following function, updateModel, gets called whenever the user changes earth’s inclination or length of day. It disposes of the current spiral path and replaces it with the start of a new one. The global variable revolutionRps is number of revolutions around the sun per second. We have set this to 1/60 so that our earth orbits the sun once every minute. The global variable revolvingEarth references the Object3D node returned by the revolveEarth function defined on the Revolving Earth page. And the variable earth is revolvingEarth‘s child in whose local space marker points get computed. updateModel defines two parameters:

  • dpy is the number of days per year: earth spins on its axis dpy full rotations per orbit around the sun.
  • tilt is Earth’s angle of inclination.
function updateModel(dpy, tilt) {
    let rps = (dpy + 1) * revolutionRps;
    earth.rps = rps;
    revolvingEarth.rotation.z = degreesToRadians(tilt);
    earth.remove(spiral);
    spiral.geometry.dispose();
    let matArgs = {color: 0x00ff00};
    spiral = createSpiral(earth, orbitRad, earthRad, matArgs, 1000);
    earth.add(spiral);
}

Earth rotations per second (rps computed on line 2) is proportional to dps+1. Since dpy is the number of rotations in earth’s own local space, we add one more revolution due to the earth’s revolution around sun. (For example, if dps=0, earth undergoes a single day over the course of a year due to its fixed axis.) The rest of the function adjusts earth’s tilt, disposes of the old spiral, and creates a new spiral which gets added to earth’s local system.

Spiral creation and update is handled by the createSpiral function. This function does two things: it initializes a new spiral path, and it defines an update function which it attaches to the spiral path. This update function gets called repeatedly over time (with every frame refresh) to extend the spiral.

CreateSpiral has six parameters. The first three — earth, orbitRad, and earthRad — are needed by the markEarth function to calculate marker points. The remaining three are used for the spiral:

  • matArgs are the material arguments for the spiral path.
  • maxPoints is the maximum number of marker points to include in the spiral path. Once the path contains this many points (i.e. line segments), for each new point that gets added, the oldest point on the path gets removed. This ensures that the spiral path does not become too large.
  • eps, for epsilon, is the amount that the path gets lifted above Earth’s surface to ensure it is visible.
function createSpiral(earth, orbitRad, earthRad, matArgs={}, maxPoints=1000, eps=0.01) {
    let geom = new THREE.BufferGeometry();
    let positions = new Float32Array(maxPoints * 3);
    geom.attributes.position = new THREE.BufferAttribute(positions, 3);
    let drawCount = 0;
    let mat = new THREE.LineBasicMaterial(matArgs);
    let spiral = new THREE.LineLoop(geom, mat, THREE.LineStrip);
    let indx = 0;
    function updateSpiral(delta) {
        let p = markEarth(earth, orbitRad, earthRad + eps);
        // insert point p into the geometry
        if (drawCount < maxPoints) {
            positions[indx++] = p.x;
            positions[indx++] = p.y;
            positions[indx++] = p.z;
            drawCount += 1;
            geom.setDrawRange(0, drawCount);
        // replace oldest point by point p
        } else {
            indx %= maxPoints * 3;
            positions[indx++] = p.x;
            positions[indx++] = p.y;
            positions[indx++] = p.z;
        }
        geom.attributes.position.needsUpdate = true;
    }
    // register the spiral for updates
    spiral.update = updateSpiral;
    subject.register(spiral);
    return spiral;
}

The spiral’s geometry is maintained in a BufferGeometry, an efficient version of the three.js Geometry class. Points are stored in the array positions of floats. The updateSpiral function first computes the new marker point p (line 10), and then updates array position in either of two ways. If the array is not yet full (line 12), it inserts the x, y, and z values of point p into the array. Alternatively (line 19), it replaces values of the oldest point in the array (at position indx) by the x, y, and z values of p.

Observe that the spiral itself is represented by a LineLoop constructed on the points in array positions (line 7). This loop contains a line segment that we don’t see since it passes through earth’s interior. The alternative, representing the spiral as a non-closed polyline, would require values to be shifted along the positions array with every update, which would be less efficient. You can see this hidden line segment by zooming into earth’s interior.