18

关于从入门three.js到做出3d地球这件事(第六篇: 装饰地球, 打点等操作)

本章让我们一起为我们孤单的地球线条添加点装饰 (后期会绘制太阳系)。

一. 星空背景

     一直以来我们的地球背景都是黑黑的, 这次我们就要让星空的图片作为地球的背景。

     这个星空背景也要是3d的, 并且是把地球包裹在其内的, 可以把这个星空想象成一个球体内侧的贴图, 我们的'相机'就处于这个球体内部。

上网找一张星空的背景图, 图片要宽度比高度大一些的, 否则显示的不清晰:

image.png

/cc_map_3d_pro/src/config/earth.config.js

export default {
    r: 80, // 半径
    bg: require("../assets/images/星空.jpg"), // 背景图 (新增)
    earthBg: require("../assets/images/地图加文字.png"), // 贴图路径
}

在生命周期函数里面我们新增初始化背景函数

mounted() {
    // ...
    this.initBg();
  },

这里我们要做的是绘制一个大大的圆球, 包裹着我们的地球与相机, 并且将其纹理设置在内部。

    initBg() {
      // 加载星空纹理
      const texture = this.textureLoader.load(envConifg.bg);
      // 生成球体
      const sphereGeometry = new THREE.SphereGeometry(1000, 50, 50);
      // 调转球体正反
      sphereGeometry.scale(-1, 1, 1);
      // 赋予纹理贴图
      const sphereMaterial = new THREE.MeshBasicMaterial({ map: texture });
      // 生成几何球体
      this.sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
      // 放入场景内
      this.scene.add(this.sphere);
    },
注意事项
  1. new THREE.SphereGeometry(1000, 50, 50)第一个参数是球体的半径, 这个半径一定不要大于我们的远视点, 否则会看不见这个球了。
  2. 球体的半径不能太小, 否则会有一种星星距离地球很近的样子, 并且之后我们会尝试绘制太阳系这个球体太小就绘制不下了,下面我会展示一下设置过小的效果。
  3. sphereGeometry.scale(-1, 1, 1)你可以想想是把一个皮球从内而外的翻转过来。

正常的效果的不同角度:
image.png

image.png

外层球体的大小超过远视角:

image.png

外层球体的大小等于地球半径:
image.png

二. 地球透光性

     上面的效果图都有一个显示问题, 就是我们的地图是中空的, 并且可以透过一边看到另一边, 我们想要让这个地球看起来更像实体, 我们现在就需要将一个球体放到地球里面, 通过控制这个球体的透明度来控制地球的透明度。

    initInside() {
      const sphereGeometry = new THREE.SphereGeometry(envConifg.r - 1, 50, 50);
      const sphereMaterial = new THREE.MeshBasicMaterial({
        color: this.bgColor,
        opacity: 0.9,
        transparent: true,
      });
      this.InsideSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
      this.scene.add(this.InsideSphere);
    },
  1. 要注意需要设置transparent: true才可以设置透明度。
  2. 之所以把内球的半径设置为envConifg.r - 1是因为怕它遮盖我们国家的连线。
  3. 如果要改变内球的颜色可以使用this.InsideSphere.material.color.set(this.bgColor)方法。

看下两种效果吧:
image.png

image.png

三. 地球光晕(精灵)

     这种特殊的材质它会始终面向相机, 也就是我们不管转到什么角度去看这个模型, 他都是正对着我们的屏幕, 精灵不会投射任何阴影。

     new THREE.Spritenew THREE.BoxGeometry性质上差不多, 只是他会创建出来一个一直面向屏幕的精灵几何体。

     THREE.SpriteMaterial一种使用Sprite的材质, 与THREE.Sprite是一对, 可以调整颜色, 透明度等等。

先准备一张类似下面的光晕图:
image.png

initSprite() {
   const texture = this.textureLoader.load(envConifg.haloBg);
   const spriteMaterial = new THREE.SpriteMaterial({
     map: texture,
     transparent: true,
     opacity: 0.7,
   });
   const sprite = new THREE.Sprite(spriteMaterial);
   sprite.scale.set(envConifg.r * Math.PI, envConifg.r * Math.PI, 1);
   this.scene.add(sprite);
 },
  1. 一如既往的引入'纹理贴图'。
  2. 只是他要用THREE.SpriteMaterialTHREE.Sprite来赋予纹理贴图。
  3. 设置sprite.scale因为精灵图普遍很小所以需要等比增大一下, 这个数不一定是π, 根据自己的实景情况输入。

image.png

image.png

     比如精灵图可以用在射击类游戏的瞄准星, 还比如3d游戏里游戏人物会头顶自己的名称, 这个名字如果不面向我们的屏幕那肯定看不清楚了。

四. ps修改精灵图颜色

     如果感觉我的光晕不好看可以自己动手打开ps修改一下:

导入图片

image.png

点击替换颜色

image.png

选择自己喜欢的颜色

image.png

五. 地球打点

     平时我们需要在地球上做一些标记, 并且这些标记有大有小, 颜色各异, 形状各异, 重要的是这个图形需要与地球中心点射出的半径线, 保持平行才能完全展示的地球表面。

     准备一张打点图, 最好是白色的方便我们以后为其赋予其他颜色:
image.png

          我们在地球组件里面添加一个markSpot方法, 此方法支持多个与单个对象的处理, 接收数组或是对象, 外部使用ref的方式调用这个方法:

    markSpot(obj) {
      if (obj instanceof Array) {
        obj.forEach((item) => {
          this.object.add(spot(item));
        });
      } else {
        this.object.add(spot(obj));
      }
    },
  1. this.object是一个new THREE.Object3D生成的容器, 可以储存多个Mesh为一组形成一个整体。
  2. spot使我们接下来要实现的打点方法。
  3. 接收的obj是个配置项, 里面包括打点的颜色、大小、透明度、形状等等的配置。

在使用组件时如此编写:

 <cc-map ref="map"></cc-map>
// ...
initMarks() {
   const arr = [
     {
       longitude: 116.2,
       latitude: 39.56,
       color: "red",
     },
     {
       longitude: 76.2,
       latitude: 49.56,
       color: "blue",
     },
    ];
   this.$refs.map.markSpot(arr);
 },
开始编写打点方法

/cc_map_3d_pro/src/utils/sport.config.js打点的一些默认属性

const config = {
    size: 7,
    opacity: .8,
    color: 'yellow',
    url: require('../assets/images/打点.png')
}


export default (options) => {
    return { ...config, ...options }
}

/cc_map_3d_pro/src/utils/spot.js, 先要把配置项导进来并且设置默认值。

import * as THREE from "three";
import envConifg from '../config/earth.config';
import lon2xyz from './lon2xyz';
import mergeConfig from './sport.config';
const geometry = new THREE.PlaneBufferGeometry(1, 1);
const textureLoader = new THREE.TextureLoader();

export default function spot(options) {
    const { longitude, latitude, color, opacity, size, url } = mergeConfig(options);
    const texture = textureLoader.load(url);
    const material = new THREE.MeshBasicMaterial({
        color,
        opacity,
        map: texture,
        transparent: true,
    });
    const mesh = new THREE.Mesh(geometry, material);
    const coord = lon2xyz(envConifg.r * 1.01, longitude, latitude)
    mesh.scale.set(size, size, size);
    mesh.position.set(coord.x, coord.y, coord.z);
    return mesh;
}
  1. 我们设置了默认的配置属性, 并与用户传进来的配置进行了合并。
  2. 将传入的经纬度转换成笛卡尔坐标系的 x, y, z 值。
  3. 大小通过放大与缩小图形实现。
  4. THREE.PlaneBufferGeometry(1, 1)生成一个平面的几何体, 里面的参数是宽与高, 之所以不用这里的宽高来操作图片的大小, 是因为这里设置不如scale里灵活。
  5. 获取经纬度时envConifg.r * 1.01是因为怕它与地球上的线重合。

效果如下:

image.png

翻转打点

     位置虽然对了, 但是角度这样肯定不行, 现在我们要计算他需要旋转多少度才能与半径线垂直, 这里开始涉及一些数学知识了。

第一步: normalize归一化

     比如现有从圆心出发的两条线段, 不管两条线段有多长它两个的夹角的度数是不会变化的, 所以在有些时候计算一些比例或是角度时, 如果数据的长度并不影响计算结果那么我们会把它归一下处理后再进行计算。

     归一化做的事是把你的向量变成一条长度为1的向量, 比如你有一条三维向量长宽高为x, y, z, 处理过后就会变成x*x + y*y + z*z = 1

  console.log(new THREE.Vector3(10, 10, 10))
  console.log(new THREE.Vector3(1, 1, 1).normalize())

image.png

image.png

如图所示, 红色为x, y, z值都为6的向量, 归一化后变为蓝色线段表示的向量。

第二步: XOY平面的法线
“法线(normal line),是指始终垂直于某平面的直线

     当前我们的打点的平面默认是在XOY平面上的, 所以它的法线可以理解成中垂线是z轴, 比如是(0, 0, z)这条线, z是多少都没关系比如你可以写成new THREE.Vector3(0, 0, 999).normalize(), 但这里推荐直接写new THREE.Vector3(0, 0, 1)可以节约一些计算的性能。

第三步: 利用 四元数 翻转平面

image.png

     四元数这个数学概念我讲不好, 但可以带你简单理解, 它用于计算某个点, 绕某条向量旋转'c度'后所在的坐标, 他的概念与复数(i*i = -1)很类似, 四元数在几何学上可写作i*i = j*j = k*k = i*j*k = -1, 通过数学的计算可以得出旋转后的坐标的一套公式。

     利用四元数设置旋转角度quaternion.setFromUnitVectors('向量1', '向量2')的参数需要使用归一化处理的, 向量1旋转到方向向量2所需的旋转角度为n, 则使目标转转n度。

     刚才我们得知我们打点图形的法线(0, 0, 1), 从圆心出发到达该点的坐标是(x, y, z), 那么我们控制其法线旋转, 使法线与(x, y, z)向量重合, 则(x, y, z)向量就会同样垂直与打点平面, 这样打点图形就会是与地球相切的效果。

    const coordVec3 = new THREE.Vector3(coord.x, coord.y, coord.z).normalize();
    const meshNormal = new THREE.Vector3(0, 0, 1);
    mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);

image.png

image.png

全部代码
import * as THREE from "three";
import envConifg from '../config/earth.config';
import lon2xyz from './lon2xyz';
import mergeConfig from './sport.config';
const geometry = new THREE.PlaneBufferGeometry(1, 1);
const textureLoader = new THREE.TextureLoader();

export default function spot(options) {
    const { longitude, latitude, color, opacity, size, url } = mergeConfig(options);
    const texture = textureLoader.load(url);
    const material = new THREE.MeshBasicMaterial({
        color,
        opacity,
        map: texture,
        transparent: true,
    });
    const mesh = new THREE.Mesh(geometry, material);
    const coord = lon2xyz(envConifg.r * 1.01, longitude, latitude)
    mesh.scale.set(size, size, size);
    mesh.position.set(coord.x, coord.y, coord.z);
    const coordVec3 = new THREE.Vector3(coord.x, coord.y, coord.z).normalize();
    const meshNormal = new THREE.Vector3(0, 0, 1);
    mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
    return mesh;
}
只要写个定时器, 不断改变打点的大小与颜色, 就可以做出动态的打点效果了, 这个后面章节专门讲动画的时候再统一样式

六. 地球光柱

     有些时候我们需要在地球上的某一坐标亮起一道光柱, 由光柱的高度来表示此处资产的密度, 或是商品的销量。

圆锥体 THREE.CylinderGeometry
  const geometry = new THREE.CylinderGeometry(1.5, 2, 5, 100, 100);
  const material = new THREE.MeshBasicMaterial({
      color: 'red'
  })
  const mesh = new THREE.Mesh(geometry, material);
  1. CylinderGeometry第一个参数上圆半径也就是圆锥的顶端, 设置为0就是一个尖尖的锥子。
  2. CylinderGeometry第一个参数下圆半径也就是圆锥的底座。
  3. 第三个个参数是圆锥的高度。
  4. 第四个圆柱侧面周围的分段数,默认为8。
  5. 第五个圆柱侧面沿着其高度的分段数,默认值为1。
  6. 注意圆柱默认中心线是在y轴上, 使用四元数翻转时有用。

image.png

封装成函数

/cc_map_3d_pro/src/utils/column.config.js

const config = {
    size: 7,
    opacity: .8,
    color: 'yellow',
}


export default (options) => {
    return { ...config, ...options }
}

/cc_map_3d_pro/src/utils/column.js

import * as THREE from "three";
import envConifg from '../config/earth.config';
import lon2xyz from './lon2xyz';
import mergeConfig from './column.config';

export default function column(options) {
    const { longitude, latitude, color, opacity, size, url } = mergeConfig(options);
    const material = new THREE.MeshBasicMaterial({
        color,
        opacity,
        transparent: true,
        side: THREE.DoubleSide,
    });
    const coord = lon2xyz(envConifg.r * 1.01, longitude, latitude)
    const coordVec3 = new THREE.Vector3(coord.x, coord.y, coord.z).normalize();
    const geometry = new THREE.CylinderGeometry(0, 3, size);
    const mesh = new THREE.Mesh(geometry, material);
    return mesh
}

image.png

再次利用四元数

     介于圆锥的特性, 我们需要让圆锥的中心高线与球面向量重合, 所以圆锥的归一化向量可以选择(0, 1, 0):

mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), coordVec3);

效果图:

image.png

image.png

全部代码
import * as THREE from "three";
import envConifg from '../config/earth.config';
import lon2xyz from './lon2xyz';
import mergeConfig from './column.config';

export default function column(options) {
    const { longitude, latitude, color, opacity, size, url } = mergeConfig(options);
    const material = new THREE.MeshBasicMaterial({
        color,
        opacity,
        transparent: true,
        side: THREE.DoubleSide,
    });
    const coord = lon2xyz(envConifg.r * 1.01, longitude, latitude)
    const coordVec3 = new THREE.Vector3(coord.x, coord.y, coord.z).normalize();
    const geometry = new THREE.CylinderGeometry(0, 3, size);
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(coord.x, coord.y, coord.z);
    mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), coordVec3);

    return mesh
}
只要写个定时器, 不断改变柱子的颜色与高度, 就可以做出动态的锥闪效果了, 这个后面章节专门讲动画的时候再统一样式
地球飞线需要之后单独一张讲了, 因为涉及的知识有点难度, 需要详细的规划一下。

end

     下一篇我们要聊聊如何区分各个国家, 鼠标悬停时可以出现当前国家的提示框, 这里面虽然会有不少数学概念, 但不要畏惧, 我会详细的把我从0开始理解这些概念的过程都描绘出来,希望与你一起进步。


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者