Threejs 开发3D地图实践总结
前段时间连续上了一个月班,加班加点完成了一个3D攻坚项目。也算是由传统web转型到webgl图形学开发中,坑不少,做了一下总结分享。
1、法向量问题






- var material1 = new __WEBPACK_IMPORTED_MODULE_0_three__["MeshLambertMaterial"]({
- emissive: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0], style.fillStyle[1], style.fillStyle[2]),
- side: __WEBPACK_IMPORTED_MODULE_0_three__["DoubleSide"],
- shading: __WEBPACK_IMPORTED_MODULE_0_three__["FlatShading"],
- vertexColors: __WEBPACK_IMPORTED_MODULE_0_three__["VertexColors"]
- });
- var material2 = new __WEBPACK_IMPORTED_MODULE_0_three__["MeshLambertMaterial"]({
- color: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0] * 0.1, style.fillStyle[1] * 0.1, style.fillStyle[2] * 0.1),
- emissive: new __WEBPACK_IMPORTED_MODULE_0_three__["Color"](style.fillStyle[0] * 0.9, style.fillStyle[1] * 0.9, style.fillStyle[2] * 0.9),
- side: __WEBPACK_IMPORTED_MODULE_0_three__["DoubleSide"],
- shading: __WEBPACK_IMPORTED_MODULE_0_three__["FlatShading"],
- vertexColors: __WEBPACK_IMPORTED_MODULE_0_three__["VertexColors"]
- });
3、POI标注
Three中创建始终朝向相机的POI可以使用Sprite类,同时可以将文字和图片绘制在canvas上,将canvas作为纹理贴图放到Sprite上。但这里的一个问题是canvas图像将会失真,原因是没有合理的设置sprite的scale,导致图片被拉伸或缩放失真。
问题的解决思路是要保证在3d世界中的缩放尺寸,经过一系列变换投影到相机屏幕后仍然与canvas在屏幕上的大小保持一致。这需要我们计算出屏幕像素与3d世界中的长度单位的比值,然后将sprite缩放到合适的3d长度。


- function fromSreenToNdc(x, y, container) {
- return {
- x: x / container.offsetWidth * 2 - 1,
- y: -y / container.offsetHeight * 2 + 1,
- z: 1
- };
- }
- function fromNdcToScreen(x, y, container) {
- return {
- x: (x + 1) / 2 * container.offsetWidth,
- y: (1 - y) / 2 * container.offsetHeight
- };
- }
- unproject: function () {
- var matrix = new Matrix4();
- return function unproject( camera ) {
- matrix.multiplyMatrices( camera.matrixWorld, matrix.getInverse( camera.projectionMatrix ) );
- return this.applyMatrix4( matrix );
- };
- }(),
将得到的3d点与相机位置结合起来做一条射线,分别与场景中的物体进行碰撞检测。首先与物体的外包球进行相交性检测,与球不相交的排除,与球相交的保存进入下一步处理。将所有外包球与射线相交的物体按照距离相机远近进行排序,然后将射线与组成物体的三角形做相交性检测。求出相交物体。当然这个过程也由Three中的RayCaster做了封装,使用起来很简单:
- mouse.x = ndcPos.x;
- mouse.y = ndcPos.y;
- this.raycaster.setFromCamera(mouse, camera);
- var intersects = this.raycaster.intersectObjects(this._getIntersectMeshes(floor, zoom), true);
5、性能优化
随着场景中的物体越来越多,绘制过程越来越耗时,导致手机端几乎无法使用。
在图形学里面有个很重要的概念叫“one draw all”一次绘制,也就是说调用绘图api的次数越少,性能越高。比如canvas中的fillRect、fillText等,webgl中的drawElements、drawArrays;所以这里的解决方案是对相同样式的物体,把它们的侧面和顶面统一放到一个BufferGeometry中。这样可以大大降低绘图api的调用次数,极大的提升渲染性能。
这样解决了渲染性能问题,然而带来了另一个问题,现在是吧所有样式相同的面放在一个BufferGeometry中(我们称为样式图形),那么在面点击时候就无法单独判断出到底是哪个物体(我们称为物体图形)被选中,也就无法对这个物体进行高亮缩放处理。我的处理方式是,把所有的物体单独生成物体图形保存在内存中,做面点击的时候用这部分数据来做相交性检测。对于选中物体后的高亮缩放处理,首先把样式面中相应部分裁减掉,然后把选中的物体图形加入到场景中,对它进行缩放高亮处理。裁剪方法是,记录每个物体在样式图形中的其实索引位置,在需要裁切时候将这部分索引制零。在需要恢复的地方在把这部分索引恢复成原状。
6、面点击移动到屏幕中央
这部分也是遇到了不少坑,首先的想法是:
- this.unprojectPan = function(deltaVector, moveDown) {
- // var getProjectLength()
- var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
- var cxv = new Vector3(0, 0, 0).setFromMatrixColumn(scope.object.matrix, 0);// 相机x轴
- var cyv = new Vector3(0, 0, 0).setFromMatrixColumn(scope.object.matrix, 1);// 相机y轴
- // 相机轴都是单位向量
- var pxl = deltaVector.dot(cxv)/* / cxv.length()*/; // 向量在相机x轴的投影
- var pyl = deltaVector.dot(cyv)/* / cyv.length()*/; // 向量在相机y轴的投影
- // offset=dx * vector(cx) + dy * vector(cy.project(xoz).normalize)
- // offset由相机x轴方向向量+相机y轴向量在xoz平面的投影组成
- var dv = deltaVector.clone();
- dv.sub(cxv.multiplyScalar(pxl));
- pyl = dv.length();
- if ( scope.object instanceof PerspectiveCamera ) {
- // perspective
- var position = scope.object.position;
- var offset = new Vector3(0, 0, 0);
- offset.copy(position).sub(scope.target);
- var distance = offset.length();
- distance *= Math.tan(scope.object.fov / 2 * Math.PI / 180);
- // var xd = 2 * distance * deltaX / element.clientHeight;
- // var yd = 2 * distance * deltaY / element.clientHeight;
- // panLeft( xd, scope.object.matrix );
- // panUp( yd, scope.object.matrix );
- var deltaX = pxl * element.clientHeight / (2 * distance);
- var deltaY = pyl * element.clientHeight / (2 * distance) * (moveDown ? -1 : 1);
- return [deltaX, deltaY];
- } else if ( scope.object instanceof OrthographicCamera ) {
- // orthographic
- // panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
- // panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
- var deltaX = pxl * element.clientWidth * scope.object.zoom / (scope.object.right - scope.object.left);
- var deltaY = pyl * element.clientHeight * scope.object.zoom / (scope.object.top - scope.object.bottom);
- return [deltaX, deltaY];
- } else {
- // camera neither orthographic nor perspective
- console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
- }
- }
7、2/3D切换
23D切换的主要内容就是当相机的视线轴与场景的平面垂直时,使用平行投影,这样用户只能看到顶面给人的感觉就是2D视图。所以要根据透视的视锥体计算出平行投影的世景体。
因为用户会在2D、3D场景下做很多操作,比如平移、缩放、旋转,要想无缝切换,这个关键在于将平行投影与视锥体相机的位置、lookAt方式保持一致;以及将他们放大缩小的关键点:distance的比例与zoom来保持一致。

- r=6378137
- resolution=2*PI*r/(2^zoom*256)
各个级别中像素与米的对应关系如下:
- resolution zoom 2048 blocksize 256 blocksize scale(dpi=160)
- 156543.0339 0 320600133.5 40075016.69 986097851.5
- 78271.51696 1 160300066.7 20037508.34 493048925.8
- 39135.75848 2 80150033.37 10018754.17 246524462.9
- 19567.87924 3 40075016.69 5009377.086 123262231.4
- 9783.939621 4 20037508.34 2504688.543 61631115.72
- 4891.96981 5 10018754.17 1252344.271 30815557.86
- 2445.984905 6 5009377.086 626172.1357 15407778.93
- 1222.992453 7 2504688.543 313086.0679 7703889.465
- 611.4962263 8 1252344.271 156543.0339 3851944.732
- 305.7481131 9 626172.1357 78271.51696 1925972.366
- 152.8740566 10 313086.0679 39135.75848 962986.1831
- 76.4370283 11 156543.0339 19567.87924 481493.0916
- 38.2185141 12 78271.51696 9783.939621 240746.5458
- 19.1092571 13 39135.75848 4891.96981 120373.2729
- 9.5546285 14 19567.87924 2445.984905 60186.63645
- 4.7773143 15 9783.939621 1222.992453 30093.31822
- 2.3886571 16 4891.96981 611.4962263 15046.65911
- 1.1943286 17 2445.984905 305.7481131 7523.329556
- 0.5971643 18 1222.992453 152.8740566 3761.664778
- 0.2985821 19 611.4962263 76.43702829 1880.832389
- 0.1492911 20 305.7481131 38.21851414 940.4161945
- 0.0746455 21
- 0.0373227 22
3D中的计算策略是,首先需要将3D世界中的坐标与墨卡托单位的对应关系搞清楚,如果已经是以mi来做单位,那么就可以直接将相机的投影屏幕的高度与屏幕的像素数目做比值,得出的结果跟上面的ranking做比较,选择不用的级别数据以及比例尺。注意3D地图中的比例尺并不是在所有屏幕上的所有位置与现实世界都满足这个比例尺,只能说是相机中心点在屏幕位置处的像素是满足这个关系的,因为平行投影有近大远小的效果。
9、poi碰撞
由于标注是永远朝着相机的,所以标注的碰撞就是把标注点转换到屏幕坐标系用宽高来计算矩形相交问题。至于具体的碰撞算法,大家可以在网上找到,这里不展开。下面是计算poi矩形的代码
- export function getPoiRect(poi, zoomLevel, wrapper) {
- let style = getStyle(poi.styleId, zoomLevel);
- if (!style) {
- console.warn("style is invalid!");
- return;
- }
- let labelStyle = getStyle(style.labelid, zoomLevel);
- if (!labelStyle) {
- console.warn("labelStyle is invalid!");
- return;
- }
- if (!poi.text) {
- return;
- }
- let charWidth = (TEXTPROP.charWidth || 11.2) * // 11.2是根据测试得到的估值
- (labelStyle.fontSize / (TEXTPROP.fontSize || 13)); // 13是得到11.2时的fontSize
- // 返回2d坐标
- let x = 0;//poi.points[0].x;
- let y = 0;//-poi.points[0].z;
- let path = [];
- let icon = iconSet[poi.styleId];
- let iconWidh = (icon && icon.width) || 32;
- let iconHeight = (icon && icon.height) || 32;
- let multi = /\//g;
- let firstLinePos = [];
- let textAlign = null;
- let baseLine = null;
- let hOffset = (iconWidh / 2) * ICONSCALE;
- let vOffset = (iconHeight / 2) * ICONSCALE;
- switch(poi.direct) {
- case 2: { // 左
- firstLinePos.push(x - hOffset - 2);
- firstLinePos.push(y);
- textAlign = 'right';
- baseLine = 'middle';
- break;
- };
- case 3: { // 下
- firstLinePos.push(x);
- firstLinePos.push(y - vOffset - 2);
- textAlign = 'center';
- baseLine = 'top';
- break;
- };
- case 4: { // 上
- firstLinePos.push(x);
- firstLinePos.push(y + vOffset + 2);
- textAlign = 'center';
- baseLine = 'bottom';
- break;
- };
- case 1:{ // 右
- firstLinePos.push(x + hOffset + 2);
- firstLinePos.push(y);
- textAlign = 'left';
- baseLine = 'middle';
- break;
- };
- default: {
- firstLinePos.push(x);
- firstLinePos.push(y);
- textAlign = 'center';
- baseLine = 'middle';
- }
- }
- path = path.concat(firstLinePos);
- let minX = null, maxX = null;
- let minY = null, maxY = null;
- let parts = poi.text.split(multi);
- let textWidth = 0;
- if (wrapper) {
- // 汉字和数字的宽度是不同的,所以必须使用measureText来精确测量
- let textWidth1 = wrapper.context.measureText(parts[0]).width;
- let textWidth2 = wrapper.context.measureText(parts[1] || '').width;
- textWidth = Math.max(textWidth1, textWidth2);
- } else {
- textWidth = Math.max(parts[0].length, parts[1] ? parts[1].length : 0) * charWidth;
- }
- if (textAlign === 'left') {
- minX = x - hOffset;
- maxX = path[0] + textWidth; // 只用第一行文本
- } else if (textAlign === 'right') {
- minX = path[0] - textWidth;
- maxX = x + hOffset;
- } else { // center
- minX = x - Math.max(textWidth / 2, hOffset);
- maxX = x + Math.max(textWidth / 2, hOffset);
- }
- if (baseLine === 'top') {
- maxY = y + vOffset;
- minY = y - vOffset - labelStyle.fontSize * parts.length;
- } else if (baseLine === 'bottom') {
- maxY = y + vOffset + labelStyle.fontSize * parts.length;
- minY = y - vOffset;
- } else { // middle
- minY = Math.min(y - vOffset, path[1] - labelStyle.fontSize / 2);
- maxY = Math.max(y + vOffset, path[1] + labelStyle.fontSize * (parts.length + 0.5 - 1));
- }
- return {
- min: {
- x: minX,
- y: minY
- },
- max: {
- x: maxX,
- y: maxY
- }
- };
- }
Threejs 开发3D地图实践总结的更多相关文章
- Threejs 开发3D地图实践总结【转】
Threejs 开发3D地图实践总结 前段时间连续上了一个月班,加班加点完成了一个3D攻坚项目.也算是由传统web转型到webgl图形学开发中,坑不少,做了一下总结分享. 1.法向量问题 法线是垂 ...
- 使用three.js开发3d地图初探
three是图形引擎,而web二维三维地图都是基于图形引擎的,所以拿three来开发需求简单的三维地图应用是没什么问题的. 1.坐标转换 实际地理坐标为经度.纬度.高度,而three.js使用的是右手 ...
- Windows 10 新特性 -- Bing Maps 3D地图开发入门(一)
本文主要内容是讲述如何创建基于 Windows Universal App 的Windows 10 3D地图应用,涉及的Windows 10新特性包括 Bing Maps 控件.Compiled da ...
- threejs和3d各种效果的学习
写给即将开始threejs学习的自己,各种尝试,各种记忆.不要怕,灰色的年华终会过去. 一个技术学习的快慢,以及你的深刻程度,还有你的以后遇到这个东西的时候的反应速度,很大程度上,取决于你的博客的深刻 ...
- Three.js实现3D地图实例分享
本文主要给大家介绍了关于利用Three.js开发实现3D地图的实践过程,文中通过示例代码介绍的非常详细,对大家学习或者使用three.js具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习 ...
- iOS开发系列--地图与定位
概览 现在很多社交.电商.团购应用都引入了地图和定位功能,似乎地图功能不再是地图应用和导航应用所特有的.的确,有了地图和定位功能确实让我们的生活更加丰富多彩,极大的改变了我们的生活方式.例如你到了一个 ...
- 转-iOS开发系列--地图与定位
来自: http://www.cnblogs.com/kenshincui/p/4125570.html#autoid-3-4-0 概览 现在很多社交.电商.团购应用都引入了地图和定位功能,似乎地图功 ...
- 初级开发者也能码出专业炫酷的3D地图吗?
好看的3D地图搭建出来,一定是要能为开发者所用与业务系统开发中才能真正地体现价值.基因于此,CityBuilder建立了与ThingJS的通道——直转ThingJS代码,支持将配置完成的3D地图一键转 ...
- 从0开发3D引擎(补充):介绍领域驱动设计
我们使用领域驱动设计(英文缩写为DDD)的方法来设计引擎,在引擎开发的过程中,领域模型会不断地演化. 本文介绍本系列使用的领域驱动设计思想的相关概念和知识点,给出了相关的资料. 上一篇博文 从0开发3 ...
随机推荐
- Python LED
led.py from gpiozero import LED from time import sleep led = LED(17) while True: print "start c ...
- 2.Java 加解密技术系列之 MD5
Java 加解密技术系列之 MD5 序 背景 正文 结束语 序 上一篇文章中,介绍了最基础的编码方式 — — BASE64,也简单的提了一下编码的原理.这篇文章继续加解密的系列,当然也是介绍比较基础的 ...
- vue+vux+axios+vuex+vue-router的项目的理解
本文主要是讲解项目前期的工作,后期考虑再详细说明. 作为一个技术团队如果你们团队选择了上面的技术栈,这说明你们的技术团体对于vue有很熟练的掌握了.在这里我想说明的是前期架构的重要.这里有一遍博客写的 ...
- 【Netty】UDP广播事件
一.前言 前面学习了WebSocket协议,并且通过示例讲解了WebSocket的具体使用,接着学习如何使用无连接的UDP来广播事件. 二.UDP广播事件 2.1 UDP基础 面向连接的TCP协议管理 ...
- Django框架全面讲解
Python的WEB框架有Django.Tornado.Flask 等多种,Django相较与其他WEB框架其优势为:大而全,框架本身集成了ORM.模型绑定.模板引擎.缓存.Session等诸多功能. ...
- springMVC+Mybatis(使用AbstractRoutingDataSource实现多数据源切换时)事务管理未生效的解决办法
业务场景: A.B两个单位,系统部署同一套代码: A.B两系统能相互访问: 要求将数据从A系统同步到B系统,再将反馈信息回发给A: 实际开发情况: 因为系统比较小,最开始设计架构的时候没有考虑到消息互 ...
- java基础(二章)
java基础(二章) 一,变量 1.变量是内存中的一个标识符号,用于存储数据 2.变量命名规则 l 必须以字母.下划线 _ .美元符号 $ 开头 l 变量中,可以包括数字 l 变量中,不能出现特 ...
- 前台跨站点获取session
var sessionId = System.Web.HttpContext.Current.Response.Cookies[System.Web.Security.FormsAuthenticat ...
- openfire极限优化
日志优化 默认是 用info 级别,最好不用openfire原生的打日志方式. 离线消息用存储不打回方式,不要用打回方式 xmpp.offline.type=store_and_drop ...
- PHPCMS V9 为今天或几天前文章加new
今天内发布: {pc:content action="lists" catid="13" order="listorder DESC" nu ...