全景圖的基本原理
全景圖是一種廣角圖。通過全景播放器可以讓觀看者身臨其境地進入到全景圖所記錄的場景中去。比如像是這個。這種看起來很高大上的效果其實背后的原理並不復雜。
通常標准的全景圖是一張2:1的圖像,其背后的實質就是等距圓柱投影。等距圓柱投影是一種將球體上的各個點投影到圓柱體的側面上的一種投影方式,投影完之后再將它展開就是一張2:1的長方形的圖像。比較常見的就是應用在地圖上的投影。

而在對全景圖進行展示之前就需要得到一張這樣的圖像,這種圖像可以自己用普通相機拍攝再自己合成,也可以直接使用專門的全景相機進行拍攝。全景照片的拍攝在網上有比較多的教程,由於這不是攝影分享就不詳細的去說了:P。
在得到了全景圖之后,就是要怎么去展示的問題了。接下來就要說說全景展示的原理。全景展示其實是等距圓柱投影的逆過程,我們要做的就是將我們得到的全景圖,貼圖到一個球體上,熟悉webgl的,可以用它畫一個球體,然后將全景圖作為材質貼到這個球體上進行渲染。由於使用webgl來進行編程的話,需要自己進行比較多的3d運算,所以也可以選擇使用api更加友好的3D庫,如THREE.JS來編程。比如下面的這張全景圖,在球面上進行貼圖。


這時我們看到的還跟預想的全景不一樣,那是因為我們在球的外面,當我們在球的里面時,看到的就是跟一開始的示例一樣的效果了。像是下面的這個示意圖這樣。

用threejs進行編程的話,關鍵的代碼如下:
//新建一個球體 var geometry = new THREE.SphereGeometry( 500, 60, 40 ); //沿x軸進行-1的scale,讓球體的面朝內(因為我們將從球內進行觀看)。 geometry.scale( - 1, 1, 1 ); //載入一張全景圖生成threejs中可以使用的材質 var material = new THREE.MeshBasicMaterial( { map: new THREE.TextureLoader().load( 'panoPic.jpg' ) } ); //將幾何體和材質進行結合。 mesh = new THREE.Mesh( geometry, material );
兼容性
雖然使用webgl可以很容易的就生成一個全景的場景,但是在web里,兼容性似乎是個揮之不去的話題。主要是由於webgl不支持Android5.0以下的機器,所以,用webgl來實現將會將很多用戶排除在外。所以只能尋求更好的解決方案。首先想到的就是css 3D transform 和 2D canvas畫布。在threejs里支持在2D的canvas里進行繪制,本是一個比較好的方案,但是經過測試之后,發現2d畫布來繪制3d的場景,性能上太吃力。所以,也被排除。剩下的就是css變換了。但是,要怎么用css來畫一個一個球體呢?答案顯然是不行的。雖然css不能繪制一個球體,但是css通過3D變換來繪制立方體還是簡單一些的。那么用立方體可不可以實現一樣的效果呢?
球體到立方體
根據全景圖的原理,我們是把視角放在球的中心,通過從球心觀看球面上正式場景在球面上的映像從而產生一種空間中全方位的視覺體驗,同理,對於立方體,應該也可以使用相同的方式來實現。而我們要做,就是把球面上的像素點映射到立方體上。

說了基本的原理,接下來就是進行數學建模了。首先我們建立一個球坐標系,坐標系描述的變量分別為半徑r,豎直方向上的夾角θ,水平方向上的夾角ø,對於球體,我們可以假定
r=1 0 < θ < π -π/4 < ø < 7π/4
這樣我們就可以得到球面上的各個點在直角坐標系中的x,y,z
x= r sin θ cos ø y= r sin θ sin ø z= r cos θ
對於球面到立方體上的投影,我們需要的是角度θ和ø相同時,延長球的半徑r直到和立方體的面相交,假設這個長度是R,由於我們設了半徑r是1所以球面上的點為 (sin θ cos ø, sin θ sin ø, cos θ) 對應的立方體上的點是(Rsin θ cos ø, Rsin θ sin ø, Rcos θ)
如果我們要求x=1這個平面上的點,則
1=Rsin θ cos ø
則可以求出來
R= 1/(sin θ cos ø)
所以在x平面上映射的點就是
(1, tan ø, cot θ / cos ø)
在立方體另外的五個平面上的投影也可以類似地得出。通過上述方法轉換一張全景圖,可以得到以下結果。
離我們想要的效果還有一些差距,圖中似乎多了一些黑色的線。導致這種現象的原因是,由於我們的處理是以像素為單位來進行處理的,通過遍歷球面圖上的每個像素然后投影到立方體上的面來實現。經過這種方式進行投射之后,立方體的面上就會有一些像素被重復設置,而一些地方的像素就會缺失,比如圖中的黑色部分(由於底色的黑色的)。為了解決這個問題,我們可以通過逆向的方法來解決,也就是遍歷立方體上面上的每個點,求得映射到球面上的位置,然后獲取球面上最接近的位置的像素。
得到了立方體上需要的圖之后,就可以用css 3D變換來實現全景圖了。
展示更多信息
單單地進行全景觀看可能還不能滿足我們的需求,也許我們還需要展示更多的信息,而這些信息可能是跟全景中的內容相關的,比如給全景中的物品打標,給全景中的內容添加評論,像是下圖這樣
對於這個實現的關鍵在於,屏幕2D坐標和空間3D坐標之間的相互轉換。第一步需要實現的是記錄,當用戶點擊屏幕,要根據點擊的位置來計算出和空間中的立方體相交的點並記下這個點的位置信息。一些3d庫當中會有一些api來幫助完成這項工作。而在THREEJS中使用的是Raycaster,Raycaster可以生成一條直線,然后可以很方便地得到三維空間中,和這條直線相交的物體和點。由於THREEJS中是以繪制的中心作為原點,而鼠標的點擊位置是以左上角為原點,所以需要進行一下轉換。
//將鼠標點擊事件中的位置信息,轉換到位置中心 var mouse = new THREE.Vector2( ( ev.clientX / _this.wrapper.width() ) * 2 - 1, -( ev.clientY / _this.wrapper.height() ) * 2 + 1 )
然后就可以初始化一個Raycaster獲得需要的內容了
//創建一個Raycaster實例 var raycaster = new THREE.Raycaster() //根據點擊的位置,從鏡頭開始初始化一和鏡頭的屏幕垂直的直線 raycaster.setFromCamera(mouse, _this.camera) //獲得和直線相交的物體 var intersects = raycaster.intersectObjects(_this.scene.children)
這樣就在空間中記錄下了一個目標位置。
有了三維中的位置之后,如果我們想要展示這個位置相關信息,比如打一個標簽,就是需要將空間中的位置還原到屏幕的二維坐標上,然后用傳統的css方法來進行展示就可以了。
//根據上一步中記錄的位置生成一個向量 var vector = new THREE.Vector3(pos.x, pos.y, pos.z) //將這個向量映射到鏡頭的平面上 vector.project(camera) //將位置信息還原成以左上角為原點的位置信息 var screenPos = { left: Math.round(( vector.x + 1 ) * wrapper.width() / 2), top: Math.round(( -vector.y + 1 ) * wrapper.height() / 2) }
作者:阿里聚划算技術團隊
鏈接:https://www.imooc.com/article/14196
來源:慕課網
本文原創發布於慕課網 ,轉載請注明出處,謝謝合作
