三维弧线的绘制方法

三维弧线的绘制方法

背景

三维模型中经常需要用到测量工具,工作上需要实现三维场景中的交互式测量的功能。对于角度的测量,需要根据捕捉点分别绘制三个捕捉点和三点之间的两条线段,但是如果只绘制三个点和两段线,整体效果看起来不像是一个角度,加上线段夹角处的弧线,观感上看起来会更好一些。

我们的三维可视化浏览器使用three.js的renderer作为渲染器,由于webgl的一些限制,web端的线宽设置无法生效,因此参考three.js官网提供的 fatLine 示例,使用实体宽线作为测量线段的显示。

本篇文章主要介绍绘制三维中角度的圆弧标识的实现。

实现思路

1、利用THREE.EllipseCurve创建圆弧曲线,然后提取圆弧上的点作为geometry的数据

2、利用第一步的数据创建mesh,添加到场景中

3、通过矩阵变幻将圆弧transform到线段的夹角之间

最终的实现效果如下图所示

以下绘制基于three.js实现,仅供参考:

先硬编码三个点,并绘制,并在场景中增加一个坐标轴参照系

const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
const positions = [];
const colors = [];
const pos = [
	new THREE.Vector3(10, 10, 0),
	new THREE.Vector3(5, 5, 7),
	new THREE.Vector3(-5, 8, 3),
const divisions = 3;
const color = new THREE.Color();
for ( let i = 0, l = divisions; i < l; i ++ ) {
	const t = i / l;
	positions.push(pos[i].x, pos[i].y, pos[i].z);
	color.setHSL( t, 1.0, 0.5 );
	colors.push( color.r, color.g, color.b );
const geometry = new LineGeometry();
geometry.setPositions( positions );
geometry.setColors( colors );
matLine = new LineMaterial( {
	color: 0xffffff,
	linewidth: 3, // in world units with size attenuation, pixels otherwise
	vertexColors: true,
	//resolution:  // to be set by renderer, eventually
	dashed: false,
	alphaToCoverage: true,
line = new Line2( geometry, matLine );
line.computeLineDistances();
line.scale.set( 1, 1, 1 );
scene.add( line );

然后调用以下函数绘制夹角圆弧

drawArcs(pos);    
function drawArcs(points) {
	const pointA = points[0];
	const pointO = points[1];
	const pointB = points[2];
	const oa = new THREE.Vector3().subVectors( pointA, pointO );
	const ob = new THREE.Vector3().subVectors( pointB, pointO );
	// aob平面的法线oc
	const oc = new THREE.Vector3().crossVectors( oa, ob ).normalize();
	// aob夹角度数
	const angle = oa.angleTo( ob );
	// 取圆弧角度一半的位置d,计算让oa旋转到od的矩阵
	const od = oa.clone();
	const m1 = new THREE.Matrix4().makeTranslation(-pointO.x, -pointO.y, -pointO.z);
	const m2 = new THREE.Matrix4().makeRotationAxis(oc, angle / 2);
	const m3 = new THREE.Matrix4().makeTranslation(pointO.x, pointO.y, pointO.z);
	m1.premultiply(m2).premultiply(m3);
	od.applyMatrix4(m1);
	// 取oa,ob最短长度的1/10作为圆弧半径
	const radius = Math.min(oa.length(), ob.length()) / 10;
	// 先根据角度和半径在原点处绘制圆弧曲线,再从圆弧曲线中取点创建geometry
	const curve = new THREE.EllipseCurve(
			0, 0, // 平面圆心坐标
			radius, radius, // 长短轴,这里取相同半径
			0, angle, // 圆弧的起始角度和终止角度
			false, // aClockwise,是否为顺时针
			0.0 // aRotation,绘制圆弧时是否旋转一定角度
	const segments = 20;
	const lineColor = new THREE.Color(0xffff00);
	const points2 = curve.getPoints( segments );
	const geometry2 = new THREE.BufferGeometry().setFromPoints( points2 );
	const color = [];
	for (let i = 0; i < segments + 1; i++) {
		color.push(lineColor.r, lineColor.g, lineColor.b);
	const geometry = new LineGeometry();
	geometry.setPositions( geometry2.attributes.position.array );
	geometry.setColors( color );
	const line = new Line2( geometry, matLine );
	line.computeLineDistances();
	scene.add(line);
	const moveVector = new THREE.Vector3( pointO.x, pointO.y, pointO.z );
	// 圆弧是二维的,添加到三维中显示时,圆心与世界坐标的原点重合,位于xy平面中, pointH是圆弧在x轴上的点
	const pointH = new THREE.Vector3( radius, 0, 0 );
	od.normalize();
	// 计算圆弧线上的中点位置d的坐标
	const d = new THREE.Vector3(pointO.x + od.x * radius, pointO.y + od.y  * radius, pointO.z + od.z * radius);
	// 法线oc转换为球坐标,根据这个球坐标坐标计算将位于x轴上的点pointH旋转到oa位置矩阵
	const spherical = new THREE.Spherical().setFromVector3( oc );
	// oc在xz平面上的投影
	const projectOnXZ = oc.clone().projectOnPlane( new THREE.Vector3( 0, 1, 0 ) );
	projectOnXZ.normalize();
	// oc与oc在xz平面上投影构成的平面,计算法线crossVec
	const crossVec = new THREE.Vector3().crossVectors( projectOnXZ, oc );
	crossVec.normalize();
	// 先将pointH绕着y轴旋转到角度theta到达oc在xz平面的投影处
	const matrix1 = new THREE.Matrix4().makeRotationY( spherical.theta );
	// o-pointH绕着crossVec旋转,到达oc的位置处,球坐标中phi的值都不会超过90
	const matrix2 = new THREE.Matrix4().makeRotationAxis( crossVec, Math.abs( Math.PI / 2 - spherical.phi ) );
	// o-pointH移动到o点处
	const matrix5 = new THREE.Matrix4().makeTranslation( moveVector.x, moveVector.y, moveVector.z );
	matrix1.premultiply( matrix2 );
	pointH.applyMatrix4( matrix1 );
        //计算pointH和起始边oa的夹角,angleTo计算出来的夹角始终是小于180度的,
	// 将一条边旋转到夹角所在平面后,还要再确定一个点的位置,
	const angle1 = pointH.angleTo( oa );
	const aoh = new THREE.Vector3().crossVectors( oa, pointH ).normalize();
	let matrix3;
	// 两者方向相时同顺时针旋转
	if ( approxEqual(oc.x, aoh.x) && approxEqual(oc.y, aoh.y) && approxEqual(oc.z, aoh.z) ) {
		matrix3 = new THREE.Matrix4().makeRotationAxis( oc.negate(), angle1 );
	} else {
		matrix3 = new THREE.Matrix4().makeRotationAxis( oc, angle1 );
	matrix1.premultiply( matrix3 );
	matrix1.premultiply( matrix5 );
	line.applyMatrix4( matrix1 );
	const mesh = new THREE.Mesh(
		new THREE.SphereGeometry( 0.2, 10,  10),
		new THREE.MeshBasicMaterial( {color: 0xff6666} ));
	mesh.position.set(d.x, d.y, d.z);
	// scene.add(mesh);
	// 测量角的法线
	const geometry5 = new LineGeometry();
	const pos3 = [
		0, 0, 0,
		oc.x, oc.y, oc.z,
	geometry5.setPositions( pos3 );
	geometry5.setColors( color );
	const line5 = new Line2( geometry5, matLine );
	line5.computeLineDistances();
	line5.scale.set( 3, 3, 3 );
	const translateMat = new THREE.Matrix4().makeTranslation(moveVector.x, moveVector.y, moveVector.z);
	line5.applyMatrix4(translateMat);
	scene.add( line5 );
function approxEqual(a, b) {
	return Math.abs(a - b) < 0.001;

补充

最近重看以前写的一些笔记,发现当时想的好复杂,看了半天...

后来终于回忆起来,于是又加上了注释,足以见得编码时注释的重要性了,=_=

// 添加到三维中的圆弧位于xy平面上,利用旋转矩阵将其旋转到和夹角共面的位置,中间将pointH旋转到oa平面的步骤用一个quaternion就可以实现了
// 再按照上面的方法将pointH和pointA对齐
const fromNormal = new THREE.Vector3(0,0,1);