WebGL可以用来做3D效果的全景图呈现,例如故宫的全景图。但有时候我们不仅仅只是呈现全景图,还需要增加互动。故宫里边可以又分了很多区域,例如外朝中路、外朝西路、外朝东路等等。我们需要在3D图上做一些标记表示某个小的区域。当点击这个标记时,界面切换到对应标记区域的全景图。下图是实现此功能的一个小DEMO:
如何实现这样的功能?通过本篇的介绍,我们可以了解到以上交互过程的代码实现方式。这里我先提出几个问题
1).如何获取3D全景图某个地址的3D坐标?
2).如何将获取的地址的3D坐标转换为屏幕上的2D坐标?
3).在旋转3D全景图时,如何让3D坐标对应的2D屏幕坐标跟着移动?
4).如何确认一个标记点是在相机的可视区域?
搞清楚以上问题,全景图的标记功能也就轻而易举了。接下来我们就围绕每个问题来实现功能。
varraycasterCubeMesh;varraycaster=newTHREE.Raycaster();varmouseVector=newTHREE.Vector3();vartags=[];这里需要在document上注册mousemove事件,实时获取鼠标对应的3D坐标。事件代码如下:
鼠标移动时,能够获取到3D坐标了。如何确认这个坐标就是我们需要的?这里还得给docuent注册一个mousedown事件。通过右键点击确认。注册事件如下:
接着代码创建了一个tagElement元素,设置样式和内容。并且附加到WebGL容器中。tagMesh自定义了updateTag函数,里边调用了两个特别重要的函数:toScreenPosition和isOffScreen。这里先不忙介绍updateTag函数。接下来通过介绍这两个函数来回答剩下的问题。
如果熟悉GIS的同学,应该知道什么叫做投影。我们将3D坐标映射到2D坐标的过程就叫做投影。toScreenPosition正是使用投影功能做的转换。函数代码如下:
functiontoScreenPosition(obj,camera){varvector=newTHREE.Vector3();varwidthHalf=0.5*renderer.context.canvas.width;varheightHalf=0.5*renderer.context.canvas.height;obj.updateMatrixWorld();vector.setFromMatrixPosition(obj.matrixWorld);vector.project(camera);vector.x=(vector.x*widthHalf)+widthHalf;vector.y=-(vector.y*heightHalf)+heightHalf;return{x:vector.x,y:vector.y};}widthHalf和heightHalf分别表示canvas容器的宽度和高度的一半。接着更新obj对象的全局坐标。然后把vector的位置指向obj的全局坐标,之后调用viector.project(camera)将vector以相机为参考,转换为2D坐标。但此时的2D坐标是笛卡尔坐标。原点在中间位置,需要转换为屏幕坐标(原点在左上角)。最后返回的即是我们需要的2D坐标了。
之前没有介绍tagMesh的updateTag函数,这里我们再看下该函数:
tagMesh.updateTag=function(){if(isOffScreen(tagMesh,camera)){tagElement.style.display="none";}else{tagElement.style.display="block";varposition=toScreenPosition(tagMesh,camera);tagElement.style.left=position.x+"px";tagElement.style.top=position.y+"px";}}这里只看else中代码,设置元素的display为block,让其可见。然后调用toScreenPosition(tagMesh,camera)获取tagMesh3D对象投影在屏幕上的坐标,所有我们直接设置给tagElement样式的left和top。这只是第一步。如果全景图旋转了,tagElement和tagMesh位置又对应不上了。所有在每次渲染时还得调用该函数去执行更新2D坐标。
functionrender(){controls.update();tags.forEach(function(tagMesh){tagMesh.updateTag();});renderer.render(scene,camera);requestAnimationFrame(render);}上面代码遍历了所有的标记集合,每次渲染都更新一次。以上两个步骤就实现了3D坐标和2D屏幕坐标的联动。
如何只按照以上的介绍来实现功能,会发现一个问题。每添加一个标记,我们在旋转全景图时发现相机的前后都会显示这个标记。这因为2D坐标没有z方向,所以空间上会有两个对称点投影到相同的2D平面上。如何解决?看最后一个问题。
我们知道相机有可视区域,如果一个3D坐标在可视区域内,那么它投影到屏幕上的坐标需要显示。而如果该3D坐标不在相机的可视区域,那么我们就不应该把该点投影到屏幕上。Three.js提供了Frustum对象解决这类问题。我们通过调用isOffScreen函数,判断3D对象是否是离屏的。代码如下:
functionisOffScreen(obj,camera){varfrustum=newTHREE.Frustum();//Frustum用来确定相机的可视区域varcameraViewProjectionMatrix=newTHREE.Matrix4();cameraViewProjectionMatrix.multiplyMatrices(camera.projectionMatrix,camera.matrixWorldInverse);//获取相机的法线frustum.setFromMatrix(cameraViewProjectionMatrix);//设置frustum沿着相机法线方向return!frustum.intersectsObject(obj);}首先创建Frustum对象,然后创建一个4*4矩阵对象。接下来的一行代码把cameraViewProjectMatrix转换为相机的法线矩阵。直接把它设置到frustum对象上。
接着调用frustum.intersectObject函数判断obj是否在frustum的可视区域内。至于内部的实现逻辑,大家可查看Three.js的源代码了解。
以上即是实现全景图标记的核心代码。至于全景图如何创建,可以从我的github上下载源代码查看。地址: