開發與維運

實現一個360全景的N種方案

作者 | 甜蝦

image.png
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>

image.png
perspective 指定觀察者與 z=0 平面的距離,使具有三維位置變換的元素產生透視效果(近大遠小原理)。transform-style 指定其為子元素提供2D還是3D的場景。

如下圖,高度為 600px 的元素,距離 z=0 的屏幕 300px,視角與屏幕距離1000px,根據相似三角形的原理,可以計算出該元素在屏幕上的投影高度為 857px,即實際我們看到的元素高度。
image.png
關於這個屬性的詳細講解可查看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;
}

image.png

旋轉各面,使“三對”面互相垂直,達到如下效果

image.png

.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); }

結果如下圖:

image.png

貌似看不出怎麼變換的?我們將以上重合的兩個面的旋轉角度稍微調整下,就可以更直觀地看出效果:

image.png

各面繼續向兩側位移

.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); }

image.png

整個過程可用下圖表示:

640 (2).gif

最終效果:

640 (3).gif

通過調整容器樣式的 perspective 屬性值,將視角放置在立方體中。將每個面的尺寸放大,添加上全景圖切出的6面圖,添加上鼠標事件,便可實現360全景效果。

640 (4).gif

掃碼看效果:

image.png

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);

640 (5).gif

將立方體的紋理顏色換成圖片紋理:

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);

640 (6).gif

將6張全景圖片替換掉上面相同的紋理:

圖片加載的順序是正X(px.jpg),負X(nx.jpg),正Y(py.jpg),負Y(ny.jpg),正Z(pz.jpg)和負Z(nz.jpg),將他們分別賦給6個材質的貼圖,作為立方體skyBox的材質。

image.png

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);

640 (7).gif

因為相機在skyBox的內部,而內部的面不會顯示,所以要將X軸或者Z軸的放大倍數變為負數,這樣才能看到內部,scale.z=-1時相當於將Z軸正向的面移到Z軸負方向上。

skyBox.geometry.scale( 1, 1, - 1 );

640 (8).gif

進一步優化體驗:

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; // 旋轉方向取反,使內部拖拽旋轉方向一致

掃碼看效果:

image.png

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',
    ]
  }
);

掃碼看效果:

image.png

@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,
      }} />
  );
}

掃碼看效果:

image.png

640 (9).gif
640 (10).gif

球型投影

以上的實現全部是利用立方體實現的,除此之外,我們還可以使用圓柱體投影的方式,three.js 和 pannellum 都支持這種方式。
image.png
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'
});

640 (11).gif

頁面和代碼可參考 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,
});

image.png

總結

本文敘述了實現360全景功能4種不同的方案,包括:CSS3、Three.js、pannellum 和 krpano,在基於 webGL 的方案中,介紹了兩種主要的投影方式:立方體投影和球型投影,並給出了 demo 代碼和頁面,推薦了基於 pannellum 開發的360全景容器組件:@ali/rmpi-cube。如有因為,歡迎指正和交流。

參考

  • Front-End Challenge Accepted: CSS 3D Cube

https://www.smashingmagazine.com/2016/07/front-end-challenge-accepted-css-3d-cube/

  • Intro to CSS 3D transforms

https://3dtransforms.desandro.com/

  • css3系列之詳解perspective

https://www.cnblogs.com/yanggeng/p/11285856.html

  • threejs

https://threejs.org/

  • pannellum

https://pannellum.org/


image.png

Leave a Reply

Your email address will not be published. Required fields are marked *