声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
摘要
3D
全景技术可以实现日常生活中的很多功能需求,比如地图的街景全景模式、数字展厅、在线看房、社交媒体的全景图预览、短视频直播平台的全景直播等。Three.js
实现全景功能也是十分方便的,当然了目前已经有很多相关内容的文章,我之前就写过一篇《Three.js 实现3D全景侦探小游戏》。因此本文内容及此专栏下一篇文章讨论的重点不是如何实现 3D
全景图功能,而是如何一步步优雅实现在多个3D全景中穿梭漫游,达到如在真实世界中前进后退的视觉效果。
全景漫游系列文章将分为上下两篇,本篇内容我们先介绍如何通过移动相机的方法来达到场景切换的目的。通过本文的学习,你将学到的知识点包括:在 Three.js
中创建全景图的几种方式、在 3D
全景图中添加交互热点、利用 Tween.js
实现相机切换动画、多个全景图之间的切换等。
效果
本文最终将实现如下的效果,左右控制鼠标旋转屏幕可以预览室内三维全景图,同时全景图内有多个交互热点,它们标识着三维场景内的一些物体,比如沙发 ?
、电视机 ?
等,交互热点会随着场景的旋转而旋转,点击热点 ?
可以弹出交互反馈提示框。
点击屏幕上有其他场景名称的按钮比如 客厅
、卧室
、书房
时,可以从当前场景切换到目标场景全景图,交互热点也会同时切换。
打开以下链接,在线预览效果,大屏访问效果更佳。
??
在线预览地址:https://dragonir.github.io/panorama-basic/
本专栏系列代码托管在 Github
仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
?
代码仓库地址:git@github.com:dragonir/threejs-odessey.git
原理
我们先来简单总结下在 Three.js
中实现三维全景功能的有哪些方式:
球体
在球体内添加 HDR
全景照片可以实现三维全景功能,全景照片是一张用球形相机拍摄的图片,如下图所示:
const geometry = new THREE.SphereGeometry(500, 60, 40);geometry.scale(- 1, 1, 1);const texture = new THREE.TextureLoader().load( 'textures/hdr.jpg');const material = new THREE.MeshBasicMaterial({ map: texture });const mesh = new THREE.Mesh(geometry, material);scene.add(mesh);
?
球体全景图 Three.js 官方示例
立方体
在立方体内添加全景图贴图的方式也可以实现三维全景图功能,此时需要对 HDR
全景照片进行裁切,分割成 6
张来分别对应立方体的 6
个面。
const textures = cubeTextureLoader.load([ '/textures/px.jpg', '/textures/nx.jpg', '/textures/py.jpg', '/textures/ny.jpg', '/textures/pz.jpg', '/textures/nz.jpg']);const materials = [];for ( let i = 0; i < 6; i ++ ) { materials.push( new THREE.MeshBasicMaterial( { map: textures[ i ] } ) );}const skyBox = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), materials );skyBox.geometry.scale( 1, 1, - 1 );scene.add( skyBox );
?
立方体全景图 Three.js 官方示例
环境贴图
使用环境贴图也可以实现全景图功能,像下面这样加载全景图片,然后将它赋值给 scene.background
和 scene.environment
即可:
const environmentMap = cubeTextureLoader.load([ '/textures/px.jpg', '/textures/nx.jpg', '/textures/py.jpg', '/textures/ny.jpg', '/textures/pz.jpg', '/textures/nz.jpg']);environmentMap.encoding = THREE.sRGBEncoding;scene.background = environmentMap;scene.environment = environmentMap;
?
具体原理和实现方式就不详细介绍了,可查看我往期的文章《Three.js 进阶之旅:多媒体应用-3D Iphone》,环境贴图段落中有详细实现介绍。
其他
除了使用 Three.js
自己实现全景图功能之外,也有一些其他功能完备的全景图库可以很方便的实现三维全景场景,比如下面几个就比较不错,其中后两个是 GUI
客户端,可以在客户端内非常方便的在全景图上添加交互热点、实现多个场景的漫游路径等,大家感兴趣的话都可以试试。
- panolens.js
- pannellum
- Photo-Sphere-Viewer
- krpano
- Pano2VR
工具全景图生成工具
- 使用球形全景相机拍摄。
- 使用
Blender
等建模软件相机360
度旋转渲染。
全景图编辑工具
下面两个网站提供丰富的三维全景背景照片及将 hdr
图片裁切成上述需要的 6
张贴图的能力,大家可以按自己需要下载和编辑。
?
HDR全景背景照片下载网站:polyhaven
?
HDR立方体材质转换工具:HDRI-to-CubeMap
实现
现在,我们使用第一种球体 ⚪
全景图的方式,来实现示例中介绍的内容。
〇 场景初始化
创建全景图前先做一些常规三维场景准备工作,由于三维全景图功能并不会涉及到新的技术点,因此像下面这样简单实现就可以。
在文件顶部引入以下资源,其中 OrbitControls
用于旋转全景图时的镜头鼠标控制;TWEEN
用于创建流程的场景切换动画,Animations
是使用 TWEEN
来控制摄像机和控制器切换的方法的封装,可以快速实现镜头的丝滑切换;rooms
是自定义的一个数组,用来保存多个全景图的信息。
import * as THREE from 'three';import { OrbitControls } from '@/utils/OrbitControls.js';import { TWEEN } from 'three/examples/jsm/libs/tween.module.min.js';import Animations from '@/utils/animations';import { rooms } from '@/views/home/data';
然后初始化渲染器、场景、相机、控制器、页面缩放适配、页面重绘动画等。
const sizes = { width: window.innerWidth, height: window.innerHeight,};// 初始化渲染器const canvas = document.querySelector('canvas.webgl');const renderer = new THREE.WebGLRenderer({ canvas });renderer.setSize(sizes.width, sizes.height);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));// 初始化场景const scene = new THREE.Scene();// 初始化相机const camera = new THREE.PerspectiveCamera(65, sizes.width / sizes.height, 0.1, 1000);camera.position.z = data.cameraZAxis;scene.add(camera);// 镜头控制器const controls = new OrbitControls(camera, renderer.domElement);controls.target.set(0, 0, 0);// 页面缩放监听window.addEventListener('resize', () => { sizes.width = window.innerWidth; sizes.height = window.innerHeight; // 更新渲染 renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 更新相机 camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix();});// 动画const tick = () => { controls && controls.update(); TWEEN && TWEEN.update(); renderer.render(scene, camera); window.requestAnimationFrame(tick);};tick();
① 创建一个球体
现在,像下面这样,我们往场景中添加一个三维球体 ⚪
,作为第一个全景图的载体。其中 THREE.SphereGeometry(radius, segmentsWidth, segmentsHeight, phiStart, phiLength, thetaStart, thetaLength)
接收 7
个参数,我们使用前 3
个参数半径、经度上的面数切片数、纬度上的切片数即可,数值可按自己的需求自行调整。
const geometry = new THREE.SphereGeometry(16, 256, 256);const material = new THREE.MeshBasicMaterial({ color: 0xffffff,});const room = new THREE.Mesh(geometry, material);scene.add(room);
② 创建全景图
现在我们对球体进行全景图片贴图,并将 side
属性设置为 THREE.DoubleSide
或者 THREE.BackSide
然后通过设置 geometry.scale(1, 1, -1)
将球体内外翻转,就能得到下面所示的效果。
const geometry = new THREE.SphereGeometry(16, 256, 256);const material = new THREE.MeshBasicMaterial({ map: textLoader.load(map), side: THREE.DoubleSide,});geometry.scale(1, 1, -1);const room = new THREE.Mesh(geometry, material);
此时,我们通过鼠标放大球体,进入到球体内部,上下左右旋转球体,就能观察到全景效果了。
③ 创建其他场景的全景图
对于数量较少,简单的场景我们可以创建多个球体全景图来实现,这种方式虽然笨重,但是控制多个场景很方便,代码也非常容易理解,下篇文章将通过另一种更优雅的方式来实现多个全景图场景,以适应更加复杂的需求。
我们先对创建球体 ⚪
全景图的方法加以封装,通过 createRoom
方法批量创建多个全景图场景,它接收的名称 name
、位置 position
以及 贴图 map
三个参数是通过上述引入的 rooms
数值配置的。
const createRoom = (name, position, map) => { const geometry = new THREE.SphereGeometry(16, 256, 256); 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;};// 批量创建rooms.map((item) => { const room = createRoom(item.key, item.position, item.map); return room;});
我们按房间位置的和贴图的配置,创建如下所示的三个房间客厅、卧室和书房。
④ 限制旋转角度
根据自己的需求,我们可以对镜头控制器 ?
做以下限制,比如开启转动惯性、禁止整个场景通过鼠标右键发生平移、设置缩放的最大级别防止暴露出球体、限制垂直方向旋转等,以增强用户体验。
// 转动惯性controls.enableDamping = true;// 禁止平移controls.enablePan = false;// 缩放限制controls.maxDistance = 12;// 垂直旋转限制controls.minPolarAngle = Math.PI / 2;controls.maxPolarAngle = Math.PI / 2;
⑤ 实现多个场景穿梭漫游
本文中实现多个场景穿梭漫游的方法原理:主要是通过移动相机和控制器的中点位置来实现的,我们先用用于生成多个场景的 rooms
数值在页面上添加一些表示切换房间的按钮,点击按钮时拿到需要跳转的目标场景信息,然后通过 Animations.animateCamera
方法将像机和控制器从当前位置平滑移动到目标位置。
// 点击切换场景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(); }};
其中 Animations.animateCamera
方法是使用 TWEEN.js
封装的一个移动相机 ?
和控制器 ?
的方法,使用它可以实现丝滑的镜头补间动画,不仅可以像本文中这样来实现多个场景的切换,还可以实现像镜头从远处拉近、点击交互点后镜头聚焦放大到某个局部,镜头场景巡航等效果。完整代码可以查看本篇文章的示例代码:
animateCamera: (camera, controls, newP, newT, time = 2000, callBack) => { const tween = new TWEEN.Tween({ x1: camera.position.x, // 相机x y1: camera.position.y, // 相机y z1: camera.position.z, // 相机z x2: controls.target.x, // 控制点的中心点x y2: controls.target.y, // 控制点的中心点y z2: controls.target.z, // 控制点的中心点z }); tween.to( { x1: newP.x, y1: newP.y, z1: newP.z, x2: newT.x, y2: newT.y, z2: newT.z, }, time, ); // ...}
⑥ 添加交互点
场景漫游穿梭的功能已经实现了,现在我们来在全景场景中添加一些交互热点 ✨
,用于实现场景物体标注和鼠标点击交互,比如我们在这个示例中,在客厅中添加了 电视机?
、沙发?
、冰箱❄️
等交互点,我们可以现在创建场景的数组中添加这些交互点的信息 interactivePoints
,以方便批量创建,根据自己的需求我们可以添加一些可选的配置参数,本文中的参数含义分别是:
key
:唯一标识符。value
:显示名称。description
:描述文案。cover
:配图。position
:在三维空间中的位置。
const rooms = [ { name: '客厅', key: 'living-room', map: new URL('@/assets/images/map/map_living_room.jpg', import.meta.url).href, 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), }, // ... ], },
然后在页面上利用 rooms
数组的 interactivePoints
来批量创建交互点的 DOM
节点: