|
@@ -0,0 +1,368 @@
|
|
|
+<script setup>
|
|
|
+import * as THREE from "three";
|
|
|
+import { ref, onMounted } from "vue";
|
|
|
+import { gsap } from "gsap";
|
|
|
+import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader";
|
|
|
+import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
|
|
|
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
|
+
|
|
|
+let scene,
|
|
|
+ camera,
|
|
|
+ controls,
|
|
|
+ renderer,
|
|
|
+ raycaster,
|
|
|
+ mouse = null;
|
|
|
+let isDragging = false;
|
|
|
+let isInertiaMoving = false;
|
|
|
+let inertiaEndFrames = 0;
|
|
|
+let lastPosition = new THREE.Vector3();
|
|
|
+let lastQuaternion = new THREE.Quaternion();
|
|
|
+let originalPositions = new Map();
|
|
|
+const zoomedIn = ref(false);
|
|
|
+const INERTIA_END_THRESHOLD = 1;
|
|
|
+const canvasContainer = ref(null);
|
|
|
+const selectedObject = ref(null);
|
|
|
+const loadingProgress = ref(0);
|
|
|
+
|
|
|
+const initThree = () => {
|
|
|
+ // 创建场景
|
|
|
+ scene = new THREE.Scene();
|
|
|
+
|
|
|
+ // 创建相机
|
|
|
+ camera = new THREE.PerspectiveCamera(
|
|
|
+ 75,
|
|
|
+ window.innerWidth / window.innerHeight,
|
|
|
+ 0.1,
|
|
|
+ 1000
|
|
|
+ );
|
|
|
+ camera.position.z = 140;
|
|
|
+
|
|
|
+ // 创建渲染器
|
|
|
+ renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
+ renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
+ renderer.setClearColor(0xececec);
|
|
|
+ canvasContainer.value.appendChild(renderer.domElement);
|
|
|
+
|
|
|
+ // 添加光源
|
|
|
+ const ambientLight = new THREE.AmbientLight(0xffffff, 5);
|
|
|
+ scene.add(ambientLight);
|
|
|
+
|
|
|
+ const highlightDirectionalLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
|
|
+ scene.add(highlightDirectionalLight);
|
|
|
+
|
|
|
+ raycaster = new THREE.Raycaster();
|
|
|
+ mouse = new THREE.Vector2();
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener("resize", () => {
|
|
|
+ camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
+ camera.updateProjectionMatrix();
|
|
|
+ renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const render = () => {
|
|
|
+ renderer.render(scene, camera);
|
|
|
+};
|
|
|
+
|
|
|
+const animate = () => {
|
|
|
+ requestAnimationFrame(animate);
|
|
|
+ if (!document.hidden) {
|
|
|
+ controls.update();
|
|
|
+ renderer.render(scene, camera);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const checkInertia = () => {
|
|
|
+ const positionChanged = camera.position.distanceTo(lastPosition) > 0.001;
|
|
|
+ const rotationChanged = camera.quaternion.angleTo(lastQuaternion) > 0.001;
|
|
|
+
|
|
|
+ if (positionChanged || rotationChanged) {
|
|
|
+ isInertiaMoving = true;
|
|
|
+ inertiaEndFrames = 0; // 重置计数器
|
|
|
+ } else if (isInertiaMoving) {
|
|
|
+ inertiaEndFrames++;
|
|
|
+ if (inertiaEndFrames >= INERTIA_END_THRESHOLD) {
|
|
|
+ isInertiaMoving = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ lastPosition.copy(camera.position);
|
|
|
+ lastQuaternion.copy(camera.quaternion);
|
|
|
+ requestAnimationFrame(checkInertia);
|
|
|
+};
|
|
|
+
|
|
|
+const initControls = () => {
|
|
|
+ controls = new OrbitControls(camera, renderer.domElement);
|
|
|
+ controls.addEventListener("change", render);
|
|
|
+ controls.enableDamping = true;
|
|
|
+ controls.dampingFactor = 0.05;
|
|
|
+ controls.minDistance = 5;
|
|
|
+ controls.maxDistance = 200;
|
|
|
+
|
|
|
+ controls.addEventListener("start", () => {
|
|
|
+ isDragging = true;
|
|
|
+ });
|
|
|
+ controls.addEventListener("end", () => {
|
|
|
+ isDragging = false;
|
|
|
+ });
|
|
|
+ checkInertia();
|
|
|
+};
|
|
|
+
|
|
|
+const initModel = () => {
|
|
|
+ // 加载模型
|
|
|
+ const loadingManager = new THREE.LoadingManager(
|
|
|
+ () => {
|
|
|
+ loadingProgress.value = 100;
|
|
|
+ },
|
|
|
+ (item, loaded, total) => {
|
|
|
+ loadingProgress.value = (loaded / total) * 100;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ const mtlLoader = new MTLLoader(loadingManager);
|
|
|
+ const objLoader = new OBJLoader(loadingManager);
|
|
|
+
|
|
|
+ mtlLoader.load("whole/whole.mtl", (materials) => {
|
|
|
+ materials.preload();
|
|
|
+
|
|
|
+ objLoader.setMaterials(materials);
|
|
|
+
|
|
|
+ objLoader.load("whole/whole.obj", (obj) => {
|
|
|
+ // 加载纹理并应用到模型
|
|
|
+ obj.position.y = -80;
|
|
|
+
|
|
|
+ obj.traverse((child) => {
|
|
|
+ if (child instanceof THREE.Mesh && child.name !== "1") {
|
|
|
+ child.userData.isModelPart = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ scene.add(obj);
|
|
|
+ renderer.domElement.addEventListener("mousemove", onMouseMove);
|
|
|
+ renderer.domElement.addEventListener("click", onMouseClick);
|
|
|
+
|
|
|
+ // 动画循环
|
|
|
+ animate();
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const onMouseMove = (event) => {
|
|
|
+ if (isDragging || isInertiaMoving || zoomedIn.value) return;
|
|
|
+
|
|
|
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
|
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
|
+
|
|
|
+ raycaster.setFromCamera(mouse, camera);
|
|
|
+
|
|
|
+ const intersects = raycaster.intersectObjects(scene.children, true);
|
|
|
+
|
|
|
+ if (selectedObject.value) {
|
|
|
+ selectedObject.value.material.color.copy(
|
|
|
+ selectedObject.value.userData.originalColor
|
|
|
+ );
|
|
|
+ selectedObject.value = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (intersects.length > 0) {
|
|
|
+ const firstIntersected = intersects[0].object;
|
|
|
+
|
|
|
+ if (
|
|
|
+ firstIntersected instanceof THREE.Mesh &&
|
|
|
+ firstIntersected.userData.isModelPart
|
|
|
+ ) {
|
|
|
+ selectedObject.value = firstIntersected;
|
|
|
+ selectedObject.value.userData.originalColor =
|
|
|
+ selectedObject.value.material.color.clone();
|
|
|
+ selectedObject.value.material.color.set(0x00ff00);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onMouseClick = () => {
|
|
|
+ if (!selectedObject.value) return;
|
|
|
+
|
|
|
+ selectedObject.value.material.color.copy(
|
|
|
+ selectedObject.value.userData.originalColor
|
|
|
+ );
|
|
|
+ if (!zoomedIn.value) {
|
|
|
+ zoomToChild(selectedObject.value);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const restoreOriginalView = () => {
|
|
|
+ // 恢复所有子模型的可见性
|
|
|
+ scene.traverse((obj) => {
|
|
|
+ obj.visible = true;
|
|
|
+ });
|
|
|
+
|
|
|
+ gsap.to(camera.position, {
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ z: 140,
|
|
|
+ duration: 2,
|
|
|
+ ease: "power2.inOut",
|
|
|
+ onUpdate: () => {
|
|
|
+ controls.target.set(0, 0, 0);
|
|
|
+ controls.update();
|
|
|
+ },
|
|
|
+ onComplete: () => {
|
|
|
+ zoomedIn.value = false;
|
|
|
+ },
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const storeOriginalStates = () => {
|
|
|
+ originalPositions.clear();
|
|
|
+ scene.traverse((obj) => {
|
|
|
+ if (obj.userData.isModelPart) {
|
|
|
+ originalPositions.set(obj, {
|
|
|
+ visible: obj.visible,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const hideOtherChildren = (target) => {
|
|
|
+ scene.traverse((obj) => {
|
|
|
+ if (obj.name && obj.name !== target.name) {
|
|
|
+ obj.visible = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const zoomToChild = (child) => {
|
|
|
+ storeOriginalStates();
|
|
|
+
|
|
|
+ hideOtherChildren(child);
|
|
|
+
|
|
|
+ const bbox = new THREE.Box3().setFromObject(child);
|
|
|
+ const center = bbox.getCenter(new THREE.Vector3());
|
|
|
+ const size = bbox.getSize(new THREE.Vector3());
|
|
|
+ const maxDim = Math.max(size.x, size.y, size.z);
|
|
|
+ const cameraDistance = maxDim * 1.5;
|
|
|
+
|
|
|
+ controls.target.copy(center);
|
|
|
+ controls.update();
|
|
|
+
|
|
|
+ gsap.to(camera.position, {
|
|
|
+ x: center.x,
|
|
|
+ y: center.y + cameraDistance * 0.1, // 稍微抬高视角
|
|
|
+ z: center.z + cameraDistance,
|
|
|
+ duration: 1,
|
|
|
+ ease: "power2.inOut",
|
|
|
+ onUpdate: () => {
|
|
|
+ controls.update();
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ zoomedIn.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ initThree();
|
|
|
+ initControls();
|
|
|
+ initModel();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div v-if="loadingProgress < 100" class="loading-wrap">
|
|
|
+ <div class="loading-container">
|
|
|
+ <div class="progress-bar" :style="{ width: loadingProgress + '%' }"></div>
|
|
|
+ <div class="progress-text">{{ loadingProgress.toFixed(0) }}%</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="zoomedIn" class="icon-back" @click="restoreOriginalView" />
|
|
|
+
|
|
|
+ <div ref="canvasContainer" class="canvas" />
|
|
|
+
|
|
|
+ <div class="logo">
|
|
|
+ <div class="img"></div>
|
|
|
+ <div class="logotxt">提 供 技 术 支 持</div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.canvas {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-back {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ left: 20px;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ cursor: pointer;
|
|
|
+ background: url("@/assets/images/back.png") no-repeat center / contain;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+
|
|
|
+.logo {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 9999;
|
|
|
+ width: 90vw;
|
|
|
+ position: absolute;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ bottom: 10px;
|
|
|
+ height: 50px;
|
|
|
+}
|
|
|
+.logo .img {
|
|
|
+ background: url("@/assets/images/logo.png");
|
|
|
+ background-size: 100% 100%;
|
|
|
+ max-width: 40vw;
|
|
|
+ max-height: 50px;
|
|
|
+ height: 40px;
|
|
|
+ width: 150px;
|
|
|
+}
|
|
|
+.logo .logotxt {
|
|
|
+ padding-bottom: 5px;
|
|
|
+ color: #666;
|
|
|
+ border-bottom: 1px solid #666;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-wrap {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: #ececec;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-container {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 20%;
|
|
|
+ width: 60%;
|
|
|
+ height: 20px;
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ border-radius: 10px;
|
|
|
+ transform: translateY(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.progress-bar {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
|
|
+ border-radius: 10px;
|
|
|
+ transition: width 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-text {
|
|
|
+ position: absolute;
|
|
|
+ top: -30px;
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+ color: #333;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+</style>
|