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 ...
随机推荐
- 那些年,让我们一起着迷的Spring
构建企业级应用框架(SpringMVC+Spring+Hibernate/ibatis[Mybatis]) 框架特点:半成品,封装了特定的处理流程和控制逻辑,成熟的,不断升级的软件.重用度高,开发效率 ...
- fiddler导致页面确定按钮无法使用(测试遇到的问题经验)
这几天在测试的是遇到几个问题,就是在删除或者保存有些提示信息的时候 比如下面这种: 点击确定的时候,一直无响应,换了几台电脑其他电脑都是正常的,本机清楚缓存.关闭浏览器重新打开.重启电脑都试过了了就是 ...
- Android Weekly Notes Issue #258
Android Weekly Issue #258 May 21st, 2017 Android Weekly Issue #258 本期内容: 围绕着Google I/O的热潮, 本周的posts除 ...
- 1.Java第一课:初识java
今天也算是正式地开始学习Java了,一天学的不是太多,旨在入门了解Java.还好现在学的都是基础,也能赶得上进度,希望以后能一直保持这种精神状态坚持学下去.下面就简单来说说今天所学的内容吧. 1计算机 ...
- 阿里云 Centos7.3安装mysql5.7.18 rpm安装
卸载MariaDB CentOS7默认安装MariaDB而不是MySQL,而且yum服务器上也移除了MySQL相关的软件包.因为MariaDB和MySQL可能会冲突,故先卸载MariaDB. 1.安装 ...
- DOUAudioStreamer 中的DOUAudioFileProvider理解笔记
概览 DOUAudioFileProvider读取音频文件local,ipod-library,remote audiofile(通过DOUSimpleHTTPRequest封装的CFHTTPMess ...
- mysql revise
DATABASE create database db_name; use db_name; alter database db_name; drop database db_name; show d ...
- maven无法加载本地jar包以及maven项目打包后本地jar包没有打进项目的问题解决办法
1.首先设置依赖项,这样maven就会把该路径下的jar包导入项目引用 <dependency> <groupId>DPSDK-Manager</groupId> ...
- SpringMVC的form:form表单的使用
为什么要使用SpringMVC的form:form表单,有两个原因:一是可以更加快捷的完成表单的开发,比如会替你做好数据类型装换等本来需要你自己动手的工作.其次就是能够更加方便的实现表单回显. 首先要 ...
- Discuz插件开发之全站论坛目录结构注释
基本上新版本的discuzX系列目录结构都差不多,刚好大神整理出来了,就拿来看吧. |-- /api uc.php UCenter通信文件 |-- /api/addon ...