如何在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM