JS 實現全景圖預覽

燦兒哈擦蘇發表於2019-07-03

WebGL(全稱 Web Graphics Library)是一種 3D 繪圖協議,這種繪圖技術標準允許把 JavaScript 和 OpenGL ES 2.0 結合在一起,通過增加 OpenGL ES 2.0 的一個 JavaScript 繫結,WebGL 可以為 HTML5 Canvas 提供硬體 3D 加速渲染,這樣 Web 開發人員就可以藉助系統顯示卡來在瀏覽器裡更流暢地展示 3D 場景和模型了,還能建立複雜的導航和資料視覺化。Three.js 是一款開源的主流 3D 繪圖 JS 引擎,它就像 jQuery 簡化了 HTML DOM 操作一樣,可以簡化 WebGL 程式設計。

一、Three.js 座標介紹

三維空間中主要有兩種幾何變換,一種是位置的變換,位置變換和二維空間的是一樣的,座標值進行增減就可。另一種就是旋轉變換,二維空間的旋轉可以看作是圍繞點的旋轉,只能基於平面上某個點為圓心進行旋轉,而三維空間的旋轉則是圍繞一條線旋轉的,有多個方向。Three.js 是自持旋轉矩陣,尤拉角,四元數這三種旋轉方式。

對於三維轉換空間的座標系,我的理解就是可以分為:世界座標系、螢幕座標系。世界座標系就是空間座標系,螢幕座標系就是我們看到的平面上的座標系,其實應該有個中間過度的座標系,就是標準裝置座標系 [-1,1],通過現有的資料處理函式來進行轉換,這個只是個人理解,可能有偏差。下面就介紹如何進行世界座標系和螢幕座標系的轉換。

螢幕座標轉世界座標:

螢幕座標是從左上角為原點,這裡全景展示裝置座標是以畫布的中心為原點,需要處理一下,得到相機座標。

    function convertTo3DCoordinate(clientX,clientY){
        var mv = new THREE.Vector3(
            (clientX / window.innerWidth) * 2 - 1,
            -(clientY / window.innerHeight) * 2 + 1,
            0.5 );  //0.5可以改變,改變後獲得的threejs座標的x,y,z數值上會改變,但是差值上不會改變
        mv.unproject(this.camera);   //用相機反投影該向量  這個地方我的理解就是裝置座標[-1,1]轉化為世界座標,這個函式實現的是先將裝置座標轉換為齊次座標,之後與相機的MP矩陣的逆矩陣相乘。有興趣的可以先看下線性代數。
        return mv;
    }
複製程式碼

世界座標轉螢幕座標:

    function convertTo2DCoordinate() {
        //獲取網格模型boxMesh的世界座標
        var worldVector = new THREE.Vector3(
            boxMesh.position.x,
            boxMesh.position.y,
            boxMesh.position.z
            );
        var standardVector = worldVector.project(camera);//用相機投影該向量,世界座標轉標準裝置座標
        var a = window.innerWidth / 2;
        var b = window.innerHeight / 2;
        var param = {}
        param.x = Math.round(standardVector.x * a + a);//標準裝置座標轉螢幕座標
        param.y = Math.round(-standardVector.y * b + b);//標準裝置座標轉螢幕座標
        return param
    }
    其中相機投影和反投影的原始碼為下:
    project: function () {
        var matrix = new Matrix4();
            return function project( camera ) {
            matrix.multiplyMatrices( camera.projectionMatrix, matrix.getInverse( camera.matrixWorld ) );
            return this.applyMatrix4( matrix );
        };
    }(),
    unproject: function () {
        var matrix = new Matrix4();
        return function unproject( camera ) {
            matrix.multiplyMatrices( camera.matrixWorld, matrix.getInverse( camera.projectionMatrix ) );
            return this.applyMatrix4( matrix );
        };
    }(),
複製程式碼

經緯度轉化為空間座標:

    //經、緯度   球體半徑
    function lgltToxyz(longitude,latitude,radius){
            //返回角度轉換成弧度之後的值
        var lg = degToRad(longitude) , lt = degToRad(latitude);
        var y = radius * Math.sin(lt);
        var temp = radius * Math.cos(lt);
        var x = temp * Math.sin(lg);
        var z = temp * Math.cos(lg);
        return {x:x , y:y ,z:z}
    }
複製程式碼

二、全景圖展示的原理

全景圖是一種廣角圖,它的原理是等距圓柱投影,引用百科的解釋:

等距圓柱投影,又稱方格投影,是假想球面與圓筒面相切於赤道,赤道為沒有變形的線。經緯線網格,同一般正軸圓柱投影,經緯線投影成兩組相互垂直的平行直線。其特性是:保持經距和緯距相等,經緯線成正方形網格;沿經線方向無長度變形;角度和麵積等變形線與緯線平行,變形值由赤道向高緯逐漸增大。該投影適合於低緯地區製圖。

說白了就是將一個球體上的所有的點,全部投影到一個圓柱體的側面上去,圓柱側面展開圖上包含了球體上所有的畫素點,所以,一張標準的全景圖的長款比例為 2:1。

現在明白了全景圖的形成過程,那麼我們要做的就是展示全景圖,怎麼辦,就是進行圓柱側面投影的逆運算,將這個過程反轉一下!所以我們就可以運用 WebGL 繪製一個球體,然後將全景圖直接貼在球體上就可以了,當然,需要進行對映的處理過程。

貼圖的過程:

    //新建一個球體 (球體半徑 /水平方向上分段數/和垂直方向上分段數)
    //這裡分割數就腦補一下吧   想想西瓜上的條紋,水平方向上再腦補上就出來了這個概念
     var geometry = new THREE.SphereGeometry( 500, 100, 100 );
    //沿x軸進行-1的scale,讓球體的面朝內(因為我們將從球內進行觀看)。 
    geometry.scale( - 1, 1, 1 ); 
    //載入圖片, 新建材質
    var material = new THREE.MeshBasicMaterial( {
    //設定紋理貼圖
     map: new THREE.TextureLoader().load( '1.jpg' )
      } ); 
    //將幾何體和材質進行結合。 
    mesh = new THREE.Mesh( geometry, material );
    //場景新增進去
    scene.add( mesh )
到這裡,再設定下相機為球體的中心點

    var camera;
        function initCamera() {
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
            camera.target = new THREE.Vector3( 0, 100, 0 );
        }
複製程式碼

基本上我們已經可以展示全景圖了,還可以再優化一下,因為球狀模型的頂點與面的數量十分龐大, 這些元素的數量越多, 耗費的瀏覽器資源就會越多。我們可以建立一個立方體,將一張全景圖分割為立方體的六個面,貼到立方體上,如果這個立方體足夠大,我們就不足以感覺到它的邊際存在,這樣就避免了兩極畫素點的重疊。程式碼如下:

    let loader = new THREE.TextureLoader();  //設定紋理貼圖
    //全景圖分解
    const imgList = [
        'top.png',
        'right.png',
        'bottom.png',
        'left.png',
        'front.png',
        'behind.png'
    ];
    //採用非同步程式設計
    Promise.all(imgList.map(function (val) {
        //載入圖片, 新建材質, 傳給下一個步驟.
        return new Promise(function (resolve, reject) {
            loader.load(val, function (texture) {
                        //定義材料外觀的物件
                resolve(new THREE.MeshBasicMaterial({
                    map: texture,
                    side: THREE.BackSide
                }));
            });
        });
    })).then(function (materials) {
        //建立正方體
        geometry = new THREE.BoxGeometry(200, 200, 200);
        cube = new THREE.Mesh(
            geometry,
            new THREE.MeshFaceMaterial(materials) //可以在該容器中為物體的各表面貼上圖片
        );
        scene.add(cube);
        animate();
    });
複製程式碼

最後通過 WebGLRenderer 就可以了。

三、photo-sphere-viewer 介紹

前面介紹了全景圖的相關背景,接下來開始使用 photo-sphere-viewer(以下簡稱 view.js)。three.js 可以理解為 jq,view.js 就是 jq 的外掛。

需要的資源:view.min.css、three.js、D.min.js、uEvent.js、doT.js、CanvasRenderer.js、Projector.js、DeviceOrientationControls.js、view.min.js、zepto_1.1.3.js。

引數介紹:

  • panorama:(必選)全景圖的路徑。
  • container:(必選)放置全景圖的容器。
  • autoload:(預設為 true)true 為自動載入全景圖,false 為遲點載入全景圖(通過load 方法)。
  • usexmpdata:(預設值為 true)photo sphere viewer 是否必須讀入 xmp 資料,false 為不必須。
  • cors_anonymous:(預設值為 true)true 為不能通過 cookies 獲得使用者
  • pano_size:(預設值為 null)全景圖的大小,是否裁切。
  • default_position:(預設值為 0)定義預設位置,使用者看見的第一個點,例如:{long: math.pi, lat: math.pi/2}。
  • min_fov:(預設值為 30)觀察的最小區域,單位 degrees,在 1-179 之間。
  • max_fov:(預設值為 90)觀察的最大區域,單位 degrees,在 1-179 之間。
  • allow_user_interactions:(預設值為 true)設定為 false,則禁止使用者和全景圖互動(導航條不可用)。
  • allow_scroll_to_zoom:(預設值為 true)若設定為 false,則使用者不能通過滑鼠滾動進行縮放圖片。
  • tilt_up_max:(預設值為 math.pi/2)向上傾斜的最大角度,單位radians。
  • tilt_down_max:(預設值為 math.pi/2)向下傾斜的最大角度,單位 radians。
  • min_longitude:(預設值為 0)能夠展示的最小經度。
  • max_longitude:(預設值為 2PI)能夠展示的最大維度。
  • zoome_level:(預設值為 0)預設的縮放級別,值在 0-100 之間。
  • long_offset:(預設值為 PI/360)mouse/touch 移動時每畫素經過的經度值。
  • lat_offset:(預設值為 PI/180)mouse/touch 移動時每畫素經過的緯度值。
  • time_anim(預設值為 2000)全景圖在 time_anim 毫秒後會自動進行動畫。(設定為false禁用它)
  • reverse_anim:(預設值為 true)當水平方向到達最大/最小的經度時,動畫方向是否反轉(僅僅是不能看到完整的圓)。
  • anim_speed:(預設值為 2rpm)動畫每秒/分鐘多少的速度。
  • vertical_anim_speed:(預設值為2rpm)垂直方向的動畫每秒/分鐘多少的速度。
  • vertical_anim_target:(預設值為0)當自動旋轉時的維度,預設為赤道。
  • navbar:(預設為false)顯示導航條。
  • navbar_style:(預設值為false)導航條的樣式。有效的屬性:
  • backgroundColor:導航條背景色(預設值rgba(61, 61, 61, 0.5));
  • buttonsColor:按鈕前景色(預設值 rgba(255, 255, 255, 0.7));
  • buttonBackgroundColor:按鈕啟用時的背景色(預設值 rgba(255, 255, 255, 0.1));
  • buttonsHeight:按鈕高度,單位px(預設值 20);
  • autorotateThickness:自動旋轉圖片的層(預設值 1);
  • zoomRangeWidth:縮放遊標的寬度,單位px(預設值 50);
  • zoomRangeThickness:縮放遊標的層(預設值 1);
  • zoomRangeDisk:縮放遊標的放大率,單位 px(預設值 7);
  • fullscreenRatio:全屏圖示的比例(預設值 4/3);
  • fullscreenThickneee:全屏圖片的層,單位 px(預設值 2)
  • loading_msg:(預設值為 Loading...)載入資訊。
  • loading_img:(預設值為 null)loading 圖片的路徑。
  • loading_html:(預設值 為null)html 載入器(新增到容器中的元素或字串)。
  • size:(預設值為 null)全景圖容器的最終尺寸,例如 {width: 500, height: 300}。
  • onready:(預設值為 null)全景圖準備好並且第一張圖片展示出來後的回撥函式。

方法介紹:

  • addAction():新增事件(外掛沒有提供執行事件的方法,似乎是提供給外掛內部使用的)。
  • fitToContainer():調整全景圖容器大小為指定大小。
  • getPosition():獲取座標經緯度。
  • getPositionInDegrees():獲取經緯度度數。
  • getZoomLevel():獲取縮放級別。
  • load():載入全景圖()。
  • moveTo(longitude, latitude):根據經緯度移動到某一點。
  • rotate(dlong, dlat):根據經緯度度數移動到某一點。
  • toggleAutorotate():是否開啟全景圖自動旋轉。
  • toggleDeviceOrientation():是否開啟重力感應方向控制。
  • toggleFullscreen():是否開啟全景圖全屏。
  • toggleStereo():是否開啟立體效果(可用於 WebVR 哦)。
  • zoom(level):設定縮放級別。
  • zoomIn():放大。
  • zoomOut():縮小。

使用:

首先建立一個 div 來作為渲染區域

    <div id="photosphere"></div>
    //初始化
    var PSV = new PhotoSphereViewer({
    container: 'photosphere',
                    panorama: panos[0].url
    })
複製程式碼

四、建立標記

展示全景圖的時候一般我們會遇到如下場景:展示一個大範圍的區域,裡面包含了很多需要介紹的詳細地點,所以我們可以通過建立標記點,來給出相關的資訊展示。

使用:

    //定義在new裡面  自執行
    markers: (function() {
               return common();  //將建立marker  封裝
            }())
    function common() {
           var a = [];
           for(var i = 0; i < Math.PI * 2; i += Math.PI / 4) {
              for(var j = -Math.PI / 2 + Math.PI / 4; j < Math.PI / 2; j += Math.PI / 4) {
                   a.push({
                       id: '#' + a.length,
                       tooltip: '點我啊',  //點選的提示資訊
                       latitude: j,    //經緯度   可以使用 x, y替換   代表全景圖上的畫素座標
                       longitude: i,
                       image: '../img/pin2.png',   //圖片標記背景圖
                       width: 32,   //寬高
                       height: 32,
                       anchor: 'bottom center',
                       data: {  //設定資料
                           deletable: true
                       }
                   });
                  }
               }
               return a  //marker接受陣列   所以以陣列的形式反出
         }
複製程式碼

自定義 SVG:

    a.push({
        id: 'circle',   //id必選
        tooltip: 'A circle of radius 30',
        circle: 20,  //半徑
        svgStyle: {
            fill: 'rgba(255,255,0,0.3)',
            stroke: 'yellow',
            strokeWidth: '2px'
        },
        //                      longitude: 0.14842681258549928,
        //                      latitude: -0.8678522571819425,
        x: 8395,
        y: 6827,
        anchor: 'center right'
    });
自定義html

    a.push({
        id: 'text',
        longitude: -0.5,
        latitude: -0.28,
        html: '♥愛你愛你麼麼噠♥',  
        anchor: 'bottom right',
        style: {  //定義樣式
        maxWidth: '320px',
        color: 'red',
        fontSize: '20px',
        fontFamily: 'Helvetica, sans-serif',
        textAlign: 'center'
        },
        tooltip: {  //提示
        content: 'An HTML marker',
        position: 'right'
        }
    });
複製程式碼

五、景圖的自動播放加背景音樂

    <audio src="../libs/source.mp3" id="audios" style="position: absolute;left: 99999999999999px;top: 999999999px;" loop="true"></audio>

    PSV.on('ready',function(){  //準備就緒
         PSV.toggleAutorotate();   //自動播放
         $('#audios')[0].play();   //播放背景音樂
         $('.scene').hide();       //loading   下面提到
         $('.playBtn').show();   //音樂播放按鈕
     });
複製程式碼

六、圖片快取,場景切換

設定 cache_texture:number,引數為快取圖片,載入後不會重複載入,number 自定義。

點選 marker 進行場景的切換:

    PSV.on('select-marker', function(marker) { //監聽marker點選事件。
    //進行場景的切換
    PSV.setPanorama(url, {longitude: 3.848,
                        latitude: -0.244}, true)
                            .then(function() {
                                $('.back').show();
                                $('.scene').hide();
                                PSV.setCaption('場景描述');
                            });
    })

    setPanorama引數:圖片地址、下一個場景的初始經緯度、transition 預設(false)
還可以設定

    navbar: [
              'autorotate', 'zoom', 'download', 'markers',
              'spacer-1',
              {
                  title: 'Change image',
                  className: 'custom-button',
                  content: '點我',
                  onClick: (function() {
                      //自定義邏輯
                  }())
              }]
複製程式碼

來自定義導航的內容。

再補充一下 panorama 可以接受一個陣列(六張 url),也可以接受一個單獨的圖片 url。如果一張的話在 _loadEquirectangularTexture 這個函式裡面內部進行了裁剪,如果是一個陣列,直接渲染,提高了效能。

切換場景的過程中載入 loading,因為我們開啟了快取,所以需要設定一個變數(開關思想)來控制 loading 框的顯示次數,比如第一次場景 1 進入場景 2,需要 loading 載入,但是從場景 2 再跳回到場景 1 因為快取就不不要 loading,只載入一次。loading 的製作是通過 svg 製作的,這裡沒有貼出程式碼。

原始碼下載地址:

download.csdn.net/download/su…


相關文章