阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • CSS

    • ✨flex 布局 👌
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
      • ✨ 事件循环 👌
    • Promise

      • ✨Promise 面试题考点 👌
      • ✨Promise 基础 👌
      • ✨Promise 的链式调用 👌
      • ✨Promise 的静态方法 👌
      • ✨async 和 await👌
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • ✨SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新
    • Cypress

      • Cypress 测试框架面试题
    • 项目

      • FOFA 实习项目

        • /interview/project/fofa/FOFA%E5%AE%9E%E4%B9%A0%E9%A1%B9%E7%9B%AE%E9%9D%A2%E8%AF%95%E7%82%B9.html
      • 低代码问卷系统

        • 低代码问卷项目面试点
      • VR 全景看房

        • VR 全景看房项目面试点
  • TS

    • 快速入门

      • Playground 👌
      • 安装与运行 👌
      • 开发相关配置 👌
      • TS 常见类型 👌
      • 类型声明 👌
  • 工具库

    • 常用第三方工具库
    • JQuery
    • Lodash
    • Animate.css
    • Axios
    • MockJS
    • Moment
    • ECharts
  • 其他知识点

    • ✨ 前端项目打包流程与编译概念详解
    • ✨ 懒加载 👌
    • ✨ 前端路由的核心原理 👌

VR 全景看房项目面试点

项目介绍

项目描述

该项目基于 Three.js 和 Vue.js 开发的 3D 虚拟房屋查看应用,实现了沉浸式看房体验。用户可以在客厅、厨房、阳台等多个房间场景中自由切换,通过第一人称视角进行 360° 全景浏览同时可以通过交互查看场景中存在的信息点。

技术栈

vue3、ts、three.js、gsap

项目亮难点

  1. 实现了第一人称视角的相机控制系统,通过监听鼠标事件调整相机旋转角度并确保旋转符合人类直觉。同时,使用 GSAP 动画库实现了房间之间的平滑过渡效果,提升用户体验。
  2. 利用射线投射交互技术实现了精确的 3D 空间交互,包括房间导航点的点击切换和信息点的悬停检测。

面试题

Q1: 请介绍一下 Three.js 的核心组件,以及它们在 VR 看房项目中的作用?

详细内容

参考答案:

面试官您好,Three.js 是一个基于 WebGL 的 JavaScript 3D 图形库,它的核心组件主要包括场景(Scene)、相机(Camera)、渲染器(Renderer)、几何体(Geometry)、材质(Material)、光源(Light)等。在我们的 VR 看房项目中,这些组件发挥了关键作用:

1. 场景(Scene)

场景是所有 3D 对象的容器,相当于一个虚拟的 3D 世界。在我们项目中,所有的房间、导航图标、信息点都被添加到同一个场景中进行统一管理。

2. 相机(Camera)

我们使用透视相机(PerspectiveCamera)来模拟人眼视角,实现第一人称的沉浸式体验。相机的位置变化实现了房间之间的切换,旋转控制实现了 360° 全景浏览。

3. 渲染器(Renderer)

使用 WebGLRenderer 将场景和相机的数据渲染到 HTML 画布上,并通过 requestAnimationFrame 实现实时渲染循环。

4. 几何体(Geometry)

主要使用立方体几何体(BoxGeometry)创建房间空间,通过翻转 Z 轴使纹理朝向内部,形成房间内部视角。

5. 材质(Material)

使用基础材质(MeshBasicMaterial)配合纹理贴图,为每个房间的六个面分别加载对应的全景图片,创建沉浸式的房间环境。

6. 精灵(Sprite)

虽然不是传统核心组件,但在我们项目中大量使用精灵对象创建导航图标和信息点,它们始终面向相机,确保良好的交互体验。

Q2: 如何实现第一人称视角的相机控制?需要注意哪些细节?

详细内容

参考答案:

第一人称视角的相机控制是 VR 看房项目的核心功能之一。我们的实现主要包括以下几个方面:

1. 相机初始化设置 首先,我们使用透视相机并将其位置设置在房间几何体内部,模拟人眼在房间中的视角。相机的初始位置设置为 (0, 0, 0.01),确保视点在立方体内部。

2. 鼠标事件监听 通过监听鼠标的 mousedown、mouseup、mouseout 和 mousemove 事件,实现拖拽控制。只有在鼠标按下状态时,才响应鼠标移动事件,避免意外的视角变化。

onMounted(() => {
  if (container.value) {
    container.value.appendChild(renderer.domElement);
    // 渲染
    render();

    // 自定义相机视角旋转动画
    let mouseDown = false;
    container.value.addEventListener(
      "mousedown",
      () => {
        mouseDown = true;
      },
      false
    );
    container.value.addEventListener(
      "mouseup",
      () => {
        mouseDown = false;
      },
      false
    );
    container.value.addEventListener("mouseout", () => {
      mouseDown = false;
    });
    // 省略...
  }
});

3. 旋转角度计算 使用鼠标移动的增量值(movementX 和 movementY)乘以灵敏度系数 0.002,将像素移动转换为弧度旋转。相机 Y 轴上的角度是由鼠标 X 轴方向的增量控制,相机 X 轴上的角度是由鼠标 Y 轴方向的增量控制。

// 鼠标移动事件处理
container.value.addEventListener("mousemove", (e) => {
  if (mouseDown) {
    // 水平旋转:鼠标左右移动控制Y轴旋转
    camera.rotation.y += e.movementX * 0.002;

    // 垂直旋转:鼠标上下移动控制X轴旋转
    camera.rotation.x += e.movementY * 0.002;

    // 关键:设置旋转顺序为YXZ,符合第一人称视角直觉
    camera.rotation.order = "YXZ";
  }
});

4. 旋转顺序优化 关键是设置相机的旋转顺序为 "YXZ",这样可以避免万向节锁问题,确保旋转行为符合第一人称视角的直觉。

Q3: 如何使用 GSAP 实现房间之间的平滑过渡?

参考答案:

每个房间导航图标都有对应的点击事件,点击事件触发后会使用 gsap.to 函数调整相机的位置(camera.position),并设置平滑的过渡时间,点击事件是全局注册的,通过计算鼠标点击的位置图标调整射线的角度,判断是否和当前的导航图标相交,如果相交就触发相应的事件。

// 使用GSAP实现平滑的房间切换动画
// 客厅到阳台
balconySprite.onClick(() => {
  gsap.to(camera.position, {
    x: 0,
    y: 0,
    z: -10, // 移动到阳台房间的位置
    duration: 1, // 1秒的平滑过渡
  });
});

// 客厅到厨房
kitchenSprite.onClick(() => {
  gsap.to(camera.position, {
    x: 1.5,
    y: 0,
    z: 10, // 移动到厨房房间的位置
    duration: 1,
  });
});

Q4: 什么是射线投射?在 VR 看房项目中如何应用?

详细内容

参考答案:

射线投射(Raycasting)是 3D 图形学中的一种技术,通过从一个点(通常是相机)发射一条无限延伸射线来检测与 3D 对象的交点。

在我们的 VR 看房项目中,射线投射主要应用于两个核心场景:

  1. 导航点击交互:当用户点击房间导航图标时,通过射线投射检测点击的是哪个导航精灵,然后执行相应的房间切换动画。

  2. 信息点悬停检测:当用户鼠标悬停在信息点上时,通过射线投射实时检测鼠标位置对应的信息点,并显示相应的提示信息。

这种技术的优势在于能够在 3D 空间中实现精确的交互检测,无需复杂的碰撞检测算法,性能高效且实现简单。

Q5: 如何实现信息点的悬停检测和视觉反馈?

参考答案:

监听在渲染场景中鼠标的移动事件(mousemove),并触发信息点悬停显示函数。在这个函数中会判断鼠标的位置,并将这个位置传递给射线投射器,同时我们会维护一个所有信息点的列表,射线投射器会判断是否和其中的信息点相交。如果相交就创建一个信息点的提示框,否则就隐藏提示框。

// 存储所有信息点精灵的数组
const spriteList: THREE.Sprite[] = [];

// 信息点悬停显示函数
function tooltipShow(e: MouseEvent, spriteList: THREE.Sprite[]) {
  e.preventDefault();
  const pointer = new THREE.Vector2();
  const raycaster = new THREE.Raycaster();

  // 1. 归一化鼠标坐标
  pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;

  // 2. 从相机位置发射射线
  raycaster.setFromCamera(pointer, camera);

  // 3. 检测射线与所有信息点的相交
  const intersects = raycaster.intersectObjects(spriteList);

  // 4. 处理相交结果
  if (intersects.length > 0) {
    // 检查是否为信息点类型
    if (intersects[0].object.userData.type === "information") {
      // 计算提示框在屏幕上的位置
      const element = e.target as HTMLElement;
      const elementWidth = element.clientWidth / 2;
      const elementHeight = element.clientHeight / 2;

      // 将3D世界坐标转换为屏幕坐标
      const worldVector = new THREE.Vector3(
        intersects[0].object.position.x,
        intersects[0].object.position.y,
        intersects[0].object.position.z
      );
      const position = worldVector.project(camera);

      // 计算提示框的屏幕位置
      const left = Math.round(
        (position.x + 1) * elementWidth - tooltipBox.value!.clientWidth / 2
      );
      const top = Math.round(
        -(position.y - 1) * elementHeight - tooltipBox.value!.clientHeight / 2
      );

      // 更新提示框位置和内容
      tooltipPosition.value = {
        left: `${left}px`,
        top: `${top}px`,
      };
      tooltipContent.value = intersects[0].object.userData;
    } else {
      // 如果不是信息点,隐藏提示框
      handleTooltipHide(e);
    }
  }
}

Q6: 如何在 Vue3 项目中集成 Three.js?有哪些最佳实践?

参考答案:

// ThreeScene.vue
<template>
  <div ref="containerRef" class="three-container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { SceneManager } from '@/utils/SceneManager'

const containerRef = ref<HTMLElement>()
let sceneManager: SceneManager | null = null
let animationId: number

onMounted(() => {
  if (containerRef.value) {
    sceneManager = new SceneManager(containerRef.value)
    sceneManager.init()
    animate()
  }
})

onUnmounted(() => {
  if (animationId) {
    cancelAnimationFrame(animationId)
  }
  sceneManager?.dispose()
})

function animate() {
  animationId = requestAnimationFrame(animate)
  sceneManager?.update()
  sceneManager?.render()
}
</script>

最佳实践:

  • 将 Three.js 逻辑封装在独立的类中
  • 在组件卸载时正确清理资源
  • 使用 TypeScript 提供类型安全
  • 合理管理动画循环

Q7: 如何处理 Three.js 场景的响应式布局?

参考答案:

监听窗口大小变化事件,并在回调函数中更新相机和渲染器的尺寸

class ResponsiveManager {
  constructor(renderer: THREE.WebGLRenderer, camera: THREE.PerspectiveCamera) {
    this.renderer = renderer;
    this.camera = camera;

    window.addEventListener("resize", this.onWindowResize.bind(this));
  }

  onWindowResize() {
    const width = window.innerWidth;
    const height = window.innerHeight;

    // 更新相机宽高比
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();

    // 更新渲染器尺寸
    this.renderer.setSize(width, height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  }

  dispose() {
    window.removeEventListener("resize", this.onWindowResize.bind(this));
  }
}
最近更新:: 2025/7/23 03:47
Contributors: AK