Threejs模仿实现滴滴官网首页地球动画(一)

时间:2024-01-27 08:08:22

滴滴官网首页下翻第六栏(大概)有个地球的三维动画,试着用there.js实现了下,基本实现了动画效果,不过还是有些问题;这个动画看似简单,其实用到好多计算和绘制方法,也是学习webgl入门的一个很好的示例,这里分关键功能实现和完整流程两篇介绍实现方式;

示例:http://39.106.166.212:8080/webgl/t4

 

 

 

一、 3d绘制环境的构建

看过或用过webgl/threejs 会知道,绘制一个3d程序首先需要添加场景灯光相机渲染器

1、渲染器

简单的说渲染器的作用就是把3d场景的内容结合照相机的位置方向渲染到页面中。

这里画布背景使用白色;

const renderer = new Three.WebGLRenderer({        canvas: this.$refs.thr});
renderer.setClearColor(0x000000);

 

2、场景

场景顾名思义,就是添加一个你发挥(绘制)的场地;

cosnt scene = new Three.Scene();

 

3、照相机

照相机我的理解就是人的视觉或怎么看场景,看场景的位置和视线的方向决定了渲染到页面的内容。故一般需要设置两个参数position、lookAt,,在webgl其实是需要三组参数视点,观察点,和上方向。thresjs中默认为Y轴为上方向,这里使用透视相机。

const camera = new THREE.PerspectiveCamera(45, 500 / 500, 1, 1500);
camera.position.set(100, 100, 1000);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(this.camera);

 

4、灯光

这里使用THREE.HemisphereLight 可以更加贴近自然的户外光照效;

let light = new THREE.HemisphereLight(0xffffff); 
light.position.set(0, 0, 200); 
scene.add(light)

 

以上我们基本的绘制要素已添加完成,下面开始绘制各个几何体内容;

几何体的绘制有三部:创建几何体,创建材料,添加网格模型;

二、地球的绘制

threejs中提供了球体的绘制,我们只需创建一个球体,材料使用纹理贴图方式添加地图;

贴图图片是我从官网上找的

 

 

const geometry = new THREE.SphereGeometry(this.radius, 100, 100); // 球体      
const textureLoader = new THREE.TextureLoader(); // 纹理贴图      
const texture = textureLoader.load(require("@/assets/map.jpg"),texture => {  
  let material = new THREE.MeshLambertMaterial({     map: texture,     transparent: true,  });
  let mesh = new THREE.Mesh(geometry, material);  
  scene.add(mesh);
});

 

三、球面坐标点的绘制

1、在绘制前先看下球坐标系,webgl中坐标方向与下图不一致,但是对点的表示是一样的;

球面上任意点可以用r,θ,φ表示,也可通过以下公式转化到webgl坐标系中

x=rsinθcosφ.
y=rsinθsinφ.
z=rcosθ

但对于地球位置我们一般也可以用经纬度表示,

threejs提供了THREE.Math.degToRad方法可以将经纬度转化为θ,φ,转化方法如下,这里为了方便后面使用,我直接返回一个向量;

 

 

 

    

通过上述方法 我们可以经纬度表示转化为球面坐标,也可通过球坐标系转化为webgl坐标系坐标

2、知道了位置的表示方法后开始绘制表示位置的点

根据示例位置点的由点和一个圆环组成,我们先绘制一个二维平面内的点和圆弧,在通过设置其位置和旋转方式移动到坐标位置点;

(1)点的绘制

我们通过THREE.Shape()绘制一个点

 

 

 

let shapePoint = new THREE.Shape();shapePoint.absarc(0, 0, r - 4, 0, 2 * Math.PI, false);let arcGeometry = new THREE.ShapeGeometry(shapePoint);let arcMaterial = new THREE.MeshBasicMaterial({ color: 0x008080 });let point = new THREE.Mesh(arcGeometry, arcMaterial);

 

(2)圆弧的绘制

 

 

 

let geometryLine = new THREE.Geometry();let arc = new THREE.ArcCurve(0, 0, r, 0, 2 * Math.PI);let points = arc.getPoints(40);
geometryLine.setFromPoints(points);let LineMateri = new THREE.LineBasicMaterial({ color: 0x20b2aa });let line = new THREE.Line(geometryLine, LineMateri);

 

(3)位置的设置

 

 

 

position.set(pos.x, pos.y, pos.z);

 

(4)二维图形旋转至球面

three提供了THREE.Spherical()方法 ,可将坐标点转化为坐标点转化回球坐标系的表示方法

 

 

 

let spherical = new THREE.Spherical();spherical.setFromCartesianCoords(pos.x, pos.y, pos.z);

 

设置位置点旋转

 

 

 

Point.rotateX(spherical.phi - Math.PI / 2);Point.rotateY(spherical.theta);

 

这里为什么要 - Math.PI / 2 是因为开始我们绘制时,平面是垂直于y轴的平面

 

四、接着绘制链接球面两点间的连线

连接两点的曲线需高于两点间的圆弧(球面),我们知道两点间可以坐出无数条曲线,那么如何确定曲线,我们需自己再选择合适的参数,来确定;

首先想的是二阶贝塞尔曲线,并且两点的中点为控制点,但有个问题,如果链接两点刚好位于球面的两个对称端点,(两点间连线为直径)时,控制点需在无穷远处;

故考虑使用三阶贝塞尔曲线,连接球面两点和球心,三点确定的一个平面如下图,

链接v1 v1,取中点c,链接oc,做一条射线,在射线取一点p,链接v1p,v2p,在v1,v2上取两点作为控制点;

可通过当vp为切线是计算出,线之间的关系为

op ~ 圆心角

控制点 ~ 圆心角的二次关系

其比例关系可通过开发中自己测试

 

 // 获取贝塞尔控制点
    getBezierPoint(v0, v3) {
      // 计算向量夹角
      let angle = (v0.angleTo(v3) * 180) / Math.PI; // 0 ~ Math.PI

      let aLen = angle * 2.5,
        hLen = angle * angle * 50;

      let p0 = new THREE.Vector3(0, 0, 0);
      // 法线向量
      let rayLine = new THREE.Ray(p0, this.getVCenter(v0.clone(), v3.clone()));
      // 顶点坐标
      let vtop = rayLine.at(hLen / rayLine.at(1).distanceTo(p0), vtop); // 几倍位置
      // 控制点坐标
      let v1 = this.getLenVcetor(v0.clone(), vtop, aLen);
      let v2 = this.getLenVcetor(v3.clone(), vtop, aLen);

      return {
        v1: v1,
        v2: v2
      };
    },

 

 

五、小球的运动轨迹

小球的动画我们使用three的帧动画,路径为上一步获取的三次贝塞尔曲线

 

 

const Ball = this.drawSportPoint(curvePoints[0]);

      let arr = [];
      for (let i = 0; i < 101; i++) {
        arr.push(i);
      }
      // 生成一个时间序列
      let times = new Float32Array(arr);

      let posArr = [];
      curvePoints.forEach(elem => {
        posArr.push(elem.x, elem.y, elem.z);
      });
      // 创建一个和时间序列相对应的位置坐标系列
      let values = new Float32Array(posArr);
      // 创建一个帧动画的关键帧数据,曲线上的位置序列对应一个时间序列
      let posTrack = new THREE.KeyframeTrack("Ball.position", times, values);
      let duration = 101;
      let clip = new THREE.AnimationClip("default", duration, [posTrack]);
      this.mixer = new THREE.AnimationMixer(Ball);
      let AnimationAction = this.mixer.clipAction(clip);
      AnimationAction.timeScale = 20;
      AnimationAction.play();