阿卡不拉阿卡不拉
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
  • 其他知识点

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

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

口头回答

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

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

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

3. 旋转角度计算 使用鼠标移动的增量值(movementX 和 movementY)乘以灵敏度系数 0.002,将像素移动转换为弧度旋转。这个系数经过调试,确保旋转速度符合用户直觉。

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

需要注意的细节包括:

  • 旋转灵敏度的调节
  • 旋转顺序的选择
  • 边界限制的处理
  • 性能优化
  • 用户体验的平滑性

项目中的具体实现细节

1. 相机初始化

// 在 App.vue 中创建透视相机
const camera = new THREE.PerspectiveCamera(
    75, // 75度视野角,接近人眼视角
    window.innerWidth / window.innerHeight, // 屏幕宽高比
    0.1, // 近裁剪面,设置较小值避免近距离物体被裁剪
    1000 // 远裁剪面,足够大以包含所有房间
);

// 关键:将相机位置设置在几何体内部
camera.position.set(0, 0, 0.01);

2. 鼠标事件监听系统

// 鼠标状态跟踪
let mouseDown = false;

// 鼠标按下事件
container.value.addEventListener(
    "mousedown",
    () => {
        mouseDown = true;
    },
    false
);

// 鼠标释放事件
container.value.addEventListener(
    "mouseup",
    () => {
        mouseDown = false;
    },
    false
);

// 鼠标移出容器事件 - 防止鼠标移出后仍然响应
container.value.addEventListener("mouseout", () => {
    mouseDown = false;
});

3. 核心旋转控制逻辑

// 鼠标移动事件处理
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. 房间切换时的相机位置控制

// 使用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,
    });
});

5. 响应式处理

// 窗口大小变化时更新相机参数
window.addEventListener("resize", () => {
    // 更新相机宽高比
    camera.aspect = window.innerWidth / window.innerHeight;

    // 更新投影矩阵 - 必须调用以应用新的宽高比
    camera.updateProjectionMatrix();

    // 更新渲染器尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 设置像素比以适应高DPI屏幕
    renderer.setPixelRatio(window.devicePixelRatio);
});

关键技术细节分析

1. 旋转顺序的重要性

// 为什么选择 "YXZ" 旋转顺序?
camera.rotation.order = "YXZ";

/*
 * YXZ 顺序的优势:
 * - Y轴旋转(水平转向)先执行,符合人类转头的自然习惯
 * - X轴旋转(垂直仰俯)后执行,避免了万向节锁问题
 * - Z轴旋转最后,在第一人称视角中通常不需要
 *
 * 如果使用默认的 "XYZ" 顺序,会出现:
 * - 仰视或俯视到极限角度时出现意外的滚转
 * - 旋转行为不符合人类直觉
 */

2. 灵敏度系数的选择

// 0.002 这个系数是如何确定的?
const sensitivity = 0.002;

/*
 * 灵敏度计算考虑因素:
 * - movementX/Y 的单位是像素
 * - 相机旋转的单位是弧度
 * - 需要将像素移动转换为合适的旋转角度
 *
 * 0.002 的选择基于:
 * - 鼠标移动500像素 = 1弧度旋转 ≈ 57.3度
 * - 这个比例让用户感觉控制自然,不会过于敏感或迟钝
 */
camera.rotation.y += e.movementX * sensitivity;
camera.rotation.x += e.movementY * sensitivity;

3. 边界限制的考虑

// 在实际项目中,可能需要添加垂直旋转的边界限制
container.value.addEventListener("mousemove", (e) => {
    if (mouseDown) {
        camera.rotation.y += e.movementX * 0.002;

        // 限制垂直旋转角度,防止过度仰视或俯视
        const newRotationX = camera.rotation.x + e.movementY * 0.002;
        camera.rotation.x = Math.max(
            -Math.PI / 2, // 最大俯视角度
            Math.min(Math.PI / 2, newRotationX) // 最大仰视角度
        );

        camera.rotation.order = "YXZ";
    }
});

4. 性能优化考虑

// 使用 requestAnimationFrame 确保渲染与浏览器刷新率同步
const render = () => {
    renderer.render(scene, camera);
    requestAnimationFrame(render); // 通常是60FPS
};

// 避免在每次鼠标移动时都重新渲染
// Three.js 的渲染循环会自动处理相机变化

用户体验优化

1. 平滑过渡动画

// 使用 GSAP 实现房间切换的平滑过渡
// 而不是瞬间跳转,提升用户体验
gsap.to(camera.position, {
    x: targetX,
    y: targetY,
    z: targetZ,
    duration: 1, // 1秒过渡时间
    ease: "power2.inOut", // 缓动函数
});

2. 鼠标状态管理

// 确保鼠标移出容器时停止旋转
// 防止用户意外操作
container.value.addEventListener("mouseout", () => {
    mouseDown = false;
});

3. 移动端适配考虑

// 在实际项目中,还需要考虑触摸事件
// 实现移动端的第一人称视角控制
container.value.addEventListener("touchstart", handleTouchStart);
container.value.addEventListener("touchmove", handleTouchMove);
container.value.addEventListener("touchend", handleTouchEnd);

技术亮点总结

  1. 旋转顺序优化:使用 "YXZ" 旋转顺序避免万向节锁,确保旋转行为符合人类直觉

  2. 灵敏度调优:通过 0.002 的系数实现自然的鼠标控制感受

  3. 状态管理:完善的鼠标事件监听,包括边界情况处理

  4. 平滑过渡:结合 GSAP 动画库实现房间切换的流畅体验

  5. 响应式适配:窗口大小变化时自动调整相机参数

  6. 性能考虑:使用 requestAnimationFrame 确保渲染性能

这种实现方式不仅技术上可靠,而且用户体验流畅自然,是 VR 应用中第一人称视角控制的最佳实践。

最近更新:: 2025/7/23 03:47
Contributors: AK