Threejs 实现 VR 看房完结

效果:

threejs 3d Vr 看房

gitee 地址:

threejs-3d-map: 1、threejs 实现3d 地图效果链接:https://blog.csdn.net/qq_57952018/article/details/1430539902、threejs 实现 vr 看房

主要代码:

src/views/PanoramicView/index.vue

<script setup>
import {computed, onMounted, onUnmounted, reactive, ref} from 'vue'
import * as THREE from 'three'
// 引入轨道控制器扩展库OrbitControls.js
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {rooms} from './data.js';
import TWEEN from 'three/examples/jsm/libs/tween.module.js';
import Animations from "@/utils/animations.js";

let animationId
let tick;
const data = reactive({
  renderer: null,
  camera: null,
  scene: null,
  controls: null,
  cameraZAxis: 2,
  currentRoom: 'living-room',
});

// 获取交互点的信息
const interactivePoints = computed(() => {
  const res = [];
  rooms.forEach((room) => {
    if (room.interactivePoints && room.interactivePoints.length > 0) {
      room.interactivePoints.forEach((point) => {
        point = {
          room: room.key,
          ...point,
        };
        res.push(point);
      });
    }
  });
  return res;
});

const container = ref(null)
// 拿到页面的宽高
const width = window.innerWidth, height = window.innerHeight;

// 创建场景
let scene = new THREE.Scene();
// 将背景颜色设置为白色
scene.background = new THREE.Color("#000000");
data.scene = scene;

// 创建相机
let camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 1000);
// 设置相机位置
camera.position.z = data.cameraZAxis;
data.camera = camera;

// // 辅助线 AxesHelper
const axesHelper = new THREE.AxesHelper(500);
scene.add(axesHelper);

// 初始化渲染器
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
data.renderer = renderer;


// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
//阻尼 更真实
controls.enableDamping = true
// controls.enablePan = false;
// 缩放限制
controls.maxDistance = 12;
data.controls = controls;

// 监听浏览器窗口的大小变化 重新调整渲染器的大小
window.addEventListener('resize', onWindowResize);

// 更新渲染器大小以匹配新的浏览器窗口尺寸
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

const textLoader = new THREE.TextureLoader();

// 加载每个房间 (客厅、卧室、书房)
const createRoom = (name, position, map) => {
  // 创建球状 为了做全景
  const geometry = new THREE.SphereGeometry(16, 256, 256);
  // z轴的-1 代表将外面的体图放在内侧
  geometry.scale(1, 1, -1);
  const material = new THREE.MeshBasicMaterial({
    // 加载纹理贴图
    map: textLoader.load(map),
    side: THREE.DoubleSide,
  });
  const room = new THREE.Mesh(geometry, material);
  room.name = name;
  room.position.set(position.x, position.y, position.z);
  room.rotation.y = Math.PI / 2;
  scene.add(room);
  return room;
}

const initRoom = () => {
// 创建网格对象
  rooms.map((item) => {
    return createRoom(item.key, item.position, item.map);
  });
  // 添加交互点
  const raycaster = new THREE.Raycaster();
  // 室内悬浮标记物
  const _points = interactivePoints.value.map((item, index) => ({
    ...item,
    element: document.querySelector(`.point-${index}`),
  }));
  // 动画
 tick = () => {
    if (renderer) {
      for (const point of _points) {
        // 获取2D屏幕位置
        const screenPosition = point.position.clone();
        const pos = screenPosition.project(camera);
        raycaster.setFromCamera(screenPosition, camera);
        const intersects = raycaster.intersectObjects(scene.children, true);
        if (intersects.length === 0) {
          // 未找到相交点,显示
          point.element.classList.add('visible');
        } else {
          // 找到相交点
          // 获取相交点的距离和点的距离
          const intersectionDistance = intersects[0].distance;
          const pointDistance = point.position.distanceTo(camera.position);
          // 相交点距离比点距离近,隐藏;相交点距离比点距离远,显示
          intersectionDistance < pointDistance
              ? point.element.classList.remove('visible')
              : point.element.classList.add('visible');
        }
        pos.z > 1
            ? point.element.classList.remove('visible')
            : point.element.classList.add('visible');
        const translateX = screenPosition.x * window.innerWidth * 0.5;
        const translateY = -screenPosition.y * window.innerHeight * 0.5;
        point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
      }
    }

    controls && controls.update();
    TWEEN && TWEEN.update();
    // 更新渲染器
   renderer && renderer.render(scene, camera);
    // 页面重绘时调用自身
    window.requestAnimationFrame(tick);
  };
 tick();
}


// 点击交互点
const handleReactivePointClick = (point) => {
  window.alert('你点击了' + point.value)
};


// 点击切换场景
const handleSwitchButtonClick = async (key) => {
  const room = rooms.filter((item) => item.key === key)[0];
  if (data.camera) {
    const x = room.position.x;
    const y = room.position.y;
    const z = room.position.z;
    // 加载切换动画
    Animations.animateCamera(data.camera, data.controls, { x, y, z: data.cameraZAxis }, { x, y, z }, 1600, () => {});
    data.controls.update();
  }
  await sleep(1600);
  data.currentRoom = room.key;
};

const sleep = (duration) => new Promise((resolve) => {
  setTimeout(resolve, duration);
});


// 渲染页面
const render = () => {
  if (!renderer) return
  // 将场景(scene)和摄像机(camera 加入进来)
  renderer.render(scene, camera)
  // 渲染下一帧的时候会调用render函数
  animationId = requestAnimationFrame(render)
  controls.update()
}

const initLight = () => {
  // 基本光源
  const ambLight = new THREE.AmbientLight('#ffffff', 0.3)
  /**
   * 设置聚光灯相关的的属性
   */
  const spotLight = new THREE.SpotLight(0xFFFFFF); // 聚光灯
  spotLight.position.set(40, 200, 10);
  spotLight.castShadow = true; // 只有该属性为true时,该点光源允许产生阴影,并且下列属性可用
  scene.add(ambLight, spotLight); // 向场景中添加光源
}

onMounted(() => {
  // 添加物体到场景
  initRoom()
  // 渲染
  // render()
  // 设置环境光
  initLight()
  // 将渲染加入到页面上
  container.value.appendChild(renderer.domElement);
})
onUnmounted(() => {
  window.removeEventListener('resize', onWindowResize)

  // 销毁 Three.js 实例,清理内存
  if (renderer) {
    if (renderer.domElement && renderer.domElement.parentNode) {
      renderer.domElement.parentNode.removeChild(renderer.domElement);
    }
    renderer.dispose();
    renderer = null;
  }
  if (data.scene) {
    data.scene.traverse((object) => {
      if (object.geometry) object.geometry.dispose();
      if (object.material) {
        if (Array.isArray(object.material)) {
          object.material.forEach((material) => material.dispose());
        } else {
          object.material.dispose();
        }
      }
    });
    data.scene = null;
  }
  if (data.controls) {
    data.controls = null
  }
  data.camera = null;
})
</script>

<template>
  <div class="home">
    <div ref="container"></div>
    <div class="vr">
      <span class="box">
        <i class="icon"></i>
        <b class="text">全景漫游</b>
      </span>
    </div>
    <!-- 场景切换点 -->
    <div class="switch">
      <span class="button" v-for="(room, index) in rooms" :key="index" @click="handleSwitchButtonClick(room.key)"
            v-show="room.key !== data.currentRoom">
        <b class="text">{{ room.name }}</b>
        <i class="icon"></i>
      </span>
    </div>
    <!-- 交互点 -->
    <div
        class="point"
        v-for="(point, index) in interactivePoints"
        :key="index"
        :class="[`point-${index}`, `point-${point.key}`]"
        @click="handleReactivePointClick(point)"
        v-show="point.room === data.currentRoom"
    >
      <div class="label" :class="[`label-${index}`, `label-${point.key}`]">
        <label class="label-tips">
          <div class="cover">
            <i
                class="icon"
                :style="{
                background: `url(${point.cover}) no-repeat center`,
                'background-size': 'contain',
              }"
            ></i>
          </div>
          <div class="info">
            <p class="p1">{{ point.value }}</p>
            <p class="p2">{{ point.description }}</p>
          </div>
        </label>
      </div>
    </div>
  </div>
</template>

<style lang="css" scoped>
.home {
  position: relative;
  .vr {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 11;
    -webkit-animation: slideInLeft 1s .15s;
    animation: slideInLeft 1s .15s;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
    .box {
      background: rgba(0, 0, 0, .3);
      -webkit-backdrop-filter: blur(8px);
      backdrop-filter: blur(8px);
      display: flex;
      align-items: center;
      justify-content: space-around;
      overflow: hidden;
      padding: 4px 20px;
      border-radius: 0 0 16px 0;
      border: 1px groove rgba(255, 255, 255, .3);
      border-top: none;
      border-left: none;
      box-shadow: 0 1px 4px rgba(0, 0, 0, .1);
      .icon {
        display: inline-block;
        height: 64px;
        width: 64px;
        background: url('@/assets/images/home/vr.png') no-repeat center;
        background-size: contain;
        margin-right: 12px;
      }
      .text {
        font-size: 24px;
        color: #ffffff;
        display: inline-block;
        font-weight: 500;
      }
    }
  }
  .switch {
    position: fixed;
    right: 24px;
    top: 40%;
    z-index: 11;
    -webkit-animation: slideInRight 1s .3s;
    animation: slideInRight 1s .3s;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
    .button {
      display: block;
      background: rgba(27, 25, 24, .5);
      border-radius: 12px;
      display: flex;
      align-items: center;
      padding: 12px 8px 12px 24px;
      -webkit-backdrop-filter: blur(4px);
      -moz-backdrop-filter: blur(4px);
      backdrop-filter: blur(4px);
      cursor: pointer;
      transition: all .25s ease-in-out;
      .text{
        color: rgba(255, 255, 255, 1);
        font-size: 24px;
        font-weight: 600;
      }
      &:not(last-child) {
        margin-bottom: 48px;
      }
      .icon {
        display: inline-block;
        height: 30px;
        width: 30px;
        background: url('@/assets/images/home/icon_arrow.png') no-repeat center;
        background-size: 100% 100%;
        transform: rotate(180deg);
        margin-left: 8px;
      }
      &:hover {
        background: rgba(27, 25, 24, .2);
        box-shadow: 1px 1px 2px rgba(0, 0, 0, .2);
      }
    }
  }
  .point {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 10;
    .label {
      position: absolute;
      top: -16px;
      left: -16px;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      background: rgba(255, 255, 255, 1);
      text-align: center;
      line-height: 32px;
      font-weight: 100;
      font-size: 14px;
      cursor: help;
      transform: scale(0, 0);
      transition: all 0.3s ease-in-out;
      backdrop-filter: blur(4px);
      &::before, &::after {
        display: inline-block;
        content: '';
        background: rgba(255, 255, 255, 1);;
        height: 100%;
        width: 100%;
        border-radius: 50%;
        position: absolute;
        left: 50%;
        top: 50%;
        margin-left: -10px;
        margin-top: -10px;
      }
      &::before {
        animation: bounce-wave 1.5s infinite;
      }
      &::after {
        animation: bounce-wave 1.5s -0.4s infinite;
      }
      .label-tips {
        height: 88px;
        width: 200px;
        overflow: hidden;
        position: absolute;
        top: -32px;
        right: -220px;
        font-size: 32px;
        background: rgba(255, 255, 255, .6);
        border: 1px groove rgba(255, 255, 255, .5);
        -webkit-backdrop-filter: blur(4px);
        -moz-backdrop-filter: blur(4px);
        backdrop-filter: blur(4px);
        border-radius: 16px;
        display: flex;
        justify-content: space-between;
        align-content: center;
        box-shadow: 1px 1px 2px rgba(0, 0, 0, .1);
        .cover {
          width: 80px;
          height: 100%;
          .icon {
            display: inline-block;
            height: 100%;
            width: 100%;
            filter: drop-shadow(1px 1px 4px rgba(0, 0, 0, .1));
          }
        }
        .info {
          width: calc(100% - 80px);
          height: 100%;
          overflow: hidden;
          padding-left: 12px;
          p {
            overflow: hidden;
            text-overflow: ellipsis;
            text-align: left;
            text-shadow: 0 1px 1px rgba(0, 0, 0, .1);
            &.p1 {
              font-size: 24px;
              color: #1D1F24;
              font-weight: 800;
              margin: 12px 0 2px;
            }
            &.p2 {
              font-size: 18px;
              color: #00aa47;
              font-weight: 500;
            }
          }
        }
      }
      &.label-sofa {
        .label-tips {
          left: -220px;
          flex-direction: row-reverse;
          .info {
            padding: 0 12px 0 0;
            p {
              text-align: right
            }
          }
        }
      }
    }
    .text {
      position: absolute;
      top: 30px;
      left: -120px;
      width: 200px;
      padding: 20px;
      border-radius: 4px;
      background: rgba(0, 0, 0, .6);
      border: 1px solid #ffffff;
      color: #ffffff;
      line-height: 1.3em;
      font-weight: 100;
      font-size: 14px;
      opacity: 0;
      transition: opacity 0.3s;
      pointer-events: none;
      text-align: justify;
      text-align-last: left;
    }
    &:hover .text {
      opacity: 1;
    }
    &.visible .label {
      transform: scale(1, 1)
    }
  }
}

.animate-point-wave::before {
  content: '';
  animation: bounce-wave 1.5s infinite;
}
.animate-point-wave::after {
  content: '';
  animation: bounce-wave 1.5s -0.4s infinite;
}

@keyframes bounce-wave {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(3.6);
    opacity: 0;
  }
}
</style>

src/views/PanoramicView/data.js

/* eslint-disable */
import { Vector3 } from 'three';

export const rooms = [
    {
        name: '客厅',
        key: 'living-room',
        map: new URL('@/assets/images/map/map_living_room.jpg', import.meta.url).href,
        showSwitch: true,
        position: new Vector3(0, 0, 0),
        interactivePoints: [
            {
                key: 'tv',
                value: '电视机',
                description: '智能电视',
                cover: new URL('@/assets/images/home/cover_living_room_tv.png', import.meta.url).href,
                position: new Vector3(-6, 2, -8),
            },
            {
                key: 'fridge',
                value: '冰箱',
                description: '豪华冰箱',
                cover: new URL('@/assets/images/home/cover_living_room_fridge.png', import.meta.url).href,
                position: new Vector3(-12, 4, 9),
            },
            {
                key: 'sofa',
                value: '沙发',
                description: '舒适沙发',
                cover: new URL('@/assets/images/home/cover_living_room_sofa.png', import.meta.url).href,
                position: new Vector3(6, 0, -8),
            },
        ],
    },
    {
        name: '卧室',
        key: 'bed-room',
        map: new URL('@/assets/images/map/map_bed_room.jpg', import.meta.url).href,
        showSwitch: true,
        position: new Vector3(-32, 0, 0),
        interactivePoints: [
            {
                key: 'bed',
                value: '床',
                description: '温暖的床',
                cover: new URL('@/assets/images/home/cover_bed_room_bed.png', import.meta.url).href,
                position: new Vector3(-38, 2, -14),
            },
        ],
    },
    {
        name: '书房',
        key: 'study-room',
        map: new URL('@/assets/images/map/map_study_room.jpg', import.meta.url).href,
        showSwitch: true,
        position: new Vector3(32, 0, 0),
        interactivePoints: [
            {
                key: 'art',
                value: '艺术品',
                description: '绝版作品',
                cover: new URL('@/assets/images/home/cover_study_room_art.png', import.meta.url).href,
                position: new Vector3(42, 6, -8),
            },
        ]
    },
];

部分内容借鉴:

https://juejin.cn/post/7215268204062490679

相关推荐
YesPMP平台官方1 天前
探索Unity:从游戏引擎到元宇宙体验,聚焦内容创作
3d·unity·游戏引擎·vr·动画制作·文娱场景
YesPMP251 天前
探索Unity:从游戏引擎到元宇宙体验,聚焦内容创作
3d·unity·游戏引擎·vr·动画制作·文娱场景
每日出拳老爷子4 天前
【Unity】【游戏开发】【VR开发】如何让按钮在被Ray选中时发声?
unity·游戏引擎·vr
jimumeta4 天前
什么是3D展厅?有哪些应用场景?
3d·vr·虚拟展厅·3d展厅·视创云展
朗迪锋4 天前
虚拟现实在制造业中的应用
大数据·人工智能·vr
LhcyyVSO4 天前
首届The VR&Animation Award 震撼启幕!VsoCloud独家赞助此次大赛!
vr·动画·影视动漫渲染
王家视频教程图书馆5 天前
vr头显都是什么操作系统
vr
starsongda5 天前
VR在线展厅重塑展览新维度,引领沉浸式科技体验与漫游新时代
科技·3d·vr
小小不董6 天前
图文深入介绍oracle资源管理(续)
linux·运维·服务器·数据库·oracle·vr