WebGL能夠用來作3D效果的全景圖呈現,例如故宮的全景圖。但有時候咱們不單單只是呈現全景圖,還須要增長互動。故宮裏邊能夠又分了不少區域,例如外朝中路、外朝西路、外朝東路等等。咱們須要在3D圖上作一些標記表示某個小的區域。當點擊這個標記時,界面切換到對應標記區域的全景圖。下圖是實現此功能的一個小DEMO:html
如何實現這樣的功能?經過本篇的介紹,咱們能夠了解到以上交互過程的代碼實現方式。這裏我先提出幾個問題git
1).如何獲取3D全景圖某個地址的3D座標?github
2).如何將獲取的地址的3D座標轉換爲屏幕上的2D座標?canvas
3).在旋轉3D全景圖時,如何讓3D座標對應的2D屏幕座標跟着移動?app
4).如何確認一個標記點是在相機的可視區域?函數
搞清楚以上問題,全景圖的標記功能也就垂手可得了。接下來咱們就圍繞每一個問題來實現功能。spa
通常獲取景區上某個地址的標記,都是經過手動獲取的。由於這些標記是無規律可尋的。因此咱們就得考慮如何經過手動去獲取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函數。接下來經過介紹這兩個函數來回答剩下的問題。
若是熟悉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座標了。
以前沒有介紹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上下載源代碼查看。地址: