三维弧线的绘制方法
背景
三维模型中经常需要用到测量工具,工作上需要实现三维场景中的交互式测量的功能。对于角度的测量,需要根据捕捉点分别绘制三个捕捉点和三点之间的两条线段,但是如果只绘制三个点和两段线,整体效果看起来不像是一个角度,加上线段夹角处的弧线,观感上看起来会更好一些。
我们的三维可视化浏览器使用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);