天天看點

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

作者 | 甜蝦
實作一個360全景的N種方案

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>           
實作一個360全景的N種方案

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

如下圖,高度為 600px 的元素,距離 z=0 的螢幕 300px,視角與螢幕距離1000px,根據相似三角形的原理,可以計算出該元素在螢幕上的投影高度為 857px,即實際我們看到的元素高度。

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

關于這個屬性的詳細講解可檢視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;
}           
實作一個360全景的N種方案

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

實作一個360全景的N種方案
.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); }           

結果如下圖:

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

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

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

各面繼續向兩側位移

.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); }           
實作一個360全景的N種方案

整個過程可用下圖表示:

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

最終效果:

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

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

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

掃碼看效果:

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

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);           
實作一個360全景的N種方案

将立方體的紋理顔色換成圖檔紋理:

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);           
實作一個360全景的N種方案

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

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

實作一個360全景的N種方案
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);           
實作一個360全景的N種方案

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

skyBox.geometry.scale( 1, 1, - 1 );           
實作一個360全景的N種方案

進一步優化體驗:

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; // 旋轉方向取反,使内部拖拽旋轉方向一緻           
實作一個360全景的N種方案

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',
    ]
  }
);           
實作一個360全景的N種方案

@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,
      }} />
  );
}           
實作一個360全景的N種方案
實作一個360全景的N種方案
實作一個360全景的N種方案

球型投影

以上的實作全部是利用立方體實作的,除此之外,我們還可以使用圓柱體投影的方式,three.js 和 pannellum 都支援這種方式。

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

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'
});           
實作一個360全景的N種方案

頁面和代碼可參考

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全景的N種方案

總結

本文叙述了實作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/
https://pannellum.org/
實作一個360全景的N種方案

繼續閱讀