如何在WebGL全景圖上作標記

    WebGL能夠用來作3D效果的全景圖呈現,例如故宮的全景圖。但有時候咱們不單單只是呈現全景圖,還須要增長互動。故宮裏邊能夠又分了不少區域,例如外朝中路、外朝西路、外朝東路等等。咱們須要在3D圖上作一些標記表示某個小的區域。當點擊這個標記時,界面切換到對應標記區域的全景圖。下圖是實現此功能的一個小DEMO:html

image

   如何實現這樣的功能?經過本篇的介紹,咱們能夠了解到以上交互過程的代碼實現方式。這裏我先提出幾個問題git

   1).如何獲取3D全景圖某個地址的3D座標?github

   2).如何將獲取的地址的3D座標轉換爲屏幕上的2D座標?canvas

   3).在旋轉3D全景圖時,如何讓3D座標對應的2D屏幕座標跟着移動?app

   4).如何確認一個標記點是在相機的可視區域?函數

   搞清楚以上問題,全景圖的標記功能也就垂手可得了。接下來咱們就圍繞每一個問題來實現功能。spa

如何獲取3D全景圖的某個地址的3D座標?

    通常獲取景區上某個地址的標記,都是經過手動獲取的。由於這些標記是無規律可尋的。因此咱們就得考慮如何經過手動去獲取3D圖上的某個地址。人機交互時經過鼠標來操做,但鼠標是2D座標,須要轉換到對應的3D座標上。Three.js爲咱們提供了Raycaster對象,咱們能夠很輕鬆的獲取到一個2D點對應的3D座標。先聲明幾個對象:3d

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

    這裏須要在document上註冊mousemove事件,實時獲取鼠標對應的3D座標。事件代碼以下:code

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直接用交互點做爲中心畫的一個球體,鼠標移動這個球體也就跟着移動。htm

    鼠標移動時,可以獲取到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

相關文章
相關標籤/搜索