当前位置: 首页 > 科技观察

Threejs开发3D地图实践总结

时间:2023-03-13 01:33:57 科技观察

前段时间连续工作一个月,加班加点完成一个3D攻城项目。也算是从传统的web向webgl图形开发的过渡,有很多坑,所以做了一个总结和分享。1.法线向量问题法线是垂直于我们要照亮的物体表面的向量。法线表示表面的方向,因此它们在建模光源和物体的相互作用方面起着决定性的作用。每个顶点都有一个关联的法向量。如果一个顶点被多个三角形共享,则共享顶点的法向量等于不同三角形中共享顶点的法向量之和。N=N1+N2;所以如果不做任何处理,直接把3D物体的点传给BufferGeometry,那么因为法向量是合成的,经过片元着色器插值后,就会得到这种暗的效果。我的处理方法要保持顶点的法向量唯一,需要在共享顶点复制一个顶点,重新计算索引。是的,多个面共享的每个顶点都有多个副本,每个副本都有一个单独的方法。矢量图,使每个面都有相同的颜色2.光源和面块颜色在开发过程中设计了一套配色。但是,一旦有了光源,最终的面块颜色就会和光源混合在一起,颜色自然会和最终设计的颜色有很大的不同。以下是兰伯特光照模型的混合算法。而且,产品的要求是在顶面保持设计的色彩,在侧面加入改变光源的效果。在地图上操作时,侧面颜色需要根据视角变化。那么我的处理方式是分别绘制顶面和侧面(创建两个Mesh)。顶面使用MeshLambertMaterial的emssive属性设置自发光颜色与设计颜色一致,所以不会有发光效果。侧面使用Emssive和Emssive组合色来应用灯光效果。3.POI标注使用Sprite类可以创建三个始终面向摄像头的POI,并在画布上绘制文字和图片,画布可以作为纹理贴图放置在Sprite上。但是这里的一个问题是canvas图片会变形,因为sprite的scale没有设置好,导致图片被拉伸或者缩小。解决问题的方法是保证3D世界中的缩放尺寸在经过一系列变换并投射到相机屏幕后,与屏幕上的画布尺寸保持一致。这需要我们计算屏幕像素与3d世界中长度单位的比例,然后将精灵缩放到合适的3d长度。4.点击拾取绘制到屏幕上的3D物体在webgl中会经历以下几个阶段。因此,要在3D应用中点击拾取,首先要将屏幕坐标系转换成ndc坐标系。这时候就得到了ndc的xy坐标。由于2d屏幕没有z值,所以屏幕点转换成3d坐标的z值可以任意选择,一般为0.5(z在-1到1之间)。functionfromSreenToNdc(x,y,container){return{x:x/container.offsetWidth*2-1,y:-y/container.offsetHeight*2+1,z:1};}functionfromNdcToScreen(x,y,container){return{x:(x+1)/2*container.offsetWidth,y:(1-y)/2*container.offsetHeight};}然后将ndc坐标转换成3D坐标:ndc=P*MV*Vec4Vec4=MV-1*P-1*ndc的过程已经在Vector3类中实现三:unproject:function(){varmatrix=newMatrix4();returnfunctionunproject(camera){matrix.multiplyMatrices(camera.matrixWorld,matrix.getInverse(camera.projectionMatrix));returnthis.applyMatrix4(matrix);};}(),将得到的3D点与相机位置结合起来做射线,分别与场景中的物体进行碰撞检测。首先检查物体与周围球体的交点,排除不与球体相交的,与球体相交的保存进入下一步。将外球体和射线相交的所有物体按照离相机的距离排序,然后检查射线和构成物体的三角形的交点。查找相交的对象。当然这个过程也被RayCaster封装在Three中,使用起来非常简单:mouse.x=ndcPos.x;mouse.y=ndcPos.y;this.raycaster.setFromCamera(mouse,camera);varintersects=this.raycaster.intersectObjects(this._getIntersectMeshes(floor,zoom),true);5.性能优化随着场景中的物体越来越多,绘制过程变得越来越耗时,导致移动端几乎无法使用。图形学中有一个很重要的概念叫onedrawall,一次绘制,就是调用绘图API的次数越少,性能越高。比如canvas中的fillRect、fillText等,webgl中的drawElements、drawArrays;所以这里的解决方案是将它们的侧面和顶部表面放入一个BufferGeometry中,用于相同样式的对象。这样可以大大减少绘图API的调用次数,大大提高渲染性能。这样解决了渲染性能的问题,但是带来了另一个问题。现在把所有风格相同的人脸都放在一个BufferGeometry中(我们称之为风格图形),这样点击人脸的时候是无法判断是哪个物体的。(我们称它为对象图形)被选中,对象不能高亮和缩放。我的处理方式是将所有的物体图形单独存放在内存中,利用这部分数据进行点面时的相交检测。对于选中对象后的高亮缩放处理,先将样式面相应部分裁掉,然后将选中对象图形添加到场景中,进行缩放高亮。裁剪方式是记录每个对象在样式图中的实际索引位置,需要裁剪时将这部分索引置零。在需要恢复的地方,将这部分索引恢复到原来的状态。6.点击表面移动到屏幕中央也遇到了很多坑。第一个思路是:表面的中心点当前在世界坐标系的坐标中。首先使用center.project(camera)获取规范化的设备坐标。根据ndc获取屏幕坐标,然后根据曲面中心点的屏幕坐标和屏幕中心点坐标进行插值得到偏移量,根据OribitControls中的pan方法更新相机位置。这种做法以失败告终,因为相机可能会进行各种变换,所以屏幕坐标的偏移与3d世界坐标系中的位置关系不是线性对应的。最终的思路是:我们现在要把点击面的中心点移动到屏幕的中心。屏幕中心的ndc坐标始终为(0,0)。也就是说,我们需要以曲面的中心点作为我们的观察点(屏幕中心永远是摄像头的观察视线),这里我们可以直接使用所谓的观察点视线在表面的中心,并使用lookAt方法获取相机矩阵,但是如果这种简单处理后的效果会给人一种相机姿态发生变化的感觉,也就是会觉得它没有平移,所以我们需要做的是保持相机的当前位姿,并以面部中心作为相机观察点。回想一下,在平移过程中将屏幕移动转换为相机变化的过程是了解屏幕偏移以找到目标。这里我们需要做的是知道target反转屏幕偏移的过程。首先根据当前目标和地表中心计算相机的偏移向量,根据相机的偏移向量计算相机在x轴和up轴上的投影长度,返回平移量根据投影长度应该在屏幕上。this.unprojectPan=function(deltaVector,moveDown){//vargetProjectLength()varelement=scope.domElement===document?scope.domElement.body:scope.domElement;varcxv=newVector3(0,0,0).setFromMatrixColumn(范围.object.matrix,0);//相机x轴varcyv=newVector3(0,0,0).setFromMatrixColumn(scope.object.matrix,1);//相机y轴//相机轴为单位向量varpxl=deltaVector.dot(cxv)/*/cxv.length()*/;//向量在相机x轴上的投影varpyl=deltaVector.dot(cyv)/*/cxv.length()*//;//向量在相机y轴的投影//offset=dx*vector(cx)+dy*vector(cy.project(xoz).normalize)//offset由相机的投影组成xoz平面上的x轴方向向量+相机y轴向量//perspectivevarposition=scope.object.position;varoffset=newVector3(0,0,0);offset.copy(position).sub(scope.target);vardistance=offset.length();distance*=Math.tan(scope.object.fov/2*Math.PI/180);//varxd=2*distance*deltaX/element.clientHeight;//varyd=2*distance*deltaY/element.clientHeight;//panLeft(xd,scope.object.matrix);//panUp(yd,scope.object.matrix);vardeltaX=pxl*element.clientHeight/(2*distance);vardeltaY=pyl*element.clientHeight/(2*distance)*(moveDown?-1:1);返回[deltaX,deltaY];}elseif(scope.objectinstanceofOrthographicCamera){//orthographic//panLeft(deltaX*(scope.object.right-scope.object.left)/scope.object.zoom/element.clientWidth,scope.object.矩阵);//panUp(deltaY*(scope.object.top-scope.object.bottom)/scope.object.zoom/element.clientHeight,scope.object.matrix);vardeltaX=pxl*element.clientWidth*scope.object.zoom/(scope.object.right-scope.object.left);vardeltaY=pyl*element.clientHeight*scope.object.zoom/(scope.object.top-scope.object.bottom);返回[deltaX,deltaY];}else{//cameraneitherorthographicnorperspectiveconsole.warn('WARNING:OrbitControls.jsencounteredanunknowncameratype-pandisabled.');}}7.2/3D切换的主要内容23D切换是当相机的视线轴垂直于场景的平面,使用平行投影,让用户只能看到顶面,给人的感觉是二维视图,所以平行投影的世界场景体积必须根据透视截锥来计算。因为用户会在2D和3D场景中做很多操作,比如平移、缩放、旋转等。要无缝切换,关键是要保持平行投影与平截头相机的位置和lookAt方法一致;并放大和缩小它们。关键点:距离的比例与缩放一致。在平行投影中,放大倍数越大,六面体的第一边和最后边的面积越小,放大倍数越大。8.Thegeographiclevelin3DisactuallythecorrespondencebetweenpixelsandmetersintheMercatorcoordinatesystem.Therearegeneralstandardsandcalculationformulasforthis:r=6378137resolution=2*PI*r/(2^zoom*256)each级别中像素与米的对应关系如下:resolutionzoom2048blocksize256blocksizescale(dpi=160)156543.03390320600133.540075016.69986097851.578271.516961160300066.720037508.34493048925.839135.75848280150033.3710018754.17246524462.919567.87924340075016.695009377.086123262231.49783.939621420037508.342504688.54361631115.724891.96981510018754.171252344.27130815557.862445.98490565009377.086626172.135715407778.931222.99245372504688.543313086.06797703889.465611.496226381252344.271156543.03393851944.732305.74811319626172.135778271.516961925972.366152.874056610313086.067939135.75848962986.183176.437028311156543.033919567.87924481493.091638.21851411278271.516969783.939621240746.545819.10925711339135.758484891.96981120373.27299.55462851419567.879242445.98490560186.636454.7773143159783.9396211222.99245330093.318222.3886571164891.96981611.496226315046.659111.1943286172445.984905305.74811317523.3295560.5971643181222.992453152.87405663761.6647780.298582119611.496226376.437028291880.8323890.149291120305.748113138.21851414940.41619450.0746455210.0373227223D中的计算策略是,首先需要将3D世界中的坐标与墨卡托单位的对应关系搞清楚,如果已经是以mi来做单位,那么可以直接将相机投射画面的高度与画面的像素点数进行比较,将结果与上面的排名进行比较,选择不同级别的数据和尺度。请注意,3D地图中的比例尺并非在所有屏幕上都可用。位置和现实世界都满足这个尺度。只能说在屏幕位置的相机中心的像素满足这个关系,因为平行投影有近的大,远的小的效果。9.POI碰撞由于标签始终面向摄像头,所以标签的碰撞就是将标签点转换到屏幕坐标系中,使用宽高计算矩形相交问题。至于具体的碰撞算法大家可以在网上查到,这里就不展开了。下面是计算poi矩形的代码