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

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

  1. Swift - 使用MapKit显示地图,并在地图上做标记

    通过使用MapKit可以将地图嵌入到视图中,MapKit框架除了可以显示地图,还支持在地图上做标记. 1,通过mapType属性,可以设置地图的显示类型 MKMapType.Standard :标准地 ...

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

    <!DOCTYPE html> <html> <head> <title>GeoLocation</title> <meta name ...

  3. 如何在Spring框架上做开发之Context启动中的“Hook”

    1.概述 有些时候,我们需要在spring启动过程中加入一些自己的逻辑,特别是一些基本框架和spring做整合的时候(例如:mybatis-spring-boot-starter),就需要使用Spri ...

  4. 如何在CentOS 7上使用vsftpd设置ftp服务器

    一.前言介绍 FTP(文件传输协议)是一种标准的客户机-服务器网络协议,允许用户在远程网络之间传输文件. 有几个开源的FTP服务器可用于Linux.最受欢迎和广泛使用的是pureftpd.proftp ...

  5. 如何在Debian 9上安装和使用Docker

    介绍 Docker是一个简化容器中应用程序进程管理过程的应用程序.容器允许您在资源隔离的进程中运行应用程序.它们与虚拟机类似,但容器更便携,更加资源友好,并且更依赖于主机操作系统. 在本教程中,您将在 ...

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

    本文介绍如何在VMware软件上安装Redhat(红帽)Linux6.9操作系统 首先需要准备 VMware软件和Redhat-Linux6.9操作系统的ISO系统镜像文件包(这里以linux6.9为 ...

  7. 如何在 SSAS服务器之间做同步

    简介: 从SQL Server 2005开始,分析服务就支持了同步的功能.本文将介绍如何在SQL Server 2012下同步Adventureworks的分析服务数据库.通过同步的功能,我们就来可以 ...

  8. 如何在iOS地图上高效的显示大量数据

    2016-01-13 / 23:02:13 刚才在微信上看到这篇由cocoachina翻译小组成员翻译的文章,觉得还是挺值得参考的,因此转载至此,原文请移步:http://robots.thought ...

  9. 如何在CentOS 7上修改主机名

    如何在CentOS 7上修改主机名 在CentOS中,有三种定义的主机名:静态的(static),瞬态的(transient),和灵活的(pretty).“静态”主机名也称为内核主机名,是系统在启动时 ...

随机推荐

  1. bash变量

    bash中的变量的种类 根据变量的生效范围等标准 本地变量:生效范围为当前shell进程:对当前shell之外的其它shell进程,包括当前shell的子shell进程均无效: 环境变量:生效范围为当 ...

  2. MySQL存储过程--带参数报错1064

    DELIMITER $$ USE `student`$$ DROP PROCEDURE IF EXISTS `sync_student`$$ CREATE DEFINER=`student`@`%` ...

  3. iOS图片填充UIImageView(contentMode)

    本文主要形象的介绍一下UIView的contentMode属性: 核心代码 [self.prp_imageView setContentMode:UIViewContentModeScaleAspec ...

  4. 空a标签 a标签空的情况下 IE6 IE7下点击无效

    最近做了好多网站专题页面,因为专题页面图片较多,个别banner上有1个到多个按钮,一种是用“图解img标签的usemap”的方法做链接,(图解img标签的usemap使用方法)[传送门] 另一种用则 ...

  5. 自动生成数学题型二(框架struts2)题型如((a+b)*c=d)

    1. 生成题目 1.1 生成单个题目 public static String[] twoOperatorAndOperator(int num1, int num2) { double first ...

  6. tmux配置

    bind k selectp -U bind j selectp -D bind h selectp -L bind l selectp -R bind -r ^k resizep -U 5 bind ...

  7. SQL语句简单整理

    转载原文:http://blog.sina.com.cn/s/blog_48df31d901017c6o.html 1.用户 - 查看当前用户的缺省表空间 select username,defaul ...

  8. socket编程之 select、poll、kqueue、epoll

    原生API select int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tim ...

  9. HYML / CSS部分

    1.什么是盒子模型? 在网页中,一个元素占有空间的大小由几个部分构成,其中包括元素的内容(content),元素的内边距(padding),元素的边框(border),元素的外边距(margin)四个 ...

  10. (知识点)JavaScript闭包

    下面是我对闭包的理解:(把他们整理出来,整理的过程也是在梳理) 1.首先,在理解闭包之前: 我们首先应该清楚下作用域和作用域链 作用域:每个函数定义时创建时自己的环境即作用域 作用域链:函数内可访问自 ...