宝哥软件园

三网融合开发和实现三维地图的实践过程总结

编辑:宝哥软件园 来源:互联网 时间:2021-09-03

本文主要介绍了trial . js的开发基础和基本原理,以及如何实现三维全景图。为了在web端实现3D全景的效果,除了全景图片和WebGL之外,还有很多细节需要处理。据我所知,krpano是国外最好的3D全景,国内很多3D全景服务都使用KrPano工具。

前段时间连续上班一个月,加班完成一个3D攻坚项目。也可以看作是从传统web到webgl图形开发的转变,有很多漏洞,所以我做了总结和分享。

三. js

为了简化WebGL开发的复杂度,降低入门难度,mrdoob)封装了一个基于WebGL标准的轻量级JS 3D库—— Three.js。

在我看来,Three.js有以下特点:

3D开发所需的功能齐全完整,基本使用WebGL可以达到的效果,使用Three.js可以更简单的实现易用的架构,设计清晰合理,通俗易懂,扩展性更好,开发效率比WebGL开源项目更高,有一群积极的贡献者。在持续的维护和升级过程中,three.js让WebGL更易于使用,可以实现很棒的3D效果,比如:

游戏hellorun数据可视化armsglobe1。法向量问题

法线是垂直于我们要照明的物体表面的矢量。法线代表曲面的方向,因此在光源和对象之间的交互建模中起着决定性的作用。每个顶点都有一个相关的法向量。

如果一个顶点被多个三角形共享,则共享顶点的法向量等于不同三角形中共享顶点的法向量之和。n=N1N2;

因此,如果您不做任何处理,直接将3 D对象的点转移到BufferGeometry,那么因为法向量是合成的,在由面片着色器插值之后,您将获得这种黑色效果

我的处理方法保持顶点的法向量唯一,所以我们需要在共享顶点复制一个顶点,并重新计算索引。是的,多个面共享的每个顶点都有多个副本,每个副本都有一个单独的法向量,这样每个面都可以有相同的颜色

2.光源和瓷砖的颜色

在开发过程中,设计给出了一套配色,但是一旦有了光源,瓷砖的最终颜色就会和光源混合在一起,颜色自然和最终的设计颜色有很大的不同。以下是兰伯特光照模型的混合算法。

而且产品的要求是顶面要保持设计的颜色,侧面要加入光源变化效果。操作地图时,侧面颜色要根据视角变化。然后我的处理方法是分别绘制顶面和侧面(创建两个Mesh)。顶面使用MeshLambertMaterial的emsfour属性设置自发光颜色与设计颜色一致,这样就不会有灯光效果,侧面使用emsfour和颜色来应用光源效果。

var material 1=new _ _ WEBPACK _ IMPORTED _ MODULE _ 0 _ three _ _[' MeshLambertMaterial ']({ emissiv e : new _ _ WEBPACK _ IMPORTED _ MODULE _ 0 _ three _ _[' Color '])(style . fill style[0],style.fillStyle[1],style.fillStyle[2]),side : _ _ WEBPACK _ IMPORTED _ MODULE _ three _[' flat shading '],vertexvar material 2=new _ _ WEBPACK _ IMPORTED _ MODULE _ 0 _ three _ _[' MeshLambertMaterial ']({ Color : new _ _ WEBPACK _ IMPORTED _ MODULE _ 0 _ three _ _[' Color '](style . fillstyle[0]* 0.1,style.fillStyle[2] * 0.1),emisssive : new _ _ WEBPACK _ IMPORTED _ MODULE _ 0 _ three _ _[' Color '](style . fillstyle[0]* 0.9,3.兴趣点标签

Sprite类可以用来创建始终面向三个镜头的兴趣点。同时,文本和图片可以在画布上绘制,画布可以放在Sprite上作为纹理贴图。但是这里有一个问题是画布图像会失真,因为sprite的比例设置不合理,导致图像被拉伸或缩放时失真。

解决问题的方法是在相机屏幕上经过一系列变换和投影后,保证3d世界中的缩放尺寸与屏幕上画布的尺寸一致。这需要我们计算3d世界中屏幕像素与长度单位的比率,然后将sprite缩放到合适的3d长度。

4.单击以选择问题

在webgl的屏幕上绘制三维对象将经历以下几个阶段

因此,为了在3d应用程序中点击和拾取,我们必须首先将屏幕坐标系转换为ndc坐标系。这时,我们得到了ndc的xy坐标。由于2d屏幕没有Z值,转换成3D坐标的屏幕点的Z可以任意选择,一般取0.5(z在-1到1之间,

函数from sreenondc(x,y,container){ return { x : x/container . offset with * 2-1,y :-y/container . offset heat * 2 1,z : 1 };} function fromndctocscreen(x,y,container){ return { x :(x 1)/2 * container . offsetwith,y :(1-y)/2 * container . offsetheight };}然后将ndc坐标转换为3D坐标:ndc=P * MV * Vec4Vec4=MV-1 * P -1 * ndc。这个过程已经在Vector3的三个类中实现:

unproject:函数(){ var矩阵=new matrix 4();返回函数unobject(camera){ matrix . multipleymatrice(camera . matrix world,matrix . getinverse(camera . projectionmatrix));返回this.applyMatrix4(矩阵);};}(),将获得的3d点与相机位置结合成一条光线,分别与场景中的物体进行碰撞检测。首先检测与物体外球的交点,消除与球的不相交,保留与球的交点进入下一步。所有与光线相交的物体都根据与摄像机的距离进行排序,然后检测光线与构成物体的三角形之间的交点。查找相交对象。当然,这个过程也被RayCaster封装在三个中,使用起来非常简单:

mouse . x=ndcpos . x;mouse . y=ndcpos . y;this.raycaster.setFromCamera(鼠标、相机);var intersections=this . ray caster . intersection objects(this。_ getIntersectMeshes(地板,缩放),true);5.性能优化

随着场景中的物体越来越多,绘制过程越来越耗时,使得手机几乎无法使用。

在图形中有一个非常重要的概念叫做“一画全”,这意味着绘图api被调用的次数越少,性能越高。例如,画布中的填充区域和填充文本、绘图元素和绘图数组;在webgl中;因此,这里的解决方案是将相同样式的对象的侧面和顶面放在一个缓冲测量中。这样可以大大减少绘图api的调用次数,大大提高渲染性能。

这解决了渲染性能的问题,但也带来了另一个问题。现在,所有具有相同样式的面都被放置在一个BufferGeometry(我们称之为样式图)中,因此在单击面时无法判断选择了哪个对象(我们称之为对象图),也无法高亮显示和缩放该对象。我的处理方法是将所有的对象单独生成保存在内存中,点击曲面时用这些数据做相交检测。对于选中对象后的高亮和缩放处理,先将样式表面对应的部分切掉,然后将选中的对象图形添加到场景中进行缩放和高亮。裁剪方法是在样式图中记录每个对象的实际索引位置,需要裁剪时使这个索引为零。将这部分索引恢复到需要恢复的原始状态。

6.单击移动到屏幕中心

这部分也遇到了很多坑。第一个想法是:

目前,平面的中心点是世界坐标系中的坐标。首先通过center.project(摄像机)得到归一化的设备坐标,然后根据ndc得到屏幕坐标,再通过平面中心点的屏幕坐标与屏幕中心坐标之间的插值得到偏移量,在OribitControls中按照pan方法更新摄像机位置。这种方法以失败告终,因为相机可能会进行各种变换,所以屏幕坐标的偏移量与3d世界坐标系中的位置关系并不是线性对应的。

最后的想法是:

现在我们想把点击表面的中心点移到屏幕的中心。屏幕中心的ndc坐标始终为(0,0)。我们观察视线的焦点和近景面的ndc坐标也是0,0;也就是说,我们应该把人脸的中心作为我们的观察点(屏幕的中心永远是相机的观察视线)。在这里,我们可以使用lookAt方法从所谓的人脸中心观测点直接计算相机矩阵。但是如果这样简单的话,效果会给人一种相机的姿态发生了变化的感觉,也就是不会被平移,所以我们要做的就是保持相机当前的姿态,以人脸中心为相机观测点。

回想一下,在翻译的时候,把屏幕移动转化为相机变化的过程,就是知道屏幕偏移,找到目标。这里我们需要做的是了解目标推回屏幕偏移量的过程。首先根据当前目标和平面中心计算摄像机的偏移向量,根据摄像机的偏移向量计算摄像机在X轴和上轴上的投影长度,根据投影长度可以推导出屏幕上的平移量。

this . unprojectpan=function(deltaVector,move down){//var getproject length()(var element=scope . domment===document?scope . DOM element . body : scope . DOM element;var cxv=新Vector3(0,0,0)。setFromMatrixcolumn(scope . object . matrix,0);//摄像机x轴varcyv=newvector3 (0,0,0)。set from matrix column(scope . object . matrix,1);//相机y轴//相机轴均为单位矢量varpxl=deltavector . dot(cxv)/*/cxv . length()*/;//矢量在摄像机X轴上的投影var Pyl=delta vector . dot(cyv)/*/cyv . length()*/;//矢量在摄像机Y轴上的投影//偏移量=DX *矢量(CX) Dy *矢量(cy。项目(xoz)。归一化)//偏移量由摄像机X轴方向向量和摄像机Y轴量在XOZ平面上的投影组成。var dv=deltaVector . clone();dv . sub(cxv . multiplyscalar(pxl));pyl=dv . length();if(scope . object instance of perspectivcamera){//perspective var position=scope . object . position;var offset=new Vector3(0,0,0);offset.copy(位置)。sub(scope . target);var距离=offset . length();距离*=Math . tan(scope . object . FOV/2 * Math。PI/180);//var xd=2 * distance * deltaX/element . client height;//var yd=2 * distance * deltaY/element . client height;//panLeft(xd,scope . object . matrix);//panUp(yd,scope . object . matrix);var deltaX=pxl * element . client height/(2 * distance);var deltaY=pyl * element . client height/(2 * distance)*(move down?-1 : 1);返回[deltaX,DeltaY];} else if(scope . object instance of OrthographicCamera){//正交//panLeft(deltaX *(scope . object . right-scope . object . left)/scope . object . zoom/element . client width,scope . object . matrix);//PanUP(deltaY *(scope . object . top-scope . object . bottom)/scope . object . zoom/element . client height,scope . object . matrix);var deltaX=pxl * element . client width * scope . object . zoom/(scope . object . right-scope . object . left);var deltaY=pyl * element . client height * scope . object . zoom/(scope . object . top-scope . object . bottom);返回[deltaX,DeltaY];} else { //相机既不是正交的也不是透视的console . warn(' warn : orbitcontrols . js遇到未知的相机类型-平移被禁用。);} }7,2/3D切换

23D切换的主要内容是当相机的视线轴垂直于场景平面时,使用平行投影,这样用户只能看到顶面作为2D视图。所以世界场景的平行投影要根据透视圆锥来计算。

因为用户会在2D和3D场景中做很多操作,比如平移、缩放、旋转等,无缝切换的关键是保持平行投影与锥形相机的位置和lookAt模式一致;而放大缩小它们的关键点:距离的比例与缩放一致。

在平行投影中,缩放越大,六面体的第一面和第二面的面积越小,放大率越大。

8.3D中的地理级别

地理级别实际上是墨卡托坐标系中像素和米的对应关系,有通用的标准和计算公式:

R=6378137分辨率=2 * pi * r/(2变焦* 256)每一级像素与米的对应关系如下:

分辨率缩放2048块大小256块大小比例(dpi=160)156543.0339 0 320600133.5 40075016.69 986097851 . 51696 1 16030066 . 7 20037508 . 344444445???首先要搞清楚三维世界中坐标和墨卡托单位的对应关系。如果已经使用mi作为单位,可以直接将摄像头投影屏幕的高度与屏幕上的像素数进行比较,将结果与上面的排名进行比较,选择不同的级别数据和比例。请注意,3D地图中的比例并不是在所有屏幕上的所有位置和现实世界中都满足这个比例,而是相机中心点的屏幕位置的像素满足这个关系,因为平行投影有近大远小的效果。

9.poi碰撞

由于注记始终面向摄像机,因此注记的碰撞就是将注记点变换到屏幕坐标系中,计算出宽高矩形的交点。至于具体的碰撞算法,大家可以在网上找到,这里就不展开了。下面是计算poi矩形的代码

导出函数getPoiRect(poi,zoomLevel,wrapper){ let style=getStyle(poi。样式id,zoomLevel);if(!style) { console.warn('style无效!');返回;}让labelStyle=getStyle(样式。labelid,zoomLevel);if(!标签样式){ console.warn('标签样式无效!');返回;} if(!poi。text){ return;}让charWidth=(文本道具。charWidth | | 11.2)*//11.2是根据测试得到的估值(标签样式。字体大小/(文本道具。字号| | 13));//13是得到11.2时的fontSize //返回2d坐标设x=0;//poi.points[0].x;设y=0;//-poi.points[0].z;让路径=[];让图标=图标集[poi。样式id];让iconWidh=(图标图标。宽度)| | 32;让图标高度=(图标图标。身高)| | 32;让multi=///g;让first linePoS=[];让文本对齐=空让基线=空让hOffset=(iconWidh/2)*图标比例;让vOffset=(图标高度/2)*图标比例;开关(poi。直接){ case 2: {//左第一行PoS。push(x-HofFest-2);第一行PoS。push(y);文本对齐='右'基线='中间;打破;};案例3: { //下第一行PoS。push(x);第一行PoS。push(y-VOffset-2);textAlign="居中";基线='顶部打破;};案例4: { //上第一行PoS。push(x);第一行PoS。push(y VOffset 2);textAlign="居中";基线='底部;打破;};案例1:{ //右第一行PoS。push(x HofFest 2);第一行PoS。push(y);文本对齐='左'基线='中间;打破;};默认值: {第一行位置。push(x);第一行PoS。push(y);textAlign="居中";基线='中间;} }路径=路径。第一行;让minX=null,maxX=null设minY=null,maxY=nulllet parts=poi。文字。拆分(多);让textWidth=0;如果(包装){ //汉字和数字的宽度是不同的,所以必须使用测量文本来精确测量让文本宽度1=包装。语境。测量文本(第[0]部分).宽度;让文本宽度2=包装。语境。测量文本(第[1]| | ' ')部分.宽度;textWidth=Math.max(textWidth1,文本宽度2);} else { text width=math。最大值(零件[0]).长度,零件[1]?零件[1]。长度: 0)* charWidth;} if(TextAlign===' left '){ MinX=x-HoffSet;maxX=路径[0]文本宽度;//只用第一行文本} else if(textAlign===' right '){ minX=path[0]-textWidth;maxX=x hOffset } else {//center minX=x-math。最大值(文本宽度/2,hOffset);maxX=x Math.max(textWidth/2,hOffset);} if(baseLine===' top '){ MaxY=y VOffset;minY=y-VOffset-标签样式。字体大小*零件。长度;} else if(baseLine===' bottom '){ MaxY=y VOffset标签样式。字体大小*零件。长度;minY=y-VOffset;} else {//middle minY=math。min(y-vOffset,路径[1]-标签样式。字号/2);maxY=Math.max(y vOffset,路径[1]labelstyle。字体大小*(部分)。长度0.5-1));}返回{ min: { x: minX,y: minY },max: { x: maxX,y: maxY } }}总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

更多资讯
游戏推荐
更多+