一、场景
在很多项目中,比如在地球上标注一个国家有多少条航线通往其他国家,就会出现一个起点连接地球各个终点的弧线,那么本文先从最简单的开始说起,先不考虑三维的场景,先在一个二维平面XOY平面进行简单的两个对称点的连接弧线。实现效果如下:
二、实现逻辑
1、准备工作画一个半径为R的球面、两个球面对称点
2、反过来想一下,形成一条弧线,看API,需要圆心、半径、开始角度、结束角度。
对于上述提到的这些条件,很明显两个点是无法做到,两个点只能做到连一条直线或者线段,需要至少三个点,对于第三个点我们该如何选择哪个呢?不如试试对称点的中点,该中点是在两者之间且在球面外,那么他的Y坐标应该是球面半径R+x。
3、计算出中点后,根据三点算出圆心。
4、算出圆心后可以得到半径。
5、根据圆心、起点、中点三个点,可以算出圆心起点向量和圆心中点向量之间的夹角,因为在XOY平面,那么起始角度就是90度减去夹角。
6、那么中点夹角就是180度减去开始角度。
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中导出的组引入到场景中即可!后面再补充如何在三维场景中如何根据球面任意两点画出划线。可以剧透下思路就是不断拆解,将三维先转为二维,在二维中画出弧线后,根据四元数的逆得到三维中的弧线。