threejs earth, starry sky, world outline drawing, flying line, coordinate ripple, rotation animation (Part 2)

Posted by Archangel915 on Wed, 09 Mar 2022 04:52:34 +0100

Continued: Detailed explanation of a beautiful 3D Earth production process based on ThreeJS (Part I)

3D Earth achievement display

github warehouse address: https://github.com/RainManGO/3d-earth

npm: https://www.npmjs.com/package/3d-earth

Support vue/react/html, easy to embed.

Implementation process

Decomposition implementation steps:

  1. ThreeJS environment initialization
  2. Starry background
  3. Add textured earth
  4. World map contour boundary drawing
  5. Earth halo
  6. Add earth clouds
  7. City Location annotation and ripple effect
  8. Add Flyline B-spline
  9. Earth rotation and zoom animation

The last part starts from the seventh step

Tool function

Each city is accurately added to the map through coordinates, which involves the transformation of longitude and latitude to spherical xyz coordinates.

In fact, it is the longitude and latitude to xyz coordinate system. This picture can be seen clearly.

Tool function code:

export const lon2xyz = (
  radius: number,
  longitude: number,
  latitude: number
) => {
  var lon = (longitude * Math.PI) / 180; //Radian value
  var lat = (latitude * Math.PI) / 180; //Radian value
  lon = -lon; // three. The z axis of JS coordinate system corresponds to longitude - 90 degrees, not 90 degrees

  // Calculation formula of longitude and latitude coordinates to spherical coordinates
  var x = radius * Math.cos(lat) * Math.cos(lon);
  var y = radius * Math.sin(lat);
  var z = radius * Math.cos(lat) * Math.sin(lon);
  // Return spherical coordinates
  return {
    x: x,
    y: y,
    z: z,
  };
};

City Location annotation and ripple effect

City Location labels adding

Here, the city position is added to the earth by two rectangular geometries, and the posture needs to be adjusted.

A map is a ripple underlay that can change color:

  • Convert the obtained longitude and latitude data into xyz coordinates
  • Add two textured geometry to the earth
var cityGeometry = new PlaneBufferGeometry(1, 1); //Default on XOY plane
    var textureLoader = new TextureLoader(); // TextureLoader creates a texture loader object
    var texture = textureLoader.load(wavePng);

    // If the transparency, color and other attributes of different mesh materials are different at the same time, the materials cannot be shared
    var cityWaveMaterial = new MeshBasicMaterial({
      color: 0x22ffcc,
      map: texture,
      transparent: true, //Use the png map with transparent background, and pay attention to turn on transparent calculation
      opacity: 1.0,
      side: DoubleSide, //Visible on both sides
      depthWrite: false, //Prohibit writing depth buffer data
    });

    //City point addition
    var pointTexture = textureLoader.load(pointPng);
    var cityPointMaterial = new MeshBasicMaterial({
      color:0xffc300,
      map: pointTexture,
      transparent: true, //Use the png map with transparent background, and pay attention to turn on transparent calculation
      depthWrite:false,//Prohibit writing depth buffer data
    });

    var cityWaveMesh = new Mesh(cityGeometry, cityWaveMaterial);

The next step is to adjust to the earth posture and fit the sphere:

var size = earthRadius * 0.12; //Dimension of rectangular plane Mesh
    (cityWaveMesh as any).size = size; //An attribute representing the static size of mesh
    cityWaveMesh.scale.set(size, size, size); //Set mesh size
    (cityWaveMesh as any)._s = Math.random() * 1.0 + 1.0; //Custom properties_ s represents the magnification of mesh based on the original size, and the aperture is in the original mesh 1 ~ 2 times based on size

    cityWaveMesh.position.set(cityXyz.x, cityXyz.y, cityXyz.z);
    cityMesh.position.set(cityXyz.x, cityXyz.y, cityXyz.z)

    // mesh pose settings
    // The normal direction of mesh on the spherical surface (the direction vector composed of the spherical center and spherical coordinates)
    var coordVec3 = new Vector3(cityXyz.x, cityXyz.y, cityXyz.z).normalize();
    // mesh is on the XOY plane by default, and the normal direction is new three along the z axis Vector3(0, 0, 1)
    var meshNormal = new Vector3(0, 0, 1);
    // quaternion attribute quaternion indicates the angular state of mesh
    //.setFromUnitVectors(); Calculate the quaternion value formed between two vectors
    cityWaveMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
    cityMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);

Ripple effect animation

In fact, this animation is to scale the geometry size and change the transparency. The specific algorithm code is as follows:

export const cityWaveAnimate = (WaveMeshArr: Mesh[]) => {
  // All fluctuating apertures have their own transparency and size state
  // A fluctuating aperture transparency change process is: 0 ~ 1 ~ 0 repeated cycle
  WaveMeshArr.forEach(function (mesh:any) {
    mesh._s += 0.007;
    mesh.scale.set(
      mesh.size * mesh._s,
      mesh.size * mesh._s,
      mesh.size * mesh._s
    );
    if (mesh._s <= 1.5) {
      mesh.material.opacity = (mesh._s - 1) * 2; //The transparency is guaranteed to vary between 0-1.5 / (1-2), which is equal to 1
    } else if (mesh._s > 1.5 && mesh._s <= 2) {
      mesh.material.opacity = 1 - (mesh._s - 1.5) * 2; //2 equals 1/(2.0-1.5) mesh scaling 2 times corresponds to 0 scaling 1.5 corresponds to 1
    } else {
      mesh._s = 1.0;
    }
  });
};

Fly line addition and animation

There are three main ways to fly the line

  • Bessel curve
  • Arc arc
  • B-spline

After trying, I found that the B-spline is more beautiful. After using this, other curves will decompose later

Main ideas:

  • Take point
  • CatmullRomCurve3 composite curve
  • flyLine animation library completes animation

The code is as follows:

import { FlyData, City } from "../types/index";
import { InitFlyLine } from "../tools/flyLine";
import { lon2xyz } from "../tools/index";
import { earthRadius } from "../config/index";
import { Vector3, CatmullRomCurve3, Object3D } from "three";
import pointPng from "../img/point.png";

export const earthAddFlyLine = (
  earth: Object3D,
  flyLineData: FlyData[],
  cityList: Record<string, City>
) => {
  let flyManager: InitFlyLine = null;

  if (flyManager == null) {
    flyManager = new InitFlyLine({
      texture: pointPng,
    });
  }

  for (var i = 0; i < flyLineData.length; i++) {
    var flyLine = flyLineData[i];
    for (var j = 0; j < flyLine.to.length; j++) {
      randomAddFlyLine(
        earth,
        flyManager,
        cityList[flyLine.from],
        cityList[flyLine.to[j]],
        flyLine.color
      );
    }
  }

  return flyManager;
};

// Add the connection after a random time interval (to avoid adding the connection at the same time and the display effect is rigid)
const randomAddFlyLine = (
  earth: Object3D,
  flyManager: InitFlyLine,
  fromCity: City,
  toCity: City,
  color: string
) => {
  setTimeout(function () {
    addFlyLine(earth, flyManager,fromCity, toCity, color);
  }, Math.ceil(Math.random() * 15000));
};

// Increase flying lines between cities
const addFlyLine = (
  earth: Object3D,
  flyManager: InitFlyLine,
  fromCity: City,
  toCity: City,
  color: string
) => {
  var coefficient = 1;
  var curvePoints = new Array();
  var fromXyz = lon2xyz(earthRadius, fromCity.longitude, fromCity.latitude);
  var toXyz = lon2xyz(earthRadius, toCity.longitude, toCity.latitude);
  curvePoints.push(new Vector3(fromXyz.x, fromXyz.y, fromXyz.z));

  //Take different points according to the distance between cities
  var distanceDivRadius =
    Math.sqrt(
      (fromXyz.x - toXyz.x) * (fromXyz.x - toXyz.x) +
        (fromXyz.y - toXyz.y) * (fromXyz.y - toXyz.y) +
        (fromXyz.z - toXyz.z) * (fromXyz.z - toXyz.z)
    ) / earthRadius;
  var partCount = 3 + Math.ceil(distanceDivRadius * 3);
  for (var i = 0; i < partCount; i++) {
    var partCoefficient =
      coefficient + (partCount - Math.abs((partCount - 1) / 2 - i)) * 0.01;
    var partTopXyz = getPartTopPoint(
      {
        x:
          (fromXyz.x * (partCount - i)) / partCount +
          (toXyz.x * (i + 1)) / partCount,
        y:
          (fromXyz.y * (partCount - i)) / partCount +
          (toXyz.y * (i + 1)) / partCount,
        z:
          (fromXyz.z * (partCount - i)) / partCount +
          (toXyz.z * (i + 1)) / partCount,
      },
      earthRadius,
      partCoefficient
    );
    curvePoints.push(new Vector3(partTopXyz.x, partTopXyz.y, partTopXyz.z));
  }
  curvePoints.push(new Vector3(toXyz.x, toXyz.y, toXyz.z));

  //Use B-spline to fit these points into a curve (Bessel curve is not used here, because the fitted points must be around the earth and cannot pass through the earth)
  var curve = new CatmullRomCurve3(curvePoints, false);

  //Get points from B-spline
  var pointCount = Math.ceil(500 * partCount);
  var allPoints = curve.getPoints(pointCount);

  //Making Flywire animation
  // @ts-ignore
  var flyMesh = flyManager.addFly({
    curve: allPoints, //The flying line is actually composed of N points
    color: color, //Point color
    width: 0.3, //Radius of point
    length: Math.ceil((allPoints.length * 3) / 5), //Length of flying line (number of points)
    speed: partCount + 10, //Flying line speed
    repeat: Infinity, //Number of cycles
  });

  earth.add(flyMesh);
};

const getPartTopPoint = (
  innerPoint: { x: number; y: number; z: number },
  earthRadius: number,
  partCoefficient: number
) => {
  var fromPartLen = Math.sqrt(
    innerPoint.x * innerPoint.x +
      innerPoint.y * innerPoint.y +
      innerPoint.z * innerPoint.z
  );
  return {
    x: (innerPoint.x * partCoefficient * earthRadius) / fromPartLen,
    y: (innerPoint.y * partCoefficient * earthRadius) / fromPartLen,
    z: (innerPoint.z * partCoefficient * earthRadius) / fromPartLen,
  };
};

Rotate animation

The principle of rotation animation is to use the tweet animation, and then update the zoom of the earth position and orbit controller.

  • The tweet animation controls rotation and scaling
  • Zoom when rotating to China

The specific codes are as follows:

//Rotating earth animation
    var rotateEarthStep = new TWEEN.Tween({
      rotateY: startRotateY,
      zoom: startZoom,
    })
      .to({ rotateY: endRotateY, zoom: endZoom }, 36000) //.to({rotateY: endRotateY, zoom: endZoom}, 10000)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(function (object: any) {
        if (that.earth3dObj) {
          that.earth3dObj.rotation.set(0, object.rotateY, 0);
        }
        (that.orbitControl as any).zoom0 = object.zoom < 1 ? 1 : object.zoom;
        that.orbitControl.reset();
      });

    var rotateEarthStepBack = new TWEEN.Tween({
      rotateY: endRotateY,
      zoom: endZoom,
    })
      .to({ rotateY: 3.15 * Math.PI * 2, zoom: startZoom }, 36000) //.to({rotateY: endRotateY, zoom: endZoom}, 10000)
      .easing(TWEEN.Easing.Quadratic.Out)
      .onUpdate(function (object: any) {
        if (that.earth3dObj) {
          that.earth3dObj.rotation.set(0, object.rotateY, 0);
        }
        (that.orbitControl as any).zoom0 = object.zoom < 1 ? 1 : object.zoom;
        that.orbitControl.reset();
      });

    rotateEarthStep.chain(rotateEarthStepBack);
    rotateEarthStepBack.chain(rotateEarthStep);

    rotateEarthStep.start();
  }

In this way, a beautiful earth has been completed, and it took half a month to complete the research results. Happy, hope to keep coding and love sharing.