細數實現全景圖VR的幾種方式(panorama/cubemap/eac)

2022-09-01 15:02:19
  1. Three.js系列: 在元宇宙看電影,享受 VR 視覺盛宴
  2. Three.js系列: 造個海洋球池來學習物理引擎
  3. Three.js系列: 遊戲中的第一、三人稱視角
  4. Three.js系列: 數實現全景圖VR的幾種方式(panorama/cubemap/eac)

本篇是 Three.js 系列的第四篇內容,想看其他內容可以看上方☝️,今天就給大家來介紹介紹全景圖相關的知識,我們知道因為最近疫情的影響,大家都沒辦法出門,很多全景的專案火了,比如各個著名的風景區都開放了VR。

除此之外,VR 裝置也是非常的火熱,在我國 2022年上半年,VR市場銷售額突破了 8億元,同比增長了 81%

而在國外呢,截至 2022 年 Q1 ,已經賣出了1480萬臺!

因為我們學習製作 VR 技術就是順勢而為,畢竟雷布斯說過,「站在風口上
,豬都可以飛起來」。

接下來我就談談目前展示主要有幾種形式。目前展示 VR 主要有 3種 主流方式,分別為 建模 、建模 + 全景圖 和 全景圖

建模 建模+全景圖 全景圖
代表作品 VR遊戲 貝殼系列看房 普通雲遊覽、雲遊覽
體驗 極好 中等

我們來實際體驗一下他們的差異

以上為 VR遊戲《僱傭戰士》的體驗,視角切換非常的流暢,且場景非常的大,玩過3D型別遊戲的朋友就能明白。這種場景都是通過建模來完成,利用 blender、3D Max、maya 等建模軟體,再使用Unity、UE 等遊戲開發平臺,各種效果可以說非常的好。

而到了貝殼這種呢,則是通過建模加上全景圖兩種方式結合使用,模型和全景圖是通過線下采集得到,但是對於這種看房頁面,要在 Web 上渲染精細的模型資源消耗是巨大的,因此他們採取了一個折中的方案,就是粗糙模型 + 全景圖,通過模型來補間場景切換的突變感,變化過程中明顯能感受的掉幀的感覺。雖然效果不如純手動建模來的精細,但是總的來說體驗也非常不錯了。

最後這種雲遊覽,則是直接通過兩張全景圖直接切換得到的,這種方式最為簡單,當然效果遠遠前面兩種,但是單張圖片的全景視角比起靜態的圖片而言,也是增加了空間感。

用表格總結起來就是以下:

建模 建模+全景圖 全景圖
代表作品 VR遊戲《僱傭戰士》 貝殼系列看房 普通雲遊覽、雲遊覽
實現難度 很難 簡單
過渡效果 極度真實 一般
模型 Blender、3D Max、maya 帶有光學感測相機 普通360相機

由於全景圖是通過一個個點位拍攝而得到的。因此它無法擁有位置資訊,也就是各個點位的依賴關係,因此當在切換場景的時候,我們無法得到沉浸式的過渡效果;而貝殼則是通過利用了模型的補間來改善過渡;VR遊戲《僱傭戰士》則是純手動建模,因此效果非常好。

今天我們主要講解的就是全景圖模式(因為它比較簡單),當然也不是想象中那麼簡單,只是相比前兩種方式而言,難度是下降了一個坡度,畢竟學習都是從興趣開始的,一開始來個高難度的,簡直就是勸退了。

首先我們先來了解一些前置知識,目前主流全景圖都有什麼格式?

我翻閱總結後目前最常用的大約為以下三種

  • 等距柱狀投影格式(Equirectangular)
  • 等角度立方體貼圖格式(Equi-Angular Cubemap)
  • 立方體貼圖(Cube Map )

等距柱狀投影

也就是最常見的世界地圖的投影方式,做法是將經線和緯線等距地(或有疏密地)投影到一個矩形平面上。

這種格式的優點是比較直觀,並且投影是矩形的。缺點也很明顯,球體的上下兩極投影出來的畫素數很多,而細節內容比較豐富的赤道區域相比來說畫素數就很少,導致還原時清晰度比較糟糕。另外,這種格式的畫面在未渲染的情況下扭曲比較明顯。

立方體貼圖

是另一種全景畫面的儲存格式,做法是將球體上的內容向外投影到一個立方體上,然後展開,而它對比等距柱狀投影的優勢是,在相同解析度下,它的圖片體積更小,約為 等距柱狀投影 的 1/3

等角度立方體貼圖

是谷歌所提出的進一步優化的格式,方法是更改優化投影時的取樣點位置,使得邊角與中心的畫素密度相等。

這樣做的好處就是在相同的源視訊解析度下可以提高細節部分的清晰度。排版如下:

我們簡單總結一下:

等距柱狀投影 立方體貼圖 等角度立方體貼圖
圖源 簡單 一般
技術實現 簡單 簡單 一般
圖片體積 V 1/3 V 1/3 ~ 1/4 V
圖片清晰度 一般 更好

v為基準體積

接下來就到了我們使用 Three.js 來實現以上效果的時刻了。

等距柱狀投影

這種方式實現起來比較簡單。首先我們在 https://www.flickr.com/ 找一張全景圖。

在前面的介紹中我們可以得到 2:1 的等距投影全景圖對應的幾何體為球形,還記得我們在前《造一個海洋球》學過,如何來建立一個球體,沒錯就是 **SphereGeometry**

... 省略場景初始化等程式碼

// 建立一個球體
const geometry = new THREE.SphereGeometry(30, 64, 32);

// 建立貼圖, 並設定為紅色
const material = new THREE.MeshBasicMaterial({ 
	color: "red",
});

// 建立物件

const skyBox = new THREE.Mesh(geometry, material);

// 新增物件到場景中

scene.add(skyBox);

// 設定在遠處觀看
camera.position.z = 100
...

然後我們就得到了一個小紅球:

嗯,現在為止你已經學會了如果建立一個小紅球,接下來還有2個步驟加油!

接下來呢,我們就將我們的2:1的全景圖貼到我們的球體上

const material = new THREE.MeshBasicMaterial({ 
-    //color: "red",
+    map: new THREE.TextureLoader().load('./images/panorama/example.jpg')
});

我們就得到了一個類似地球儀的球體。

現在我們要做的,就是我們不想在遠處觀看這些內容,而是要「身臨其境」!

所以我們需要把相機移動到球體的內部,而不是在遠處觀看

- camera.position.z = 100
+ camera.position.z = 0.01

這個時候我們發現,突然漆黑一篇。

小問題,這是由於在 3d 渲染中,預設物體只會渲染一個面,這也是為了節省效能。當然我們可以也通過讓物體預設只渲染內部,這就需要通過宣告貼圖的法線方向了,過程不是本節課的討論範圍這裡只提供一個思路。幸好 Three.js 給我們提供了一個簡單的方法 THREE.DoubleSide ,通過這個方法,就能讓我們的物體渲染兩個面。這樣我們即使在物體內部也能看到貼圖啦。

const material = new THREE.MeshBasicMaterial({ 
  map: new THREE.TextureLoader().load('./images/panorama/example.jpg'),
+	side: THREE.DoubleSide,
});

現在我們只用了 **SphereGeometry** 球體快速實現了全景的效果。

立方體貼圖

立方體貼圖就和它的名字一樣,我們只需要使用一個立方體就能渲染出來一個全景效果,但是2:1 的全景圖肯定是不能直接使用的,我們首先需要通過 工具來進行轉化,目前有兩種比較方便的方式。

最終我們可以得到以下6張圖

開始來寫我們的程式碼


... 省略場景初始化等程式碼
// 建立立方體
const box = new THREE.BoxGeometry(1, 1, 1);

// 建立貼圖
function getTexturesFromAtlasFile(atlasImgUrl, tilesNum) {
	const textures = [];
	for (let i = 0; i < tilesNum; i++) {
	    textures[i] = new THREE.Texture();
	}
	new THREE.ImageLoader()
	    .load(atlasImgUrl, (image) => {
	        let canvas, context;
	        const tileWidth = image.height;
	        for (let i = 0; i < textures.length; i++) {
	            canvas = document.createElement('canvas');
	            context = canvas.getContext('2d');
	            canvas.height = tileWidth;
	            canvas.width = tileWidth;
	            context.drawImage(image, tileWidth * i, 0, tileWidth, tileWidth, 0, 0, tileWidth, tileWidth);
	            textures[i].image = canvas;
	            textures[i].needsUpdate = true;
	        }
	    });
	return textures;
}

const textures = getTexturesFromAtlasFile( './images/cube/example-cube.jpg', 6 );
const materials = [];

for ( let i = 0; i < 6; i ++ ) {
	materials.push( new THREE.MeshBasicMaterial( { 
	    map: textures[ i ],
	    side: THREE.DoubleSide
	} ) );
}
const skyBox = new THREE.Mesh(box, materials);
scene.add(skyBox);
...

這裡有一個注意點,就是在 Three.js 中如果有多張貼圖,是支援以陣列形式傳入的,例如此例子中,傳入的順序為 「左右上下前後」

此時我們也得到了上方一樣的效果。

等角度立方體貼圖

這裡也和 cubemap 一樣,我們需要通過工具轉化才能得到對應格式的圖片。這裡只需要了 5.x 的 ffmpeg,因為它自帶一個 360 filter ,能夠處理 EAC 的轉化。首先通過以下命令得到一張 eac 的圖。

ffmpeg -i example.jpg -vf v360=input=equirect:output=eac example-eac.jpg

這裡由於 Three.js 預設不支援 EAC 的渲染,因此我們使用了一個 egjs-view360來進行渲染 ,原理為自己手寫一個 shader 來處理 EAC 這種情況,這裡暫時先不展開講解,過程比較枯燥,後續單開一個章節來說明。

使用 egjs-view360 來渲染 EAC 圖,整體比較簡單

...省略依賴庫
<div class="viewer" id="myPanoViewer">
</div>

<script>

    var PanoViewer = eg.view360.PanoViewer;
    var container = document.getElementById("myPanoViewer");
    var panoViewer = new PanoViewer(container, {
        image: "./images/eac/example-eac.jpg",
        projectionType: "cubestrip",
        cubemapConfig: {
            order: "BLFDRU",
            tileConfig: [{ rotation: 0 }, { rotation: 0 }, { rotation: 0 }, { rotation: 0 }, { rotation: -90 }, { rotation: 180 }],
            trim: 3
        }
    });

    PanoControls.init(container, panoViewer);
    PanoControls.showLoading();
</script>


我們最終也能得到以上的結果。

這裡再給一組檔案體積的資料:(所有圖片統一使用了 tinypng 進行了壓縮去除無效資訊)

最終得出了一個這樣的排名:

體驗: EAC > CubeMap > Equirectangular

檔案體積:CubeMap < EAC < Equirectangular

上手難度 :EAC < CubeMap < Equirectangular

所以如果你想要高體驗高畫質,那麼你就選擇 EAC,如果想要頻寬小,那麼就選擇 CubeMap,如果你是個初學者想要快速實現效果,那麼就使用 Equirectangular !

以上所有程式碼均在:https://github.com/hua1995116/Fly-Three.js 中可以找到。

這裡最後補充一個小提示,球形貼圖的一個好處就是天然地可以作為小行星的展示,例如這種特效。

本期我們通過了從VR的發展現狀、VR的幾種實現方式,再到通過 Equirectangular、CubeMap、Equi-Angular Cubemap三種全景圖來實現 VR 進行了講解,希望對你有所幫助,我們下期再見。