以下代码支持:各类仪器虚拟拆解、仪器虚拟展示。
仪器拆解关键代码如下:
import {DragControls} from "three/addons";
let dragControls = null;// 声明 拖拽 变量
const objects = []; // 用于存储可以拖拽的对象
let mouse = new THREE.Vector2();//拆解模块
model.traverse((child) => {
if (child.isMesh) {
//深度写入 将位置信息写入 防止透视或丢失
child.material.depthWrite = true;
}
objects.push(child); // 将可拖拽对象添加到数组中
});
}
// 8. 拆解模块
// 初始化Raycaster
raycaster = new THREE.Raycaster();
// 初始化DragControls
dragControls = new DragControls(objects, camera, renderer.domElement);
dragControls.addEventListener('dragstart', (event) => {
controls.enabled = false; // 禁用OrbitControls
});
dragControls.addEventListener('dragend', (event) => {
controls.enabled = true; // 启用OrbitControls
});
// 监听鼠标单击事件
renderer.domElement.addEventListener('click', onClick, false);
// 鼠标点击事件处理函数
const onClick = (event) => {
event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
const selectedObject = intersects[0].object;
console.log('选中物体:', selectedObject);
}
};
完整代码如下
<template>
<div class="container">
<el-container>
<el-header style="margin: 0; padding: 0; height: 70px;">
<el-row>
<el-col :span="8">图标区域</el-col>
<el-col :span="8">
<div style="display: flex;justify-content: center;">
<el-menu
mode="horizontal"
:ellipsis="false"
@select="handleSelect"
class="custom-menu"
>
<el-menu-item index="0">
<div class="icon-container">
<i class="bx bx-home icon"></i>
<span class="icon-text">原始视图</span> <!-- 图标下方的文字 -->
</div>
</el-menu-item>
<el-menu-item index="1">
<div class="icon-container">
<i class="bx bx-grid-alt icon"></i>
<span class="icon-text">爆炸视图</span> <!-- 图标下方的文字 -->
</div>
</el-menu-item>
<el-menu-item index="2">
<div class="icon-container">
<i class="bx bxs-hand-up icon"></i>
<span class="icon-text">拆解部件</span> <!-- 图标下方的文字 -->
</div>
</el-menu-item>
<el-menu-item index="3">
<div class="icon-container">
<i class="bx bx-redo icon"></i>
<span class="icon-text">返回</span> <!-- 图标下方的文字 -->
</div>
</el-menu-item>
</el-menu>
</div>
</el-col>
<el-col :span="8">
<div>
<!-- 右侧区域(如果有的话)-->
</div>
</el-col>
</el-row>
</el-header>
<el-main style="margin: 0;padding: 0;height: 100%;">
<div ref="sceneContainer" class="three-scene-container"></div>
</el-main>
</el-container>
</div>
</template>
<script lang="js" setup>
import {onMounted, onUnmounted, ref} from 'vue';
import * as THREE from 'three';
// 引入扩展库GLTFLoader.js 读取GLTF模型文件
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
//进行旋转
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import WebGL from 'three/addons/capabilities/WebGL.js';
import router from "@/router";
import {DragControls} from "three/addons";
// 使用 refs 来引用 DOM 元素
const sceneContainer = ref(null);
let handleResize = null; // 声明 handleResize 变量
let controls = null; // 声明 视角旋转 变量
let camera = null; // 声明 相机 变量
let scene = null; // 声明 屏幕 变量
let renderer = null; // 声明 渲染器 变量
let raycaster = null;// 声明 拆解射线 变量
let dragControls = null;// 声明 拖拽 变量
const objects = []; // 用于存储可以拖拽的对象
let mouse = new THREE.Vector2();//拆解模块
const modelUrl = ref('/3DMODEL/SwitchingMatrix.glb')
onMounted(async () => {
//检测是否支持WebGL2
if (WebGL.isWebGL2Available() ) {
//初始化
// 确保 ref 指向的 DOM 元素已存在
if (!sceneContainer.value) return;
// 1. 创建场景
scene = new THREE.Scene();
// 2. 创建相机(相机是 3D 场景中至关重要的一部分,它就像你的眼睛,决定你看到场景的角度和范围。)
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0.15, 0.55); // 调整相机位置,确保能看到模型
// 3. 创建渲染器(渲染器负责把 3D 场景渲染成 2D 图像,并显示在网页上。)
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor("#E6E6E6"); // 设置背景颜色(类比舞台幕布)
sceneContainer.value.appendChild(renderer.domElement); // 将渲染器的 canvas 添加到页面
// 4. 添加光源
// 增加定向光的强度,并重新定位光源以照亮不同方向
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5); // 提高强度
directionalLight.position.set(50, 50, 50); // 正面光源
scene.add(directionalLight);
const additionalLight1 = new THREE.DirectionalLight(0xffffff, 2.5); // 提高强度
additionalLight1.position.set(-50, 50, -50); // 左后方光源
scene.add(additionalLight1);
const additionalLight2 = new THREE.DirectionalLight(0xffffff, 2.5); // 提高强度
additionalLight2.position.set(50, -50, -50); // 右前方光源
scene.add(additionalLight2);
// 为底部和后方增加更多的光源
const additionalLight3 = new THREE.DirectionalLight(0xffffff, 1.0); // 适当调整强度
additionalLight3.position.set(0, -50, 0); // 从下方照射
scene.add(additionalLight3);
const additionalLight4 = new THREE.DirectionalLight(0xffffff, 1.0); // 适当调整强度
additionalLight4.position.set(0, 50, -50); // 从后面照射
scene.add(additionalLight4);
// 通过增加点光源增加局部亮度
const pointLight1 = new THREE.PointLight(0xffffff, 2.0, 50); // 提高点光源强度
pointLight1.position.set(0, 10, 0); // 设置点光源位置
scene.add(pointLight1);
// 更强的环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0); // 增加环境光强度
scene.add(ambientLight);
// 半球光,增强场景的柔和光照
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.5); // 增加半球光的强度
hemisphereLight.position.set(0, 200, 0); // 从顶部照射
scene.add(hemisphereLight);
// 5. 初始化 OrbitControls
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.25; // 阻尼系数 数值越大减速越快 让转动更加丝滑 需搭配update
controls.screenSpacePanning = false; // 禁用屏幕空间平移
// 修改鼠标按键行为
controls.mouseButtons = {
LEFT: THREE.MOUSE.ROTATE, // 左键旋转
MIDDLE: THREE.MOUSE.PAN, // 中键平移
RIGHT: THREE.MOUSE.ROTATE // 右键旋转
};
// 5. 窗口大小监听(提前设置,确保即使模型未加载也能适应窗口变化)
// 现在定义 handleResize 函数
handleResize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
// 监听窗口大小变化
window.addEventListener("resize", handleResize);
// 6. 渲染函数(此时可以暂时只渲染背景或初始内容)
function animate() {
controls.update(); // 更新 OrbitControls,必要时启用阻尼效果
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
// 7. 加载 3D 模型
const loader = new GLTFLoader();
await loader.load(
modelUrl.value,
(gltf) => {
console.log("模型加载成功");
scene.add(gltf.scene); // 将模型添加到场景
const model = gltf.scene;
if (modelUrl.value == "/3DMODEL/Modem.glb") {
model.traverse((child) => {
if (child.isMesh) {
if (child.material.name.includes("不锈钢")) {
//深度写入 将位置信息写入 防止透视或丢失
child.material.depthWrite = true;
} else {
child.material.depthWrite = true;
child.material.roughness = 0.6
}
objects.push(child); // 将可拖拽对象添加到数组中
}
})
return;
} else {
// 遍历模型的每个子节点以调整材质设置(解决模型丢失问题)
model.traverse((child) => {
if (child.isMesh) {
//深度写入 将位置信息写入 防止透视或丢失
child.material.depthWrite = true;
}
objects.push(child); // 将可拖拽对象添加到数组中
});
}
},
undefined,
(error) => {
console.error("模型加载失败:", error);
}
)
// 8. 拆解模块
// 初始化Raycaster
raycaster = new THREE.Raycaster();
// 初始化DragControls
dragControls = new DragControls(objects, camera, renderer.domElement);
dragControls.addEventListener('dragstart', (event) => {
controls.enabled = false; // 禁用OrbitControls
});
dragControls.addEventListener('dragend', (event) => {
controls.enabled = true; // 启用OrbitControls
});
// 监听鼠标单击事件
renderer.domElement.addEventListener('click', onClick, false);
}
//WebGL不支持提示
else {
const warning = WebGL.getWebGL2ErrorMessage();
document.getElementById( 'container' ).appendChild( warning );
}
});
// 鼠标点击事件处理函数(拆解)
const onClick = (event) => {
event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
const selectedObject = intersects[0].object;
console.log('选中物体:', selectedObject);
}
};
onUnmounted(() => {
// 确保在组件卸载时移除事件监听器
if (handleResize) {
window.removeEventListener("resize", handleResize);
}
});
//上方按钮
const handleSelect = (key) => {
switch (key) {
case "0":
resetScene(); // 回到最初始状态
break;
case "1":
break;
case "2":
break;
case "3":
router.push({ path: '/layout/InstrumentLibNew' });
break;
}
}
//原始视图
const resetScene = () => {
// 1. 重置相机位置
camera.position.set(0, 0.15, 0.55); // 初始位置
camera.rotation.set(-Math.PI / 6, 0, 0); // 初始旋转
// 2. 重置视角
controls.reset(); // Reset OrbitControls
// 3. 重置模型
// 如果场景中包含任何需要清理的状态,比如物理、动画等,可以在此重置
// 如果场景中的某些物体不想完全重置,可以忽略它们的操作。
renderer.clear(); // 清除渲染器的缓存或内容,确保不会出现旧内容
};
</script>
<style scoped>
.container {
height: 100vh;
overflow: hidden;
}
.three-scene-container {
width: 100%;
height: 100%;
}
.custom-menu {
background-color: white !important; /* 白色背景 */
border-bottom: none !important; /* 去除底部灰色线条 */
margin-top: 10px;
}
/* 设置选中项的背景色和底部线条颜色 */
.custom-menu .el-menu-item.is-active {
background-color: rgba(244, 67, 54, 0) !important; /* 红色的浅背景 */
border-bottom: 4px solid #f44336 !important; /* 红色底部线条 */
}
.custom-menu .el-menu-item.is-active .icon {
color: #f44336 !important; /* 选中项图标颜色为红色 */
}
.custom-menu .el-menu-item.is-active .icon-text {
color: #f44336 !important; /* 选中项文字颜色为红色 */
}
.icon-container {
margin-right: 10px;
margin-left: 10px;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 15px; /* 增加底部间距 */
}
.icon {
color: #c7000b; /* 图标颜色 */
font-size: 40px; /* 图标大小 */
}
.icon-text {
margin-top: 5px;
font-size: 14px; /* 文字大小 */
line-height: 1; /* 行高 */
}
/* 鼠标悬停时的样式 */
.custom-menu .el-menu-item:hover {
background-color: rgba(244, 67, 54, 0) !important; /* 红色浅背景 */
border-bottom: 4px solid #f44336 !important; /* 红色底部线条 */
}
.custom-menu .el-menu-item:hover .icon-text {
color: #f44336 !important; /* 悬停时文字颜色为红色 */
}
</style>