Vue使用Three.js加载glb (gltf) 文件模型及实现简单的选中高亮、测距、测面积
安装:
# three.jsnpm install --save three
附中文网:
5. gltf不同文件形式(.glb) | Three.js中文网
附官网:
安装 – three.js docs
完整代码(简易demo):
<template><div class="siteInspection" ref="threeContainer"><div class="measurement-buttons"><button @click="startDistanceMeasurement">测距</button><button @click="startAreaMeasurement">测面积</button></div></div>
</template><script lang="ts" setup>
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";const lzModel = ref("/lz.glb"); // 此处为你的 模型文件 .glb或 .gltf
const threeContainer = ref(null);// 创建场景、相机和渲染器
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let model: THREE.Group | null = null;
let controls: OrbitControls;
let raycaster: THREE.Raycaster;
let mouse: THREE.Vector2;
let selectedObject: THREE.Mesh | null = null; // 用于跟踪当前选中的对象
let line: THREE.Line | null = null; // 用于存储高亮线
let label: THREE.Sprite | null = null; // 用于存储标签// 测量相关变量
let isDistanceMeasuring = false;
let isAreaMeasuring = false;
let distancePoints: THREE.Vector3[] = [];
let areaPoints: THREE.Vector3[] = [];
let distanceLine: THREE.Line | null = null;
let areaLines: THREE.Line[] = [];
let distanceLabel: THREE.Sprite | null = null;
let areaLabel: THREE.Sprite | null = null;
let distanceDots: THREE.Mesh[] = [];
let areaDots: THREE.Mesh[] = [];// 初始化
function initThree() {scene = new THREE.Scene();scene.background = new THREE.Color(0xffffff); // 设置背景颜色为亮色camera = new THREE.PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,1000);renderer = new THREE.WebGLRenderer();renderer.setSize(window.innerWidth, window.innerHeight);threeContainer.value.appendChild(renderer.domElement);// 添加光源const ambientLight = new THREE.AmbientLight(0x404040); // 环境光scene.add(ambientLight);const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);directionalLight.position.set(1, 1, 1).normalize();scene.add(directionalLight);// 设置相机位置camera.position.z = 5;camera.lookAt(0, 0, 0);// 创建GLTF加载器对象const loader = new GLTFLoader();loader.load(lzModel.value, function (gltf: any) {model = gltf.scene;scene.add(model);});// 初始化OrbitControlscontrols = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true; // 启用阻尼效果controls.dampingFactor = 0.25; // 阻尼系数controls.enableZoom = true; // 启用缩放controls.enablePan = true; // 启用平移// 初始化Raycaster和Vector2raycaster = new THREE.Raycaster();mouse = new THREE.Vector2();// 渲染循环function animate() {requestAnimationFrame(animate);controls.update(); // 更新OrbitControlsrenderer.render(scene, camera);}animate();// 清理事件监听器onUnmounted(() => {controls?.dispose();threeContainer.value?.removeEventListener("mousedown", onDocumentMouseDown);if (line) {scene.remove(line);}if (label) {scene.remove(label);}if (distanceLine) {scene.remove(distanceLine);}areaLines.forEach((areaLine) => {scene.remove(areaLine);});if (distanceLabel) {scene.remove(distanceLabel);}if (areaLabel) {scene.remove(areaLabel);}distanceDots.forEach((dot) => {scene.remove(dot);});areaDots.forEach((dot) => {scene.remove(dot);});});// 添加鼠标点击事件监听threeContainer.value.addEventListener("mousedown", onDocumentMouseDown);
}function onDocumentMouseDown(event: MouseEvent) {// 将鼠标位置归一化到-1到1之间mouse.x = (event.clientX / window.innerWidth) * 2 - 1;mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;// 更新射线投射器raycaster.setFromCamera(mouse, camera);// 计算交点const intersects = raycaster.intersectObjects(model ? model.children : [],true);if (intersects.length > 0) {const intersect = intersects[0];const point = intersect.point;if (isDistanceMeasuring) {if (event.button === 0) {// 左键点击distancePoints.push(point);addDot(point, distanceDots);if (distancePoints.length === 2) {calculateDistance();}} else if (event.button === 2) {// 右键点击cancelDistanceMeasurement();}} else if (isAreaMeasuring) {if (event.button === 0) {// 左键点击areaPoints.push(point);addDot(point, areaDots);if (areaPoints.length >= 3) {updateAreaLines();// calculateArea();}} else if (event.button === 2) {// 右键点击if (areaPoints.length >= 3) {calculateArea();} else {cancelAreaMeasurement();}}} else {// 原有的点击选中功能if (selectedObject) {(selectedObject.material as THREE.MeshStandardMaterial).emissive.set(0x000000); // 恢复 emissive 颜色为黑色if (line) {scene.remove(line);line = null;}if (label) {scene.remove(label);label = null;}}if (intersect.object instanceof THREE.Mesh) {(intersect.object.material as THREE.MeshStandardMaterial).emissive.set(0x16d46b); // 设置 emissive 颜色为绿色selectedObject = intersect.object; // 更新选中的对象// 创建线段const startPoint = intersect.point;const endPoint = startPoint.clone().add(new THREE.Vector3(0, 0, 0.5)); // 延伸到外部const geometry = new THREE.BufferGeometry().setFromPoints([startPoint,endPoint,]);const material = new THREE.LineBasicMaterial({color: 0x16d46b,});line = new THREE.Line(geometry, material);scene.add(line);// 创建标签const canvas = document.createElement("canvas");const context = canvas.getContext("2d");if (context) {const textWidth = context.measureText(intersect.object.name).width;canvas.width = textWidth + 20;canvas.height = 50;context.font = "16px";// context.fillStyle = "rgba(0,0,0,0.8)";context.fillRect(0, 0, canvas.width, canvas.height);context.fillStyle = "#0179d4";context.textAlign = "center";context.fillText(intersect.object.name, canvas.width / 2, 30);const texture = new THREE.CanvasTexture(canvas);texture.needsUpdate = true; // 确保纹理更新const spriteMaterial = new THREE.SpriteMaterial({ map: texture });label = new THREE.Sprite(spriteMaterial);label.position.copy(endPoint);label.scale.set(0.1, 0.1, 1); // 调整标签大小scene.add(label);}}}} else {// 如果没有点击到任何对象,恢复当前选中的对象的颜色并移除线和标签if (selectedObject) {(selectedObject.material as THREE.MeshStandardMaterial).emissive.set(0x000000); // 恢复 emissive 颜色为黑色selectedObject = null; // 清除选中的对象if (line) {scene.remove(line);line = null;}if (label) {scene.remove(label);label = null;}}}
}function startDistanceMeasurement() {isDistanceMeasuring = true;isAreaMeasuring = false;distancePoints = [];distanceDots.forEach((dot) => {scene.remove(dot);});distanceDots = [];if (distanceLine) {scene.remove(distanceLine);distanceLine = null;}if (distanceLabel) {scene.remove(distanceLabel);distanceLabel = null;}
}function startAreaMeasurement() {isAreaMeasuring = true;isDistanceMeasuring = false;areaPoints = [];areaDots.forEach((dot) => {scene.remove(dot);});areaDots = [];areaLines.forEach((areaLine) => {scene.remove(areaLine);});areaLines = [];if (areaLabel) {scene.remove(areaLabel);areaLabel = null;}
}function calculateDistance() {const startPoint = distancePoints[0];const endPoint = distancePoints[1];const distance = startPoint.distanceTo(endPoint);// 创建线段const geometry = new THREE.BufferGeometry().setFromPoints([startPoint,endPoint,]);const material = new THREE.LineBasicMaterial({color: 0x00ff00,});distanceLine = new THREE.Line(geometry, material);scene.add(distanceLine);// 创建标签const canvas = document.createElement("canvas");const context = canvas.getContext("2d");if (context) {const text = `距离: ${distance.toFixed(2)}`;const textWidth = context.measureText(text).width;canvas.width = textWidth + 25;canvas.height = 35;context.font = "12px Arial";context.fillStyle = "#ffffff";context.textAlign = "center";context.fillText(text, canvas.width / 2, 15);const texture = new THREE.CanvasTexture(canvas);texture.needsUpdate = true; // 确保纹理更新const spriteMaterial = new THREE.SpriteMaterial({ map: texture });distanceLabel = new THREE.Sprite(spriteMaterial);const midPoint = startPoint.clone().add(endPoint).divideScalar(2);distanceLabel.position.copy(midPoint);distanceLabel.scale.set(0.1, 0.1, 1); // 调整标签大小scene.add(distanceLabel);}isDistanceMeasuring = false;
}function calculateArea() {let area = 0;const numPoints = areaPoints.length;for (let i = 0; i < numPoints; i++) {const j = (i + 1) % numPoints;area +=areaPoints[i].x * areaPoints[j].y - areaPoints[j].x * areaPoints[i].y;}area = Math.abs(area) / 2;// 创建标签const canvas = document.createElement("canvas");const context = canvas.getContext("2d");if (context) {const text = `面积: ${area.toFixed(2)}`;const textWidth = context.measureText(text).width;canvas.width = textWidth + 25;canvas.height = 35;context.font = "12px Arial";context.fillStyle = "#ffffff";context.textAlign = "center";context.fillText(text, canvas.width / 2, 15);const texture = new THREE.CanvasTexture(canvas);texture.needsUpdate = true; // 确保纹理更新const spriteMaterial = new THREE.SpriteMaterial({ map: texture });areaLabel = new THREE.Sprite(spriteMaterial);const centroid = new THREE.Vector3();areaPoints.forEach((point) => {centroid.add(point);});centroid.divideScalar(numPoints);areaLabel.position.copy(centroid);areaLabel.scale.set(0.1, 0.1, 1); // 调整标签大小scene.add(areaLabel);}isAreaMeasuring = false;
}function cancelDistanceMeasurement() {isDistanceMeasuring = false;distancePoints = [];distanceDots.forEach((dot) => {scene.remove(dot);});distanceDots = [];if (distanceLine) {scene.remove(distanceLine);distanceLine = null;}if (distanceLabel) {scene.remove(distanceLabel);distanceLabel = null;}
}function cancelAreaMeasurement() {isAreaMeasuring = false;areaPoints = [];areaDots.forEach((dot) => {scene.remove(dot);});areaDots = [];areaLines.forEach((areaLine) => {scene.remove(areaLine);});areaLines = [];if (areaLabel) {scene.remove(areaLabel);areaLabel = null;}
}// 添加点击圆点
function addDot(point: THREE.Vector3, dots: THREE.Mesh[]) {const geometry = new THREE.SphereGeometry(0.01, 12, 12);const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });const dot = new THREE.Mesh(geometry, material);dot.position.copy(point);scene.add(dot);dots.push(dot);
}function updateAreaLines() {areaLines.forEach((areaLine) => {scene.remove(areaLine);});areaLines = [];const numPoints = areaPoints.length;for (let i = 0; i < numPoints; i++) {const j = (i + 1) % numPoints;const geometry = new THREE.BufferGeometry().setFromPoints([areaPoints[i],areaPoints[j],]);const material = new THREE.LineBasicMaterial({color: 0x00ff00,});const line = new THREE.Line(geometry, material);scene.add(line);areaLines.push(line);}
}onMounted(() => {initThree();
});
</script><style lang="scss" scoped>
.siteInspection {width: 100%;height: 100vh;position: relative;
}.measurement-buttons {position: absolute;bottom: 10px;left: 50%;transform: translateX(-50%);display: flex;gap: 10px;
}
</style>
相关文章:
Vue使用Three.js加载glb (gltf) 文件模型及实现简单的选中高亮、测距、测面积
安装: # three.jsnpm install --save three 附中文网: 5. gltf不同文件形式(.glb) | Three.js中文网 附官网: 安装 – three.js docs 完整代码(简易demo): <template><div class"siteInspe…...
<el-table>右侧有空白列解决办法
问题如图: 解决办法:.box 为本页面最外层的class名,保证各个页面样式不会互相污染。 .box::v-deep .el-table th.gutter {display: none;width: 0}.box ::v-deep.el-table colgroup col[namegutter] {display: none;width: 0;}.box::v-deep …...
Linux网络 网络层
IP 协议 协议头格式 4 位版本号(version): 指定 IP 协议的版本, 对于 IPv4 来说, 就是 4. 4 位头部长度(header length): IP 头部的长度是多少个 32bit, 也就是 4 字节,4bit 表示最大的数字是 15, 因此 IP 头部最大长度是 60 字节. 8 位服务类型(Type Of Service):…...
系统讨论Qt的并发编程——逻辑上下文的分类
目录 前言 首先,讨论Qt里常见的三种上下文 同一线程的串行执行 同一线程的异步执行 多线程的执行 moveToThread办法 前言 笔者最近看了一个具备一定启发性质的Qt教程,在这里,笔者打算整理一下自己的笔记。分享在这里. 首先,…...
《Linux Shell 脚本深度探索:原理与高效编程》
1. 基本结构 Shebang 行 #!/bin/bash # Shebang 行指定了脚本使用的解释器。 /bin/bash 表示使用 Bash 解释器执行脚本。 注释 # 这是注释,不会被执行 2. 变量 定义变量 variable_namevalue # 不需要加 $ 来定义变量。 # 变量名不能包含空格或特殊字符。 访…...
深入剖析:基于红黑树实现自定义 map 和 set 容器
🌟 快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。🌟 在 C 标准模板库(STL)的大家庭里,map和set可是超级重要的关联容器成员呢😎&#x…...
在大数据项目中如何设计和优化数据模型
在大数据项目中,设计和优化数据模型是一个涉及多个步骤和维度的复杂过程。以下是我通常采取的方法: 一、数据模型设计 明确业务需求: 深入了解项目的业务场景和目标,明确数据模型需要解决的具体问题。与业务团队紧密合作…...
JavaScript querySelector()、querySelectorAll() CSS选择器解析(DOM元素选择)
文章目录 基于querySelector系列方法的CSS选择器深度解析一、方法概述二、基础选择器类型1. 类型选择器2. ID选择器3. 类选择器4. 属性选择器 三、组合选择器1. 后代组合器2. 子元素组合器3. 相邻兄弟组合器4. 通用兄弟组合器 四、伪类与伪元素1. 结构伪类2. 状态伪类3. 内容伪…...
Linux系统中处理子进程的终止问题
1. 理解子进程终止的机制 在Unix/Linux系统中,当子进程终止时,会向父进程发送一个SIGCHLD信号。父进程需要捕捉这个信号,并通过调用wait()或waitpid()等函数来回收子进程的资源。这一过程被称为“回收僵尸进程”。 如果父进程没有及时调用w…...
Docker 不再难懂:快速掌握容器命令与架构原理
1. Docker 是容器技术的一种 容器(Container)概述 容器(Container)是一种轻量级的虚拟化技术,它将应用程序及其所有依赖环境打包在一个独立的、可移植的运行时环境中。容器通过操作系统级的虚拟化提供隔离࿰…...
取消票证会把指定的票证从数据库中删除,同时也会把票证和航班 等相关表中的关联关系一起删除。但在删除之前,它会先检查当前用户是否拥有这张票
在做航班智能客服问答系统时会遇到取消票证的场景,这里涉及数据库的操作时会把指定的票证从数据库中删除,同时也会把票证和航班等相关表中的关联关系一起删除。但在删除之前,需要先检查当前用户是否拥有这张票,只有票主才有权限取…...
力扣-贪心-763 划分字母区间
思路 先统计字符串中每一个字母出现的最后下标,然后从end初始化为第一个字母出现的最后下标,在i<end时,不断更新end,因为一旦囊括新的字母就最起码要遍历到新字母出现的最后下标,在i>end时,说明遍历…...
【Redis 原理】网络模型
文章目录 用户空间 && 内核空间阻塞IO非阻塞IO信号驱动IO异步IOIO多路复用selectpollepoll Web服务流程Redis 网络模型Redis单线程网络模型的整个流程Redis多线程网络模型的整个流程 用户空间 && 内核空间 为了避免用户应用导致冲突甚至内核崩溃,用…...
cpp中的继承
一、继承概念 在cpp中,封装、继承、多态是面向对象的三大特性。这里的继承就是允许已经存在的类(也就是基类)的基础上创建新类(派生类或者子类),从而实现代码的复用。 如上图所示,Person是基类&…...
DeepSeek全栈接入指南:从零到生产环境的深度实践
第一章:DeepSeek技术体系全景解析 1.1 认知DeepSeek技术生态 DeepSeek作为新一代人工智能技术平台,构建了覆盖算法开发、模型训练、服务部署的全链路技术栈。其核心能力体现在: 1.1.1 多模态智能引擎 自然语言处理:支持文本生成(NLG)、语义理解(NLU)、情感分析等计算…...
CSS 真的会阻塞文档解析吗?
在网页开发领域,一个常见的疑问是 CSS 是否会阻塞文档解析。理解这一问题对于优化网页性能、提升用户体验至关重要。要深入解答这个问题,需要从浏览器渲染网页的原理说起。 浏览器渲染网页的基本流程 浏览器在接收到 HTML 文档后,会依次进行…...
大模型的UI自动化:Cline 使用Playwright MCP Server完成测试
大模型的UI自动化:Cline 使用Playwright MCP Server完成测试 MCP MCP(Model Context Protocol),是一个开发的协议,标准化了应用程序如何为大模型提供上下文。MCP提供了一个标准的为LLM提供数据、工具的方式,使用MCP会更容易的构建Agent或者是基于LLM的复杂工作流。 最近…...
碰撞检测 | 图解凸多边形分离轴定理(附ROS C++可视化)
目录 0 专栏介绍1 凸多边形碰撞检测2 多边形判凸算法3 分离轴定理(SAT)4 算法仿真与可视化4.1 核心算法4.2 仿真实验 0 专栏介绍 🔥课设、毕设、创新竞赛必备!🔥本专栏涉及更高阶的运动规划算法轨迹优化实战,包括:曲线…...
Python 基本数据类型
目录 1. 字符串(String) 2. 列表(List) 3. 字典(Dictionary) 4. 集合(Set) 5. 数字(Number) 6. 布尔值(Boolean) 1. 字符串&…...
突破“第一崇拜“:五维心理重构之路
一、视频介绍 在这个崇尚"第一"的时代,我们如何找到自己的独特价值?本视频将带您踏上五维心理重构之旅,从诗意人生的角度探讨如何突破"圣人之下皆蝼蚁"的局限。我们将穿越人生的不同阶段,从青春的意气风发到…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
篇章二 论坛系统——系统设计
目录 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 1. 数据库设计 1.1 数据库名: forum db 1.2 表的设计 1.3 编写SQL 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 通过需求分析获得概念类并结合业务实现过程中的技术需要&#x…...
TCP/IP 网络编程 | 服务端 客户端的封装
设计模式 文章目录 设计模式一、socket.h 接口(interface)二、socket.cpp 实现(implementation)三、server.cpp 使用封装(main 函数)四、client.cpp 使用封装(main 函数)五、退出方法…...
Mysql故障排插与环境优化
前置知识点 最上层是一些客户端和连接服务,包含本 sock 通信和大多数jiyukehuduan/服务端工具实现的TCP/IP通信。主要完成一些简介处理、授权认证、及相关的安全方案等。在该层上引入了线程池的概念,为通过安全认证接入的客户端提供线程。同样在该层上可…...
