Three.js 进阶之旅:全景漫游-初阶移动相机版
声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
摘要
3D
全景技术可以实现日常生活中的很多功能需求,比如地图的街景全景模式、数字展厅、在线看房、社交媒体的全景图预览、短视频直播平台的全景直播等。Three.js
实现全景功能也是十分方便的,当然了目前已经有很多相关内容的文章,我之前就写过一篇《Three.js 实现3D全景侦探小游戏》。因此本文内容及此专栏下一篇文章讨论的重点不是如何实现 3D
全景图功能,而是如何一步步优雅实现在多个3D全景中穿梭漫游,达到如在真实世界中前进后退的视觉效果。
全景漫游系列文章将分为上下两篇,本篇内容我们先介绍如何通过移动相机的方法来达到场景切换的目的。通过本文的学习,你将学到的知识点包括:在 Three.js
中创建全景图的几种方式、在 3D
全景图中添加交互热点、利用 Tween.js
实现相机切换动画、多个全景图之间的切换等。
效果
本文最终将实现如下的效果,左右控制鼠标旋转屏幕可以预览室内三维全景图,同时全景图内有多个交互热点,它们标识着三维场景内的一些物体,比如沙发 、电视机
等,交互热点会随着场景的旋转而旋转,点击热点
可以弹出交互反馈提示框。
点击屏幕上有其他场景名称的按钮比如 客厅
、卧室
、书房
时,可以从当前场景切换到目标场景全景图,交互热点也会同时切换。
打开以下链接,在线预览效果,大屏访问效果更佳。
本专栏系列代码托管在 Github
仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
原理
我们先来简单总结下在 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);
立方体
在立方体内添加全景图贴图的方式也可以实现三维全景图功能,此时需要对 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 );
环境贴图
使用环境贴图也可以实现全景图功能,像下面这样加载全景图片,然后将它赋值给 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
客户端,可以在客户端内非常方便的在全景图上添加交互热点、实现多个场景的漫游路径等,大家感兴趣的话都可以试试。
工具
全景图生成工具
- 使用球形全景相机拍摄。
- 使用
Blender
等建模软件相机360
度旋转渲染。
全景图编辑工具
下面两个网站提供丰富的三维全景背景照片及将 hdr
图片裁切成上述需要的 6
张贴图的能力,大家可以按自己需要下载和编辑。
HDR全景背景照片下载网站:polyhaven
HDR立方体材质转换工具:HDRI-to-CubeMap
实现
现在,我们使用第一种球体 全景图的方式,来实现示例中介绍的内容。
〇 场景初始化
创建全景图前先做一些常规三维场景准备工作,由于三维全景图功能并不会涉及到新的技术点,因此像下面这样简单实现就可以。
<canvas class="webgl"></canvas>
在文件顶部引入以下资源,其中 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
节点:
<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>
用样式表把交互点设置成自己喜欢的样式 ,需要注意的一点是,交互点
初始的样式中设置了
transform: scale(0, 0)
, 即它的宽高都为 0
,是隐藏看不见的,这样设置的目的是为了实现只有交互点出现在相机可视区域时才显示在场景中,其他转动到相机背面时应该隐藏掉。当交互点被添加 .visible
类时,交互点变为显示状态。本示例中还使用交互点内 .label::before
、.label::after
等伪元素和子元素添加了一些波纹扩散动画及其其他文案信息等。
.point
position: fixed
top: 50%
left: 50%
.label
position: absolute
&::before, &::after
display inline-block
content ''
&::before
animation: bounce-wave 1.5s infinite
&::after
animation: bounce-wave 1.5s -0.4s infinite
.label-tips
height 88px
width 200px
position absolute
&.visible .label
transform: scale(1, 1)
隐藏显示的交互也可以通过
display:none
、visibility:hidden
、及使用js
变量控制元素隐藏显示等方式来实现。
创建完交互点 元素之后,我们还需要在页面重绘方法
tick()
中像下面这样添加一个方法,来将交互点显示在三维场景中,并根据与相机的关系来控制每个交互点的显示与隐藏,原理是使用 THREE.Raycaster
来检测元素是否被遮挡:
const raycaster = new THREE.Raycaster();
const tick = () => {
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 * sizes.width * 0.5;
const translateY = -screenPosition.y * sizes.height * 0.5;
point.element.style.transform = `translateX(${translateX}px) translateY(${translateY}px)`;
}
// ...
};
关于使用
Raycaster
来检测元素是否被遮挡的详细介绍,可以看看我的这篇文章《Three.js 打造缤纷夏日3D梦中情岛》。
⑦ 页面优化和加载进度管理
最后,因为创建多个三维全景图场景需要加载很多张图片,而且全景图的图片一般比较大,我们可以预先加载完所有图片后再进行渲染,本文使用的是自己添加的一个预加载方法,也可以使用像 preload.js
等其他库来预加载图片。除了加载进度显示之外,现实开发场景中应该还有很多个性化的需求,比如可以在点击交互点的时候弹出一个详细弹窗、点击电视的时候开始播放一段视频、点击沙发的时候镜头聚焦放大到沙发、点击开关的时候变为夜间模式……这些交互的原理和本文中的交互点是差不多的 。
总结
本文中主要包含的知识点包括:
- 在
Three.js
中实现全景图的原理和多种实现方式。 - 与全景图相关的生成工具、编辑工具的使用。
- 创建多个全景图并实现多个场景间的漫游穿梭功能。
- 在三维全景图中添加交互热点。
本文到这里就结束了,本文中通过移动相机镜头和控制的方法来实现几个全景图之间漫游穿梭效果还是不错的,但是它的缺点也是很明显的,就是当全景场景数量特别多时,就需要创建非常多的球体,此时计算出每个场景的位置非常困难,并且会造成页面性能耗损问题,因此需要进行优化。下篇文章将会介绍另一种更加优雅的方式来实现全景图之间的漫游功能,过渡动画也会更加流畅丝滑。
想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 。
附录
- [1]. Three.js 打造缤纷夏日3D梦中情岛
- [2]. Three.js 实现炫酷的赛博朋克风格3D数字地球大屏
- [3]. Three.js 实现2022冬奥主题3D趣味页面,含冰墩墩
- [4]. Three.js 实现3D开放世界小游戏:阿狸的多元宇宙
- [5]. 掘金1000粉!使用Three.js实现一个创意纪念页面
...
- 【Three.js 进阶之旅】系列专栏访问
- 更多往期【3D】专栏访问
- 更多往期【前端】专栏访问
参考
- [1]. threejs.org
本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17263717.html
Three.js 进阶之旅:全景漫游-初阶移动相机版的更多相关文章
- Three.js 进阶之旅:新春特典-Rabbit craft go 🐇
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 兔年到了,祝大家身体健,康万事顺利.本文内容作为兔年新春纪念页面,将使用 ...
- three.js全景漫游实践
Hello 小伙伴们,如果觉得本文还不错,记得给个 star , 小伙伴们的 star 是我持续更新的动力!GitHub 地址 简介 全景图分两种 由六张正方形图片组成的SkyBox 一整张的宽高比为 ...
- <Three.js>(第三节)全景漫游
一.实验内容 通过上次实验,了解了Three.js创建场景的基本步骤.这一节,我们将通过Three.js实现全景漫游功能.如下图: 全景图是获取一个3D场景中的不同角度的图片,然后通过拼接.融合实现3 ...
- 打造H5里的“3D全景漫游”秘籍
近来风生水起的VR虚拟现实技术,抽空想起年初完成的“星球计划”项目,总结篇文章与各位分享一下制作基于Html5的3D全景漫游秘籍. QQ物联与深圳市天文台合作,在手Q“发现新设备”-“公共设备”里,连 ...
- 打造自己的3D全景漫游
three.js 示例: 打造H5里的"3D全景漫游"秘籍 - 腾讯ISUX QQ物联星球计划 通过pano2vr直接将鱼眼全景图生成立体空间的六个面:也可通过Photos ...
- 我的Android进阶之旅------>解决Jackson等第三方转换Json的开发包在开启混淆后转换的实体类数据都是null的bug
1.错误描述 今天测试人员提了一个bug,说使用我们的app出现了闪退的bug,后来通过debug断点调试,发现我们的app转换服务器发送过来的json数据后,都是为null.而之前已经提测快一个月的 ...
- 2. web前端开发分享-css,js进阶篇
一,css进阶篇: 等css哪些事儿看了两三遍之后,需要对看过的知识综合应用,这时候需要大量的实践经验, 简单的想法:把qq首页全屏另存为jpg然后通过ps工具切图结合css转换成html,有无从下手 ...
- 【 D3.js 进阶系列 】 进阶总结
进阶系列的文章从去年10月开始写的,晃眼又是4个多月了,想在年前总结一下. 首先恭祝大家新年快乐.今年是羊年吧.前段时间和朋友聊天,聊到十二生肖里为什么没猫,我张口就道:不是因为十二生肖开会的时候猫迟 ...
- web前端开发分享-css,js进阶篇
一,css进阶篇: 等css哪些事儿看了两三遍之后,需要对看过的知识综合应用,这时候需要大量的实践 经验, 简单的想法:把qq首页全屏另存为jpg然后通过ps工具切图结合css转换成html,有无 从 ...
- [置顶] 我的Android进阶之旅------>介绍一款集录制与剪辑为一体的屏幕GIF 动画制作工具 GifCam
由于上一篇文章:我的Android进阶之旅------>Android之动画之Frame Animation实例 中展示的是Frame动画效果,但是之前我是将图片截取下来,不好说明确切的动画过程 ...
随机推荐
- 修改mysql 一张表中某列字段值
UPDATE 表名 SET 字段名 = replace(字段名,'原来值','修改值'): 例: UPDATE pd_purchase SET type_status =replace(type_ ...
- mybatis-plus 3.4.3.1 进行批量 saveOrUpdate
service类通过 SqlHelper.saveOrUpdateBatch 实现通过自定义的 唯一索引 进行 批量保存更新 import com.baomidou.mybatisplus.core. ...
- windows系统下查找开放端口的监听程序
C:\Program Files (x86)\Nmap>nmap 127.0.0.1Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-15 1 ...
- 请求GET和POST的区别
实际上GET和POST他们只有语义上的区别,之所以有我们所谓的区别是由于前端与后端达成的协议. 区别1:get请求不会附带请求体,而post请求有请求体. 区别2: get请求传递的信息量是有限的,适 ...
- 一本通c++约瑟夫问题
#include<bits/stdc++.h>using namespace std;long long m,nn;struct n{ long long da; n *next;};n ...
- Verilog标识符与关键字
Verilog标识符与关键字 1.标识符: Verilog HDL中的标识符是指用来声明数据,变量,端口,例化名等除关键字外的所有名称的组合.如:input a, 这里a就是一个标识符,用来代表一个输 ...
- 荔枝派Licheepi nano裸机移植ZLG_GUI和3D旋转立方体
一:前言 以前申请到了荔枝派zero,在发了两个开箱贴后就放在一边吃灰了.后来又购买了荔枝派nano,刷了几个教程中的系统之后又放到一边吃灰了.虽然有屯板子的习惯,却没有使用板子的能力. 后来,经过断 ...
- java 转换指定文件夹文件编码工具
import java.io.*; public class test { public static void main(String[] args) { printFiles(new File(& ...
- 原生JS及jQuery中事件委托的写法
在绑定节点事件处理程序时遇到的问题: 每个 函数都是对象,都会占用内存:内存中的对象越多,性能就越差. 其次,必须事先指定所有事件处理程 序而导致的 DOM访问次数,会延迟整个页面的交互就绪时间. 采 ...
- Postman请求Https接口与认证
http://t.zoukankan.com/embedded-linux-p-12656769.html