使用WebGL加載Google街景圖


    我們要實現的功能比較簡單:首先通過坐標定位、我的位置、地址搜索等方式,調用google map api獲取地址信息。然后根據地址信息中的全景信息獲取當前縮放級別的全景信息。最終把這些全景信息通過WebGL方法顯示在屏幕上。

    了解了Google街景圖的呈現原理,像國內的街景呈現,景區全景呈現不外乎都是相似的原理。區別只是調用的api不同而已。在實現功能過程中,我們也可以了解到全景呈現的一些原理。

    在介紹代碼之前,先大概的描述下實現的步驟:

    1)、使用google api呈現地圖,實現地圖定位、搜索功能。這兩種方式我們獲取的都是地址信息,地址信息中我們關注兩項數據:坐標、全景ID。

    2)、使用google提供的全景api,傳遞zoom和當前坐標位置以及全景ID。獲取當前位置全景圖片。

    3)、把獲取的圖片集合通過對應的坐標位置,呈現到同一個canvas上,拼湊成一張完整的圖片。

    4)、把canvas作為一個紋理,貼到類型為Sphere的球體上,攝像頭在球體中心位置觀察。

    通過以上步驟就可實現google街景圖的呈現。下面的內容將介紹具體的實現。

google地圖呈現

    顯示看下google地圖的界面實現,界面如下:

image

    要使用google地圖,先得引入googe api。如何引入,自己查看下google api介紹就知道了。這里需要特別注意的是,如果是本地調試,在獲取api時,必須傳遞key。否則,地圖就不能使用。

   界面實現代碼如下:

    <div id="pano"></div>
    <div id="options" class="hide">
        <div id="map"></div>

        <div class="block">
            <form id="map_form">
                <input type="text" name="address" id="address" />
                <button type="submit" class="primary button" id="searchButton" >查詢</button>
            </form>
        </div>

        <div class="block">
            <button type="submit" id="myLocationButton" style="width: 148px" class="button">使用我的位置</button>
            <button type="submit" id="fullscreenButton" style="width: 148px" class="button">全屏</button>
        </div>

        <div class="block">
            <b>質量</b>
            <form id="pano_form" style="position: absolute; right: 0; top: 0">
                <button name="scale" style="width: 4em" id="scale1" class="left button">低</button>
                <button name="scale" style="width: 6em" id="scale2" class="middle button">中</button>
                <button name="scale" style="width: 4em" id="scale3" class="middle button">高</button>
                <button name="scale" style="width: 7em" id="scale4" class="right button">超清</button>
            </form>
        </div>

        <div class="block" id="status" >
            <div id="message" ></div>
            <div id="error" ></div>
        </div>
    </div>

    <div id="preloader">
        <div id="bar"></div>
    </div>

    <script type="text/javascript" src="libs/GSVPano.js"></script>
    <script type="text/javascript" src="libs/three.js"></script>
    <script type="text/javascript" src="libs/RequestAnimationFrame.js"></script>
    <!-- 本地調試google map api, 必須申請key-->
    <script type="text/javascript" src="//maps.google.com/maps/api/js?key=AIzaSyA6idYqH50rvzc2QBZyBdJT_ipSH2DrABk&sensor=false"></script>

    pano是用來渲染全景圖的容器。options是顯示google地圖的容器,可以使用我的位置定位、全屏、地址查詢、切換縮放級別。也可以直接在地圖上定位。

    以上的操作我們可以分為兩類,一類是獲取坐標,另一類是設置容器大小。像我的位置定位、地址查詢、地圖定位最終都是為了獲取當前地址的經緯度。而設置zoom,最終改變的是繪制全景圖的容器的大小。

    也就是說,我們操作有兩個輸出:location、zoom。接下來就介紹着兩個輸出的使用。

使用zoom設置全景容器大小

    界面上有四個按鈕,設置Zoom的大小。事件綁定代碼如下:

//設置zoom按鈕,並綁定事件
            for(var j = 1; j < 5; j++){
                var el = document.getElementById("scale" + j);
                scaleButtons.push(el);
                (function(z){
                    el.addEventListener("click", function(e){
                        e.preventDefault();
                        setZoom(z);
                    }, false);
                })(j);
            }

    每個事件都會調用setZoom函數設置當前縮放級別。setZoom函數如下:

function setZoom( z ){
            zoom = z;
            loader.setZoom( z);
            for(var j = 0; j < scaleButtons.length; j++){
                scaleButtons[j].className = scaleButtons[j].className.replace("active", "");
                if(z == (j + 1)) scaleButtons[j].className += " active";
            }
            if(activeLocation) loader.load(activeLocation);
        }

    這里有兩行代碼比較重要:loader.setZoom( z ),以及if(activeLocation) ….。第二段代碼是和坐標有關系的,現在先不介紹。loader是一個類型為GSVPANO的對象,所有和全景相關的代碼都包含在這個類型中。這個對象包含了setZoom函數。setZoom函數代碼如下:

this.setZoom = function(z){
        _zoom = z;
        console.log(z);
        this.adaptTextureToZoom();
    };

    代碼調用了adaptTextureToZoom()函數,從字面上看,是為了把紋理適配到設置的Zoom大小上。代碼如下:

this.adaptTextureToZoom = function(){
        var w = widths[ _zoom ],//當前zoom,一張圖片的寬度
            h = heights[ _zoom ]; // 當前zoom,一張圖片的高度

        _wc = Math.ceil(w / maxW); // x方向,圖片數量
        _hc = Math.ceil(h / maxH); // y方向,圖片數量

        _canvas = []; // canvas集合
        _ctx = []; //上下文集合

        var ptr = 0;
        for(var y = 0; y < _hc; y++){ // y方向
            for(var x = 0; x < _wc; x++ ){ // x方向
                var c = document.createElement("canvas"); //創建一個新的canvas
                if( x < (_wc - 1)) c.width = maxW;
                else c.width = w - (maxW * x);
                if( y < (_hc - 1)) c.height = maxH;
                else c.height = h - (maxH * y );

                console.log("新創建canvas:" + c.width + " * " + c.height );
                _canvas.push( c );
                _ctx.push(c.getContext("2d"));
                ptr++;
            }
        }
    };

    這段代碼我也花了一些時間研究,這里有幾個參數需要先說明下:

var widths = [416, 832, 1664, 3328, 6656],
        heights = [416, 416, 832, 1664, 3328];
...
 if(gl){
        var maxTexSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
        maxW = maxH = maxTexSize;
    }
...

    widths和heights存放了每個縮放級別,一張圖片的寬度和高度。maxW、maxH為紋理默認的最大尺寸。

    adaptTextureToZoom函數中,先獲取每張圖片的尺寸w、h。然后使用Math.ceil(w / maxW)獲取在x方向,圖片的個數。但通過我自的調試發現,w、h都是小於maxW、maxH的。所以x、y方向的圖片個數_wc、_hc等為1。有了這樣的結論,繼續看adaptTextureToZoom函數下面的代碼。使用for循環遍歷依次遍歷每一行,每一列。每個位置創建一個canvas。但由於_wc和_hc都為1,所有僅僅會創建一個canvas。

    並且這唯一一個canvas的寬度為w、高度為h。_canvas和_ctx集合的長度都為1。

    我們在界面上設置zoom,也就是為了獲取這個canvas。以提供繪制全景圖的容器。但這里有個奇怪的地方,widths和height的尺寸都是416的倍數。而接下里我們獲取的全景圖,每張的大小確實512。先把這個問題留着。繼續看location的使用。

使用location獲取當前位置的全景信息

    我們先回到之前介紹的setZoom函數,代碼如下:

function setZoom( z ){
            zoom = z;
            loader.setZoom( z);
            ...
            if(activeLocation) loader.load(activeLocation);
        }

    現在我們就來分析loader.load(activeLocation)函數。activeLocation表示我們已經獲取到的位子的坐標經緯度信息。load函數代碼如下:

this.load = function(location){
        var self = this;
        var url = 'https://cbks0.google.com/cbk?cb_client=maps_sv.tactile&authuser=0&hl=en&output=polygon&it=1%3A1&rank=closest&ll=' + location.lat() + ',' + location.lng() + '&radius=350';

        var http_request = new XMLHttpRequest();
        http_request.open("GET", url, true);
        http_request.onreadystatechange = function(){
            if(http_request.readyState === 4 && http_request.status == 200){
                var data = JSON.parse(http_request.responseText);
                self.loadPano(location, data.result[0].id);
            }
        }
        http_request.send(null);
    }

    代碼其實很簡單,通過傳遞的location組裝獲取地址信息的url。然后使用XMLHttpRequest發送請求。返回的結果信息中,我們主要用到的是地址的id:data.result[0].id。請求回調函數最后調用了self.loadPano(location, data.result[0].id)函數。該函數代碼如下:

this.loadPano = function(location, id){
        var self = this;
        _panoClient.getPanoramaById(id, function(result, status){
            if(status === google.maps.StreetViewStatus.OK){
                …                
                _panoId = result.location.pano;
                self.location = location;
                self.composePanorama();
            }else{
                if(self.onNoPanoramaData) self.onNoPanoramaData(status);
                self.throwError("不能獲取panorama,原因如下:" + status);
            }
        });
    };

    _panoClient是類型為google.maps.StreetViewService的一個google api對象。包含有getPanoramaById(id, callback)函數。該函數通過傳遞的地址信息id獲取全景信息。這里我們最關系的數據是_panoId(全景信息id)。獲取了全景信息id,最后繼續調用了self.composePanorama()函數。

this.composePanorama = function(){
        this.setProgress(0);

        var w = levelsW[ _zoom], //x方向的個數
            h = levelsH[ _zoom], //y方向的個數
            self = this,
            url,
            x,
            y;

        _count = 0;
        _total = w * h;

        var self = this;
        for(var y = 0; y < h; y++){
            for(var x = 0; x < w; x++){
                //var url = 'https://cbks2.google.com/cbk?cb_client=maps_sv.tactile&authuser=0&hl=en&panoid=' + _panoId + '&output=tile&zoom=' + _zoom + '&x=' + x + '&y=' + y + '&' + Date.now();
                var url = 'https://geo0.ggpht.com/cbk?cb_client=maps_sv.tactile&authuser=0&hl=en&panoid=' + _panoId + '&output=tile&x=' + x + '&y=' + y + '&zoom=' + _zoom + '&nbt&fover=2';

                (function(x, y){
                    if(_parameters.useWebGL){
                        var texture = THREE.ImageUtils.loadTexture(url, null, function(){
                            self.composeFromTile(x, y, texture);
                        });
                    }else{
                        var img = new Image();
                        img.addEventListener("load", function(){
                            self.composeFromTile(x, y, this);
                        });
                        img.crossOrigin = "";
                        img.src = url;
                    }
                })(x, y);
            }
        }
    };

    這里又有兩個新變量levelsW、levelsH。還是先看下賦值:

var levelsW = [1, 2, 4, 7, 13, 26];
var levelsH = [1, 1, 2, 4, 7, 13];

    levelsW存放每個級別x方向圖片個數,levelsH存放每個級別y方向圖片個數。composePanorama函數中的_total變量有什么用?y用處也不大,加載進度需要用一下。接着每行每列一次遍歷每個格子。假如當前zoom級別為2,那么對應的w和h分別是4,2。如下圖所示:

image    遍歷上圖的每個格子,通過格式的編號x、y組裝圖片地址,創建一個Image對象(這里我們沒有設置useWebGL),設置src等於圖片地址url。這樣Image就獲取到了圖片信息。圖片加載成功后,調用self.composeFromTile(x, y, this)函數。composeFormTile函數代碼如下:

this.composeFromTile = function(x, y, texture){
        x *= 512; //x方向編號編號所在像素位置
        y *= 512; // y方向編號所在像素位置
        var px = Math.floor(x / maxW), py = Math.floor( y / maxH); // px和py表示當前圖片所在像素位置

        x -= px * maxW;
        y -= py * maxH;

        _ctx[py * _wc + px].drawImage(texture, 0, 0, texture.width, texture.height, x, y, 512, 512); //每個canvas上畫圖的位置和地圖上x、y對應上。
        this.progress();
    }

    函數包含三個參數,前兩個參數表示格子的位子,而texture表示圖片對象。x、y都乘以了512,意思是把格子編號轉換為格子左上角坐在的像素坐標。在計算px和py時,這里有個疑問,由於x和y始終是小於maxW和maxH的。所有px和py都等於0。那么x和y的像素位置是沒改變的。並且py * _wc + px是指都是0。也就是說,所有格子對應的圖片都是存放在_ctx[0]的畫布上。這里的drawImage函數需要着重說明下,drawImage函數聲明如下:

void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

    呈現如下:

drawImage

    調用drawImage函數傳遞的參數,texture表示image對象,第二個參數和第三個參數都為0,表示我們從圖片對象上的左上角(0, 0)開始獲取,獲取的寬度為texture.width,獲取的高度為texture.height。也就是說獲取整個圖片。

    接下來是后面的四個參數:x、y表示從canvas上的x、y坐標處開始繪制,繪制的大小為(512,512)。x和y不同,繪制的其實坐標就不一樣。最終,我們可以把所有的全景格子圖像繪制到canvas上。並呈現出一張完整的全景紋理 圖。

    這里又要提出:為什么這里設置圖片尺寸為512 * 512,而畫布大小卻是416的倍數(而不是512的倍數)?

    google地圖返回的圖片尺寸都是512 * 512,但由於全景相機拍攝有部分是盲區,所有最后一行圖片的底下部分都是黑色的被丟棄的。這部分黑色的高度正好是yc * (512 - 416)。我們截取一張最后一行的全景圖片:

image

    很明顯的看到底部的黑色區域。如果你手動測試,會發現它的高度正好是yc * (512 – 416)。如果黑色部分被丟棄,算下來,相當於每個圖片的大小實際為416。這就是為什么畫布畫每個格子的尺寸是512。而實際畫布大小是416的倍數。

    現在一個繪制了全景圖的完整canvas已經准備好了,剩下的就是把它當做紋理繪制到球體網格上。當加載繪制完了。會觸發onPanoramaLoad事件。該事件注冊的 函數為:

loader.onPanoramaLoad = function(){
                activeLocation = this.location;
                mesh.material.uniforms.map.value = new THREE.Texture( this.canvas[0]);
                mesh.material.uniforms.map.value.needsUpdate = true;
                showMessage('Panorama tiles loaded.<br/>The images are ' + this.copyright);
                showProgress( false );
            }

    首先需要更新網格上材質的map屬性,重新創建一個紋理對象賦值給它。這里直接把canvas作為圖片傳遞給了Texture構造函數。下一行代碼設置了map的needsUpdate屬性為true,表示渲染時需要重新繪制紋理了。

介紹完了

   通過以上的介紹,我們應該能大概明白,全景圖究竟是如何通過WebGL呈現出來的。也明白了在渲染過程中的一些全景技術。完整的帶還包括了像全景圖的旋轉等功能,這里就不在做介紹了。需要了解的可以以下地址獲取實例完整代碼:

https://github.com/heavis/threejs-demo/tree/master/google_street 

    代碼中有我申請的google map api,可直接使用。另外,如果想查看實例,自己還得有個翻牆工具。over了!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM