Three.js-边绑定

需求来源

1

最近的一个项目,需要将公司的各项技术和应用的关系,以3D的形式呈现出来。如果直接用直线连接,会密密麻麻很难看,分不清具体的关系。因此我们考虑通过边绑定的方式,对同一个源头和目标的连线,做一些聚合。我写了个Demo,效果如上图所示。

思路

大致分为如下几个步骤:

  • 构建数据:将从A分类到B分类的数据梳理出来,形成若干个数组

  • 计算每一组数据的中心点

    • 从键值中获取所有技术模块的id,然后根据id获取技术模块的节点,进而获取坐标,形成一个几何多边形

    • 求多边形的中心点坐标P1

  • 计算边绑定的点

    • 根据键名,得到应用类型的id,取到应用类型节点,得到其坐标P2

    • 根据P1和P2,取该直线上的点,作为绑定点的坐标

  • 绘制连线

关键代码

计算N个点的中心点

这个如果要严格计算,会比较麻烦,得构建三角形,求每个三角形重心,然后根据重心算中心点。

我为了简单,直接用这N个点的x、y、z求平均值,作为中心点,效果看起来还可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取技术模块的中心点
* TODO:先用了一个简单的求和算法,后面估计得修改
* @param edges
*/
getCenterOfTechModules(edges: Array<Edge>): [number, number, number] {
let totalX = 0;
let totalY = 0;
let totalZ = 0;
edges.forEach(edge => {
// from就是技术模块的id
const techModuleNode = this.getNodeById(edge.from) as Node;
const [x, y, z] = techModuleNode.position as [number, number, number];
totalX += x;
totalY += y;
totalZ += z;
});

return [totalX / edges.length, totalY / edges.length, totalZ / edges.length];
}

计算3D空间中,直线上的点

参考这个文章:

https://blog.csdn.net/weixin_42795611/article/details/120796681

我是按百分比取点的,因此参数中有个percent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 取3D空间两个向量形成的直线上的点
* @see https://blog.csdn.net/weixin_42795611/article/details/120796681
* @param vector1
* @param vector2
* @param percent
*/
getPointByPercent(vector1: Vector3, vector2: Vector3, percent: number) {
function getLength(vector: Vector3) {
const x = vector.x ** 2;
const y = vector.y ** 2;
const z = vector.z ** 2;
return Math.sqrt(x + y + z);
}

function getEquationParameters(v1: Vector3, v2: Vector3) {
const direction: Vector3 = v2.sub(v1);
const lengthOfVector = getLength(direction);

const normalizedVector = new Vector3();
normalizedVector.copy(direction);
normalizedVector.normalize();
const lengthOfNormalizedVector = getLength(normalizedVector);

return {
dx: normalizedVector.x,
dy: normalizedVector.y,
dz: normalizedVector.z,
t: lengthOfVector / lengthOfNormalizedVector
};
}

const { dx, dy, dz, t } = getEquationParameters(vector1, vector2);
return new Vector3(
vector1.x + dx * t * percent,
vector1.y + dy * t * percent,
vector1.z + dz * t * percent
);
}

绘制曲线

算出绘制曲线所需的点后,直接用THREE.CatmullRomCurve3就可以画出来了。

为什么没有用贝塞尔曲线(THREE.CubicBezierCurve3):因为贝塞尔中间那段无法收拢。

贝塞尔曲线与 CatmullRom 曲线的区别在于,CatmullRom 曲线可以平滑的通过所有点,一般用于绘制轨迹,而贝塞尔曲线通过中间点来构造切线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 绘制曲线
* @param points -
* @param pointCount -
* @returns
*/
export function drawCurveLine(
vectors: Array<Vector3>,
materialParameters: LineBasicMaterialParameters = { color: 0xcccccc },
pointCount = 50
) {
const curve2 = new CatmullRomCurve3(vectors);

const points2 = curve2.getPoints(pointCount);
const geometry2 = new BufferGeometry().setFromPoints(points2);

const material2 = new LineBasicMaterial(materialParameters);

return new Line(geometry2, material2);
}

drawCurveEdges(edges: Array<Edge>, vectors: Array<Vector3>) {
const verticalLineLengthOfTechModule = 0.5;
const verticalLineLengthOfApp = 0.2;
edges.forEach(techModuleEdge => {
const techModuleNode = this.getNodeById(techModuleEdge.from);
const appNode = this.getNodeById(techModuleEdge.to);

const [x, y, z] = techModuleNode.position as [number, number, number];
// 注意:这里要从下往上画,即从应用到技术,顺序别反了
const points = [
new Vector3(...(appNode.position as [number, number, number])),
new Vector3(
appNode.position[0],
appNode.position[1] + verticalLineLengthOfApp,
appNode.position[2]
),
...vectors,
// TODO:再加一个低一点的点,让线条先垂直一段距离,再进行边绑定
new Vector3(x, y - verticalLineLengthOfTechModule, z),
new Vector3(x, y, z)
];

const line = drawCurveLine(points);

this._scene.add(line);
});
}

资料

(TODO)山东大学可视化实验室的边绑定算法:

https://zhuanlan.zhihu.com/p/149112508