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. 字符串&…...
突破“第一崇拜“:五维心理重构之路
一、视频介绍 在这个崇尚"第一"的时代,我们如何找到自己的独特价值?本视频将带您踏上五维心理重构之旅,从诗意人生的角度探讨如何突破"圣人之下皆蝼蚁"的局限。我们将穿越人生的不同阶段,从青春的意气风发到…...
龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...
