Animate a mesh across a sphere's surface
Planted:
Status: seed
Hits: 640
Intended Audience: Creative coders, Front-end developers
Tech: three.js, GSAP
How to animate a mesh across the surface of a sphere using three.js and GSAP.
Point on a sphere
We first need to define two positions on the surface to animate between.
A convenient way to do this is by using longitude and latitude as a coordinate system.
These values act as an intuitive UI (user interface) for selecting any point on the sphere.
We can then convert them to 3D coordinates using latLongToVector3.
/index.js
function latLongToVector3({latitude,longitude,center = new THREE.Vector3(...config.meshes.sphere.position),radius = config.meshes.sphere.radius}) {const { sin, cos, PI } = Math;const phi = (90 - latitude) * (PI / 180);const theta = (longitude + 180) * (PI / 180);const x = -radius * sin(phi) * cos(theta);const y = radius * cos(phi);const z = radius * sin(phi) * sin(theta);return new THREE.Vector3(x, y, z).add(center);}function moveMarker({ marker, latitude, longitude }) {const pos = latLongToVector3({ latitude, longitude });marker.position.copy(pos);}
Path
Next we need to create a path between the two positions.
This is done by calculating a series of points between them, using calcPathPoints.
I'm rendering a line (using createPath) only for visualizing purposes — the animation will only require the points.
/index.js
// Generate a series of points along the shortest path, between two positions, on a sphere's surface (great circle arc).function calcPathPoints({start,end,center = new THREE.Vector3(...config.meshes.sphere.position),radius = config.meshes.sphere.radius,segments = 64}) {const points = [];// Moves the start and end points as if the sphere's center is at (0, 0, 0).// Making it easier to work with rotations.const startLocal = start.clone().sub(center);const endLocal = end.clone().sub(center);// Unit vectors pointing from the sphere center to the positions on the surface.const startNorm = startLocal.normalize();const endNorm = endLocal.normalize();// Calculate the rotation to move from start to end.const quaternion = new THREE.Quaternion().setFromUnitVectors(startNorm,endNorm);// Loop runs from t=0 (start point) to t=1 (end point)for (let i = 0; i <= segments; i++) {const t = i / segments;// Using spherical linear interpolation (slerp) to compute a quaternion that's t percent of the way from the start to the end rotation.const stepQuat = new THREE.Quaternion().slerpQuaternions(new THREE.Quaternion(), // identity quaternion (no rotation)quaternion, // full rotation from start to endt // the fraction of how far along the path we are);// Rotates the start vector gradually towards the end vector along the great circle path.const pointLocal = startNorm.clone().applyQuaternion(stepQuat)// Multiplying by radius scales the unit vector back to the sphere's radius..multiplyScalar(radius);// Move the point so it no longer treats the sphere's center as being at (0, 0, 0). Instead use the passed in value.const pointWorld = pointLocal.add(center);points.push(pointWorld);}return points;}function createPath({ start, end }) {const { width, color } = config.meshes.path;const points = calcPathPoints({ start, end });const mesh = new Line2(new LineGeometry().setFromPoints(points),new LineMaterial({color,linewidth: width, // in world unitsresolution: new THREE.Vector2(config.viewport.width,config.viewport.height),dashed: false}));mesh.computeLineDistances();return mesh;}
Animate
Finally we add a mesh, called box, that travels along surface.
It's animated by first creating a spline using our path points.
A spline is a smooth curve that passes through or near a set of points.
Next, we use GSAP to interpolate (gradually change) a value, t, from 0 to 1.
t represents the animation's progress and what point on the spline box should be positioned at:
- â–ª
t=0: start of animation and the first point on the spline. - â–ª
t=0.5: middle of animation and the middle point on the spline. - â–ª
t=1: end of animation and the last point on the spline.
/index.js
function animateMeshAlongPath({ mesh, path, points }) {// Creates a smooth spline that passes through all points. Allowing us to interpolate any position between them smoothly.const spline = new THREE.CatmullRomCurve3(points);const tweenTarget = { t: 0 };gsap.to(tweenTarget, {t: 1,duration: 5,ease: "power1.inOut",onUpdate: () => {const t = tweenTarget.t;const point = spline.getPoint(t);const quaternion = calcMeshQuaterionAlongPath({ spline, point, t });mesh.position.copy(point);if (config.gui.orientateBox) {mesh.quaternion.copy(quaternion);}},repeat: -1,yoyo: true});}
Geometry origin
By default, a mesh's origin (pivot point) is at (0, 0, 0) in local space, which is usually the center of its geometry.
As a result, when box moves along the path, it passes through the surface of the sphere instead of resting on top of it.
To fix this, we shift box's geometry up so the bottom aligns with the mesh's local y = 0 position.
/index.js
function setOriginYBottom(geometry) {// Populate the .boundingBox property (three.js doesn't do this automatically)geometry.computeBoundingBox();// The lowest Y value of all vertices in the geometry.const yOffset = -geometry.boundingBox.min.y;geometry.translate(0, yOffset, 0);}
Mesh rotation
Another thing we need to do is rotate the mesh so it:
- â–ª faces forward along the spline (make the +Z axis point in the direction of movement) and
- â–ª sits upright on the surface of the sphere (make the +Y axis points away from the sphere's center).
We do this by calling calcMeshQuaterionAlongPath everytime the mesh moves to a new position.
/index.js
function calcMeshQuaterionAlongPath({spline,point,t,sphereCenter = new THREE.Vector3(...config.meshes.sphere.position)}) {// Create a unit vector that points forward (along the direction of movement) using the tangent at time 't' along the spline.// We'll use this so the mesh's +Z points towards the direction it's travelling.const forward = spline.getTangent(t).normalize();// Create a unit vector from the center of the sphere to the mesh's position.// A normal vector — indicates which direction a surface is facing.// We'll use this so the mesh's +Y points away from the center of the sphere.// So the mesh sits up-right relative to the sphere's surface.const up = point.clone().sub(sphereCenter).normalize();// Create a unit vector to use for the mesh's +X by calculating a direction perpendicular to both up and forward.// Required for creating a rotation matrix.const right = new THREE.Vector3().crossVectors(up, forward).normalize();// Recompute the forward vector// Due to floating-point errors, the forward vector may be misaligned after calculating up and right.// This ensures the forward vector is perpendicular to both right and up.const correctedForward = new THREE.Vector3().crossVectors(right, up).normalize();const rotationMatrix = new THREE.Matrix4().makeBasis(right, // X axisup, // Y axiscorrectedForward // Z axis);// Convert rotationMatrix to quaternionreturn new THREE.Quaternion().setFromRotationMatrix(rotationMatrix);}
Feedback
Have any feedback about this note or just want to comment on the state of the economy?