ThreeJS——在3D地球上标记中国地图板块

Threejs3D地球标记中国地图位置

先看效果

地球预览视频效果

用到的库

  1. TweenJS (动画库)用来做相机转场的动画
  2. Jquery(这里只用到一个 each 循环方法,可以使用 js 去写)
  3. ThreeJS (3D 地球制作)
  4. 100000.json(全国城市经纬度)
  5. d3.v6.js用来设置平面转3D效果(本来考虑做成3D的中国地图板块,最后因效果看起来比较美观还是考虑用线条嵌入球体的方式去实现,这里有小伙伴考虑制作3D的地图板块可以下载这个库)

适用范围

用于获取地图的位置以及到下一个目的地的总路程,可以将实际路程转成自己配置的路程,以及正在路上的标识,可以用头像表示,经过的地方可以嵌入链接点击进行跳转

设置基础场景

html 复制代码
<div id="map">
  <canvas id="c3d" class="c2d"></canvas>
</div>
<div id="demo"></div>
js 复制代码
const Dom = document.querySelector("#c3d");
const width = Dom.clientWidth;
const height = Dom.clientHeight;

如果是 Vue 写的话需要从onMounted生命周期中获取 Dom 元素

js 复制代码
// 纹理加载器
const loader = new THREE.TextureLoader();
// 渲染器
let renderer;
// 相机
let camera;
// 场景
let scene;
// 灯光
let light;
// 相机控制
let controls;
// 动画
let tween;
// 其他
let earthMesh,
  stars,
  radius,
  labelRenderer,
  label,
  labels,
  labelsable,
  labelimg;
js 复制代码
/**
 * 初始化渲染器
 * */
function initRenderer() {
  // antialias: true, alpha: true 抗锯齿设置
  renderer = new THREE.WebGLRenderer({
    canvas: Dom,
    antialias: true,
    alpha: true,
  });
  // window.devicePixelRatio 设备像素比
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);
  labelRenderer = new CSS2DRenderer();
  labelRenderer.domElement.style.position = "absolute";
  labelRenderer.domElement.style.top = "0px";
  labelRenderer.domElement.style.pointerEvents = "none";
  labelRenderer.setSize(width, height);
  document.getElementById("map").appendChild(labelRenderer.domElement);
}

/**
 * 初始化相机
 */
function initCamera() {
  camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
  camera.position.set(0, 0, 10);
  camera.lookAt(0, 0, 0);
  window.camera = camera;
}
/**
 * 初始化场景
 */
function initScene() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1c3262);
  // 雾
  // scene.fog = new THREE.Fog(0x020924, 200, 1000)
  window.scene = scene;
}

/**
 * 初始化 相机控制
 */
function initControls() {
  controls = new OrbitControls(camera, renderer.domElement);
  // 阻尼惯性
  controls.enableDamping = true;
  controls.dampingFactor = 0.1;
  controls.enableZoom = true;
  controls.autoRotate = false;
  controls.rotateSpeed = 0.1;
  controls.autoRotateSpeed = 1;

  controls.enablePan = true;
  controls.addEventListener("change", function () {
    //相机位置与目标观察点距离
    const dis = controls.getDistance();
    console.log(camera.position);
  });
}

/**
 * 初始化光
 */
function initLight() {
  // 环境光
  const ambientLight = new THREE.AmbientLight(0xcccccc, 0.2);
  scene.add(ambientLight);
  // 平行光
  let directionalLight = new THREE.DirectionalLight(0xffffff, 0.2);
  directionalLight.position.set(1, 0.1, 0).normalize();
  // 平行光2
  let directionalLight2 = new THREE.DirectionalLight(0xff2ffff, 0.2);
  directionalLight2.position.set(1, 0.1, 0.1).normalize();
  scene.add(directionalLight);
  scene.add(directionalLight2);
  // 半球光
  let hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1);
  hemiLight.position.set(0, 1, 0);
  scene.add(hemiLight);
  // 平行光3
  let directionalLight3 = new THREE.DirectionalLight(0xffffff);
  directionalLight3.position.set(1, -200, -20);
  let directionalLight4 = new THREE.DirectionalLight(0xffffff);
  directionalLight4.position.set(0, 500, 500);
  // 开启阴影
  directionalLight3.castShadow = true;
  // 设置光边界
  directionalLight3.shadow.camera.top = 18;
  directionalLight3.shadow.camera.bottom = -10;
  directionalLight3.shadow.camera.left = -52;
  directionalLight3.shadow.camera.right = 12;
  scene.add(directionalLight3);
}
js 复制代码
// 旋转队列
const rotateSlowArr = [];
// 放大并透明 队列
const bigByOpacityArr = [];
// 移动 队列
const moveArr = [];

// 边界 绘制点集合
const lines = [];
// 炫光粒子 几何体
const geometryLz = new THREE.BufferGeometry();
// 炫光粒子 透明度
let opacitys = [];
/**
 * 渲染函数
 * */
function renders(time) {
  time *= 0.0;

  // 3D对象 旋转
  // _y 初始坐标 _s 旋转速度
  rotateSlowArr.forEach((obj) => {
    obj.rotation.y = obj._y + time * obj._s;
  });
  bigByOpacityArr.forEach(function (mesh) {
    //  目标 圆环放大 并 透明
    mesh._s += 0.01;
    mesh.scale.set(1 * mesh._s, 1 * mesh._s, 1 * mesh._s);
    if (mesh._s <= 2) {
      mesh.material.opacity = 2 - mesh._s;
    } else {
      mesh._s = 1;
    }
  });
  moveArr.forEach(function (mesh) {
    mesh._s += 0.01;
    let tankPosition = new THREE.Vector3();
    tankPosition = mesh.curve.getPointAt(mesh._s % 1);
    mesh.position.set(tankPosition.x, tankPosition.y, tankPosition.z);
  });

  if (geometryLz.attributes.position) {
    geometryLz.currentPos += geometryLz.pointSpeed;
    for (let i = 0; i < geometryLz.pointSpeed; i++) {
      opacitys[(geometryLz.currentPos - i) % lines.length] = 0;
    }

    for (let i = 0; i < 200; i++) {
      opacitys[(geometryLz.currentPos + i) % lines.length] =
        i / 50 > 2 ? 2 : i / 50;
    }
    geometryLz.attributes.aOpacity.needsUpdate = true;
  }

  renderer.clear();
  labelRenderer.render(scene, camera);
  renderer.render(scene, camera);

  // requestAnimationFrame(renders)

  // earthMesh.rotation.y += 0.01
  stars.rotation.y += 0.001;
}

动画渲染函数

用于制作开始场景镜头动画(由远到近并附带略微旋转)

js 复制代码
let p1 = { x: 100, y: 200, z: 200 };
function initTWEEN() {
  let tweena = cameraCon({ x: 100, y: 200, z: 200 }, 1000);
  let tweenb = cameraCon({ x: 0, y: 0, z: 10 }, 4000);
  tweena.chain(tweenb);
  // tweenb.chain(tweenc);
  tweenb.onComplete(function () {
    console.log("结束");
    drawChart();
    //相机位置与观察目标点最小值
    controls.minDistance = 7;
    //相机位置与观察目标点最大值
    controls.maxDistance = 50;
    // 上下旋转范围
    /* 		controls.minPolarAngle = -Math.PI /6;
                        controls.maxPolarAngle = Math.PI /4; */
    // 左右旋转范围
    controls.minAzimuthAngle = -Math.PI / 6;
    controls.maxAzimuthAngle = Math.PI / 6;
  });
  tweena.start();
}
function cameraCon(p2 = { x: p1.x, y: p1.y, z: p1.z }, time = 5000) {
  var tween1 = new TWEEN.Tween(p1)
    .to(p2, time)
    .easing(TWEEN.Easing.Sinusoidal.InOut);
  var update = function () {
    camera.position.set(p1.x, p1.y, p1.z);
  };
  tween1.onUpdate(update);

  return tween1;
}
function cameraCon2(
  p2 = { x: camera.position.x, y: camera.position.y, z: camera.position.z },
  time = 2000
) {
  var tween1 = new TWEEN.Tween(camera.position)
    .to(p2, time)
    .easing(TWEEN.Easing.Sinusoidal.Out);
  var update = function () {
    camera.position.set(
      camera.position.x,
      camera.position.y,
      camera.position.z
    );
  };
  tween1.onUpdate(update);

  return tween1;
}
function animate() {
  window.requestAnimationFrame((time) => {
    if (controls) controls.update();
    TWEEN.update();
    renders(time);
    animate();
  });
}

cameraCon2这个动画在后面会用到,是根据滚动下方内容进行左右镜头旋转的动画效果

星空背景

js 复制代码
/**
 * 创建 方形纹理
 * */
function generateSprite() {
  const canvas = document.createElement("canvas");
  canvas.width = 16;
  canvas.height = 16;

  const context = canvas.getContext("2d");
  // 创建颜色渐变
  const gradient = context.createRadialGradient(
    canvas.width / 2,
    canvas.height / 2,
    0,
    canvas.width / 2,
    canvas.height / 2,
    canvas.width / 2
  );
  gradient.addColorStop(0, "rgba(255,255,255,1)");
  gradient.addColorStop(0.2, "rgba(0,255,255,1)");
  gradient.addColorStop(0.4, "rgba(0,0,64,1)");
  gradient.addColorStop(1, "rgba(0,0,0,1)");

  // 绘制方形
  context.fillStyle = gradient;
  context.fillRect(0, 0, canvas.width, canvas.height);
  // 转为纹理
  const texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;
  return texture;
}

/**
 * 背景绘制
 * */
function bg() {
  const positions = [];
  const colors = [];
  // 创建 几何体
  const geometry = new THREE.BufferGeometry();

  for (let i = 0; i < 10000; i++) {
    let vertex = new THREE.Vector3();
    vertex.x = Math.random() * 2 - 1;
    vertex.y = Math.random() * 2 - 1;
    vertex.z = Math.random() * 2 - 1;
    positions.push(vertex.x, vertex.y, vertex.z);
  }
  // 对几何体 设置 坐标 和 颜色
  geometry.setAttribute(
    "position",
    new THREE.Float32BufferAttribute(positions, 3)
  );
  // 默认球体
  geometry.computeBoundingSphere();

  // ------------- 1 ----------
  // 星星资源图片
  // ParticleBasicMaterial 点基础材质
  var starsMaterial = new THREE.ParticleBasicMaterial({
    map: generateSprite(),
    size: 2,
    transparent: true,
    opacity: 1,
    //true:且该几何体的colors属性有值,则该粒子会舍弃第一个属性--color,而应用该几何体的colors属性的颜色
    // vertexColors: true,
    blending: THREE.AdditiveBlending,
    sizeAttenuation: true,
  });
  // 粒子系统 网格
  stars = new THREE.ParticleSystem(geometry, starsMaterial);
  stars.scale.set(300, 300, 300);
  scene.add(stars);
}

此时星空就已经搭建好了,可以左右旋转试试效果

搭建 3D 地球

其实也就是创建一个球,然后贴个准确的贴图放在圆上调整一些光感即可

js 复制代码
// 地球,月亮 3D层
const landOrbitObject = new THREE.Object3D();
// 地球3D层
const earthObject = new THREE.Object3D();
// 月亮3D层
const moonObject = new THREE.Object3D();
// 地球半径
const globeRadius = 5;
/**
 * 球相关加载
 * */
function earth() {
  radius = globeRadius;
  const widthSegments = 100;
  const heightSegments = 100;
  const sphereGeometry = new THREE.SphereGeometry(
    radius,
    widthSegments,
    heightSegments
  );
  function shine() {
    var texture = loader.load("./images/blue.png");
    var spriteMaterial = new THREE.SpriteMaterial({
      map: texture,
      transparent: true,
      opacity: 0.5,
      depthWrite: false,
    });
    var sprite = new THREE.Sprite(spriteMaterial);
    sprite.scale.set(radius * 3, radius * 3, 1);
    sprite.rotation.set(-Math.PI / 2, 0, 0);
    scene.add(sprite);
  }
  shine();

  // 地球
  const earthTexture = loader.load("./images/微信图片_20230711093004 (1).jpg");
  const earthMaterial = new THREE.MeshStandardMaterial({
    map: earthTexture,
  });
  earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial);

  // 月球
  const moonTexture = loader.load("./images/yueqiu.jpg");
  const moonMaterial = new THREE.MeshPhongMaterial({ map: moonTexture });
  const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial);
  moonMesh.scale.set(0.1, 0.1, 0.1);
  moonMesh.position.x = 10;

  moonObject.add(moonMesh);
  // 加入动画队列
  moonObject._y = 0;
  moonObject._s = 1;
  rotateSlowArr.push(moonObject);

  // 地球加入 地球3D层
  earthObject.add(earthMesh);
  earthObject.rotation.set(0.5, 2.9, 0.1);
  earthObject._y = 2.0;
  earthObject._s = 0.1;
  // 加入动画队列
  // rotateSlowArr.push(earthObject)

  // 加入 地球3D层
  landOrbitObject.add(earthObject);
  // 加入 月亮3D层
  landOrbitObject.add(moonObject);

  scene.add(landOrbitObject);
}

/**
 * 经维度 转换坐标
 * THREE.Spherical 球类坐标
 * lng:经度
 * lat:维度
 * radius:地球半径
 */
function lglt2xyz(lng, lat, radius) {
  // 以z轴正方向为起点的水平方向弧度值
  const theta = (90 + lng) * (Math.PI / 180);
  // 以y轴正方向为起点的垂直方向弧度值
  const phi = (90 - lat) * (Math.PI / 180);
  return new THREE.Vector3().setFromSpherical(
    new THREE.Spherical(radius, phi, theta)
  );
}

绘制红色圆点和点与点之间的飞线效果

js 复制代码
/**
 * 绘制 目标点
 * */
function spotCircle(spot) {
  // 圆
  const geometry1 = new THREE.CircleGeometry(0.02, 100);
  const material1 = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    side: THREE.DoubleSide,
  });
  const circle = new THREE.Mesh(geometry1, material1);
  circle.position.set(spot[0], spot[1], spot[2]);
  // mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
  var coordVec3 = new THREE.Vector3(spot[0], spot[1], spot[2]).normalize();
  // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
  var meshNormal = new THREE.Vector3(0, 0, 1);
  // 四元数属性.quaternion表示mesh的角度状态
  //.setFromUnitVectors();计算两个向量之间构成的四元数值
  circle.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  earthObject.add(circle);

  // 圆环
  const geometry2 = new THREE.RingGeometry(0.03, 0.04, 100);
  // transparent 设置 true 开启透明
  const material2 = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    side: THREE.DoubleSide,
    transparent: true,
  });
  const circleY = new THREE.Mesh(geometry2, material2);
  circleY.position.set(spot[0], spot[1], spot[2]);

  // 指向圆心
  circleY.lookAt(new THREE.Vector3(0, 0, 0));
  earthObject.add(circleY);
  label.position.set(spot[0] - 0.15, spot[1] + 0.04, spot[2]);

  // 加入动画队列
  bigByOpacityArr.push(circleY);
}
js 复制代码
/**
 * 绘制 两个目标点并连线
 * */
function lineConnect(posStart, posEnd) {
  const v0 = lglt2xyz(posStart[0], posStart[1], globeRadius);
  const v3 = lglt2xyz(posEnd[0], posEnd[1], globeRadius);

  // angleTo() 计算向量的夹角
  const angle = v0.angleTo(v3);
  let vtop = v0.clone().add(v3);
  // multiplyScalar 将该向量与所传入的 标量进行相乘
  vtop = vtop.normalize().multiplyScalar(globeRadius);

  let n;
  if (angle <= 1) {
    n = (globeRadius / 3) * angle;
  } else if (angle > 1 && angle < 2) {
    n = (globeRadius / 3) * Math.pow(angle, 2);
  } else {
    n = (globeRadius / 3) * Math.pow(angle, 1.5);
  }

  const v1 = v0
    .clone()
    .add(vtop)
    .normalize()
    .multiplyScalar(globeRadius + n);
  const v2 = v3
    .clone()
    .add(vtop)
    .normalize()
    .multiplyScalar(globeRadius + n);
  // 三维三次贝塞尔曲线(v0起点,v1第一个控制点,v2第二个控制点,v3终点)
  const curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3);

  // 绘制 目标位置
  spotCircle([v0.x, v0.y, v0.z]);
  spotCircle([v3.x, v3.y, v3.z]);
  //   线上移动物体
  moveSpot(curve);

  const lineGeometry = new THREE.BufferGeometry();
  // 获取曲线 上的50个点
  var points = curve.getPoints(50);
  var positions = [];
  var colors = [];
  var color = new THREE.Color();

  // 给每个顶点设置演示 实现渐变
  for (var j = 0; j < points.length; j++) {
    if (j < 25 || j == 25) {
      color.set(0xffffff); // 粉色
    } else if (j < 50 && j > 25) {
      color.set(0xfffdaa); // 粉色
    }

    colors.push(color.r, color.g, color.b);
    positions.push(points[j].x, points[j].y, points[j].z);
  }
  // 放入顶点 和 设置顶点颜色
  lineGeometry.addAttribute(
    "position",
    new THREE.BufferAttribute(new Float32Array(positions), 3, true)
  );
  lineGeometry.addAttribute(
    "color",
    new THREE.BufferAttribute(new Float32Array(colors), 3, true)
  );

  const material = new THREE.LineBasicMaterial({
    vertexColors: true,
    side: THREE.DoubleSide,
  });
  const line = new THREE.Line(lineGeometry, material);
  earthObject.add(line);
}
js 复制代码
/**
 * 线上移动物体
 * */
function moveSpot(curve) {
  // 线上的移动物体
  const aGeo = new THREE.SphereGeometry(0.04, 0.04, 0.04);
  const aMater = new THREE.MeshPhongMaterial({
    color: 0xff0000,
    side: THREE.DoubleSide,
  });
  const aMesh = new THREE.Mesh(aGeo, aMater);
  // 保存曲线实例
  aMesh.curve = curve;
  aMesh._s = 0;

  moveArr.push(aMesh);
  earthObject.add(aMesh);
}

用画布渲染城市信息以及每个点到点的路程的总距离

js 复制代码
/**
 * 画图
 * */

function drawChart() {
  const loader = new THREE.FileLoader();
  let centers;
  loader.load("./js/100000_full.json", (data) => {
    // 点与点
    let objName = [
      { name: "黑龙江省", url: "https://www.baidu.com" },
      { name: "内蒙古自治区" },
      { name: "四川省" },
      { name: "" },
    ];
    // 线与线
    let city = [{ to: "黑龙江省" }, { to: "内蒙古自治区" }, { to: "四川省" }];
    // 当前所在地
    let location = [{ type: 0 }];
    const jsondata = JSON.parse(data);
    let transformedData = [];
    // 循环
    $.each(jsondata.features, function (index, item) {
      const { centroid, center, name } = item.properties;
      /*    const point = centroid || center || [0, 0];
                       const depth = Math.random() * 0.3 + 0.3; */
      let proName = item.properties.name;
      let proName1 = item.properties.name;
      centers = item.properties.center;
      objName.forEach((v) => {
        if (v.name == proName) {
          labels = createLabel(name, v.url);
          earthObject.add(labels);
          if (centers != undefined) {
            let markPos = lglt2xyz(centers[0], centers[1], 5);
            spotCircle([markPos.x, markPos.y, markPos.z]);
          }

          // lineConnect(item.properties.center,[150,100,100])
        }
      });
      let lastIndex = city.length - 2;
      city.map((v, index) => {
        if (v.to == proName1) {
          v.tojwd = item.properties.center;
          let indexNum = index + 1;
          if (indexNum < city.length) {
            let formCity = city[index];
            let toCity = city[index + 1];
            setTimeout(() => {
              let combinedCity = {
                to: formCity.to,
                tojwd: v.tojwd,
                form: toCity.to,
                formjwd: toCity.tojwd,
              };

              if (combinedCity.formjwd != []) {
                let distance = getDistance(
                  combinedCity.tojwd[0],
                  combinedCity.tojwd[1],
                  combinedCity.formjwd[0],
                  combinedCity.formjwd[1]
                );
                combinedCity.dist = distance;
                transformedData.push(combinedCity);

                const coordinates = kilometersToCoordinates(
                  distance,
                  combinedCity.tojwd[1],
                  combinedCity.tojwd[0]
                );
                console.log(
                  "从" +
                    combinedCity.to +
                    "到" +
                    combinedCity.form +
                    "的距离为" +
                    distance +
                    "公里"
                ); // 输出两个坐标之间的距离,单位为公里

                // 示例用法
                if (index == lastIndex) {
                  sessionStorage.setItem("last", JSON.stringify(combinedCity));
                  let par = JSON.parse(sessionStorage.getItem("last"));
                } else {
                  lineConnect(combinedCity.tojwd, combinedCity.formjwd);
                }
                // lineConnect(transformedData[0].tojwd, transformedData[0].formjwd)
              }
            }, 100);
          }
        }
      });
      location.forEach((i) => {
        if (i.type == item.properties.subFeatureIndex) {
          setTimeout(() => {
            let last = JSON.parse(sessionStorage.getItem("last"));
            console.log(last.dist);
            // 更换自定义路线(公里)
            last.dist = 300;
            // 将经纬度转换为三维坐标
            const point1 = last.tojwd;
            const point2 = last.formjwd;
            // 个人的总步数(公里)
            let lucheng = 100;
            let bfb = lucheng / last.dist;
            let s = point1[0] + (point2[0] - point1[0]) * bfb * 1;
            let d = point1[1] + (point2[1] - point1[1]) * bfb * 1;
            const div = document.createElement("div");
            div.style.color = "#fff";
            div.style.fontSize = "14px";
            div.style.textShadow = "1px 1px 2px #047cd6";
            div.innerHTML = `<img style="width:30px;border-radius:50%" src="./images/head.jpg" />`;
            labelimg = new CSS2DObject(div);
            labelimg.scale.set(0.01, 0.01, 0.01);

            let markPos = lglt2xyz(s, d, 5);
            labelimg.position.set(markPos.x - 0.1, markPos.y + 0.04, markPos.z);
            earthObject.add(labelimg);

            // 定义距离为1000公里
            setTimeout(() => {
              lineConnect(point1, [s, d]);
            }, 101);
          }, 200);
        }
      });
    });
  });
}
function getDistance(lat1, lon1, lat2, lon2) {
  const R = 6371; // 地球半径,单位为公里

  const rLat1 = toRadians(lat1);
  const rLat2 = toRadians(lat2);
  const deltaLat = toRadians(lat2 - lat1);
  const deltaLon = toRadians(lon2 - lon1);

  const a =
    Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
    Math.cos(rLat1) *
      Math.cos(rLat2) *
      Math.sin(deltaLon / 2) *
      Math.sin(deltaLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  const distance = R * c;
  return distance;
}
function kilometersToCoordinates(distance, latitude, longitude) {
  const earthRadius = 6371.393; // 地球半径,单位为公里

  // 计算纬度差值
  const latDiff = distance / earthRadius;

  // 根据纬度计算经度差值
  const lonDiff =
    distance / (earthRadius * Math.cos((Math.PI * latitude) / 180));

  // 计算新的经度和纬度
  const newLatitude = latitude + latDiff * (180 / Math.PI);
  const newLongitude = longitude + lonDiff * (180 / Math.PI);

  // 返回经度和纬度
  return { latitude: newLatitude, longitude: newLongitude };
}
js 复制代码
// 将角度转换为弧度
function toRadians(degree) {
  return degree * (Math.PI / 180);
}

创建可点击跳转的 url

js 复制代码
const createLabel = (name, url) => {
  const div = document.createElement("div");
  div.style.color = "#fff";
  div.style.fontSize = "10px";
  div.style.textShadow = "1px 1px 2px #047cd6";
  div.textContent = name;
  div.style.pointerEvents = "auto";
  div.style.cursor = "pointer";
  label = new CSS2DObject(div);
  div.addEventListener("click", function (event) {
    if (url != undefined) {
      window.location.href = url;
    } else {
      return;
    }
  });
  label.scale.set(0.01, 0.01, 0.01);
  return label;
};

创建描边炫光路径

js 复制代码
const vertexShader = `
			attribute float aOpacity;
			uniform float uSize;
			varying float vOpacity;
			void main(){
				gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
				gl_PointSize = uSize;
	
				vOpacity=aOpacity;
			}
			`;
const fragmentShader = `
			  varying float vOpacity;
			  uniform vec3 uColor;
	
			  float invert(float n){
				  return 1.-n;
			  }
	
			  void main(){
				if(vOpacity <=0.2){
					discard;
				}
				vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
				vec2 cUv=2.*uv-1.;
				vec4 color=vec4(1./length(cUv));
				color*=vOpacity;
				color.rgb*=uColor;
	
				gl_FragColor=color;
			  }
			  `;

/**
 * 边界炫光路径
 * */
function dazzleLight() {
  const loader = new THREE.FileLoader();
  loader.load("./js/100000.json", (data) => {
    const jsondata = JSON.parse(data);
    console.log(jsondata);
    // 中国边界
    const feature = jsondata.features[0];
    const province = new THREE.Object3D();
    province.properties = feature.properties.name;
    // 点数据
    const coordinates = feature.geometry.coordinates;

    coordinates.forEach((coordinate) => {
      // coordinate 多边形数据
      coordinate.forEach((rows) => {
        // 绘制线
        // const line = lineDraw(rows, 0xaa381e)
        const line = lineDraw(rows, 0xaa381e);
        province.add(line);
      });
    });
    // 添加地图边界
    earthObject.add(province);

    // 拉平 为一维数组
    const positions = new Float32Array(lines.flat(1));
    // 设置顶点
    geometryLz.setAttribute(
      "position",
      new THREE.BufferAttribute(positions, 3)
    );

    // 设置 粒子透明度为 0
    opacitys = new Float32Array(positions.length).map(() => 0);

    geometryLz.setAttribute("aOpacity", new THREE.BufferAttribute(opacitys, 1));

    geometryLz.currentPos = 0;
    // 炫光移动速度
    geometryLz.pointSpeed = 10;

    // 控制 颜色和粒子大小
    const params = {
      pointSize: 2.0,
      pointColor: "#4ec0e9",
    };

    // 创建着色器材质
    const material = new THREE.ShaderMaterial({
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      transparent: true, // 设置透明
      uniforms: {
        uSize: {
          value: params.pointSize,
        },
        uColor: {
          value: new THREE.Color(params.pointColor),
        },
      },
    });
    const points = new THREE.Points(geometryLz, material);
    earthObject.add(points);
  });
}

/**
 * 边框 图形绘制
 * @param polygon 多边形 点数组
 * @param color 材质颜色
 * */
let indexBol = true;
function lineDraw(polygon, color) {
  const lineGeometry = new THREE.BufferGeometry();
  const pointsArray = new Array();
  polygon.forEach((row) => {
    // 转换坐标
    const xyz = lglt2xyz(row[0], row[1], globeRadius);
    // 创建三维点
    pointsArray.push(xyz);

    if (indexBol) {
      // 为了好看 这里只要内陆边界
      lines.push([xyz.x, xyz.y, xyz.z]);
    }
  });
  indexBol = false;
  // 放入多个点
  lineGeometry.setFromPoints(pointsArray);
  const lineMaterial = new THREE.LineBasicMaterial({
    color: color,
  });
  return new THREE.Line(lineGeometry, lineMaterial);
}

最后当 ul 中的 li 发生滚动时调用前面所执行的相机转场动画的效果

js 复制代码
let { scrollY } = window;
let currentSection = 0;
window.addEventListener("scroll", () => {
  scrollY = window.scrollY;
  const newSection = Math.round(scrollY / height);
  console.log(newSection);
  console.log(camera.position);
  if (newSection !== currentSection) {
    currentSection = newSection;
    console.log(currentSection);
    if (currentSection == 0) {
      currentSection = newSection;
      // console.log('changed', currentSection)
      let tweena = cameraCon2(camera.position, 1000);
      console.log(camera.position);
      let tweenb = cameraCon2({ x: 0, y: 1, z: 10 }, 1000);
      tweena.chain(tweenb);
      tweena.start();
    } else {
      currentSection = newSection;
      // console.log('changed', currentSection)
      let tweena = cameraCon2(camera.position, 1000);
      console.log(camera.position);
      let tweenb = cameraCon2({ x: 0, y: -1, z: 10 }, 1000);
      tweena.chain(tweenb);
      tweena.start();
    }
  }
});

全局初始化

js 复制代码
window.onload = () => {
  // 初始化
  initTWEEN();
  initRenderer();
  initCamera();
  initScene();
  initLight();
  initControls();
  // 绘制
  bg();
  earth();
  dazzleLight();
  // 渲染
  animate();
};

完整代码地址 earthling ,此项目仅用于交流学习

相关推荐
hackeroink2 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.5 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235246 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css