如何用 Three.js 做 3D 地球仪

1,172 阅读21分钟

首先看下最终呈现效果

1.png

看下 gif 效果 (可以用OBS Studio 录屏和嗨格式转换器转成 gif)

2025-02-24 11-28-43.gif

主要功能点包括

  1. 自动旋转
  2. 扫光动画
  3. 卫星环绕
  4. 地球大气层云朵飘飘特效
  5. 涟漪波纹动画
  6. 飞线标注动画
  7. 文本标注
  8. 点击交互

技术选型考量:

首先,echart-gl 其实也是可以实现 3D 地球的,应该说大部分功能,echarts 都能实现,但是 echarts 偏向于实用,很多动画特效、很多炫酷的效果,用 echarts 来实现比较吃力,总体上,视觉效果远不如 three.js 。但是,用 three.js 来做 3d 地图,思路是很好,但这里面可能涉及到很多 webGL 方面的知识点、空间几何数学知识点、动画分析和图片分解的能力,总体上难度会更大一点,也会更有挑战性,效果也会更惊艳。

一、创建基本场景

用 three.js 来做世界地图,需要初始化 3D 场景,并将需要的物体加入到这个场景当中,基本元素包括:

  1. Scene —— 即3d场景,后续所有内容都会加入到场景当中,可以理解为一个大容器;
  2. Camera 相机 —— 可以理解为人的眼睛,可以设置相机的位置,模拟人眼在某个角度去看待 3D 世界的东西;
  3. Renderer 渲染器 —— 及时捕捉页面的元素的动态变化,并渲染到 web 页面上;
  4. OrbitControls 轨道控制器 —— 主要功能就是支持用户在页面上拖动元素、缩放元素,并进行更多细节的调整。

二、球体的基本实现

首先地球是个球体模型,three.js 本身就已经支持,所以不需要去加载外部的模型。地球的样式,使用贴图来实现就可以,但是贴图需要按照一定的比例,长宽基本上是 2:1 , 而且地理边界线要比较准确,下面是我在网上找到的地球贴图。

earth0.jpg

earth1.jpg

earth2.jpg

geo1.jpg

贴图找到之后,就是要按照 three.js 的要求来创建物体。

第1步,建立一个球模型

const earth_geometry = new SphereBufferGeometry(earth.radius, 50, 50 );

第2步,创建球的材质

this.uniforms.map.value = this.options.textures.earth;
const earth_material = new ShaderMaterial({
   uniforms: this.uniforms,
   vertexShader: earthVertex,
   fragmentShader: earthFragment,
});
earth_material.needsUpdate = false;

注:以上代码涉及到顶点着色器、片元着色器,这些逻辑比较复杂,后面有时间,可以继续研究,暂时就先理解为创建材质就可以了。

第3步:创建地球,并加入场景中

 this.earth = new Mesh(earth_geometry, earth_material);
 this.earth.name = "earth";
 this.earthGroup.add(this.earth);

呈现的效果如下:

1.png

当然也可以用其他贴图,效果如下:

1.png

用上面那张很鲜艳的图,效果如下:

1.png

球体做完了,怎么做自动旋转呢?首先我们需要了解 three.js 的坐标系, 我们在空间,加上辅助线,代码如下:

//注解:加上辅助线,试一下(红色X轴,绿色Y轴,蓝色Z轴)
const axesHelper = new AxesHelper(200);
this.scene.add(axesHelper);

这时候,在3D空间,生成了坐标系,其中红色线表示X轴, 绿色线表示Y轴, 蓝色线表示Z轴,效果如下:

1.png

很明显,这跟我们高中数学课上,见到的坐标系是一致的,three.js 的坐标系是这样,但是其他 3D 的坐标系可能不是这样的,所以,选定什么样的技术框架,就要适应它的坐标系。

2.png

这时候,我们如果想要地球自西往东(高中地理常识)旋转,这时候,只需要控制球体,绕Y轴旋转就可以了,具体怎么旋转,参考右手法则,大拇指指向Y轴正方向,其它四指弯曲的方向,就是旋转方向;如果是负数,则大拇指指向Y轴的反方向,其它四指弯曲的方向,就是旋转方向。

自西往东旋转,代码应该是:

this.earthGroup.rotation.y += 0.01;

三、大气层云朵飘飘效果

方案一:可以在地球的外面再套一个透明的球,这个球贴上云层图片,然后旋转速度跟地球的旋转速度不一样,形成相对运动,这样就会有云朵飘飘的效果。

可以在网上找一张透明的云层贴图:

1.png

加上云层后,效果如下:

1.png

由于地球的贴图本身又云朵纹理,外面又加上一层流动的云朵,整体看起来,云朵有点多,可以让设计师去掉地球贴图的云朵,这样视觉效果更好。

方案二:上面的实现方式有点单调,原因是外层的球,设置了自西往东,跟地球转动方向一致,虽然跟地球的转动速度不一样,形成了相对运动,但是云朵也只能局限于左右运动,不能向其他方向运动,这个效果比较死板。如果想要往其他方向运动,可以创建着色器材质,靠顶点着色器、片元着色器来辅助。

着色器相关知识点简介如下:

1、绘制简单的几何体,加载贴图

例如创建一个球体,加载云层纹理贴图

const fgeometry = new THREE.SphereGeometry(this.earth.radius * 1.002, 60, 60); 
const cloudTexture = new THREE.TextureLoader().load('/static/images/cloud.png');

2、使用uniform变量

这里除了将纹理贴图传到着色器中,还传递了一个时间,这个时间来让纹理动起来。云朵的纹理的 wrapS 和 wrapT 设置成 THREE.RepeatWrapping,这是让纹理简单地重复到无穷大,而不至于 0,0 到 1,1 的范围。

uniforms = {
   cloudTexture: {
      value: cloudTexture
   },
   time: {
      value: 0.0
   },
}

3、顶点着色器

顶点着色器确定每个顶点的位置,并通过 varying 向片元着色器传递相关变量。

//顶点着色器
const VSHADER_SOURCE =
  `varying vec2 v_Uv;
   void main () {
      //顶点纹理坐标
      v_Uv = uv;
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
   }
 `;

4、片元着色器

片元着色器获取该点的位置后,去纹理图片找对应位置的色块,并通过时间参数 time 改变纹理坐标的偏移量,将色块赋值。

//片元着色器
const FSHADER_SOURCE =
  `
    //时间变量
    uniform float time;
    //大气纹理图像
    uniform sampler2D cloudTexture;
    //片元纹理坐标
    varying vec2 v_Uv; 
    void main () {
       //向量加法,根据时间变量计算新的纹理坐标
       vec2 new_Uv= v_Uv + vec2(0.01, 0.02) * time;
       //提取大气纹理图像的颜色值(纹素)
       vec4 colors = texture2D(cloudTexture, new_Uv);  
       gl_FragColor = vec4(colors.rgb, colors.a * 0.6);
    }
 `

5、整体代码

createEarthAperture() {
    //顶点着色器
    const VSHADER_SOURCE =
    `
      varying vec2 v_Uv;
      void main () {
        //顶点纹理坐标
        v_Uv = uv;
        gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
      }
    `;
    //片元着色器
    const FSHADER_SOURCE =
    `
      uniform float time; //时间变量
      uniform sampler2D cloudTexture; //大气纹理图像
      varying vec2 v_Uv; //片元纹理坐标
      void main () {
        //向量加法,根据时间变量计算新的纹理坐标
        vec2 new_Uv= v_Uv + vec2(0.01, 0.02) * time;
        //利用噪声随机使纹理坐标随机化
        //vec4 noise_Color = texture2D( cloudTexture, new_Uv );    
        //new_Uv.x += noise_Color.r * 0.2;
        //new_Uv.y += noise_Color.g * 0.2;
        //提取大气纹理图像的颜色值(纹素)
        vec4 colors = texture2D(cloudTexture, new_Uv);
        gl_FragColor = vec4(colors.rgb, colors.a * 0.6);
      }
    `
    //着色器材质
    const flowMaterial = new THREE.ShaderMaterial({
      uniforms: this.cloud_uniforms,
      //顶点着色器
      vertexShader: VSHADER_SOURCE,
      //片元着色器
      fragmentShader: FSHADER_SOURCE,
      transparent: true
    })
    //创建比基础球体略大的球状几何体
    const fgeometry = new THREE.SphereGeometry(this.options.earth.radius * 1.002, 60, 60);
    //创建大气球体
    const fsphere = new THREE.Mesh(fgeometry, flowMaterial);
    this.group.add(fsphere);
  }

这样,云朵飘飘的效果就已经形成。

四、地球辉光

如果想要地球边缘发光的效果,那么可以借助贴图,通过精灵来实现。

精灵

这里说的精灵是 three.js 自定义的一种特质物品,就是无论空间怎么翻转、移动,该物体总是对着照相机,也就是说,物体总是面对着用户,创建精灵物体需要精灵材质,典型的代码如下:

createEarthGlow() {
    //注解:地球半径
    const R = this.options.earth.radius;
    //注解:TextureLoader创建一个纹理加载器对象,可以加载图片作为纹理贴图
    const texture = this.options.textures.glow;
    /*
    注解:创建精灵材质对象SpriteMaterial,为什么是精灵材质,因为这个背景图始终要面向用户,
    所以要创建精灵,精灵就要选用精灵材质
    */
    const spriteMaterial = new SpriteMaterial({
      //注解:设置精灵纹理贴图
      map: texture,
      color: 0x4390d1,
      //注解:开启透明,如果是false,将是一个正方形,遮挡后面景观
      transparent: true,
      //注解:可以通过透明度整体调节光圈,就是可以控制发光亮度
      opacity: 0.7,
      //禁止写入深度缓冲区数据
      depthWrite: false,
    });
    //注解:创建表示地球光圈的精灵模型
    const sprite = new Sprite(spriteMaterial);
    sprite.name = 'glow';
    //注解:适当缩放精灵
    sprite.scale.set(R * 3.0, R * 3.0, 1);
    //注解:将精灵加到 earthGroup
    this.earthGroup.add(sprite);
  }

看下精灵贴图:

1.png

五、图片标注

地球上面已经创建好了,现在如果想要在地球表面进行一些标注,该怎么实现呢?首先这里涉及到坐标的变换。我们拿到的数据可能是经纬度的数据,那这些数据怎么转化为三维空间的坐标呢?

这里需要空间几何的一些计算方法,见下图: 1.jpeg

2.jpeg

经纬度坐标,转球面坐标,代码如下:

//注解:{地球半径} R  {经度(角度值)} longitude  {维度(角度值)} latitude
export const lon2xyz = (R:number, longitude:number, latitude:number): Vector3 => {
  //转弧度值
  let lon = longitude * Math.PI / 180;
  //转弧度值
  const lat = latitude * Math.PI / 180;
  //js坐标系z坐标轴对应经度-90度,而不是90度
  lon = -lon;
  //经纬度坐标转球面坐标计算公式
  const x = R * Math.cos(lat) * Math.cos(lon);
  const y = R * Math.sin(lat);
  const z = R * Math.cos(lat) * Math.sin(lon);
  //返回球面坐标
  return new Vector3(x, y, z);
}

有了这个坐标转化,下面的工作就容易开展下去了。

场景:我们要在地球仪某个地点上标注一个平面,这时,不得不说 three.js 一个很重要的几何体 PlaneBufferGeometry, PlaneBufferGeometry 是平面的意思,创建这个平面之后,该平面默认是紧贴着 XOY 平面的,而且跟 Z 轴 垂直,很好理解。

但是平面既然默认是贴在 XOY 平面上,那怎么旋转、移动到球面上呢?

第一步:将平面位置移动到球面某个点上

mesh.position.set(coord.x, coord.y, coord.z);

第二步:旋转平面,使之贴近地球表面

原来平面的单位法向量(与这个平面垂直的向量称为法向量)是 (0,0,1), 而地球某个点 (x,y,z) 的单位法向量是:

new Vector3(coord.x, coord.y, coord.z).normalize()

这样通过旋转法向量,就可以将平面贴在地球上

完整代码如下

//注解:光柱底座矩形平面
export const createPointMesh = (options: {
  radius: number,
  lon: number,
  lat: number,
  material: MeshBasicMaterial
}) => {
  //注解:创建一个平面几何体,这个几何体长和宽都是1,大小先不纠结,因为后面通过 scale 可以缩放,注意这个平面默认在三维空间的 XOY 平面上
  const geometry = new PlaneBufferGeometry(1, 1);
  //注解:通过传过来的平面材质,生成物体 mesh ,并且通过 scale 缩放这个平面的大小
  const mesh = new Mesh(geometry, options.material);
  const size = options.radius * 0.05;
  mesh.scale.set(size, size, size);
  //注解:经纬度转球面坐标,并设置这个平面的位置(注意这个1.0015,主要还是比地球稍微高出一点)
  const coord = lon2xyz(options.radius * 1.0021, options.lon, options.lat);
  mesh.position.set(coord.x, coord.y, coord.z);
  //注解:将这个向量,转化为长度为1,方向不变的变量(这个其实就是这个平面的单位法向量)
  const coordVec3 = new Vector3(coord.x, coord.y, coord.z).normalize();
  //注解:这个是z轴正方向,长度为1的向量
  const meshNormal = new Vector3(0, 0, 1);
  //注解:将这个平面进行旋转,使之贴近球面,怎么旋转? 首先我们创建的平面,默认是垂直于z轴的,所以需要从(0,0,1)旋转到平面位置对应的法向量,也就是 coordVec3 代表的向量
  mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  return mesh;
}

我们稍微将这个平面放大一点,用上蓝色的波纹贴图,效果如下:

1.png

现在这2个平面就是紧贴地球了。

六、涟漪波纹动画

上面已经做好了平面,现在可以让这个波纹动起来,原理就是不断地放大缩小这个平面,从而形成波纹效果。 在 requestanimationframe 里面,改变缩放大小、透明度,从而形成动画,代码如下:

//注解:通过不断改变这个缩放,让波纹动起来 mesh.scale.set 、 mesh.material.opacity 来改变大小和材质
    if (this.waveMeshArr.length) {
      this.waveMeshArr.forEach((mesh: Mesh) => {
        mesh.userData['scale'] += 0.007;
        mesh.scale.set(
          mesh.userData['size'] * mesh.userData['scale'],
          mesh.userData['size'] * mesh.userData['scale'],
          mesh.userData['size'] * mesh.userData['scale']
        );
        if (mesh.userData['scale'] <= 1.5) {
          (mesh.material as Material).opacity = (mesh.userData['scale'] - 1) * 2;
//保证透明度在0\~1之间变化
        } else if (mesh.userData['scale'] > 1.5 && mesh.userData['scale'] <= 2) {
          (mesh.material as Material).opacity = 1 - (mesh.userData['scale'] - 1.5) * 2;
//2等于1/(2.0-1.5) mesh缩放2倍对应0 缩放1.5被对应1
        } else {
          mesh.userData['scale'] = 1;
        }
      });
    }

效果如下

1.png

七、文本标注

上面通过创建平面 PlaneBufferGeometry 并将贴图放在这个平面上,通过位置移动、旋转来在球面上进行标注,但是,如果我们想要标注文本,文本内容是动态的,那该怎么办呢?

答案依旧是使用贴图,不过这张贴图不是 png 的固定图片,而是在页面上创建一个 div, 并将 div 转化为 canvas, 然后用 canvas 作为贴图,这样就可以实现文本标注。

需要注意的是:这个文本是要给用户看的,所以无论地球怎么旋转,这个文本都要面朝用户,这就是典型的精灵的特性,所以我们用精灵来实现。

代码如下:

const p = lon2xyz(this.options.earth.radius * 1.001, e.E, e.N);
        //注解:根据城市名称,生成html
        const div = `<div class="fire-div">${e.name}</div>`;
        const shareContent = document.getElementById("html2canvas");
        shareContent.innerHTML = div;
        //注解:将以上的 html 转化为 canvas,再将 canvas 转化为贴图
        const opts = {
          //注解:这样表示背景透明
          backgroundColor: null,
          scale: 6,
          dpi: window.devicePixelRatio,
        };
        const canvas = await html2canvas(document.getElementById("html2canvas"), opts);
        const dataURL = canvas.toDataURL("image/png");
        const map = new TextureLoader().load(dataURL);
        //注解:根据精灵材质,生成精灵,为什么选用精灵?因为精灵的特点就是,始终面向用户
        const material = new SpriteMaterial({
          map: map,
          transparent: true,
        });
        const sprite = new Sprite(material);
        //注解:这里的缩放,是根据一个单位来计算的,精灵是二维的,所以第三个无论怎么设置,都是不会变动的, 所以干脆设置为1
        const len = 5 + (e.name.length - 2) * 2;
        sprite.scale.set(len, 3, 1);
        //注解:精灵图片悬空,很好理解,将精灵加入到 eath 这个物体上,而没有加入 eathgroup 中, 其实好像都是一样的
        sprite.position.set(p.x * 1.1, p.y * 1.1, p.z * 1.1);

最后呈现的效果:

1.png

八、光柱标注

首先看下光柱的贴图

1.png

原理:在地球上标记光柱,其实也是通过创建2个平面来实现,每个平面的贴图如上,这两个平面十字相交(让它看起来更加立体,所以十字相交),最后垂直于地球平面即可。

十字相交的形态,可以看下图: 1.png

首先创建一个平面 PlaneBufferGeometry, 然后设置贴图为光柱图片,再克隆一个一模一样的平面,将这两个平面通过旋转,形成十字相交状态。 再创建一个组合,将这两个平面都加入到组合当中,设置整体组合的位置,再类似旋转法向量一样,将平面组合垂直于地球表面,整体代码如下:

//注解:创建柱状,需要注意的是,translate 之后如果还设置组合的位置,那这个组合的中心点为 translate 之前的那个中心点
export const createLightPillar = (options: { radius: number, lon: number, lat: number, index: number, textures: Record\<string, Texture>, punctuation: punctuation }) => {
  //注解:这里是设置光柱的高度
  const height = options.radius * 0.3;
  //注解:这里是创建一个平面,默认是垂直于z轴的,经过旋转
  const geometry = new PlaneBufferGeometry(options.radius * 0.1, height);
  //注解:经过旋转,这个平面与 ZOX 平面平行
  geometry.rotateX(Math.PI / 2);
  //注解:经过转移Z轴,使得这个平面全部都在Z轴的正方向
  geometry.translate(0, 0, height/2);
  //注解:将这个平面和材质生成一个物体
  const material = new MeshBasicMaterial({
    map: options.textures.light_column,
    color: options.punctuation.lightColumn.endColor,
    transparent: true,
    side: DoubleSide,
    //是否对深度缓冲区有任何的影响
    depthWrite: false,
  });

  const mesh = new Mesh(geometry, material);
  //注解:生成一个物体组合
  const group = new Group();
  //注解:这里是两个光柱十字交叉,也就是两个平面互相垂直,互相在中心点交叉
  group.add(mesh, mesh.clone().rotateZ(Math.PI / 2));
  //注解:将经纬度坐标转化为球面坐标,然后将这个组合设置在这个坐标上面(这时候还没完,因为这个组合没有垂直于球体)
  const SphereCoord = lon2xyz(options.radius * 1.0023, options.lon, options.lat);
  group.position.set(SphereCoord.x, SphereCoord.y, SphereCoord.z);
  //注解:将位置向量变成程度为1的同方向的向量
  const coordVec3 = new Vector3(
    SphereCoord.x,
    SphereCoord.y,
    SphereCoord.z
  ).normalize();
  const meshNormal = new Vector3(0, 0, 1);
  //按这个向量的位置旋转,使得上面的组合体能都垂直于地球表面
  group.quaternion.setFromUnitVectors(meshNormal, coordVec3);
  return group;
}

九、流光特效与卫星环绕

使用 three.js 来实现 3D 流光动画,一种比较简单的处理方式,就是使用贴图,效果如下:

1.png

使用的贴图如下:

2.png

1、创建顶点数组

//注解:创建圆环点组合(这个这些点,都是在 ZOX 平面上)
const circlePoints = getCirclePoints({
  //注解:圆的半径
  radius: this.options.earth.radius * 1.8,
  //注解:圆的切割数量
  number: 100,
  //注解:表示闭合
  closed: true,
});

//注解:这里是将ZOX平面上以原点为圆心,半径为R的圆切割成 N 个点
export const getCirclePoints = (option) => {
  const list = [];
  for (
    let j = 0;
    j < 2 * Math.PI - 0.1;
    j += (2 * Math.PI) / (option.number || 100)
  ) {
    list.push([
      parseFloat((Math.cos(j) * (option.radius || 10)).toFixed(2)),
      0,
      parseFloat((Math.sin(j) * (option.radius || 10)).toFixed(2)),
    ]);
  }
  if (option.closed) list.push(list[0]);
  return list;
}

2、设置曲线的材质,并设置贴图

this.options.textures.flyline.wrapT =  THREE.RepeatWrapping;
this.options.textures.flyline.wrapS = THREE.RepeatWrapping;
this.options.textures.flyline.repeat.set(1, 2);
//注解:圆环材质
const circleMaterial = new MeshBasicMaterial({
  //color: new Color("#0cd1eb"),
  map: this.options.textures.flyline,
  side: DoubleSide,
  transparent: true,
  depthWrite: false,
  opacity: 1,
});
circleMaterial.needsUpdate = true;

3、利用顶点数组、材质创建一条管道

//注解:创建圆环轨道(已掌握)
export const createAnimateLine = (option) => {
  //注解:由多个点数组构成的曲线,通常用于道路
  const linePoint = option.pointList.map((item: any) => new Vector3(item[0], item[1], item[2]));
  //注解:曲线路径
  const curve = new CatmullRomCurve3(linePoint);
  //注解:根据曲线路径,生成管道体
  const tubeGeometry = new TubeGeometry(
    curve,
    option.number || 50,
    option.radius || 1,
    option.radialSegments
  );
  return new Mesh(tubeGeometry, option.material);
}

4、不断推进贴图的 offset,使之看起来像流动的光线

this.options.textures.flyline.offset.x = this.options.textures.flyline.offset.x + 0.01

最终呈现效果

1.png

但是这个红色的光圈好像不太好看,后面我把它改成淡蓝色的了。

十、点击交互

three.js 这个框架中,实现点击事件,只能通过射线(Ray)与射线拾取(Raycaster)来实现。

1、创建射线

//创建射线对象 
let raycaster = new THREE.Raycaster(); 
//创建映射用,用于保存映射结果的顶点 
let mouse = new THREE.Vector2();

2、监听页面的点击事件

//创建射线对象
let raycaster = new THREE.Raycaster();
//创建映射用,用于保存映射结果的顶点
let mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click',e=>{
//获取鼠标点击的位置
let x = e.clientX;
let y = e.clientY;
//我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向
mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1;
})

11.png

我们点击的位置,一般不是三维空间中的实际位置,所以需要将点击位置转化为 -1 至 1 的坐标中,再通过照相机往这个方向发射一条射线,看击中那些物体,再响应相关的事件。

2.png

3、创建射线,并拾取物体

//创建射线对象
let raycaster = new THREE.Raycaster();
//创建映射用,用于保存映射结果的顶点
let mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click',e=>{
    //获取鼠标点击的位置
    let x = e.clientX;
    let y = e.clientY;
    mouse.x = (x / window\.innerWidth ) * 2 - 1;
    mouse.y = - ( y / window\.innerHeight ) * 2 + 1;
    //使用当前相机和映射点修改当前射线属性
    raycaster.setFromCamera(mouse,camera);
    // 计算物体和射线的交点
    let intersects = raycaster.intersectObjects( scene.children );
    console.log(intersects);
})

以上代码中,射线拾取到的物体会放在集合 intersects 中,实际上射线击中物体后,可能还是再击中吉他物体,如果想要设置射线集中某些物体,减少遍历的个数,可以在 intersectObjects 这个方法中,传递待需要响应点击事件的物体。

//创建一个待点击的物品集合
this.earth.clickMesh = [];
//将需要响应点击事件的物体,加入以上集合中
this.earth.clickMesh.push(mesh1);
//遍历拾取的物体,做响应
const intersects = this.raycaster.intersectObjects( this.earth.clickMesh );
if(intersects && intersects.length > 0){
    const firstObj = intersects[0];
    const message = firstObj.object.userData;
    this.option.callback(message);
}

如果物体很多,不好区分,那么可以在物体中添加一些自定义的属性,如 id、name 等,如下:

//将这个精灵存放起来
sprite.userData['event_type'] = 'sprite';
sprite.userData['event_name'] = e.name;
this.clickMesh.push(sprite);

这样点击后,alert 出来,效果如下

1.png

十一、飞线动画

应用场景:例如在地球仪上,画一条从上海到纽约的飞线。

这里会涉及到比较多的数学知识,几何知识,空间矩阵变换等等。

几何图分析.jpg

飞线轨道的画法,比较复杂,首先需要进行2次坐标的旋转。例如下图,假设 A为上海, B为纽约,首先 A 和 B 都是在地球表面上, O为地球的圆心, 这时候,需要将 ABO 这个平面旋转到 XOY 平面上,具体怎么旋转?正如上文所说的,用法向量来辅助旋转。

ABO 这个平面的单位法向量计算方式如下:

//注解:飞线起点与球心构成方向向量,方向是球心指向开始地点
const startDir = startSphere.clone().sub(origin);
//注解:飞线结束点与球心构成方向向量。方向是球心指向结束地点
const endDir = endSphere.clone().sub(origin);
//注解:cross 将会生成一个法向量,垂直于上面2个向量,并且指向遵守右手法则,这里通过 normalize 将这个法向量长度变成1
const normal = startDir.clone().cross(endDir).normalize();

旋转到XOY平面上,对应的法向量为 (0,0,1)也就是 Z 轴,这很好理解吧?!

好,通过上面的旋转,我们就可以将 ABO 这个平面贴到 XOY 这个平面上,但是,ATB 这条圆弧,还不是关于Y轴对称的,所以还需要再旋转一次。

也就是说,目前 OG 还没有和 Y轴重叠,那么怎么将 OG 和 Y轴 重叠呢?这时,再借助向量的旋转来实现: 可以计算出 AB 这条弦(直线)的中点,这个中点和圆心O的连线称为一个向量,Y轴的单位向量是 (0,1,0),这时候再旋转一次:

/*计算第二次旋转的四元数*/
//注解:获取这两个点的中点,那么圆心和这个中点的连线必定垂直于开始点和结束点的连线
const middleV3 = startSphereXOY.clone().add(endSphereXOY).multiplyScalar(0.5);
//注解:圆心与这个重点的连线,必定垂直于开始点和结束点的连线,这里取法向量
const midDir = middleV3.clone().sub(origin).normalize();
//注解:这里表示将旋转到Y轴的正方向,注意这里取单位向量
const yDir = new Vector3(0, 1, 0);
//注解:这里旋转为以Y轴为对称的坐标,需要生成第二个四元数
const quaternionXOY_Y = new Quaternion().setFromUnitVectors(midDir, yDir);
//第二次旋转:使旋转到XOY平面的点再次旋转,实现关于Y轴对称
const startSpherXOY_Y = startSphereXOY.clone().applyQuaternion(quaternionXOY_Y);
const endSphereXOY_Y = endSphereXOY.clone().applyQuaternion(quaternionXOY_Y);

这时候,就旋转出上图的结果, AB两点关于Y轴对称,这时候,来画出飞线轨道,也就是圆弧 ATB 就比较容易了,首先定一下 T点,设置为地球半径的某个倍数(大于1,当然也不要太大,否则飞线太高),这样,ATB三点已知,就可以计算出飞线轨道的圆心,也就是 G 的坐标。同时可以计算出 BGA 的角度是多少,飞线开始角度为 NGA, 结束角度为 BGN, 那么取这个范围,就可以画出轨道上较为高亮的线。

综上所述,我们旋转了两次,最后画出了飞线轨道,那怎么还原回来呢?这里涉及到四元数,也就是每旋转一次,都产生一个四元数矩阵,将矩阵反推会去,就可以旋转回来。大致原理是这样,有兴趣的话,直接研究源码吧。

十二、昼夜交换特效

这个特效,原理是:做一张白天的地球贴图,再做一张夜晚的地球贴图,通过获取白天贴图某个色块的值,再获取夜晚这个位置的色块的值,两个值分别乘以一个百分数(两个百分数加起来等于 100%)来达到昼夜交换的效果。

怎么取这两个贴图的色块呢?那就需要 Shader 编程了,类似上面做白云飘飘的效果那样,比较复杂,暂时没有实现,后续会跟进这个特效。

彩蛋

1.png

3D版中国地图已经在预研中,后续有时间会更新下博客。

看下动态效果

new.gif

加上旋转、跳动的动画

big_gif.gif

由于时间忙,暂时停更博客,3d 版地图,看这一篇

juejin.cn/post/747649…