You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 lines
7.9 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="div3D" ref="div3D" v-loading="loading" element-loading-text="渲染模型..."></div>
<!-- 固定位置的控制按钮 -->
<div class="control-buttons">
<el-button
type="warning"
@click="tranlate(new Vector3(28, 10, -10), new Vector3(0, 0, 0))"
:icon="Refresh"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
// threejs
import {
AxesHelper,
Color,
PerspectiveCamera,
Scene,
WebGLRenderer,
EquirectangularReflectionMapping,
Group,
Vector3,
AmbientLight,
DirectionalLight,
PointLight
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import gsap from 'gsap';
import { Star, Bell, MagicStick, Refresh, Compass } from '@element-plus/icons-vue';
const loading = ref(true); // loading
const div3D = ref<HTMLDivElement>();
//场景
const scene = new Scene();
//相机
const camera = new PerspectiveCamera(
45,
div3D.value?.clientWidth! / div3D.value?.clientHeight!,
0.05,
200
); // 初始比例设为1
camera.position.set(30, 15, -15);
camera.lookAt(0, 0, 0);
scene.add(camera);
//渲染器
const render = new WebGLRenderer({
antialias: true, // 关闭抗锯齿提升性能
powerPreference: 'high-performance', // 使用高性能GPU
stencil: false, // 如果不需要模板缓冲区
depth: true,
logarithmicDepthBuffer: true // 对大场景有帮助
});
render.setClearColor(new Color('#131519'));
// 启用渲染优化
render.shadowMap.enabled = false; // 如果不需要阴影
//添加控制器
const controls = new OrbitControls(camera, render.domElement);
// 禁用滚轮缩放
// controls.enableZoom = false;
// controls.autoRotate = false;
// controls.enablePan = false; // 禁用右键平移
controls.autoRotateSpeed = 1;
controls.dampingFactor = 0.2;
controls.rotateSpeed = 0.3; // 降低旋转速度(默认为 1.0
controls.enableDamping = true;
controls.enableRotate = true;
controls.minDistance = 1; // 允许更近距离观察模型细节
// 设置视角限制向上45度向下45度
// 从正前方开始计算向上最大45度向下最大45度
controls.minPolarAngle = Math.PI / 4; // 向上最多45度 (π/4)
controls.maxPolarAngle = (Math.PI * 3) / 4; // 向下最多45度 (3π/4)
// 大场景优化配置
const startTime = Date.now();
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
loader.setDRACOLoader(dracoLoader);
let meshArr: Group[] = [];
// public\models\nitrogenInjection\nitrogenInjection.gltf
loader.load('/models/substation/substation.gltf', (gltf: any) => {
// 保存模型引用
loadedModel = gltf.scene;
scene.add(gltf.scene);
const loadTime = Date.now() - startTime;
// 立即渲染一帧
// controls.update();
render.render(scene, camera);
console.log(`大场景模型已加载并显示,总耗时:${loadTime}ms`);
loading.value = false;
});
// // 环境光:增强整体亮度
// const ambientLight = new AmbientLight('#ffffff', 1.2); // 强度从 0.6 提高到 1.0
// scene.add(ambientLight);
// // 平行光
// const directionalLight = new DirectionalLight('#ffffff', 1.4); // 强度从 0.2 提高到 0.8
// scene.add(directionalLight);
// directionalLight.position.set(20, 20, 10);
// // // 点光源
// const pointLight = new PointLight('#ffffff', 1.4, 1800); // 强度从 0.5 提高到 1.0
// scene.add(pointLight);
// pointLight.position.set(0, 40, 0);
// 环境贴图(保存引用以便清理)
const rgbELoader = new RGBELoader().load(
'/models/texture/Alex_Hart-Nature_Lab_Bones_2k.hdr',
(envMap: any) => {
envMap.mapping = EquirectangularReflectionMapping;
scene.environment = envMap;
envMapTexture = envMap; // 保存引用
}
);
//补间动画
const timeline1 = gsap.timeline();
const timeline2 = gsap.timeline();
// 保存模型引用以便清理
let loadedModel: any = null;
let envMapTexture: any = null;
//相机移动
function tranlate(position: Vector3, target: Vector3) {
timeline1.to(camera.position, {
x: position.x,
y: position.y,
z: position.z,
duration: 1,
ease: 'power2.inOut'
});
timeline2.to(controls.target, {
x: target.x,
y: target.y,
z: target.z,
duration: 1,
ease: 'power2.inOut'
});
}
//物体
// const axes = new AxesHelper(20);
// scene.add(axes);
// 监听容器大小变化
function handleResize() {
if (!div3D.value) return;
const container = div3D.value;
const width = container.clientWidth;
const height = container.clientHeight;
// 更新相机比例
camera.aspect = width / height;
camera.updateProjectionMatrix();
// 更新渲染器大小
render.setSize(width, height);
}
function init() {
requestAnimationFrame(init);
controls.update();
render.render(scene, camera);
}
onMounted(() => {
// 初始化时设置正确大小
handleResize();
// 启动渲染循环
init();
// 将渲染器添加到容器
div3D.value?.appendChild(render.domElement);
// 添加容器大小监听
const resizeObserver = new ResizeObserver(handleResize);
if (div3D.value) {
resizeObserver.observe(div3D.value);
}
// 保存观察器引用以便销毁
(window as any)._resizeObserver = resizeObserver;
});
onUnmounted(() => {
console.log('开始清理 Three.js 资源...');
// 1. 停止渲染循环(通过标记)
// requestAnimationFrame 会自动停止
// 2. 清理 ResizeObserver
if ((window as any)._resizeObserver) {
(window as any)._resizeObserver.disconnect();
delete (window as any)._resizeObserver;
}
// 3. 停止 GSAP 动画
timeline1.pause();
timeline1.clear();
timeline2.pause();
timeline2.clear();
gsap.globalTimeline.clear();
// 4. 移除 DOM 元素
if (div3D.value && render.domElement.parentNode === div3D.value) {
div3D.value.removeChild(render.domElement);
}
// 5. 清理控制器
controls.dispose();
// 6. 清理模型
if (loadedModel) {
loadedModel.traverse((child: any) => {
if (child.isMesh) {
// 清理几何体
if (child.geometry) {
child.geometry.dispose();
}
// 清理材质
if (child.material) {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.forEach((material: any) => {
// 清理材质属性
[
'map',
'normalMap',
'roughnessMap',
'metalnessMap',
'emissiveMap',
'aoMap',
'displacementMap'
].forEach((key) => {
if (material[key]) {
material[key].dispose();
}
});
// 清理材质本身
material.dispose();
});
}
}
});
// 从场景中移除模型
scene.remove(loadedModel);
}
// 7. 清理环境贴图
if (envMapTexture) {
envMapTexture.dispose();
}
// 8. 清理灯光
scene.traverse((child: any) => {
if (child.isLight) {
child.dispose();
}
});
// 9. 清理场景
while (scene.children.length > 0) {
scene.remove(scene.children[0]);
}
// 10. 清理渲染器
render.dispose();
render.forceContextLoss();
render.domElement.remove();
// 11. 清空场景引用
scene.clear();
console.log('Three.js 资源清理完成');
});
</script>
<style scoped>
.div3D {
margin: 0;
padding: 0;
/* width: 500px;
height: 500px; */
width: 100%;
height: 100%;
position: relative;
}
.control-buttons {
position: absolute;
top: 50%;
left: 2%;
transform: translateY(-50%); /* 垂直居中 */
transform: translateZ(0);
display: flex;
flex-direction: column;
gap: 20px;
z-index: 100;
align-items: flex-start; /* 确保所有按钮左对齐 */
}
</style>