使用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地图的界面实现,界面如下:
要使用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。如下图所示:
遍历上图的每个格子,通过格式的编号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函数传递的参数,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)。我们截取一张最后一行的全景图片:
很明显的看到底部的黑色区域。如果你手动测试,会发现它的高度正好是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了!!!
使用WebGL加载Google街景图的更多相关文章
- ArcGIS API for Silverlight中加载Google地形图(瓦片图)
原文:ArcGIS API for Silverlight中加载Google地形图(瓦片图) 在做水利.气象.土地等行业中,若能使用到Google的地形图那是再合适不过了,下面就介绍如何在ArcGIS ...
- Flex加载google地图、百度地图以及天地图作底图
一 Flex加载Google地图作底图 (1)帮助类GoogleLayer.as /* * 根据输入的地图类型加载Google地图(by chenyuming) */ package Layers ...
- ArcGIS API for Silverlight 加载google地图
原文:ArcGIS API for Silverlight 加载google地图 using System; using System.Net; using System.Windows; using ...
- ArcGIS API for Silverlight加载google地图(后续篇)
原文:ArcGIS API for Silverlight加载google地图(后续篇) 之前在博客中(http://blog.csdn.net/taomanman/article/details/8 ...
- ARCGIS FLEX API加载google地图、百度地图、天地图(转)
http://www.cnblogs.com/chenyuming507950417/ Flex加载google地图.百度地图以及天地图作底图 一 Flex加载Google地图作底图 (1)帮助类G ...
- SDL2.0的加载图片贴图
加载图片贴图,采用了SDL_Window.SDL_Renderer.SDL_Texture和SDL_Image库 实例: #include <stdio.h> #include <m ...
- 《ArcGIS Runtime SDK for Android开发笔记》——(13)、图层扩展方式加载Google地图
1.前言 http://mt2.google.cn/vt/lyrs=m@225000000&hl=zh-CN&gl=cn&x=420&y=193&z=9& ...
- Echarts动态加载饼状图实例(二)
一.引入echarts.js文件(下载页:http://echarts.baidu.com/download.html) 二.HTML代码: <div class="ui-contai ...
- Echarts动态加载饼状图的实例
一.引入echarts.js文件(下载页:http://echarts.baidu.com/download.html) 二.HTML代码: <div style="width: 10 ...
随机推荐
- node.js下mongoose简单操作实例
Mongoose API : http://mongoosejs.com/docs/api.html // mongoose 链接var mongoose = require('mongoose'); ...
- python的with语句,超级强大
With语句是什么? 有一些任务,可能事先需要设置,事后做清理工作.对于这种场景,Python的with语句提供了一种非常方便的处理方式.一个很好的例子是文件处理,你需要获取一个文件句柄,从文件中读取 ...
- docker使用Let’s Encrypt协议构建免费https协议
简介:我们可以把自己的image上传到dockerhub或者阿里云的docker镜像仓库,但在实际使用中我们很多时候都用的是自己的registry,便于内部的共享等等优点,docker镜像默认支持ht ...
- Struts(五)之OGNL、contextMap
一.OGNL 1.1.定义 OGNL是Object-Graph Navigation Language的缩写,它是一个单独的开源项目. Struts2框架使用OGNL作为默认的表达式语言.它是一种功能 ...
- PRINCE2重要性--光环国际培训
项目的重要性 答:对于当今的组织来说,一个关键的挑战,就是能够成功地平衡以下两个并存的.互相竞争的方面:保持现有的商业运营--盈利能力.服务质量.客户关系.品牌忠实度.生产效率.市场信心等,这些被称为 ...
- 持续集成:TestNG中case之间的关系
持续集成:TestNG中case之间的关系 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq: ...
- 老李分享:网页爬虫java实现
老李分享:网页爬虫java实现 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq:908821 ...
- MCDownloader(iOS下载器)说明书
示例 前言 很多iOS应用中都需要下载数据,并对这些下载的过程和结果进行管理,因此我才有了写这个MCDownloader的想法.在IOS 文件下载器-MCDownloadManager这篇文章中,我使 ...
- 实现简单的跨站脚本攻击(XSS)
我们来通俗的了解一下什么是跨站脚本攻击(XSS):在表单中提交 一段 js代码 ,提交的内容被展示到页面时 ,js会被浏览器解析 打个比方吧,比如我现在写的这篇博客,写完以后我要发表对吧? 发表这个过 ...
- Redis数据类型之列表List
Redis列表简介 Redis列表是简单的字符串列表,一个列表最多可以包含 232 - 1 个元素.列表按照插入顺序排序,可以从列表的头部或者尾部添加元素 上图演示了使用LPUSH向列表中插入元素,并 ...