zoukankan      html  css  js  c++  java
  • 如何在WebGL全景图上做标记

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

    image

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

       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

  • 相关阅读:
    DRUPAL-PSA-CORE-2014-005 && CVE-2014-3704 Drupal 7.31 SQL Injection Vulnerability /includes/database/database.inc Analysis
    WDCP(WDlinux Control Panel) mysql/add_user.php、mysql/add_db.php Authentication Loss
    Penetration Testing、Security Testing、Automation Testing
    Tomcat Server Configuration Automation Reinforcement
    Xcon2014 && Geekpwn2014
    phpMyadmin /scripts/setup.php Remote Code Injection && Execution CVE-2009-1151
    Linux System Log Collection、Log Integration、Log Analysis System Building Learning
    The Linux Process Principle,NameSpace, PID、TID、PGID、PPID、SID、TID、TTY
    Windows Management Instrumentation WMI Security Technology Learning
    IIS FTP Server Anonymous Writeable Reinforcement, WEBDAV Anonymous Writeable Reinforcement(undone)
  • 原文地址:https://www.cnblogs.com/w-wanglei/p/6853716.html
Copyright © 2011-2022 走看看