小工具知识网

当前位置: 首页 > 行业知识 > ThreeJS——在3D地球上标记中国地图板块

行业知识

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

2026-11-03 01:14:31 来源:互联网转载

Threejs3D地球标记中国地图位置

话不多说先看实际效果

地���预览视频效果

用到的库

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

适用范围

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

设置基础场景

  
const Dom = document.querySelector("#c3d");const width = Dom.clientWidth;const height = Dom.clientHeight;

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

// 纹理加载器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;
/** * 初始化渲染器 * */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);}
// 旋转队列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       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  

动画渲染函数

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

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这个动画在后面会用到,是根据滚动下方内容进行左右镜头旋转的动画效果

星空背景

/** * 创建 方形纹理 * */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  

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

搭建 3D 地球

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

// 地球,月亮 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)  );}

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

/** * 绘制 目标点 * */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);}
/** * 绘制 两个目标点并连线 * */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     n = (globeRadius / 3) * angle;  } else if (angle  1 && angle  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);}
/** * 线上移动物体 * */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);}

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

/** * 画图 * */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  {              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 p = document.createElement("p");            p.style.color = "#fff";            p.style.fontSize = "14px";            p.style.textShadow = "1px 1px 2px #047cd6";            p.innerHTML = ``;            labelimg = new CSS2DObject(p);            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 };}
// 将角度转换为弧度function toRadians(degree) {  return degree * (Math.PI / 180);}

创建可点击跳转的 url

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

创建描边炫光路径

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   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 发生滚动时调用前面所执行的相机转场动画的效果

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();    }  }});

全局初始化

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

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

上一篇:蒜台美味秘诀,健康营养绝佳吃法大揭秘!

下一篇:怎么关推荐服务器信息提醒?