我們知道3D的表現形式即讓我們透過平面可從不同角度看到真實物體的展示效果。
在電腦世界裡,3D世界是由點組成,兩個點能夠組成一條直線,三個不在一條直線上的點就能夠組成一個三角形面,無數三角形面就能夠組成各種形狀的物體,如下圖。
Three中模型解析器的原理是將頂點數組將模型的頂點用數組儲存起來,再利用three中的face函數取得定點數組中的三個或四個頂點的索引構成空間平面。如此反复,模型就被完整地建構出來了。
於是,越複雜的物體就需要越多的網面拼接。而css中是不存在根據座標建立空間平面的能力的。
(插個題外話,其實css有一個屬性與座標有關,那就是clip-path。這個屬性的特性賦予了css3一定的建模能力。實作方法可參考這篇文章 純clip-path打造的3D模型渲染器)
CSS3實現3D全景
。上篇文章介紹了Web3D的一些表現形式,這裡著重談談怎麼以CSS3實現3D全景。以下將探討Three實現全景的方案,因為WebGL門檻和學習成本還是比較高的,不適用於快速開發。造物節的CSS3d全景已有文章對其進行了技術探秘,但都未深入談及具體實現方式。
要清楚理解實作方式,必須對CSS3的transform、perspective有一定的認識。
原理方面的東西我就不深入講了,大家可以先看看這篇文章,對CSS3D有一個大致的概念。
玩轉 CSS 3D - 原理篇
CSS全景可透過建立柱形或立方體再透過貼圖方式實現。也許會有人問,球體行不行?實際上是不行的,球體模型由無數個極小的平面拼接構成連貫曲面,而CSS缺乏使平面扭曲的屬性。球體模型我們可以用上文提過的Clip-3d建造出,但是,貼圖問題就解決不了了。
天空盒子
相信很多打造過或有了解過3d全景的同行們都知道這個概念。實際上Skybox就是一個立方體,透過給六個面貼上不同的,邊緣可以無縫貼合的圖片,再將視角伸入盒子內部。可以想像成我們自己站入了一個巨型立方體盒子內部,移動視角便能看到不同的場景。
1、貼圖
來看一張天空盒子的貼圖,剪頭指向的邊緣代表需要無縫貼合的邊。
從上圖可以看出只要相互貼合的兩個面上的影像能夠無縫拼接,那麼再透過對各個面進行一定的旋轉變換,天空盒子就能被打造出來了。
那麼問題來了,要怎麼去拍攝製作這樣的圖片呢?這就需要經過一些專業軟體了,像是pano2vr,max等。其實,需要用到這些專業工具打造的全景對畫質和拼合度的要求都非常高了,而單純依靠CSS3中的變化給不了它們很好的體驗。
但我們今天討論的是某些營運活動H5打造的全景,此全景不一定真實存在,或者是和真實場景有一定的比例差距。例如星空、海底。對於這類貼合度可人為改變的全景圖的打造,我們可以採用現有的高清圖片,再經由PS轉換成六面全景圖。
貼一篇文章 Create a Skybox From Photos
其實主要思想是
在一張大圖上勾勒出六個面的選取>
選擇大圖中某個面的相鄰面將其旋轉到需要拼合的盒子的某面上,使他們完美貼合>
得到最合理的六面貼圖後,觀察有無創造出新的邊緣,透過蒙版等工具使他們自然融合。
2、構造貼圖完成就可以創建立方體了。首先將創建好的六個面切割出來,以front、back、left、right…命名標記位置。
.sence { -webkit-perspective: 1000px; } .cube { width: 500px; height: 500px; margin: 100px auto; transform-style: preserve-3d; } .cube img { width: 130px; height: 130px; position: absolute; } .cube img:nth-child(1) { } .cube img:nth-child(2) { transform: rotateY(180deg); } .cube img:nth-child(3) { transform: rotateY(90deg); } .cube img:nth-child(4) { transform: rotateY(-90deg); } .cube img:nth-child(5) { transform: rotateX(90deg); } .cube img:nth-child(6) { transform: rotateX(-90deg); }
<div class="sence"> <div class="cube"> <img src="img/skybox/front.jpg" alt="" /> <img src="img/skybox/back.jpg" alt="" /> <img src="img/skybox/left.jpg" alt="" /> <img src="img/skybox/right.jpg" alt="" /> <img src="img/skybox/top.jpg" alt="" /> <img src="img/skybox/bottom.jpg" alt="" /> </div> </div>
準備好6個面,載入貼圖。透過旋轉,使得每個面旋轉到相印的位置。如左邊的面由原本面朝我們的圖片繞Y軸逆時針旋轉90°得到。 (注意Y軸逆時針旋轉是正數)
此時會得到下圖這樣的效果:
但是由於每個面的旋轉中心都在其正中位置,因此還不能形成正方體。於是我們需要讓每個面產生一定的位移。
貼一張座標系圖以助於大家理解。
現在首先讓front位移到應該到的位置,由於全景圖的鏡頭在立方體內部,因此,可以想像一下,我們需要將圖片往後移動。移動距離很明顯為立方體邊長的一半。這裡是65px。得到下圖結果。
.cube img:nth-child(1) { transform: translateZ(-65px); }
照這樣看,是不是back位移為translateZ(65px),left為translateX(-65px),top translateY(-65px)呢?但結果不是我們想要的。
重新看回上文空間座標系的那張貼圖,我們會發現,平面旋轉後,其對應的三個軸的位置也改變了。如圖片繞Y旋轉後,Z軸指向為螢幕的水平方向。繞X旋轉後,Z軸指向垂直方向。因此我們很容易發現,其實要將貼面移到正確的位置,都只需要讓他們translateZ(-width/2px)就可以了。
為了讓大家容易理解,我這裡設定了一個較大的perspective。想要得到全景的效果,我們將鏡頭拉近讓它進入到box裡面就可以了。
接下來綁定手勢,就可以讓它動起來啦。
部分代碼:
viewer.on('touchstart', function(e) { x1 = e.targetTouches[0].pageX; - $(this).offset().left; y1 = e.targetTouches[0].pageY; - $(this).offset().top; }); viewer.on('touchmove',function(){ var dist_x = x2 - x1, dist_y = y2 - y1, deg_x = Math.atan2(dist_y, perspective) / Math.PI * 180, deg_y = -Math.atan2(dist_x, perspective) / Math.PI * 180, i, c_x_deg += deg_x; c_y_deg += deg_y; cube.css('transform', 'rotateX(' + deg_x + 'deg) rotateY(' + deg_y + 'deg)'); })
Math.atan2(y,x) 方法:得到從 x 軸到點 (x,y) 之間的角度。對於空間左邊係比較難理解,大家可以想像成一張以空間Z軸為Y軸的平面繞X軸正方向旋轉的角度即為cube繞空間Y軸旋轉的角度。
柱形
柱形全景也不算複雜。關於圓柱形的打造方法,大家可以參考下這篇文章CSS3 3D transforms系列教學-3D旋轉木馬
有了這個基礎,我們可以寫一段函數快速構造柱形全景。
先來看下頁面結構
<style> body { height: 100%; overflow: hidden; } .scene { width: 100%; height: 1170px; transform: translateX(-50%) translateY(-50%); top: 50%; left: 50%; position: absolute; } .cube { transform-style: preserve-3d; height: 100%; width: 100%; margin: 0px auto; } .cube_bg { transform-style: preserve-3d; height: 100%; width: 128px; margin: 0px auto; } .cube_bg div { height: 100%; /* 这里为圆柱形的每个面都设定了同样的背景图 那么在建造柱形时不再需要手动切图 */ background-image: url("img/zao/zao.png"); background-repeat: no-repeat; position: absolute; top: 0; } </style> <body> <div class="scene"> <div class="cube"> <div class="cube_bg"> <!-- 这里是柱形全景背景贴图 --> </div> <div class="cube_item"> <!-- 这里是柱形全景中的小元件 --> </div> </div> </div> </body>
function creCylinder(lenZ,pieceWid,angle,slice){ /* pieceWid 表示单个柱形块状宽度 angle表示柱形内角 slice表示有多少个面拼接 slice越多,拼合的面越接近曲面 */ var l = pieceWid*slice; // 画布全长 var ag = angle/slice // 旋转角度 var html = ''; /* 设置每个面的旋转角度和位移 因为要分割成多个面,所以应该为每个面的背景图设置不同的`background-position` */ for(var i=0,len=slice;i<len;i++){ html+='<div style="transform: rotateY(-'+ag*i+'deg) '+ 'translateZ('+lenZ+'px);'+ 'width:'+(pieceWid)+'px;'+ 'background-position: -'+(i*pieceWid)+'px 0;'+ 'background-size: '+(l)+'px 100%;"></div>'; } return html; } function renderPano(pieceWid,angle,slice){ var vw = $(window).width(); var RADIAN = 0.017453293; // 弧度制 将角度转成弧度 var innerAngle = angle/(2*slice); //内角,用来计算translateZ // 这里的原理和上文旋转木马链接一致 var lenZ = -(pieceWid/2)*Math.tan((90-innerAngle)*RADIAN); /* 因为默认是由画布的最左端开始旋转 所以处于我们面前的是画布的最左端和最右端及其连接处 要想画布中央显示再我们面前,这里需要给cube_bg加上一定的绕Y旋转角度 */ var rotate = ((angle/slice)*(slice-1))/2, perspective = -lenZ-5; var cube_bg = $('.cube_bg'), scene = $('.scene'); var cylinder = creCylinder(lenZ,pieceWid,angle,slice); cube_bg.html(cylinder).css('transform','rotateY('+rotate+'deg)'); scence.css('-webkit-perspective',perspective+'px'); //最后调用一下 renderPano(128,360,20);
這裡解釋一下perspective為什麼要設為 -lenZ-5
看一張圖,上面的lenZ即translateZ值,為負值。
perspective為鏡頭到螢幕的距離,因為此時鏡頭在柱體內部,因此無法看到柱體後面的影像。
當perspective值為-lenZ值時,正好柱體back面能與鏡頭在同一平面上,為了避免它有一定的機率遮擋鏡頭,我們可以將鏡頭拉近一些。便設成了-lenZ-5。這時候就能確保鏡頭處於柱體內部,同時也能更廣角度地觀察到柱體全景。
大家可以複製程式碼體驗一下。這裡的背景圖我選用的是自己拼合成的造物節背景圖。
優劣勢對比
相信大家也有體會,天空盒製造起來會相對的簡單,並且天空和地面都能被考慮進去。但是由於面面間的貼合角度太大,若物體正好處於相互貼合的兩個面,會給人一種被攔腰折斷的感覺。而長條圖對這種情況有了比較好的解決,但是天空和地面的貼圖就比較困難了,一般情況下只能透過給scene添加背景圖片模擬。