ThreeJs 平面两点连接弧线

377 阅读4分钟

一、场景

在很多项目中,比如在地球上标注一个国家有多少条航线通往其他国家,就会出现一个起点连接地球各个终点的弧线,那么本文先从最简单的开始说起,先不考虑三维的场景,先在一个二维平面XOY平面进行简单的两个对称点的连接弧线。实现效果如下:

Snipaste_2024-10-27_20-41-52.png

二、实现逻辑

1、准备工作画一个半径为R的球面、两个球面对称点
2、反过来想一下,形成一条弧线,看API,需要圆心、半径、开始角度、结束角度。
      对于上述提到的这些条件,很明显两个点是无法做到,两个点只能做到连一条直线或者线段,需要至少三个点,对于第三个点我们该如何选择哪个呢?不如试试对称点的中点,该中点是在两者之间且在球面外,那么他的Y坐标应该是球面半径R+x。
3、计算出中点后,根据三点算出圆心。
4、算出圆心后可以得到半径。
5、根据圆心、起点、中点三个点,可以算出圆心起点向量和圆心中点向量之间的夹角,因为在XOY平面,那么起始角度就是90度减去夹角。
6、那么中点夹角就是180度减去开始角度。

Snipaste_2024-10-27_21-30-24.png
A为起点 B为中点 C为中点 阿尔法为起点与中点及圆心行成的角度。那么开始就是就是四分之一圆弧-夹角。

三、代码实现

// utils.js
// 创建球面方法
function createSphere(radius) {
  const geometry = new THREE.SphereGeometry(radius, 40, 40);
  const material = new THREE.MeshLambertMaterial({
    color: 0x006666,
    transparent: true,
    opacity: 0.5,
  });
  const mesh = new THREE.Mesh(geometry, material);
  return mesh;
}

// 根据圆弧三点计算出圆心
function threePointCenter(p1, p2, p3) {
  var L1 = p1.lengthSq(); //p1到坐标原点距离的平方
  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;
}

// 根据三点计算出夹角
function twoSideAngle(startPoint, endPoint, centerPoint) {
  const p1 = startPoint.clone().sub(centerPoint).normalize();
  const p2 = endPoint.clone().sub(centerPoint).normalize();
  const cosValue = p1.clone().dot(p2.clone());
  const angle = Math.acos(cosValue);
  return angle;
}

// 绘制一条弧线
function circleLine(ax, ay, xRadius, startAngle, endAngle) {
  const curve = new THREE.ArcCurve(
    ax,
    ay,
    xRadius,
    startAngle,
    endAngle,
    false
  );
  const points = curve.getPoints(100);
  const geometry = new THREE.BufferGeometry();
  geometry.setFromPoints(points);
  const material = new THREE.LineBasicMaterial({
    color: 0x00ffff,
  });
  const line = new THREE.Line(geometry, material);
  return line;
}

export {createSphere, threePointCenter, twoSideAngle, circleLine }

// modal.js
import {createSphere, threePointCenter, twoSideAngle, circleLine } from 'utlis.js'

const sumGroup = new THREE.Group();

const startPoint = new THREE.Vector3( -148.15325108927067, 23.46516975603463, 0)
const endPoint = new THREE.Vector3( 148.15325108927067, 23.46516975603463, 0)

const R = 150;
const sphere = createSphere(R);
sumGroup.add(sphere);


function renderLine(startPoint, endPoint) {
  // 算出两点之间的中间点的坐标
  const midV3 = startPoint.clone().add(endPoint).multiplyScalar(0.5);
  // 中间点的单位向量
  const midDir = midV3.clone().normalize();
  // 计算出起点、终点及球的圆心行成的角度值
  const earthRadianAngle = twoSideAngle(
    startPoint,
    endPoint,
    new THREE.Vector3(0, 0, 0)
  );
  // 根据一定的正比例函数关系 确定与圆心在一条线上的圆弧上的坐标, 这边这个关系自定义或者按照开发要求即可
  const arcTopCoord = midDir
    .clone()
    .multiplyScalar(R + R * earthRadianAngle * 0.2);
  // 计算出起始点及圆弧点三点的圆心
  const flyArcCenter = threePointCenter(startPoint, endPoint, arcTopCoord);
  // 计算出圆的半径
  const flyRadius = Math.abs(flyArcCenter.y - arcTopCoord.y);
  // 计算出起点与圆心的夹角值
  const flyRadianAngle = twoSideAngle(
    startPoint,
    new THREE.Vector3(0, -1, 0),
    flyArcCenter
  );
  // 计算起始角度 这里是-Math.PI/2 是因为我们是顺时针画的弧线,那么起始角度是负的
  const startAngle = -Math.PI / 2 + flyRadianAngle;
  // 计算终点角度
  const endAngle = Math.PI - startAngle;
  const arcLine = circleLine(
    flyArcCenter.x,
    flyArcCenter.y,
    flyRadius,
    startAngle,
    endAngle
  );
  sumGroup.add(arcLine);
}

export default sumGroup

四、最后

上述省略了场景、灯光、相机等基础代码,只需要将modal.js中导出的组引入到场景中即可!后面再补充如何在三维场景中如何根据球面任意两点画出划线。可以剧透下思路就是不断拆解,将三维先转为二维,在二维中画出弧线后,根据四元数的逆得到三维中的弧线。