前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >threejs地球、星空、世界轮廓绘制、飞线、坐标涟漪 、旋转动画(下篇)

threejs地球、星空、世界轮廓绘制、飞线、坐标涟漪 、旋转动画(下篇)

作者头像
星宇大前端
发布2022-03-09 11:25:46
3.2K0
发布2022-03-09 11:25:46
举报
文章被收录于专栏:大宇笔记

接上篇:一个基于ThreeJS 实现的漂亮的3D 地球制作过程详解(上篇)

3D 地球成果展示

github仓库地址:https://github.com/RainManGO/3d-earth

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

支持vue/react/html 嵌入简单。

请添加图片描述
请添加图片描述

实现过程

实现步骤分解:

  1. ThreeJS环境初始化
  2. 星空背景
  3. 添加带纹理的地球
  4. 世界地图轮廓边界绘制
  5. 地球光晕
  6. 添加地球云层
  7. 城市位置标注和涟漪效果
  8. 添加飞线B样条
  9. 地球自转和镜头缩放动画

接上篇从第七步骤说起

工具函数

每个城市都是通过坐标准确的添加到地图,那么就涉及到经纬度转球面xyz坐标。

其实就是经纬度转xyz坐标系,这张图可以看明白。

在这里插入图片描述
在这里插入图片描述

工具函数代码:

代码语言:javascript
复制
export const lon2xyz = (
  radius: number,
  longitude: number,
  latitude: number
) => {
  var lon = (longitude * Math.PI) / 180; //转弧度值
  var lat = (latitude * Math.PI) / 180; //转弧度值
  lon = -lon; // three.js坐标系z坐标轴对应经度-90度,而不是90度

  // 经纬度坐标转球面坐标计算公式
  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 {
    x: x,
    y: y,
    z: z,
  };
};

城市位置标注和涟漪效果

城市位置标注添加

这里城市位置是两个长方形几何体加到地球上,需要调整下姿势。

一个贴图是是涟漪底图可以更改颜色:

在这里插入图片描述
在这里插入图片描述
  • 将拿到的经纬度数据转换成xyz坐标
  • 将带有纹理的两个几何体添加到地球上
代码语言:javascript
复制
var cityGeometry = new PlaneBufferGeometry(1, 1); //默认在XOY平面上
    var textureLoader = new TextureLoader(); // TextureLoader创建一个纹理加载器对象
    var texture = textureLoader.load(wavePng);

    // 如果不同mesh材质的透明度、颜色等属性同一时刻不同,材质不能共享
    var cityWaveMaterial = new MeshBasicMaterial({
      color: 0x22ffcc,
      map: texture,
      transparent: true, //使用背景透明的png贴图,注意开启透明计算
      opacity: 1.0,
      side: DoubleSide, //双面可见
      depthWrite: false, //禁止写入深度缓冲区数据
    });

    //城市点添加
    var pointTexture = textureLoader.load(pointPng);
    var cityPointMaterial = new MeshBasicMaterial({
      color:0xffc300,
      map: pointTexture,
      transparent: true, //使用背景透明的png贴图,注意开启透明计算
      depthWrite:false,//禁止写入深度缓冲区数据
    });

    var cityWaveMesh = new Mesh(cityGeometry, cityWaveMaterial);

下一步需要调整到地球姿势,贴合球体:

代码语言:javascript
复制
var size = earthRadius * 0.12; //矩形平面Mesh的尺寸
    (cityWaveMesh as any).size = size; //自顶一个属性,表示mesh静态大小
    cityWaveMesh.scale.set(size, size, size); //设置mesh大小
    (cityWaveMesh as any)._s = Math.random() * 1.0 + 1.0; //自定义属性._s表示mesh在原始大小基础上放大倍数  光圈在原来mesh.size基础上1~2倍之间变化

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

    // mesh姿态设置
    // mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
    var coordVec3 = new Vector3(cityXyz.x, cityXyz.y, cityXyz.z).normalize();
    // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
    var meshNormal = new Vector3(0, 0, 1);
    // 四元数属性.quaternion表示mesh的角度状态
    //.setFromUnitVectors();计算两个向量之间构成的四元数值
    cityWaveMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
    cityMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
涟漪效果动画
请添加图片描述
请添加图片描述

这个动画其实就是将几何体大小进行缩放和透明度变化,具体算法代码如下:

代码语言:javascript
复制
export const cityWaveAnimate = (WaveMeshArr: Mesh[]) => {
  // 所有波动光圈都有自己的透明度和大小状态
  // 一个波动光圈透明度变化过程是:0~1~0反复循环
  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; //2等于1/(1.5-1.0),保证透明度在0~1之间变化
    } else if (mesh._s > 1.5 && mesh._s <= 2) {
      mesh.material.opacity = 1 - (mesh._s - 1.5) * 2; //2等于1/(2.0-1.5) mesh缩放2倍对应0 缩放1.5被对应1
    } else {
      mesh._s = 1.0;
    }
  });
};

飞线添加和动画

飞线主要有三种方式

  • 贝塞尔曲线
  • 圆弧arc
  • B样条

都试了试发现B样条比较好看,使用了这个其他曲线后期会分解

主要思路:

  • 取点
  • CatmullRomCurve3合成曲线
  • flyLine 动画库完成动画

代码如下:

代码语言:javascript
复制
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;
};

// 随机个时间间隔后,再添加连线(以免同时添加连线,显示效果死板)
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));
};

// 增加城市之间飞线
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));

  //根据城市之间距离远近,取不同个数个点
  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));

  //使用B样条,将这些点拟合成一条曲线(这里没有使用贝赛尔曲线,因为拟合出来的点要在地球周围,不能穿过地球)
  var curve = new CatmullRomCurve3(curvePoints, false);

  //从B样条里获取点
  var pointCount = Math.ceil(500 * partCount);
  var allPoints = curve.getPoints(pointCount);

  //制作飞线动画
  // @ts-ignore
  var flyMesh = flyManager.addFly({
    curve: allPoints, //飞线飞线其实是N个点构成的
    color: color, //点的颜色
    width: 0.3, //点的半径
    length: Math.ceil((allPoints.length * 3) / 5), //飞线的长度(点的个数)
    speed: partCount + 10, //飞线的速度
    repeat: Infinity, //循环次数
  });

  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,
  };
};

旋转动画

旋转动画的原理主要是利用tween 动画,然后更新地球位置和轨道控制器的zoom 。

  • tween 动画控制旋转和缩放
  • 旋转到中国的时候缩放

具体代码如下:

代码语言:javascript
复制
//旋转地球动画
    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();
  }

这样就完成了一个漂亮的地球了,花了半个月的时间的研究成果。开心,希望一直保持coding,热爱分享。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 3D 地球成果展示
  • 实现过程
    • 工具函数
      • 城市位置标注和涟漪效果
        • 城市位置标注添加
        • 涟漪效果动画
      • 飞线添加和动画
        • 旋转动画
        相关产品与服务
        图像处理
        图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档