在开发一些物联网、数字孪生Web3D可视化项目的时候,比如一个工厂、化工厂、变电站、园区等场景,有时候,需要进行一些尺寸测量,比如属于鼠标点击选择模型表面上两点,然后计算两点之间尺寸距离,然后使用箭头和数字进行标注。
3D在线测量 体验地址( 先用鼠标点击按钮进入测量状态,在通过鼠标点击拾取模型任意两点,然后会自动标注 )
代码参考资料:threej中文网:www.webgl3d.cn/
模型表面选择两点( 用于尺寸标注计算 )
比如鼠标单击时候,通过射线从threejs模型表面Mesh上获取到两个坐标点,然后计算两点之间距离尺寸。
如果你不了解射线,更多关于射线具体内容参考threej中文网介绍
js
// 射线拾取选择场景模型表面任意点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相减返回一个向量,计算向量长度,表示两点之间距离
js
// 计算模型上选中两点的距离
function length(p1, p2) {
return p1.clone().sub(p2).length()
}
线段可视化两点之间距离
你可以用一条线段,线段两端使用三角形、小球或者箭头标注下,把要标注的两个点p1、p2可视化出来。
两点之间绘制一条直线线段,把p1到p2两点之间的距离可视化表示出来。
javascript
// 两点绘制一条直线 用于标注尺寸
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
线段两个端点位置,可以用箭头、小球、三角形平面、直线端等等任何你想要的形状可视化
比如圆锥形状箭头
javascript
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))
javascript
比如球形表示端点
const geometry = new THREE.SphereGeometry(r);
其他形状表示的线段端点
标注两点尺寸
标注两点之间尺寸方法有很多,比如CSS2DRenderer、CSS3DRenderer渲染器渲染的HTML元素标签、精灵模型Sprite+Canvas画布贴图、借助FontLoader类实现的3D Mesh文字...
上面这些标注具体知识点讲解可以参考threejs中文网文档标签章节
HTML元素作为标签
javascript
// 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画布贴图标注
javascript
// 精灵模型标注
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;
}
按钮触发测量
后面内容都是一些与前端交互界面相关,看不看基本无所谓了。通过前面内容把思路整理清楚即可。
鼠标与界面交互比较多,可以设置一个按钮或其它交互方式,控制是否进入测量中,当进入测量状态后,鼠标点击才能开始测量标注
javascript
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);
}
javascript
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)"
}
}
<template>
<div class="pos">
<div id="Home" class="bu" :style="{background: background}" @click="sizeBool()">测量</div>
</div>
</template>
<style scoped>
.pos {
/* background-color: aqua; */
position: absolute;
left: 50%;
margin-left: -30px;
bottom: 50px;
}
.bu {
background: rgba(0, 0, 0, 0.3);
width: 60px;
height: 60px;
line-height: 60px;
text-align: center;
color: #fff;
display: inline-block;
border-radius: 30px;
}
.bu:hover {
cursor: pointer;
}
</style>
点击按钮 非测量状态 隐藏标注的线段和标签
点击按钮,进入非测量状态,这时候可以隐藏标注的线段和标签。
隐藏就非常简单了,对于threejs模型而言,可以通过.visible
属性控制,对于HTML元素标签,可以通过CSS属性控制。
javascript
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坐标是否发生变化
js
// 通过鼠标按下抬起的时间差或者说距离差,来区分判断是鼠标拖动事件,还是鼠标拖动旋转事件
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;
}
}
}
})