如何在WebGL全景图上做标记

WebGL可以用来做3D效果的全景图呈现,例如故宫的全景图。但有时候我们不仅仅只是呈现全景图,还需要增加互动。故宫里边可以又分了很多区域,例如外朝中路、外朝西路、外朝东路等等。我们需要在3D图上做一些标记表示某个小的区域。当点击这个标记时,界面切换到对应标记区域的全景图。下图是实现此功能的一个小DEMO:

如何实现这样的功能?通过本篇的介绍,我们可以了解到以上交互过程的代码实现方式。这里我先提出几个问题

1).如何获取3D全景图某个地址的3D坐标?

2).如何将获取的地址的3D坐标转换为屏幕上的2D坐标?

3).在旋转3D全景图时,如何让3D坐标对应的2D屏幕坐标跟着移动?

4).如何确认一个标记点是在相机的可视区域?

搞清楚以上问题,全景图的标记功能也就轻而易举了。接下来我们就围绕每个问题来实现功能。

如何获取3D全景图的某个地址的3D坐标?

一般获取景区上某个地址的标记,都是通过手动获取的。因为这些标记是无规律可寻的。所以我们就得考虑如何通过手动去获取3D图上的某个地址。人机交互时通过鼠标来操作,但鼠标是2D坐标,需要转换到对应的3D坐标上。Three.js为我们提供了Raycaster对象,我们可以很轻松的获取到一个2D点对应的3D坐标。先声明几个对象:

var raycasterCubeMesh;
var raycaster = new THREE.Raycaster();
var mouseVector = new THREE.Vector3();
 var tags = [];

这里需要在document上注册mousemove事件,实时获取鼠标对应的3D坐标。事件代码如下:

function onMouseMove(event){
            mouseVector.x = 2 * (event.clientX / window.innerHeight) - 1;
            mouseVector.y = - 2 * (event.clientY / window.innerHeight) + 1;

            raycaster.setFromCamera(mouseVector.clone(), camera);
            var intersects = raycaster.intersectObjects([cubeMesh]);

            if(raycasterCubeMesh){
                scene.remove(raycasterCubeMesh);
            }
            activePoint = null;
            if(intersects.length > 0){
                var points = [];
                points.push(new THREE.Vector3(0, 0, 0));
                points.push(intersects[0].point);

                var mat = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.5});
                var sphereGeometry = new THREE.SphereGeometry(100);

                raycasterCubeMesh = new THREE.Mesh(sphereGeometry, mat);
                raycasterCubeMesh.position.copy(intersects[0].point);
                scene.add(raycasterCubeMesh);
                activePoint = intersects[0].point;
            }
        }

代码中的大部分我已经在“如何实现对象交互”有介绍。这里只介绍和当前功能相关代码。intersects包含了鼠标当前位置下拾取到的3D对象集合。如果长度大于0,表示已经拾取到3D对象了。由于我们给intersectObjects函数只传递了cubeMesh对象(即全景图),所以intersects的长度肯定为1。intersects[0].point表示鼠标投射到cubeMesh对象表面上的坐标。这个坐标正是我们需要的3D标记点。所以我把这个点存储在activePoint。raycasterCubeMesh直接用交互点作为中心画的一个球体,鼠标移动这个球体也就跟着移动。

鼠标移动时,能够获取到3D坐标了。如何确认这个坐标就是我们需要的?这里还得 给docuent注册一个mousedown事件。通过右键点击确认。注册事件如下:

function onMouseDown(event){
            if(event.buttons === 2 &&  activePoint){
                var tagMesh = new THREE.Mesh(new THREE.SphereGeometry(1), new THREE.MeshBasicMaterial({color: 0xffff00}));
                tagMesh.position.copy(activePoint);
                tagObject.add(tagMesh);

                var tagElement = document.createElement("div");
                tagElement.innerHTML = "<span>标记" + (tags.length + 1) + "</span>";
                tagElement.style.background = "#00ff00";
                tagElement.style.position  = "absolute";
                tagElement.addEventListener("click", function(evt){
                    alert(tagElement.innerText);
                });
                tagMesh.updateTag = function(){
                    if(isOffScreen(tagMesh, camera)){
                        tagElement.style.display = "none";
                    }else{
                        tagElement.style.display = "block";
                        var position = toScreenPosition(tagMesh, camera);
                        tagElement.style.left = position.x + "px";
                        tagElement.style.top = position.y + "px";
                    }
                }
                tagMesh.updateTag();
                document.getElementById("WebGL-output").appendChild(tagElement);
                tags.push(tagMesh);
            }
        }

代码第一行有if判断,只有鼠标右键触发,并且activePoint不为空,才执行下面的代码。首先创建一个球体tagMesh并且设置坐标为activePoint,然后把它添加到tagObject对象中。tagObject是一个Object3D对象,用来存放所有的tagMesh,便于统一管理。

接着代码创建了一个tagElement元素,设置样式和内容。并且附加到WebGL容器中。tagMesh自定义了updateTag函数,里边调用了两个特别重要的函数:toScreenPosition和isOffScreen。这里先不忙介绍updateTag函数。接下来通过介绍这两个函数来回答剩下的问题。

如何将获取的地址的3D坐标转换为屏幕上的2D坐标?

如果熟悉GIS的同学,应该知道什么叫做投影。我们将3D坐标映射到2D坐标的过程就叫做投影。toScreenPosition正是使用投影功能做的转换。函数代码如下:

function toScreenPosition(obj, camera){
            var vector = new THREE.Vector3();
            var widthHalf = 0.5 * renderer.context.canvas.width;
            var heightHalf = 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坐标了。

在旋转3D全景图时,如何让3D坐标对应的2D屏幕坐标跟着移动?

之前没有介绍tagMesh的updateTag函数,这里我们再看下该函数:

tagMesh.updateTag = function(){
                    if(isOffScreen(tagMesh, camera)){
                        tagElement.style.display = "none";
                    }else{
                        tagElement.style.display = "block";
                        var position = toScreenPosition(tagMesh, camera);
                        tagElement.style.left = position.x + "px";
                        tagElement.style.top = position.y + "px";
                    }
                }

这里只看else中代码,设置元素的display为block,让其可见。然后调用toScreenPosition(tagMesh, camera)获取tagMesh 3D对象投影在屏幕上的坐标,所有我们直接设置给tagElement样式的left和top。这只是第一步。如果全景图旋转了,tagElement和tagMesh位置又对应不上了。所有在每次渲染时还得调用该函数去执行更新2D坐标。

function render(){
            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对象是否是离屏的。代码如下:

function isOffScreen(obj, camera){
            var frustum = new THREE.Frustum(); //Frustum用来确定相机的可视区域
            var cameraViewProjectionMatrix = new THREE.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上下载源代码查看。地址:

https://github.com/heavis/threejs-demo

时间: 2024-07-28 19:01:56

如何在WebGL全景图上做标记的相关文章

在Google map图上做标记,并把标记相连接

1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>GeoLocation</title> 5 <meta name="viewport" content="initial-scale=1.0, user-scalable=no"> 6 <meta charset="utf-8"> 7 <style> 8 html

如何在Java平台上使用脚本语言做Java开发

如何在Java平台上使用脚本语言做Java开发     最近开始流行区分Java平台和Java语言,但很多Java开发者还是不能确定如何在 Java应用程序开发中结合脚本.本篇文章,Gregor Roth给出了在Java平台上使用脚本的方法.通过这篇文章,你可以了解怎样在你的Java应用程序中使用脚本,是否你要通过使用Groovy和 Jython把不同的Java应用程序模块粘合在一起,或者写一个你自己的基于JRuby的应用程序,适用于Java平台. 作为一个Java开发者,你可能已经注意到了,J

如何在VMware软件上安装Red hat(红帽)Linux6.9操作系统

本文介绍如何在VMware软件上安装Redhat(红帽)Linux6.9操作系统 首先需要准备 VMware软件和Redhat-Linux6.9操作系统的ISO系统镜像文件包(这里以linux6.9为例) 第一步: 打开VMware软件,在主页中点击创建新的虚拟机,也可以点击左上角文件,在列表中点击新建虚拟机,如图标记处: 第二步: 点击新建虚拟机后,会弹出个向导窗口,新手建议选择典型配置(较为简单),有一定经验的可以选择自定义(高级)配置.(这里以自定义模式为例)选择好后点击下一步 第三步:

在js版搜索地图上添加标记

由于我们做的是有关于旅游方面的项目,所以涉及到了地图功能.我接到的其中一个任务就是,在地图上显示指定的几个景点,并在地图上加上标记. 我们项目用的是搜狗地图,使用的是js版本.大家有兴趣的话,可以参考搜索地图api以及示例代码. 在地图上添加标记是地图的一个基本功能.这个标记叫做Marker.可以从这里看官网上对于Marker类的介绍. 实现的基本步骤,首先在页面上创建一个地图,然后地图上添加一个marker.你可以对这个marker指定位置.显示内容,在地图上的显隐等.具体请看一下代码: <h

《屏幕上的聪明决策》:4星。人类在手机/电脑上做选择的心理学研究的综述。不流畅的文本有助于理解和记忆,淘汰赛制可以有效降低选择后懊悔。

本书是人类在手机/电脑上做选择的心理学研究的综述,比较可信,有许多有趣的事实与结论.比较重要的结论有这么几个:1:人类对网站.文章的判断很大程度上以来瞬间感知和审美:2:不流畅的文本有助于理解和记忆:3:可选项太多的情况下,可以考虑淘汰赛制,每次让用户从4个选项中选择一个最满意的.这样可以有效降低选择后懊悔. 总体评价4星. 以下是书中一些重要的结论与信息的摘抄,#号后面是kindle电子书中的页码: 1:人的注意力已经成为“21世纪的低硫原油”.如果可以控制人类注意力的杠杆,那么你几乎可以获取

如何在Linux系统上安装字体

如何在Linux系统上安装字体 最后更新 2015年12月11日 分类 Linux 101 最新文章 标签 font linux 字体 一般电脑用户使用的字体大部分属于TrueType字体,TrueType字体文件的扩展名是.ttf,ttf就是TrueType Font的首字母缩写.这篇教程将向读者介绍如何在Linux系统上安装TrueType字体.本文先介绍通用的安装方式,不管你用的是哪个Linux发行版,都可以用这种方法来安装.然后介绍Debian系和Redhat系发行版安装字体的一些捷径或

My97DatePicker日历插件特殊日期做标记的使用

最近项目中有用到日历表,针对特殊日期做标记.项目中需求是查询到本月有录像则在对应日期做标记.项目中使用到了jQuery的My97DatePicker日历插件. 下面就介绍下如何使用My97DatePicker日历插件及实现对应功能. 一.项目中引入My97DatePicker包 二.jsp页面中引入WdatePicker.js <script type="text/javascript" src="/common/js/My97DatePicker/WdatePicke

#1 如何在 HTML页面上显示HTML代码

今天把数据库里面的文章内容输出到界面上,遇到了一个问题.文章内容没有全部书出来,在某个地方被阶段了,纠结了好久,后来发现问题. 问题出现在:“<meta charset="utf-8″>” 在数据库文章表里的 文章中有 “<meta charset="utf-8″>”   这个东西,然后查询出来到服务端.查询出来的结果是没问题的. 但是我用 response.write(item.Content);   // item.Content 是文章表里面内容字段. 输

如何在Linux服务器上开启安全的SNMP代理

服务器监控工具可以帮助我们从任何一个地方实时了解服务器的性能和功能.监控宝服务器监控套装,可以实时CPU使用率.内存使用率.平均负载.磁盘I/O.网络流量.磁盘使用率等,能够同时为你带来短期效益和长期效益. 监控宝目前提供的服务器监控项目包括: 1. CPU使用率,监控CPU的使用率比例,包括用户态(User).内核态(System).I/O等待(IOWait).空闲(Idle)等 2. CPU负载,监控Linux服务器的平均负载(load average),包括最近1分钟.5分钟.15分钟等