当前位置: 首页 > news >正文

【threejs】VR看房项目经验总结

先进行了总结哈,具体的请往后看

核心步骤实现思路总结

第一步:搭建基本场景

实现思路: 创建Three.js三要素,通过new THREE.Scene()new THREE.PerspectiveCamera()new THREE.WebGLRenderer()来初始化,再用requestAnimationFrame()创建渲染循环,让画面持续刷新。

第二步:立方体内部贴图

实现思路: 加载6张贴图作为材质数组,创建立方体后使用box.geometry.scale(1, 1, -1)翻转Z轴,让贴图面朝内部,实现"站在房间里看墙壁"的效果。

第三步:相机控制

实现思路: 设置相机位置通过camera.position.set(0, 0, 0.01)实现,具体值需结合立方体大小位置来设置。通过监听鼠标事件,用camera.rotation.x/y += event.movementY/X * 0.01实现拖拽转动视角。

第四步:多房间拼接

实现思路: 创建多个立方体房间,通过Vector3精确控制位置让房间无缝拼接,如第一个房间在(0,0,0),第二个在(0,0,-10),两房间共享边界实现连通。

第五步:导航精灵

实现思路: 用Canvas绘制文字标签,转换为CanvasTexture贴到Sprite上。通过Raycaster射线检测判断鼠标点击,将屏幕坐标转换为NDC坐标(-1到+1),检测到点击后执行相机移动。

第六步:信息点处理

实现思路: 创建小图标Sprite放置在场景中,通过userData存储详细信息。鼠标悬停时用射线检测判断命中,再用worldVector.project(camera)将3D坐标转换为2D屏幕坐标,在对应位置显示提示框。

第七步:模块化封装

实现思路: 将重复的房间创建逻辑封装成Class,通过构造函数传入房间名称、贴图路径、位置等参数,内部自动完成材质加载、几何体创建、位置设置的标准流程,提高代码复用性。

核心技术要点:

  • 坐标转换:屏幕坐标 → NDC坐标 → 3D世界坐标
  • 射线检测:Raycaster实现点击和悬停交互
  • 几何体翻转:scale(1,1,-1)让贴图朝向内部
  • 位置计算:精确的Vector3坐标让房间无缝拼接

那么接下来详细的介绍一下:

第一步:搭建基本场景

// 创建场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);// 渲染循环
const render = () => {renderer.render(scene, camera);requestAnimationFrame(render);
};// 窗口自适应
window.addEventListener("resize", () => {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);
});

第二步:立方体内部贴图

这一步就是经常使用的天空盒效果,非常常见,核心就是将正方体的六个面贴图处理,这里我们将图片贴在内部,下面的代码就是比较固定的方式
// 加载6面贴图材质
const materials = [];
const faces = ['room_r', 'room_l', 'room_u', 'room_d', 'room_f', 'room_b'];
faces.forEach(face => {const texture = new THREE.TextureLoader().load(`./textures/main/${face}.jpg`);materials.push(new THREE.MeshBasicMaterial({ map: texture }));
});// 创建立方体房间
const geometry = new THREE.BoxGeometry(10, 10, 10);
const box = new THREE.Mesh(geometry, materials);
box.geometry.scale(1, 1, -1); // 翻转Z轴,让贴图朝向内部
scene.add(box);

关键点:scale(1, 1, -1) - 翻转Z轴使贴图面向房间内部

第三步:相机控制

将相机移动到正方体的内部,并且使鼠标可以转动
// 相机位置:房间中心点稍微偏移,避免计算错误
camera.position.set(0, 0, 0.01); // (X轴, Y轴, Z轴)// 鼠标拖拽控制
let isMouseDown = false;
container.addEventListener("mousedown", () => { isMouseDown = true; });
container.addEventListener("mouseup", () => { isMouseDown = false; });container.addEventListener("mousemove", (event) => {if (isMouseDown) {camera.rotation.x += event.movementY * 0.01; // 上下转动camera.rotation.y += event.movementX * 0.01; // 左右转动camera.rotation.order = "YXZ"; // 旋转顺序,避免万向锁}
});

关键点:

  • Vector3(0, 0, 0.01) - 房间中心微偏移
  • rotation.order = "YXZ" - 更加符合人类的习惯

第四步:多房间拼接

其实就是再创建一个正方体,这里需要注意设置好位置,避免出现缝隙。其实你也可以不使用Vector3设置位置,通过position.set()也行
// 第二个房间材质
const bedroomMaterials = [];
const bedroomFaces = ['bedroom_r', 'bedroom_l', 'bedroom_u', 'bedroom_d', 'bedroom_f', 'bedroom_b'];
bedroomFaces.forEach(face => {const texture = new THREE.TextureLoader().load(`./textures/bedroom/${face}.jpg`);bedroomMaterials.push(new THREE.MeshBasicMaterial({ map: texture }));
});// 创建第二个房间
const bedroomGeometry = new THREE.BoxGeometry(10, 10, 10);
const bedroomBox = new THREE.Mesh(bedroomGeometry, bedroomMaterials);
bedroomBox.geometry.scale(1, 1, -1);// 房间位置:Z轴-10,与第一个房间(Z轴-5到+5)相连
const bedroomPosition = new THREE.Vector3(0, 0, -10);
bedroomBox.position.copy(bedroomPosition);
scene.add(bedroomBox);// 相机移动到第二个房间
gsap.to(camera.position, { duration: 1, x: 0, y: 0, z: -10 });

关键点:

  • Vector3(0, 0, -10) - 房间1占据Z(-5到+5),房间2占据Z(-15到-5),无缝拼接
  • Euler - 控制物体旋转角度(绕X轴, 绕Y轴, 绕Z轴)

第五步:导航精灵

极其常见的功能,就是一个卡片效果,用来展示信息或者点击之类的操作,这部分代码可以拿去复用,经常能用到;这里有一个重点,那就是如何确定拿到我想和点击的元素,那就是使用Raycaster ,注意Raycaster 的值是负一到一需要转换一下 :
// 假设屏幕尺寸 1920x1080
// 鼠标在屏幕中心点击:clientX=960, clientY=540pointer.x = (960 / 1920) * 2 - 1 = 0.5 * 2 - 1 = 0  // 中心X
pointer.y = -(540 / 1080) * 2 + 1 = -0.5 * 2 + 1 = 0 // 中心Y
// 结果:(0, 0) 正好是NDC坐标的中心点
// Canvas创建文字标签
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 1024;
const context = canvas.getContext("2d");
context.fillStyle = "rgba(50,50,50,.7)";
context.fillRect(0, 256, canvas.width, canvas.height / 2);
context.font = "bold 200px Arial";
context.fillStyle = "white";
context.fillText("Room B", canvas.width / 2, canvas.height / 2);// 创建精灵
const spriteTexture = new THREE.CanvasTexture(canvas);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: spriteTexture, transparent: true 
}));
sprite.position.set(0, 0, -4); // 两房间之间位置
scene.add(sprite);// 点击检测
const raycaster = new THREE.Raycaster(); // 射线检测器
const pointer = new THREE.Vector2(); // 鼠标坐标window.addEventListener("click", (event) => {// 鼠标坐标转换为NDC坐标(-1到+1)pointer.x = (event.clientX / window.innerWidth) * 2 - 1;pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;raycaster.setFromCamera(pointer, camera);const intersects = raycaster.intersectObjects([sprite]);if (intersects.length > 0) {gsap.to(camera.position, { duration: 1, x: 0, y: 0, z: -10 });}
});

关键点:

  • Sprite - 始终面向相机的2D元素
  • Raycaster - 从相机发射射线检测点击物体
  • NDC坐标 - 标准化设备坐标,范围(-1, +1)

第六步:信息点处理

1. 鼠标悬停 → 射线检测命中sprite
2. 获取userData → intersects[0].object.userData,如果射线命中了物体,返回数组(按距离排序,近到远)
3. 更新Vue数据 → tooltipContent.value = userData
4. 计算2D位置 → 3D坐标转屏幕坐标
5. 显示HTML元素 → Vue响应式更新DOM

// 创建信息点
const infoTexture = new THREE.TextureLoader().load("./images/marker.png");
const infoSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: infoTexture, transparent: true 
}));
infoSprite.scale.set(0.2, 0.2, 0.2); // 缩放到20%
infoSprite.position.set(1.5, -0.1, -3);
infoSprite.userData = { // 存储自定义数据type: "information",name: "展示品A",description: "精美装饰品"
};// 鼠标悬停检测
function showTooltip(event) {pointer.x = (event.clientX / window.innerWidth) * 2 - 1;pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;raycaster.setFromCamera(pointer, camera);const intersects = raycaster.intersectObjects([infoSprite]);if (intersects.length > 0) {// 3D坐标转屏幕坐标const worldVector = intersects[0].object.position.clone();const screenPos = worldVector.project(camera);const x = (screenPos.x + 1) * window.innerWidth / 2;const y = (-screenPos.y + 1) * window.innerHeight / 2;// 显示tooltiptooltipElement.style.left = `${x}px`;tooltipElement.style.top = `${y}px`;}
}

关键点:

  • userData - 存储自定义数据的属性
  • project() - 将3D坐标投影到2D屏幕坐标

第七步:模块化封装

class Room {constructor(name, prefix, imagePath, scene, position = new THREE.Vector3(0,0,0)) {const materials = [];const faces = ['r', 'l', 'u', 'd', 'f', 'b'];faces.forEach(face => {const texture = new THREE.TextureLoader().load(`${imagePath}${prefix}_${face}.jpg`);materials.push(new THREE.MeshBasicMaterial({ map: texture }));});const geometry = new THREE.BoxGeometry(10, 10, 10);const room = new THREE.Mesh(geometry, materials);room.geometry.scale(1, 1, -1);room.position.copy(position);scene.add(room);}
}// 使用
new Room("主厅", "hall", "./textures/main/", scene);
new Room("卧室", "bedroom", "./textures/bedroom/", scene, new THREE.Vector3(0, 0, -10));

核心API速查

  • Vector3(x, y, z) - 3D空间位置坐标
  • Euler(x, y, z) - 3D物体旋转角度
  • scale(x, y, z) - 缩放几何体
  • Raycaster - 射线检测,用于点击/悬停判断
  • Sprite - 始终面向相机的2D元素
  • project() - 3D坐标转2D屏幕坐标
  • userData - 存储自定义数据

补充

intersects[0]的来源

1. intersects数组的产生

const raycaster = new THREE.Raycaster();
const intersects = raycaster.intersectObjects([infoSprite]);
//     ↑
//  这里返回一个数组

intersects数组结构:

// 如果射线命中了物体,返回数组(按距离排序,近到远)
intersects = [{distance: 3.2,           // 第1个物体,距离3.2object: infoSprite,      // 被命中的物体point: Vector3(1,2,3),   // 命中点坐标// ...其他信息},{distance: 5.8,           // 第2个物体,距离5.8object: anotherSprite,   // ...}
]// intersects[0] = 距离最近的被命中物体信息
// intersects.length > 0 表示至少命中了一个物体

2. 为什么用[0]?

if (intersects.length > 0) {// intersects[0] = 距离相机最近的物体// 通常我们只关心最近的那个const hitObject = intersects[0].object;const userData = intersects[0].object.userData;
}

3D坐标转2D屏幕坐标详解

1. 获取3D坐标

// 从被命中的物体获取3D世界坐标
const worldVector = new THREE.Vector3(intersects[0].object.position.x,  // 物体的X坐标intersects[0].object.position.y,  // 物体的Y坐标intersects[0].object.position.z   // 物体的Z坐标
);

2. 3D转NDC坐标

// project()方法:3D世界坐标 → NDC坐标(-1到+1)
const screenPosition = worldVector.project(camera);
// 结果:screenPosition.x 和 screenPosition.y 都在 -1 到 +1 范围内

3. NDC转屏幕像素坐标

// NDC坐标(-1到+1) → 屏幕像素坐标(0到屏幕宽高)
const elementWidth = window.innerWidth / 2;   // 屏幕宽度的一半
const elementHeight = window.innerHeight / 2; // 屏幕高度的一半const left = screenPosition.x * elementWidth + elementWidth;
const top = -screenPosition.y * elementHeight + elementHeight;
//           ↑ 注意这里有负号,因为屏幕Y轴向下,3D Y轴向上

4. 转换公式详解

// X轴转换:NDC(-1到+1) → 屏幕像素(0到屏幕宽)
// screenPosition.x = -1 时:-1 * (宽/2) + (宽/2) = 0     (屏幕左边)
// screenPosition.x = 0  时:0 * (宽/2) + (宽/2) = 宽/2   (屏幕中心)  
// screenPosition.x = +1 时:+1 * (宽/2) + (宽/2) = 宽    (屏幕右边)// Y轴转换:NDC(-1到+1) → 屏幕像素(0到屏幕高)
// screenPosition.y = +1 时:-1 * (高/2) + (高/2) = 0     (屏幕顶部)
// screenPosition.y = 0  时:0 * (高/2) + (高/2) = 高/2   (屏幕中心)
// screenPosition.y = -1 时:1 * (高/2) + (高/2) = 高     (屏幕底部)

5. 完整流程示例

// 假设物体在3D空间的位置是 (1.5, -0.1, -3)
const worldVector = new THREE.Vector3(1.5, -0.1, -3);// 转换为NDC坐标,假设结果是 (0.2, 0.1, ...)
const screenPosition = worldVector.project(camera);// 假设屏幕是1920x1080
const left = 0.2 * 960 + 960 = 1152;   // X像素位置
const top = -0.1 * 540 + 540 = 486;    // Y像素位置// 最终HTML元素显示在屏幕的(1152, 486)位置
tooltipElement.style.left = '1152px';
tooltipElement.style.top = '486px';

关键概念:

  • intersects[0] - 射线检测返回的最近物体
  • project() - Three.js内置方法,3D转NDC坐标
  • NDC坐标 - 标准化设备坐标(-1到+1)
  • 坐标系差异 - 3D Y轴向上,屏幕Y轴向下
http://www.lryc.cn/news/571789.html

相关文章:

  • java高级——注解和反射
  • MySQL 数据处理函数全面详解
  • 【windows常见文件后缀】
  • 客户端软件开发技术选择、填空解析
  • python中学物理实验模拟:杠杆平衡条件
  • 从0开始学linux韦东山教程第四章问题小结(5)
  • Java项目:基于SSM框架实现的学生二手书籍交易平台管理系统【ssm+B/S架构+源码+数据库+毕业论文+答辩PPT+任务书+开题】
  • 猿人学js逆向比赛第一届第六题
  • excel 待办日历软件(需要宏)特别推荐
  • 《挑战你的控制力!开源项目小游戏学习“保持平衡”开发解析:用HTML+JS+CSS实现物理平衡挑战》​
  • 吉林大学软件工程章节测试答案-第八章
  • 数学基础(线性代数、概率统计、微积分)缺乏导致概念难以理解问题大全
  • 每日一篇博客:理解Linux动静态库
  • 一文学懂快浮点数据格式
  • 【深度学习】卷积神经网络(CNN):计算机视觉的革命性引擎
  • 蚂蚁百宝箱+MCP打造p 人解放神器agent,解放大脑
  • 设置环境变量(linux,windows,windows用指令和用界面)
  • HarmonyOS性能优化——感知流畅优化
  • 鸿蒙网络编程系列54-仓颉版实现Smtp邮件发送客户端
  • LVS +Keepalived 高可用群集
  • 51c大模型~合集141
  • maven编译报错java: Compilation failed: internal java compiler error
  • 基于C++实现(控制台)机械提取词频
  • Hive的分区表(静态分区、动态分区)、分桶表、四种排序方式和数据加载方式
  • Linux操作系统之进程(六):进程的控制(上)
  • 鼎捷T100开发语言-Genero FGL 终极技术手册
  • Linux软件管理包-yum和基础开发工具-vim
  • 6.18 redis面试题 日志 缓存淘汰过期删除 集群
  • 【Leetcode】每日一题 —— No.2966
  • milvus和attu的搭建