Threejs实现3d地球记录(4)

三、地球信息流可视化(飞线)

1、曲线介绍

Three.js基础曲线函数有三种:

  • 样条曲线:在三维空间中设置5个顶点,输入三维样条曲线CatmullRomCurve3函数作为参数,然后返回更多个顶点,通过返回的顶点数据,构建一个几何体,然后绘制出来一条沿着5个顶点的光滑样条曲线。

  • 三维三次贝赛尔曲线: 由起点、终点、及两个控制点定义,通过三维三次贝塞尔曲线(CubicBezierCurve3)绘制出一条平滑的曲线
    Threejs实现3d地球记录(4)_第1张图片

  • 圆弧曲线:类似于画一个圆,取其中一段作为弧线;将圆心坐标,圆半径,圆弧起始角度作为ArcCurve参数,绘制一段圆弧。
    Threejs实现3d地球记录(4)_第2张图片

2、使用圆弧曲线绘制轨迹线

(1)、xoy平面上关于y轴对称的圆弧曲线

Threejs实现3d地球记录(4)_第3张图片

  • 根据起始点确定两点间的顶点坐标(上图青色的点)
    为了美观我们设定距离越远的两点,中间顶点距离球面也越远,简单来说就是顶点到球面的距离与两点之间和球心构成夹角成正相关,夹角越大距离越远,相反则越近。因此我们需要先计算出两点之间和球心构成夹角的弧度值:
//计算两点之间和球心构成夹角的弧度值方法
function radianAOB(A, B, O) {
  // dir1、dir2:球面上两个点和球心构成的方向向量
  var dir1 = A.clone().sub(O).normalize();
  var dir2 = B.clone().sub(O).normalize();
  //.dot()计算夹角余弦值
  var cosAngle = dir1.clone().dot(dir2);
  var radianAngle = Math.acos(cosAngle);//余弦值转夹角弧度值
  return radianAngle
}

首先算出两点的中点,然后根据中点确定出顶点与球心构成的方向向量,最后由顶点到球面的距离与两点之间和球心构成夹角成正相关关系式计算出顶点坐标:

// 计算两点的中点
  var middleV3 = new THREE.Vector3().addVectors(startPoint, endPoint).multiplyScalar(0.5);
  // 顶点与球心构成的方向向量
  var dir = middleV3.clone().normalize()
  // 计算夹角的弧度值
  var earthRadianAngle = radianAOB(startPoint, endPoint, new THREE.Vector3(0, 0, 0))
  //弧度值 * R * 0.2:表示飞线轨迹圆弧顶部距离地球球面的距离,与弧度值成正相关即可,可自行调整
  var arcTopCoord = dir.multiplyScalar(R + earthRadianAngle * R * 0.2)
  • 通过三个点确定圆弧线
    绘制圆弧曲线要使用到ArcCurve函数,那就得先计算出圆心坐标,圆半径以及圆弧的起始角度
    1)、计算圆心坐标:
//求p1, p2, p3三个点的外接圆圆心
function threePointCenter(p1, p2, p3) {
  var L1 = p1.lengthSq();//到坐标原点距离的平方
  var L2 = p2.lengthSq();
  var L3 = p3.lengthSq();
  var x1 = p1.x, y1 = p1.y, x2 = p2.x, y2 = p2.y, x3 = p3.x, y3 = p3.y;
  var S = x1 * y2 + x2 * y3 + x3 * y1 - x1 * y3 - x2 * y1 - x3 * y2;
  var x = (L2 * y3 + L1 * y2 + L3 * y1 - L2 * y1 - L3 * y2 - L1 * y3) / S / 2;
  var y = (L3 * x2 + L2 * x1 + L1 * x3 - L1 * x2 - L2 * x3 - L3 * x1) / S / 2;
  // 三点外接圆圆心坐标
  var center = new THREE.Vector3(x, y, 0);
  return center
}

2)、计算圆半径(通过顶点y坐标减去圆心y坐标获得)

var flyArcR = Math.abs(flyArcCenter.y - arcTopCoord.y);

3)、计算圆弧的起始角度(可使用到上边计算两点之间和球心构成夹角的弧度值方法)

  var flyRadianAngle = radianAOB(startPoint, new THREE.Vector3(0, -1, 0), flyArcCenter);
  var startAngle = -Math.PI / 2 + flyRadianAngle;//飞线圆弧开始角度
  var endAngle = Math.PI - startAngle;//飞线圆弧结束角度

4)、通过ArcCurve函数计算出圆弧曲线,使用getSpacedPoints(x)方法返回生成圆弧线的点坐标,返回多少个点由x决定,x越大圆弧越圆,但渲染性能越低,最后用setFromPoints渲染几何体顶点坐标以Line绘制出来

function circleLine(x, y, r, startAngle, endAngle) {
  var geometry = new THREE.BufferGeometry();
  // THREE.ArcCurve创建圆弧曲线
  var arc = new THREE.ArcCurve(x, y, r, startAngle, endAngle, false);
  //getSpacedPoints是基类Curve的方法,返回一个vector2对象作为元素组成的数组
  var points = arc.getSpacedPoints(50); //分段数50,返回51个顶点
  geometry.setFromPoints(points);// setFromPoints方法从points中提取数据改变几何体的顶点属性vertices
  var material = new THREE.LineBasicMaterial({ color: 0xffffff, });//线条材质
  var line = new THREE.Line(geometry, material);//线条模型对象
  return line;
}

绘制圆弧曲线完整方法:

function arcXOY(startPoint, endPoint) {
  var middleV3 = new THREE.Vector3().addVectors(startPoint, endPoint).multiplyScalar(0.5);
  var dir = middleV3.clone().normalize()
  var earthRadianAngle = radianAOB(startPoint, endPoint, new THREE.Vector3(0, 0, 0))
  var arcTopCoord = dir.multiplyScalar(R + earthRadianAngle * R * 0.2)

  var flyArcCenter = threePointCenter(startPoint, endPoint, arcTopCoord)
  var flyArcR = Math.abs(flyArcCenter.y - arcTopCoord.y);

  var flyRadianAngle = radianAOB(startPoint, new THREE.Vector3(0, -1, 0), flyArcCenter);
  var startAngle = -Math.PI / 2 + flyRadianAngle;
  var endAngle = Math.PI - startAngle;

  var arcline = circleLine(flyArcCenter.x, flyArcCenter.y, flyArcR, startAngle, endAngle)
  arcline.center = flyArcCenter;//飞线圆弧自定一个属性表示飞线圆弧的圆心
  arcline.topCoord = arcTopCoord;//飞线圆弧自定一个属性表示飞线圆弧中间也就是顶部坐标
  return arcline
}
  • 场景中随机点绘制圆弧曲线
	var num = 10//批量绘制多组
    for (var i = 1; i < num; i++) {
      var startAngle = Math.PI / 2 / num * i;//圆弧起点和坐标原点构成的角度
      var x = R * Math.cos(startAngle); //飞线圆弧起点横坐标
      var y = R * Math.sin(startAngle); //飞线圆弧起点纵坐标
      
      var startPoint = new THREE.Vector3(x, y, 0)
      var endPoint = new THREE.Vector3(-x, y, 0)

      var arcline = arcXOY(startPoint, endPoint);
      scene.add(arcline); //飞线插入场景中

效果:
Threejs实现3d地球记录(4)_第4张图片

(2)、解析经纬度坐标绘制轨迹线

  • 经纬度坐标转球面坐标
  var sphereCoord1 = lon2xyz(R, lon1, lat1);//经纬度坐标转球面坐标
  //轨迹线起点球面坐标
  var startSphereCoord = new THREE.Vector3(sphereCoord1.x, sphereCoord1.y, sphereCoord1.z);
  var sphereCoord2 = lon2xyz(R, lon2, lat2);
  //轨迹线结束点球面坐标
  var endSphereCoord = new THREE.Vector3(sphereCoord2.x, sphereCoord2.y, sphereCoord2.z);
  • 把3D球面上任意的两个飞线起点和结束点绕球心旋转到到XOY平面上,
    同时保持关于y轴对称
    上面我们知道如何在xoy平面上绘制关于y轴对称的圆弧曲线,在已知初始点和结束点的情况下,我们可以将这两点通过两次旋转得到在xoy平面上且关于y轴对称的两点,然后绘制圆弧曲线,最后再逆旋转回初始位置,此处我们需要用到‘四元数Quaternion’的概念,我将它简单理解为记录旋转的值。
    1)、计算第一次旋转的四元数(旋转至xoy平面)
    第一次旋转可看作两点初始位置与球心构成平面的法向量旋转至xoy平面的法向量
  var origin = new THREE.Vector3(0, 0, 0);//球心坐标
  var startDir = startSphere.clone().sub(origin);//飞线起点与球心构成方向向量
  var endDir = endSphere.clone().sub(origin);//飞线结束点与球心构成方向向量
  // 两点初始位置与圆心构成平面的法向量
  var normal = startDir.clone().cross(endDir).normalize();
  var xoyNormal = new THREE.Vector3(0, 0, 1);//XOY平面的法线
  //计算旋转到xoy平面的四元数
  var quaternion3D_XOY = new THREE.Quaternion().setFromUnitVectors(normal, xoyNormal);
  //第一次旋转后的起点与终点
  var startSphereXOY = startSphere.clone().applyQuaternion(quaternion3D_XOY);
  var endSphereXOY = endSphere.clone().applyQuaternion(quaternion3D_XOY);

2)、计算第二次旋转的四元数(将旋转至xoy平面的两点再旋转至两点关于y轴对称)
第二次旋转可看作在xoy平面两点的中点与球心构成的方向向量旋转至y轴方向向量

  //第一次旋转后的两点中点
  var middleV3 = startSphereXOY.clone().add(endSphereXOY).multiplyScalar(0.5);
  var midDir = middleV3.clone().sub(origin).normalize();// 第一次旋转后的两点中点和球心构成的方向向量
  var yDir = new THREE.Vector3(0, 1, 0);//y轴方向向量
  //第二次旋转的四元数
  var quaternionXOY_Y = new THREE.Quaternion().setFromUnitVectors(midDir, yDir);

  //第二次旋转后的起点与终点
  var startSpherXOY_Y = startSphereXOY.clone().applyQuaternion(quaternionXOY_Y);
  var endSphereXOY_Y = endSphereXOY.clone().applyQuaternion(quaternionXOY_Y);

3)、计算逆旋转的四元数
一个四元数表示一个旋转过程,.invert()方法表示四元数的逆,简单说就是把旋转过程倒过来,两次旋转的四元数执行.invert()求逆,然后执行.multiply()相乘

var quaternionInverse = quaternion3D_XOY.clone().invert().multiply(quaternionXOY_Y.clone().invert())

完整方法为:

function _3Dto2D(startSphere, endSphere) {
  //第一次旋转
  var origin = new THREE.Vector3(0, 0, 0);
  var startDir = startSphere.clone().sub(origin);
  var endDir = endSphere.clone().sub(origin);
  var normal = startDir.clone().cross(endDir).normalize();
  var xoyNormal = new THREE.Vector3(0, 0, 1);//XOY平面的法线
  var quaternion3D_XOY = new THREE.Quaternion().setFromUnitVectors(normal, xoyNormal);
  var startSphereXOY = startSphere.clone().applyQuaternion(quaternion3D_XOY);
  var endSphereXOY = endSphere.clone().applyQuaternion(quaternion3D_XOY);
  //第二次旋转
  var middleV3 = startSphereXOY.clone().add(endSphereXOY).multiplyScalar(0.5);
  var midDir = middleV3.clone().sub(origin).normalize();
  var yDir = new THREE.Vector3(0, 1, 0);
  var quaternionXOY_Y = new THREE.Quaternion().setFromUnitVectors(midDir, yDir);

  var startSpherXOY_Y = startSphereXOY.clone().applyQuaternion(quaternionXOY_Y);
  var endSphereXOY_Y = endSphereXOY.clone().applyQuaternion(quaternionXOY_Y);

  var quaternionInverse = quaternion3D_XOY.clone().invert().multiply(quaternionXOY_Y.clone().invert())
  return {
    // 返回两次旋转四元数的逆四元数
    quaternion: quaternionInverse,
    // 范围两次旋转后在XOY平面上关于y轴对称的圆弧起点和结束点坐标
    startPoint: startSpherXOY_Y,
    endPoint: endSphereXOY_Y,
  }
}
  • 调用arcXOY函数绘制圆弧轨迹
    计算得到绘制圆弧需要的关于y轴对称的起点、结束点和旋转四元数,通过arcXOY函数绘制出xoy平面关于y轴对称的圆弧轨迹,将得到的轨迹线乘以逆旋转的四元数,旋转回初始位置
  var startEndQua = _3Dto2D(startSphereCoord, endSphereCoord)
  // 调用arcXOY函数绘制圆弧轨迹
  var arcline = arcXOY(startEndQua.startPoint, startEndQua.endPoint);
  arcline.quaternion.multiply(startEndQua.quaternion)

完整方法:

function flyArc(lon1, lat1, lon2, lat2) {
  var sphereCoord1 = lon2xyz(R, lon1, lat1);
  var startSphereCoord = new THREE.Vector3(sphereCoord1.x, sphereCoord1.y, sphereCoord1.z);
  var sphereCoord2 = lon2xyz(R, lon2, lat2);
  var endSphereCoord = new THREE.Vector3(sphereCoord2.x, sphereCoord2.y, sphereCoord2.z);

  var startEndQua = _3Dto2D(startSphereCoord, endSphereCoord)
  var arcline = arcXOY(startEndQua.startPoint, startEndQua.endPoint);
  arcline.quaternion.multiply(startEndQua.quaternion)
  return arcline;
}
  • 最后调用绘制方法绘制出相关曲线
var flyArcGroup = new THREE.Group();
Object.keys(data).map(item => { //data为起始国家经纬度坐标
  data[item].end.forEach((coord) => {
    var arcline = flyArc(data[item].start.E, data[item].start.N, coord.E, coord.N)
    flyArcGroup.add(arcline); 
  });
})

效果:
Threejs实现3d地球记录(4)_第5张图片

3、使用圆弧绘制飞线

  • 绘制一段飞线,圆心做坐标原点
    使用顶点颜色渲染vertexColors实现飞线颜色渐变
  function createFlyLine(r, startAngle, endAngle) {
  var geometry = new THREE.BufferGeometry(); 
  var arc = new THREE.ArcCurve(0, 0, r, startAngle, endAngle, false);
  var pointsArr = arc.getSpacedPoints(50); 
  geometry.setFromPoints(pointsArr);

  // 批量计算所有顶点颜色数据
  var colorArr = [];
  for (var i = 0; i < pointsArr.length; i++) {
    var color1 = new THREE.Color(0xffffff); //白色
    var color2 = new THREE.Color(0xffff00); //黄色
    var color = color1.lerp(color2, i / pointsArr.length) //颜色的渐变
    colorArr.push(color.r, color.g, color.b);
  }
  // 设置几何体顶点颜色数据
  geometry.attributes.color = new THREE.BufferAttribute(new Float32Array(colorArr), 3);
  // 点模型渲染几何体每个顶点
  var material = new THREE.PointsMaterial({
    size: 3.0, 
    vertexColors: THREE.VertexColors, 
  });
  var FlyLine = new THREE.Points(geometry, material);
  return FlyLine;
}
  • 将生成的飞线作为原轨迹线的子元素,此时飞线将继承原轨迹线的旋转等变换
    生成的飞线是以坐标原点为圆心的,为了使之与原轨迹线重合,需将y坐标平移到xoy平面上关于y轴对称的圆弧线y坐标上
function arcXOY(startPoint, endPoint) {
  ...
  var flyAngle = (endAngle - startAngle) / 7; //设置飞线圆弧的弧度和轨迹线弧度相关
  var flyLine = createFlyLine(flyArcR, startAngle, startAngle + flyAngle);
  flyLine.position.y = flyArcCenter.y;//平移飞线圆弧和飞线轨迹圆弧重合
  //飞线段flyLine作为飞线轨迹arcLine子对象,继承飞线轨迹平移旋转等变换
  arcline.add(flyLine);
  //飞线段运动范围startAngle~flyEndAngle
  flyLine.flyEndAngle = endAngle - startAngle - flyAngle;
  flyLine.startAngle = startAngle;
  // 飞线段当前角度位置,设置随机值让飞线不是同步运动
  flyLine.AngleZ = arcline.flyEndAngle * Math.random();
  arcline.flyLine = flyLine;
  return arcline
}
  • 要使飞线运动起来需在循环渲染函数中添加操作
    飞线动画可通过绕z轴旋转实现,旋转到终点后再从头开始旋转
	const animate = function () {
      // 批量设置所有飞线的运动动画
      flyArr.forEach((fly) => {
        fly.rotation.z += 0.02; //调节飞线速度
        if (fly.rotation.z >= fly.flyEndAngle) fly.rotation.z = fly.startAngle;
      });
      ...
    };

效果:

4、实现小蝌蚪状飞线

小蝌蚪状飞线是指飞线段从头到尾点大小逐渐变小,要实现这种效果需要修改顶点着色器的源码,
顶点着色器源码可在three/src/renserers/shaders/ShaderLib/points.glsl.js中查看。

  • 通过设置attributes.percent 用于控制点的渲染大小
  var percentArr = []; //attributes.percent的数据
  for (var i = 0; i < pointsArr.length; i++) { //pointsArr飞线点集合
    percentArr.push(i / pointsArr.length);
  }
  var percentAttribue = new THREE.BufferAttribute(new Float32Array(percentArr), 1);
  // 通过顶点数据percent点模型从大到小变化,产生小蝌蚪形状飞线
  geometry.attributes.percent = percentAttribue;
  • 修改顶点着色器源码(注意:不同版本细节可能会稍微会有区别,不过整体思路是一样的)
  material.onBeforeCompile = function (shader) {
    // 顶点着色器中声明一个attribute变量:百分比
    shader.vertexShader = shader.vertexShader.replace(
      'void main() {',
      [
        'attribute float percent;', //顶点大小百分比变量,控制点渲染大小
        'void main() {',
      ].join('\n') // .join()把数组元素合成字符串
    );
    /*此处相当于将'void main() {'替换为: 
    attribute float percent;'
        void main() {*/
    // 调整点渲染大小计算方式
    shader.vertexShader = shader.vertexShader.replace(
      'gl_PointSize = size;',
      [
        'gl_PointSize = percent * size;',
      ].join('\n')
    );
  };

完整代码为:

function createFlyLine(r, startAngle, endAngle) {
  var geometry = new THREE.BufferGeometry(); 
  var arc = new THREE.ArcCurve(0, 0, r, startAngle, endAngle, false);
  var pointsArr = arc.getSpacedPoints(50);
  geometry.setFromPoints(pointsArr);
  var percentArr = []; 
  for (var i = 0; i < pointsArr.length; i++) {
    percentArr.push(i / pointsArr.length);
  }
  var percentAttribue = new THREE.BufferAttribute(new Float32Array(percentArr), 1);
  geometry.attributes.percent = percentAttribue;
  
  var colorArr = [];
  for (var i = 0; i < pointsArr.length; i++) {
    var color1 = new THREE.Color(0xffffff);
    var color2 = new THREE.Color(0xffff00); 
    var color = color1.lerp(color2, i / pointsArr.length)
    colorArr.push(color.r, color.g, color.b);
  }

  geometry.attributes.color = new THREE.BufferAttribute(new Float32Array(colorArr), 3);

  var material = new THREE.PointsMaterial({
    size: 3.0, 
    vertexColors: THREE.VertexColors,
  });

  material.onBeforeCompile = function (shader) {
    shader.vertexShader = shader.vertexShader.replace(
      'void main() {',
      [
        'attribute float percent;', 
        'void main() {',
      ].join('\n') 
    );
    
    shader.vertexShader = shader.vertexShader.replace(
      'gl_PointSize = size;',
      [
        'gl_PointSize = percent * size;',
      ].join('\n') 
    );
  };
  var FlyLine = new THREE.Points(geometry, material);
  return FlyLine;
}

效果:

你可能感兴趣的:(3d,javascript,开发语言)