Three.js基础曲线函数有三种:
样条曲线:在三维空间中设置5个顶点,输入三维样条曲线CatmullRomCurve3函数作为参数,然后返回更多个顶点,通过返回的顶点数据,构建一个几何体,然后绘制出来一条沿着5个顶点的光滑样条曲线。
三维三次贝赛尔曲线: 由起点、终点、及两个控制点定义,通过三维三次贝塞尔曲线(CubicBezierCurve3)绘制出一条平滑的曲线
//计算两点之间和球心构成夹角的弧度值方法
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)
//求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); //飞线插入场景中
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 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,
}
}
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);
});
})
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;
}
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
}
const animate = function () {
// 批量设置所有飞线的运动动画
flyArr.forEach((fly) => {
fly.rotation.z += 0.02; //调节飞线速度
if (fly.rotation.z >= fly.flyEndAngle) fly.rotation.z = fly.startAngle;
});
...
};
效果:
小蝌蚪状飞线是指飞线段从头到尾点大小逐渐变小,要实现这种效果需要修改顶点着色器的源码,
顶点着色器源码可在three/src/renserers/shaders/ShaderLib/points.glsl.js中查看。
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;
}
效果: