作者 | 甜蝦

360全景浏覽是一種成本效益很高的虛拟現實解決方案,給人一種全新的浏覽體驗,讓你足不出戶就能身臨其境地感受到現場的環境。該技術被廣泛地應用在房産、酒店、家居等領域。在網際網路訂房已經普及的時代,在網站上用全景展示酒店飯店的各種餐飲和住宿設施,是吸引顧客的好辦法。利用網絡,遠端虛拟浏覽飯店的外型、大廳、客房、會議廳等各項服務場所,展現飯店舒适的環境,給客戶以實在感受,促進客戶預定客房。在酒店大堂提供客房的全景展示,再也不用麻煩客戶在各個房間會場穿梭,就能觀看各房間的真實場景,更友善客戶确認和挑選客房,進而提高效率,使用者體驗更勝一籌。
下面我們使用三種方法讨論一個360全景的實作。
CSS3
利用 CSS 中的變換、旋轉等屬性就可以實作一個360全景。實作的基本思路如下:
- 利用 CSS3 做出一個 3D 立方體。
- 在立方體的6個面設定目标圖檔(利用全景工具導出的圖檔,一共有6張)。
- 使用perspective、translateZ、transform-style: preserve-3d 等屬性改變視圖的大小。
- 添加觸摸事件改變translateX、translateY的角度數即可實作一個基本的全景圖效果。
下面我們嘗試使用 div 做出一個 3D 立方體:
寫出6個面:
<div class="scene">
<div class="cube">
<div class="cube__face cube__face--front">front</div>
<div class="cube__face cube__face--back">back</div>
<div class="cube__face cube__face--right">right</div>
<div class="cube__face cube__face--left">left</div>
<div class="cube__face cube__face--top">top</div>
<div class="cube__face cube__face--bottom">bottom</div>
</div>
</div>
perspective 指定觀察者與 z=0 平面的距離,使具有三維位置變換的元素産生透視效果(近大遠小原理)。transform-style 指定其為子元素提供2D還是3D的場景。
如下圖,高度為 600px 的元素,距離 z=0 的螢幕 300px,視角與螢幕距離1000px,根據相似三角形的原理,可以計算出該元素在螢幕上的投影高度為 857px,即實際我們看到的元素高度。
關于這個屬性的詳細講解可檢視css3系列之詳解perspective。
寫出基本定位
.scene {
width: 200px;
height: 200px;
perspective: 600px;
}
.cube {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
}
.cube__face {
position: absolute;
width: 200px;
height: 200px;
}
旋轉各面,使“三對”面互相垂直,達到如下效果
.cube__face--front { transform: rotateY( 0deg); }
.cube__face--right { transform: rotateY( 90deg); }
.cube__face--back { transform: rotateY(180deg); }
.cube__face--left { transform: rotateY(-90deg); }
.cube__face--top { transform: rotateX( 90deg); }
.cube__face--bottom { transform: rotateX(-90deg); }
結果如下圖:
貌似看不出怎麼變換的?我們将以上重合的兩個面的旋轉角度稍微調整下,就可以更直覺地看出效果:
各面繼續向兩側位移
.cube__face--front { transform: rotateY( 0deg) translateZ(100px); }
.cube__face--right { transform: rotateY( 90deg) translateZ(100px); }
.cube__face--back { transform: rotateY(180deg) translateZ(100px); }
.cube__face--left { transform: rotateY(-90deg) translateZ(100px); }
.cube__face--top { transform: rotateX( 90deg) translateZ(100px); }
.cube__face--bottom { transform: rotateX(-90deg) translateZ(100px); }
整個過程可用下圖表示:
最終效果:
通過調整容器樣式的 perspective 屬性值,将視角放置在立方體中。将每個面的尺寸放大,添加上全景圖切出的6面圖,添加上滑鼠事件,便可實作360全景效果。
掃碼看效果:
Three.js
Three.js 是一款運作在浏覽器中的 webGL 引擎,我們可以用它建立各種三維場景,包括了攝影機、光影、材質等各種對象,利用 Three.js 我們可以輕松地實作各種想要的幾何體。
上面的方案中,我們用 CSS3 “拼”出了一個立方體,而webGL中立方體等各種幾何體是最常見的。我們可以利用 Threejs 中的立方體實作全景圖功能,把立方體當成天空盒子,将無縫銜接的圖檔貼上,看起來就像在一個場景中,相機一般放置在中央,隻要離邊緣足夠遠就看不出是立方體,但如果超出邊界就能看到他們。
下面是簡單的實作過程:
初始化一個立方體幾何,給材料上色,便得到了一個立方體:
const geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial({ color: 0x156289 });
const skyBox = new THREE.Mesh(geometry, material);
将立方體的紋理顔色換成圖檔紋理:
const texture = new THREE.TextureLoader().load('textures/crate.gif');
const geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { map: texture } );
const skyBox = new THREE.Mesh(geometry, material);
将6張全景圖檔替換掉上面相同的紋理:
圖檔加載的順序是正X(px.jpg),負X(nx.jpg),正Y(py.jpg),負Y(ny.jpg),正Z(pz.jpg)和負Z(nz.jpg),将他們分别賦給6個材質的貼圖,作為立方體skyBox的材質。
const geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
const textures = [
'https://img.alicdn.com/imgextra/i3/O1CN01LsO1Bk20QbKpFTUQr_!!6000000006844-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i3/O1CN01uTWCLc1XOCOuA92H0_!!6000000002913-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i4/O1CN016lU3YJ1JdrJuFTcWt_!!6000000001052-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i2/O1CN01nYe2Mn1ohkmBVyKpp_!!6000000005257-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i4/O1CN014TNffn1nlaTfA98Fg_!!6000000005130-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i1/O1CN01sS5m781ya6JgLSaVk_!!6000000006594-0-tps-1500-1500.jpg',
];
const materials = [];
const textureLoader = new THREE.TextureLoader();
for (let i = 0; i < 6; i ++ ) {
materials.push( new THREE.MeshBasicMaterial({ map: textureLoader.load(textures[i]) }));
}
const skyBox = new THREE.Mesh(geometry, materials);
因為相機在skyBox的内部,而内部的面不會顯示,是以要将X軸或者Z軸的放大倍數變為負數,這樣才能看到内部,scale.z=-1時相當于将Z軸正向的面移到Z軸負方向上。
skyBox.geometry.scale( 1, 1, - 1 );
進一步優化體驗:
camera.position.z = 0.01; // 将相機放在裡面
const controls = new OrbitControls( camera, renderer.domElement );
controls.enableZoom = false; // 禁用放大
controls.enablePan = false; // 禁用雙指縮放
controls.enableDamping = true; // 開啟阻尼效果
controls.rotateSpeed = -0.25; // 旋轉方向取反,使内部拖拽旋轉方向一緻
pannellum
pannellum 是一個輕量級、免費、開源的基于 webGL 的全景播放器。支援多種投影方式、支援熱點、支援漫遊、視訊等,且文檔清晰明了、簡單易用、API 比較豐富,易于功能擴充。
使用:
pannellum.viewer(
'container',
{
type: 'cubemap',
cubeMap: [
'https://img.alicdn.com/imgextra/i4/O1CN014TNffn1nlaTfA98Fg_!!6000000005130-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i3/O1CN01LsO1Bk20QbKpFTUQr_!!6000000006844-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i1/O1CN01sS5m781ya6JgLSaVk_!!6000000006594-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i3/O1CN01uTWCLc1XOCOuA92H0_!!6000000002913-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i4/O1CN016lU3YJ1JdrJuFTcWt_!!6000000001052-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i2/O1CN01nYe2Mn1ohkmBVyKpp_!!6000000005257-0-tps-1500-1500.jpg',
]
}
);
@ali/rmpi-cube
@ali/rmpi-cube 是我們基于 pannellum 開發的一個360全景容器元件,支援了立方體投影和球型投影兩種方式、支援 pannellum 的所有配置、支援場景切換、陀螺儀效果及添加熱點(開發中)等。目前該元件已應用在飛豬酒店詳情中。
import React, { useEffect, useState } from 'react';
import Cube from '@ali/rmpi-cube';
export default function CubeDemo() {
const [scenes, setScenes] = useState([]);
useEffect(() => {
// 模拟異步請求
setTimeout(() => {
setScenes(
[
{
preview: 'https://img.alicdn.com/imgextra/i1/O1CN01dVOIEe1IhEcaIPw2z_!!6000000000924-0-tps-100-100.jpg',
title: '客廳',
// type: 'equirectangular' 時需要
// 或 panorama: 'https://img.alicdn.com/imgextra/i4/6000000007276/O1CN01Hp5gIf23cSOvzzA9k_!!6000000007276-0-hotel.jpg',
// type: 'cubemap' 時需要
cubeMap: [
// 順序是:前、右、後、左、上、下
'https://gw.alicdn.com/imgextra/i3/O1CN01550SRA1JcwWgs0sIj_!!6000000001050-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i4/O1CN01e796bV1P2CRfCQkrA_!!6000000001782-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i4/O1CN01GcW84X29SHK4oJlWc_!!6000000008066-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i2/O1CN01ZHLck11GX2ZgBHA4o_!!6000000000631-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i2/O1CN019c9xKu1ig1aC7pWPk_!!6000000004441-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i4/O1CN01XfaKOu1kzNYODz7HD_!!6000000004754-0-tps-1500-1500.jpg'
]
},
{
preview: 'https://img.alicdn.com/imgextra/i1/O1CN01KU3hrj1uJNO2OdyaC_!!6000000006016-0-tps-100-100.jpg',
// 或 panorama: 'https://img.alicdn.com/imgextra/i4/6000000004110/O1CN01lKLSsP1gEQSNAMIsJ_!!6000000004110-0-hotel.jpg',
title: '書房',
cubeMap: [
'https://img.alicdn.com/imgextra/i1/O1CN01fWDIfB1bWgC3NnVVa_!!6000000003473-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i2/O1CN01xt97cb1YMeg4BOCbI_!!6000000003045-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i1/O1CN01xKTq1u1DR8cdeMeYt_!!6000000000212-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i3/O1CN01Zko8Qy1p1uCLUYBji_!!6000000005301-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i3/O1CN01k3AVvK28W71UNWXW7_!!6000000007939-0-tps-1500-1500.jpg',
'https://img.alicdn.com/imgextra/i1/O1CN015MBT6P1N8x3J83Fqo_!!6000000001526-0-tps-1500-1500.jpg'
]
}
......
]
);
}, 1000);
}, []);
return (
<Cube
container="viwer"
config={{
type: 'cubemap', // or 'equirectangular'
scenes,
}} />
);
}
球型投影
以上的實作全部是利用立方體實作的,除此之外,我們還可以使用圓柱體投影的方式,three.js 和 pannellum 都支援這種方式。
three.js 中:
const geometry = new THREE.SphereBufferGeometry( 300, 60, 40 ); // 初始化一個球體
geometry.scale( - 1, 1, 1 ); // 翻轉x軸,将所有面朝向裡
const texture = new THREE.TextureLoader().load( 'https://img.alicdn.com/imgextra/i2/6000000004217/O1CN01djW9bE1h1QprTMP5d_!!6000000004217-0-hotel.jpg' ); // 加載全景紋理
const material = new THREE.MeshBasicMaterial( { map: texture } );
mesh = new THREE.Mesh( geometry, material );
pannellum 中:
pannellum.viewer('panorama', {
type: 'equirectangular',
panorama: 'https://img.alicdn.com/imgextra/i2/6000000004217/O1CN01djW9bE1h1QprTMP5d_!!6000000004217-0-hotel.jpg'
});
頁面和代碼可參考
https://threejs.org/examples/#webgl_panorama_equirectangular球型全景和立方體全景兩種方式比較起來各有自己的特點,比如:
- 球型全景圖更貼近人眼的構模組化式,隻用一張圖,但圖檔的尺寸可能會很大
- 球型全景圖,從赤道到兩極,橫向拉伸不斷加劇,導緻南北極存在扭曲的情況
- 立方體相容性更好,還可以用css3 實作
- 從模型上來說,球型是由非常多的三角面拼接出來的,比立方體更複雜,是以立方體有更高的性能
飛豬酒店詳情中,服務端對這兩種方式都提供了圖檔資源,但在使用球型全景的方式時少數酒店上傳的圖檔過大,導緻部分手機上元件加載資源出錯,是以最終決定采用立方體的方式。
krpano
krpano 是一款較專業的全景引擎平台。目前市場中較大的全景平台大部分都用了 krpano 。但由于它用法較複雜,有一定的學習成本,需要掌握其使用的 XML 的各種配置、功能較多較重且是收費的,對于酒店詳情的簡單的業務場景來說有點重了,是以權衡之後沒有選擇它,而是選擇了相對輕量的 pannellum。
<div id="container"></div>
embedpano({
xml: 'https://raw.githubusercontent.com/xiaotianxia/three.js-learning/gh-pages/examples/config.xml',
target: 'container',
html5: 'auto',
mobilescale: 1.0,
});
總結
本文叙述了實作360全景功能4種不同的方案,包括:CSS3、Three.js、pannellum 和 krpano,在基于 webGL 的方案中,介紹了兩種主要的投影方式:立方體投影和球型投影,并給出了 demo 代碼和頁面,推薦了基于 pannellum 開發的360全景容器元件:@ali/rmpi-cube。如有因為,歡迎指正和交流。
參考
- Front-End Challenge Accepted: CSS 3D Cube
- Intro to CSS 3D transforms
- css3系列之詳解perspective
- threejs