在开发一些物联网、数字孪生Web3D可视化项目的时候,比如一个工厂、化工厂、变电站、园区等场景,有时候,需要进行一些尺寸测量,比如属于鼠标点击选择模型表面上两点,然后计算两点之间尺寸距离,然后使用箭头和数字进行标注。

3D在线测量 体验地址( 先用鼠标点击按钮进入测量状态,在通过鼠标点击拾取模型任意两点,然后会自动标注 )

视频讲解

代码参考资料:threej中文网:http://www.webgl3d.cn/

模型表面选择两点( 用于尺寸标注计算 )

比如鼠标单击时候,通过射线从threejs模型表面Mesh上获取到两个坐标点,然后计算两点之间距离尺寸。

如果你不了解射线,更多关于射线具体内容参考threej中文网介绍

// 射线拾取选择场景模型表面任意点xyz坐标

function rayChoosePoint(event, model, camera) {

const px = event.offsetX;

const py = event.offsetY;

//屏幕坐标转标准设备坐标

const x = (px / window.innerWidth) * 2 - 1;

const y = -(py / window.innerHeight) * 2 + 1;

const raycaster = new THREE.Raycaster();

//.setFromCamera()在点击位置生成raycaster的射线ray

raycaster.setFromCamera(new THREE.Vector2(x, y), camera);

// 射线交叉计算拾取模型

const intersects = raycaster.intersectObject(model, true);

let v3 = null;

if (intersects.length > 0) {

// 获取模型上选中的一点坐标

v3 = intersects[0].point

}

return v3;

}

计算两点之间距离

通过向量可以计算两点之间距离,如果你不了解向量相关数学几何计算,参考threej中文网进阶内容

两点坐标p1、p2相减返回一个向量,计算向量长度,表示两点之间距离

// 计算模型上选中两点的距离

function length(p1, p2) {

return p1.clone().sub(p2).length()

}

线段可视化两点之间距离

你可以用一条线段,线段两端使用三角形、小球或者箭头标注下,把要标注的两个点p1、p2可视化出来。

两点之间绘制一条直线线段,把p1到p2两点之间的距离可视化表示出来。

// 两点绘制一条直线 用于标注尺寸

function createLine(p1, p2) {

const material = new THREE.LineBasicMaterial({

color: 0xffff00,

depthTest: false,//不进行深度测试,后渲染,叠加在其它模型之上(解决两个问题)

// 1.穿过模型,在内部看不到线条

// 2.线条与mesh重合时候,深度冲突不清晰

});

const geometry = new THREE.BufferGeometry(); //创建一个几何体对象

//类型数组创建顶点数据

const vertices = new Float32Array([p1.x, p1.y, p1.z, p2.x, p2.y, p2.z]);

geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);

const line = new THREE.Line(geometry, material);

return line

}

//如果你想绘制有粗细的线段,可以参考threejs扩展库:Line2.js

线段两个端点位置,可以用箭头、小球、三角形平面、直线端等等任何你想要的形状可视化

比如圆锥形状箭头

function createMesh(p,dir,camera) {

const L = camera.position.clone().sub(p).length()

const h = L/20

//尺寸你可以根据需要自由设置,比如距离相机距离,比如直接根据场景渲染范围给固定尺寸

const geometry = new THREE.CylinderGeometry(0,L/200,h);

geometry.translate(0,-h/2,0)

const material = new THREE.MeshBasicMaterial({

color: 0x00ffff, //设置材质颜色

depthTest: false,

});

const mesh = new THREE.Mesh(geometry, material);

//通过四元数表示默认圆锥需要旋转的角度,才能和标注线段的方向一致

const quaternion = new THREE.Quaternion();

//参数dir表示线段方向,通过两点p1、p2计算即可,通过dir来控制圆锥朝向

quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dir)

mesh.quaternion.multiply(quaternion)

mesh.position.copy(p);

return mesh;

}

const dir = p1.clone().sub(p2).normalize()

model.add(createMesh(p1,dir,camera))

model.add(createMesh(p2,dir.clone().negate(),camera))

比如球形表示端点

const geometry = new THREE.SphereGeometry(r);

其他形状表示的线段端点

标注两点尺寸

标注两点之间尺寸方法有很多,比如CSS2DRenderer、CSS3DRenderer渲染器渲染的HTML元素标签、精灵模型Sprite+Canvas画布贴图、借助FontLoader类实现的3D Mesh文字…

上面这些标注具体知识点讲解可以参考threejs中文网文档标签章节

HTML元素作为标签

// CSS2或CSS3渲染标注

const div = document.createElement('div')

document.body.appendChild(div)

div.style.fontSize = "20px"

div.style.marginTop = "-20px"

div.style.color = "#ffffff"

// div.style.padding = "5px 10px"

// div.style.background = "rgba(0,0,0,0.9)"

div.textContent = size+ 'm' ;

const tag = new CSS2DObject(div);

const center = p1.clone().add(p2).divideScalar(2)

tag.position.copy(center);

model.add(tag);

Sprite作为标签:Sprite+Canvas画布贴图标注

// 精灵模型标注

const canvas = createCanvas(size+'m')

const texture = new THREE.CanvasTexture(canvas);

const spriteMaterial = new THREE.SpriteMaterial({

map: texture,

depthTest: false,

});

const sprite = new THREE.Sprite(spriteMaterial);

const center = p1.clone().add(p2).divideScalar(2)

const y = camera.position.clone().sub(center).length()/25;//精灵y方向尺寸

// sprite宽高比和canvas画布保持一致

const x = canvas.width / canvas.height * y;//精灵x方向尺寸

sprite.scale.set(x, y, 1);// 控制精灵大小

sprite.position.copy(center);

sprite.position.y += y / 2;

model.add(sprite);

// 生成一个canvas对象,标注文字为参数name

function createCanvas(name) {

/**

* 创建一个canvas对象,绘制几何图案或添加文字

*/

const canvas = document.createElement("canvas");

const arr = name.split(""); //分割为单独字符串

let num = 0;

const reg = /[\u4e00-\u9fa5]/;

for (let i = 0; i < arr.length; i++) {

if (reg.test(arr[i])) { //判断是不是汉字

num += 1;

} else {

num += 0.5; //英文字母或数字累加0.5

}

}

// 根据字符串符号类型和数量、文字font-size大小来设置canvas画布宽高度

const h = 240; //根据渲染像素大小设置,过大性能差,过小不清晰

const w = h + num * 110;

canvas.width = w;

canvas.height = h;

const h1 = h * 0.8;

const c = canvas.getContext('2d');

// 定义轮廓颜色,黑色半透明

c.fillStyle = "rgba(0,0,0,0.4)";

// 绘制半圆+矩形轮廓

const R = h1 / 2;

c.arc(R, R, R, -Math.PI / 2, Math.PI / 2, true); //顺时针半圆

c.arc(w - R, R, R, Math.PI / 2, -Math.PI / 2, true); //顺时针半圆

c.fill();

// 绘制箭头

c.beginPath();

const h2 = h - h1;

c.moveTo(w / 2 - h2 * 0.6, h1);

c.lineTo(w / 2 + h2 * 0.6, h1);

c.lineTo(w / 2, h);

c.fill();

// 文字

c.beginPath();

c.translate(w / 2, h1 / 2);

c.fillStyle = "#ffffff"; //文本填充颜色

c.font = "normal 128px Times New Roman"; //字体样式设置

c.textBaseline = "middle"; //文本与fillText定义的纵坐标

c.textAlign = "center"; //文本居中(以fillText定义的横坐标)

c.fillText(name, 0, 0);

return canvas;

}

按钮触发测量

后面内容都是一些与前端交互界面相关,看不看基本无所谓了。通过前面内容把思路整理清楚即可。

鼠标与界面交互比较多,可以设置一个按钮或其它交互方式,控制是否进入测量中,当进入测量状态后,鼠标点击才能开始测量标注

const testBool = ref(false);//测量状态

const background = ref("rgba(0, 0, 0, 0.3)")

const sizeBool = () => {

testBool.value = !testBool.value

if(testBool.value){

background.value = "rgba(0, 0, 0, 0.8)"

}else{

background.value = "rgba(0, 0, 0, 0.3)"

}

}

let clickNum = 0;//记录点击次数

let p1 = null;

let p2 = null;

let L = 0

renderer.domElement.addEventListener('click', function (event) {

if (testBool.value) {

clickNum += 1;

if (clickNum == 1) {

p1 = rayChoosePoint(event, model, camera)

console.log('p1', p1);

} else {

clickNum = 0;

p2 = rayChoosePoint(event, model, camera)

if (p1 && p2) {

L = length(p1, p2).toFixed(2)

console.log('L', L);

sizeTag(p1, p2, L, camera);//尺寸标注 箭头线段、尺寸数值

}

p1 = null;

p2 = null;

}

}

})

//线段尺寸标注

function sizeTag(p1, p2, size, camera) {

const line = createLine(p1, p2);

sizeTagGroup.add(line)

const dir = p1.clone().sub(p2).normalize()

sizeTagGroup.add(createMesh(p1, dir, camera))

sizeTagGroup.add(createMesh(p2, dir.clone().negate(), camera))

// 精灵模型标注

// const canvas = createCanvas(size+'m')

// const texture = new THREE.CanvasTexture(canvas);

// const spriteMaterial = new THREE.SpriteMaterial({

// map: texture,

// depthTest: false,

// });

// const sprite = new THREE.Sprite(spriteMaterial);

// const center = p1.clone().add(p2).divideScalar(2)

// const y = camera.position.clone().sub(center).length()/25;//精灵y方向尺寸

// // sprite宽高比和canvas画布保持一致

// const x = canvas.width / canvas.height * y;//精灵x方向尺寸

// sprite.scale.set(x, y, 1);// 控制精灵大小

// sprite.position.copy(center);

// sprite.position.y += y / 2;

// model.add(sprite);

// CSS2或CSS3渲染标注

const div = document.createElement('div')

document.body.appendChild(div)

div.style.fontSize = "20px"

div.style.marginTop = "-20px"

div.style.color = "#ffffff"

// div.style.padding = "5px 10px"

// div.style.background = "rgba(0,0,0,0.9)"

div.textContent = size + 'm';

const tag = new CSS2DObject(div);

const center = p1.clone().add(p2).divideScalar(2)

tag.position.copy(center);

sizeTagGroup.add(tag);

}

const testBool = ref(false);//测量状态

const background = ref("rgba(0, 0, 0, 0.3)")

const sizeBool = () => {

testBool.value = !testBool.value

if(testBool.value){

background.value = "rgba(0, 0, 0, 0.8)"

}else{

background.value = "rgba(0, 0, 0, 0.3)"

}

}

点击按钮 非测量状态 隐藏标注的线段和标签

点击按钮,进入非测量状态,这时候可以隐藏标注的线段和标签。

隐藏就非常简单了,对于threejs模型而言,可以通过.visible属性控制,对于HTML元素标签,可以通过CSS属性控制。

const sizeBool = () => {

testBool.value = !testBool.value

if (testBool.value) {

background.value = "rgba(0, 0, 0, 0.8)"

sizeTagGroup.visible = true

const domArr = document.body.getElementsByClassName("sizeTag")

for (let i = 0; i < domArr.length; i++) {

domArr[i].style.visibility = "visible"

}

} else {

background.value = "rgba(0, 0, 0, 0.3)"

// sizeTagGroup组对象包含了所有标注线段或标签可以整体隐藏

sizeTagGroup.visible = false

// 如果你的标签是HTML,也可以增加CSS代码隐藏所有标注文字

const domArr = document.body.getElementsByClassName("sizeTag")

for (let i = 0; i < domArr.length; i++) {

domArr[i].style.visibility = "hidden"

}

}

}

鼠标事件冲突小问题

3D场景一般会通过鼠标拖动旋转视角,这时候要注意鼠标拖动事件,与鼠标点击测试事件的冲突,避免拖动的时候,产生意外的尺寸测量。

思路很简单,你可以记录鼠标按下和抬起的时间差,或者更好的方式,判断鼠标按下和抬起时候,鼠标的x、y坐标是否发生变化

// 通过鼠标按下抬起的时间差或者说距离差,来区分判断是鼠标拖动事件,还是鼠标拖动旋转事件

let mousedownX = 0;

let mousedownY = 0;

twin.renderer.domElement.addEventListener('mousedown', function (event) {

mousedownX = event.offsetX;

mousedownY = event.offsetX;

})

twin.renderer.domElement.addEventListener('mouseup', function (event) {

const x = event.offsetX;

const y = event.offsetX;

if(Math.abs(x-mousedownX)<1 && Math.abs(y-mousedownY)<1){

if (store.testSizeBool) {

clickNum += 1;

if (clickNum == 1) {

p1 = rayChoosePoint(event, twin.model, twin.camera)

console.log('p1', p1);

} else {

clickNum = 0;

p2 = rayChoosePoint(event, twin.model, twin.camera)

console.log('p2', p2);

if (p1 && p2) {

L = length(p1, p2).toFixed(2)

console.log('L', L);

sizeTag(p1, p2, L, twin.camera);//尺寸标注 箭头线段、尺寸数值

}

p1 = null;

p2 = null;

}

}

}

})

精彩内容

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: