效果

在这里插入图片描述

环境准备

首先,你需要在项目中引入 Three.js。可以通过 npm 安装:

npm install three

确保你的项目结构中有一个名为 images 的文件夹,并放入一张全景图像(如 001.jpg),以便后续使用。

代码示例

下面是实现可交互360度全景视图的 Vue.js 组件代码:

<template>
  <div class="panorama-viewer">
    <!-- 全景图渲染容器 -->
    <div ref="container" class="viewer-container"></div>

    <!-- 加载提示 -->
    <div v-if="isLoading" class="loading-indicator">加载中...</div>

    <!-- 控制面板 -->
    <div class="controls-panel">
      <div class="info">
        <p>拖动鼠标: 旋转视角</p>
        <p>滚轮: 缩放</p>
      </div>
      <div class="panorama-switch">
        <button :class="{ active: currentPanorama === 0 }" @click="switchPanorama(0)">
          场景 1
        </button>
        <button :class="{ active: currentPanorama === 1 }" @click="switchPanorama(1)">
          场景 2
        </button>
        <button :class="{ active: currentPanorama === 2 }" @click="switchPanorama(2)">
          场景 3
        </button>
        <button :class="{ active: currentPanorama === 3 }" @click="switchPanorama(3)">
          场景 4
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import img1 from './components/000.jpg';
import img2 from './components/we.jpg';
import img3 from './components/3.png';
import img4 from './components/4.png';

// DOM引用
const container = ref(null);

// 状态管理
const currentPanorama = ref(0); // 当前显示的全景图索引
const isLoading = ref(true); // 加载状态
const animationId = ref(null); // 动画帧ID

// Three.js核心对象
let scene, camera, renderer;
let controls;
let sphere; // 用于展示全景图的球体
let textures = []; // 存储全景图纹理

// 全景图路径
const panoramaImages = [
  img1, // 示例全景图1
  img2, // 示例全景图2
  img3, // 示例全景图3
  img4, // 示例全景图3
];

/**
 * 初始化Three.js场景 - 关键调整:缩小场景视野(相机+球体参数)
 */
const initScene = () => {
  if (!container.value) return;

  // 1. 创建场景(无修改)
  scene = new THREE.Scene();

  // 2. 相机参数调整:缩小视野范围
  // 关键修改:
  // - fov从75→50:减小视场角,避免视角过广导致的“拉伸感”(数值越小,视野越窄,场景越“紧凑”)
  // - far从1000→500:缩短远裁剪面,减少无效渲染范围
  camera = new THREE.PerspectiveCamera(
    100, // 视场角:从75缩小到50,核心缩小场景的参数
    container.value.clientWidth / container.value.clientHeight, // 宽高比(保持不变)
    1, // 近裁剪面(保持不变,避免过近导致穿模)
    500 // 远裁剪面:从1000缩短到500,匹配球体尺寸
  );
  camera.position.set(40, 0, 0); // 相机仍在中心(全景图核心逻辑)

  // 3. 创建渲染器(无修改)
  renderer = new THREE.WebGLRenderer({
    antialias: true, // 抗锯齿(保持,避免画面模糊)
    alpha: true,
  });
  renderer.setSize(container.value.clientWidth, container.value.clientHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  // 清除旧画布(避免重复渲染)
  while (container.value.firstChild) {
    container.value.removeChild(container.value.firstChild);
  }
  container.value.appendChild(renderer.domElement);

  // 4. 初始化控制器(优化缩放范围,匹配缩小后的场景)
  initControls();

  // 5. 加载纹理(无修改)
  loadTextures();

  // 6. 监听窗口 resize(无修改)
  window.addEventListener('resize', onWindowResize);

  // 初始渲染(无修改)
  renderer.render(scene, camera);
};

/**
 * 初始化控制器 - 关键调整:匹配缩小后的场景,限制缩放范围
 */
const initControls = () => {
  if (!renderer || !renderer.domElement) {
    console.error('渲染器DOM元素不存在');
    return;
  }

  controls = new OrbitControls(camera, renderer.domElement);

  // 基础交互配置(保持不变)
  controls.enableZoom = true; // 允许缩放
  controls.enableRotate = true; // 允许旋转
  controls.enablePan = false; // 禁用平移(全景图不需要)
  controls.rotateSpeed = 0.5; // 旋转速度(保持,避免过快)
  controls.enableDamping = true; // 阻尼效果(保持,旋转更平滑)
  controls.dampingFactor = 0.05; // 阻尼强度(保持)
  controls.minPolarAngle = 0; // 垂直旋转下限(保持)
  controls.maxPolarAngle = Math.PI; // 垂直旋转上限(保持)

  // 关键修改:限制缩放范围,匹配缩小后的场景
  // 缩小场景后,不需要过大的缩放区间,避免缩放过小导致“空场景”
  controls.minDistance = 50; // 最小缩放距离:从默认0→50(避免太近穿模)
  controls.maxDistance = 300; // 最大缩放距离:从默认无限→200(避免太远看不到场景)

  controls.update(); // 强制更新控制器状态
  console.log('控制器初始化完成,旋转和缩放已启用(匹配缩小场景)');
};

/**
 * 加载全景图纹理(无修改)
 */
const loadTextures = () => {
  const loader = new THREE.TextureLoader();
  loader.crossOrigin = 'anonymous';

  panoramaImages.forEach((url, index) => {
    loader.load(
      url,
      texture => {
        texture.wrapS = THREE.ClampToEdgeWrapping; // 避免纹理边缘重复
        texture.wrapT = THREE.ClampToEdgeWrapping;
        textures[index] = texture;

        // 第一张图加载完成后初始化球体
        if (index === 0) {
          initSphere(texture);
          isLoading.value = false;
        }
      },
      xhr => {
        console.log(`全景图 ${index + 1} 加载中: ${Math.round((xhr.loaded / xhr.total) * 100)}%`);
      },
      error => {
        console.error(`加载全景图 ${index + 1} 失败:`, error);
        isLoading.value = false;
      }
    );
  });
};

/**
 * 初始化全景球体 - 关键调整:缩小球体尺寸(核心“场景缩小”逻辑)
 */
const initSphere = texture => {
  // 关键修改:球体半径从500→200(直接缩小球体尺寸,场景自然缩小)
  // 分段数60/40保持不变,确保球体表面平滑,避免纹理拉伸
  const geometry = new THREE.SphereGeometry(200, 60, 40);

  // 反转球体UV:使纹理显示在球体内侧(全景图核心逻辑,无修改)
  geometry.scale(-1, 1, 1);

  // 材质配置(修复原代码注释错误,DoubleSide→FrontSide,避免性能浪费)
  const material = new THREE.MeshBasicMaterial({
    map: texture,
    side: THREE.FrontSide, // 因球体已反转,FrontSide即可显示内侧纹理(比DoubleSide更高效)
  });

  // 创建球体并添加到场景(无修改)
  sphere = new THREE.Mesh(geometry, material);
  scene.add(sphere);
};

/**
 * 切换全景图(无修改)
 */
const switchPanorama = index => {
  if (index < 0 || index >= textures.length || !textures[index] || index === currentPanorama.value)
    return;

  isLoading.value = true;
  currentPanorama.value = index;

  if (sphere && sphere.material) {
    sphere.material.map = textures[index];
    sphere.material.needsUpdate = true; // 强制Three.js更新材质
    setTimeout(() => {
      isLoading.value = false;
    }, 300); // 延迟隐藏加载提示,确保纹理渲染完成
  }
};

/**
 * 窗口大小变化处理(无修改)
 */
const onWindowResize = () => {
  if (!container.value || !camera || !renderer) return;

  const width = container.value.clientWidth;
  const height = container.value.clientHeight;

  // 更新相机宽高比(保持场景比例正确)
  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  // 更新渲染器尺寸(保持全屏)
  renderer.setSize(width, height);
};

/**
 * 动画循环(无修改)
 */
const animate = () => {
  animationId.value = requestAnimationFrame(animate);

  // 阻尼效果必须更新控制器(保持)
  if (controls) {
    controls.update();
  }

  // 渲染场景(保持)
  if (renderer && scene && camera) {
    renderer.render(scene, camera);
  }
};

// 监听全景图切换(无修改)
watch(currentPanorama, newVal => {
  if (newVal >= 0 && newVal < textures.length) {
    switchPanorama(newVal);
  }
});

// 组件挂载初始化(无修改)
onMounted(() => {
  setTimeout(() => {
    if (container.value) {
      initScene();
      animate();
    }
  }, 100); // 延迟初始化,确保DOM加载完成
});

// 组件卸载清理(无修改)
onUnmounted(() => {
  if (animationId.value) {
    cancelAnimationFrame(animationId.value);
  }
  window.removeEventListener('resize', onWindowResize);
  if (controls) controls.dispose();
  if (renderer) {
    renderer.dispose();
    if (container.value && renderer.domElement) {
      container.value.removeChild(renderer.domElement);
    }
  }
  textures.forEach(texture => {
    if (texture) texture.dispose();
  });
  if (scene) scene.clear();
});
</script>

<style scoped>
.panorama-viewer {
  position: relative;
  width: 100vw;
  height: 90vh;
  overflow: hidden;
}

.viewer-container {
  width: 100%;
  height: 100%;
  pointer-events: auto;
}

.loading-indicator {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  padding: 10px 20px;
  border-radius: 4px;
  z-index: 200;
}

.controls-panel {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(0, 0, 0, 0.6);
  color: white;
  padding: 15px 20px;
  border-radius: 8px;
  font-family: Arial, sans-serif;
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 100;
  pointer-events: auto;
}

.info {
  font-size: 14px;
  line-height: 1.5;
}

.panorama-switch {
  display: flex;
  gap: 10px;
  margin-top: 5px;
}

.panorama-switch button {
  background-color: rgba(255, 255, 255, 0.2);
  color: white;
  border: none;
  padding: 8px 15px;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.panorama-switch button:hover {
  background-color: rgba(255, 255, 255, 0.3);
}

.panorama-switch button.active {
  background-color: #42b983;
}
</style>


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐