We have what we need to build the helical path Sun traces over Earth’s surface.
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.