
宣告:本文涉及圖文和模型素材僅用于個人學習、研究和欣賞,請勿二次修改、非法傳播、轉載、出版、商用、及進行其他獲利行為,
摘要
3D 全景技術可以實作日常生活中的很多功能需求,比如地圖的街景全景模式、數字展廳、在線看房、社交媒體的全景圖預覽、短視頻直播平臺的全景直播等,Three.js 實作全景功能也是十分方便的,當然了目前已經有很多相關內容的文章,我之前就寫過一篇《Three.js 實作3D全景偵探小游戲》,因此本文內容及此專欄下一篇文章討論的重點不是如何實作 3D 全景圖功能,而是如何一步步優雅實作在多個3D全景中穿梭漫游,達到如在真實世界中前進后退的視覺效果,
全景漫游系列文章將分為上下兩篇,本篇內容我們先介紹如何通過移動相機的方法來達到場景切換的目的,通過本文的學習,你將學到的知識點包括:在 Three.js 中創建全景圖的幾種方式、在 3D 全景圖中添加互動熱點、利用 Tween.js 實作相機切換影片、多個全景圖之間的切換等,
效果
本文最終將實作如下的效果,左右控制滑鼠旋轉螢屏可以預覽室內三維全景圖,同時全景圖內有多個互動熱點,它們標識著三維場景內的一些物體,比如沙發 ?? 、電視機 ?? 等,互動熱點會隨著場景的旋轉而旋轉,點擊熱點 ?? 可以彈出互動反饋提示框,

點擊螢屏上有其他場景名稱的按鈕比如 客廳、臥室、書房 時,可以從當前場景切換到目標場景全景圖,互動熱點也會同時切換,

打開以下鏈接,在線預覽效果,大屏訪問效果更佳,
?????在線預覽地址:https://dragonir.github.io/panorama-basic/
本專欄系列代碼托管在 Github 倉庫【threejs-odessey】,后續所有目錄也都將在此倉庫中更新,
??代碼倉庫地址:[email protected]: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

實作
現在,我們使用第一種球體 ? 全景圖的方式,來實作示例中介紹的內容,
〇 場景初始化
創建全景圖前先做一些常規三維場景準備作業,由于三維全景圖功能并不會涉及到新的技術點,因此像下面這樣簡單實作就可以,
<canvas ></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
v-for="(point, index) in interactivePoints"
:key="index"
:
@click="handleReactivePointClick(point)"
v-show="point.room === data.currentRoom"
>
<div :>
<label >
<div >
<i
:style="{
background: `url(${point.cover}) no-repeat center`,
'background-size': 'contain',
}"
></i>
</div>
<div >
<p >{{ point.value }}</p>
<p >{{ 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 等其他庫來預加載圖片,除了加載進度顯示之外,現實開發場景中應該還有很多個性化的需求,比如可以在點擊互動點的時候彈出一個詳細彈窗、點擊電視的時候開始播放一段視頻、點擊沙發的時候鏡頭聚焦放大到沙發、點擊開關的時候變為夜間模式……這些互動的原理和本文中的互動點是差不多的 ??,

??原始碼地址: https://github.com/dragonir/threejs-odessey
總結
本文中主要包含的知識點包括:
- 在
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/548372.html
標籤:JavaScript
上一篇:Microsoft Flow | 微信 | LDP 整合開發
下一篇:vue組件化開發---插槽的使用
