three.js 物体3D模型——尺寸测量标注

在开发一些物联网、数字孪生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)"
    }
}
<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属性控制。

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;
        }
    }
    }

})

最近更新

  1. TCP协议是安全的吗?

    2024-01-30 04:24:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-30 04:24:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-30 04:24:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-30 04:24:01       20 阅读

热门阅读

  1. LeetCode 第十七天

    2024-01-30 04:24:01       45 阅读
  2. CountDownLatch使用及原理介绍

    2024-01-30 04:24:01       40 阅读
  3. AcWing.873.欧拉函数

    2024-01-30 04:24:01       29 阅读
  4. VUE就是最强!

    2024-01-30 04:24:01       34 阅读
  5. 十个鼠标事件

    2024-01-30 04:24:01       40 阅读
  6. 1.基于C#的Dbf读写(文件结构概述)

    2024-01-30 04:24:01       32 阅读
  7. cpp-stub 打桩失败

    2024-01-30 04:24:01       40 阅读
  8. 题解:CF1922C(Closest Cities)

    2024-01-30 04:24:01       35 阅读
  9. 面试 CSS 框架八股文十问十答第一期

    2024-01-30 04:24:01       42 阅读
  10. C++算法学习心得七.贪心算法(2)

    2024-01-30 04:24:01       21 阅读
  11. Quartz在spring boot项目中重启后不能继续执行问题

    2024-01-30 04:24:01       32 阅读
  12. OpenSSH 9.6/9.6p1 (2023-12-18)的发布说明(中文译文)

    2024-01-30 04:24:01       41 阅读