導讀
本文從繪圖基礎開始講起,詳細介紹瞭如何使用Three.js
開發一個功能齊全的全景外掛。
我們先來看一下外掛的效果:
如果你對Three.js
已經很熟悉了,或者你想跳過基礎理論,那麼你可以直接從全景預覽開始看起。
本專案的github
地址:github.com/ConardLi/tp…
一、理清關係
1.1 OpenGL
OpenGL
是用於渲染2D、3D
量圖形的跨語言、跨平臺的應用程式程式設計介面(API)
。
這個介面由近350
個不同的函式呼叫組成,用來從簡單的圖形位元繪製複雜的三維景象。
OpenGL ES
是OpenGL
三維圖形API
的子集,針對手機、PDA
和遊戲主機等嵌入式裝置而設計。
基於OpenGL
,一般使用C
或Cpp
開發,對前端開發者來說不是很友好。
1.2 WebGL
WebGL
把JavaScript
和OpenGL ES 2.0
結合在一起,從而為前端開發者提供了使用JavaScript
編寫3D
效果的能力。
WebGL
為HTML5 Canvas
提供硬體3D
加速渲染,這樣Web
開發人員就可以藉助系統顯示卡來在瀏覽器裡更流暢地展示3D
場景和模型了,還能建立複雜的導航和資料視覺化。
1.3 Canvas
Canvas
是一個可以自由制定大小的矩形區域,可以通過JavaScript
可以對矩形區域進行操作,可以自由的繪製圖形,文字等。
一般使用Canvas
都是使用它的2d
的context
功能,進行2d
繪圖,這是其本身的能力。
和這個相對的,WebGL
是三維,可以描畫3D
圖形,WebGL
,想要在瀏覽器上進行呈現,它必須需要一個載體,這個載體就是Canvas
,區別於之前的2dcontext
,還可以從Canvas
中獲取webglcontext
。
1.4 Three.js
我們先來從字面意思理解下:Three
代表3D
,js
代表JavaScript
,即使用JavaScript
來開發3D
效果。
Three.js
是使用JavaScript
對 WebGL
介面進行封裝與簡化而形成的一個易用的3D
庫。
直接使用WebGL
進行開發對於開發者來說成本相對來說是比較高的,它需要你掌握較多的計算機圖形學知識。
Three.js
在一定程度上簡化了一些規範和難以理解的概念,對很多API
進行了簡化,這大大降低了學習和開發三維效果成本。
下面我們來具體看一下使用Three.js
必須要知道的知識。
二、Three.js基礎知識
使用Three.js
繪製一個三維效果,至少需要以下幾個步驟:
-
建立一個容納三維空間的場景 —
Sence
-
將需要繪製的元素加入到場景中,對元素的形狀、材料、陰影等進行設定
-
給定一個觀察場景的位置,以及觀察角度,我們用相機物件(
Camera
)來控制 -
將繪製好的元素使用渲染器(
Renderer
)進行渲染,最終呈現在瀏覽器上
拿電影來類比的話,場景對應於整個佈景空間,相機是拍攝鏡頭,渲染器用來把拍攝好的場景轉換成膠捲。
2.1 場景
場景允許你設定哪些物件被three.js
渲染以及渲染在哪裡。
我們在場景中放置物件、燈光和相機。
很簡單,直接建立一個Scene
的例項即可。
_scene = new Scene();
複製程式碼
2.2 元素
有了場景,我們接下來就需要場景裡應該展示哪些東西。
一個複雜的三維場景往往就是由非常多的元素搭建起來的,這些元素可能是一些自定義的幾何體(Geometry
),或者外部匯入的複雜模型。
Three.js
為我們提供了非常多的Geometry
,例如SphereGeometry
(球體)、 TetrahedronGeometry
(四面體)、TorusGeometry
(圓環體)等等。
在Three.js
中,材質(Material
)決定了幾何圖形具體是以什麼形式展現的。它包括了一個幾何體如何形狀以外的其他屬性,例如色彩、紋理、透明度等等,Material
和Geometry
是相輔相成的,必須結合使用。
下面的程式碼我們建立了一個長方體體,賦予它基礎網孔材料(MeshBasicMaterial
)
var geometry = new THREE.BoxGeometry(200, 100, 100);
var material = new THREE.MeshBasicMaterial({ color: 0x645d50 });
var mesh = new THREE.Mesh(geometry, material);
_scene.add(mesh);
複製程式碼
能以這個角度看到幾何體實際上是相機的功勞,這個我們下面的章節再介紹,這讓我們看到一個幾何體的輪廓,但是感覺怪怪的,這並不像一個幾何體,實際上我們還需要為它新增光照和陰影,這會讓幾何體看起來更真實。
基礎網孔材料(MeshBasicMaterial
)不受光照影響的,它不會產生陰影,下面我們為幾何體換一種受光照影響的材料:網格標準材質(Standard Material
),併為它新增一些光照:
var geometry = new THREE.BoxGeometry(200, 100, 100);
var material = new THREE.MeshStandardMaterial({ color: 0x645d50 });
var mesh = new THREE.Mesh(geometry, material);
_scene.add(mesh);
// 建立平行光-照亮幾何體
var directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-4, 8, 12);
_scene.add(directionalLight);
// 建立環境光
var ambientLight = new THREE.AmbientLight(0xffffff);
_scene.add(ambientLight);
複製程式碼
有了光線的渲染,讓幾何體看起來更具有3D
效果,Three.js
中光源有很多種,我們上面使用了環境光(AmbientLight
)和平行光(DirectionalLight
)。
環境光會對場景中的所有物品進行顏色渲染。
平行光你可以認為像太陽光一樣,從極遠處射向場景中的光。它具有方向性,也可以啟動物體對光的反射效果。
除了這兩種光,Three.js
還提供了其他幾種光源,它們適用於不同情況下對不同材質的渲染,可以根據實際情況選擇。
2.3 座標系
在說相機之前,我們還是先來了解一下座標系的概念:
在三維世界中,座標定義了一個元素所處於三維空間的位置,座標系的原點即座標的基準點。
最常用的,我們使用距離原點的三個長度(距離x
軸、距離y
軸、距離z
軸)來定義一個位置,這就是直角座標系。
在判定座標系時,我們通常使用大拇指、食指和中指,並互為90
度。大拇指代表X
軸,食指代表Y
軸,中指代表Z
軸。
這就產生了兩種座標系:左手座標系和右手座標系。
Three.js
中使用的座標系即右手座標系。
我們可以在我們的場景中新增一個座標系,這樣我們可以清楚的看到元素處於什麼位置:
var axisHelper = new THREE.AxisHelper(600);
_scene.add(axisHelper);
複製程式碼
其中紅色代表X
軸,綠色代表Y
軸,藍色代表Z
軸。
2.4 相機
上面看到的幾何體的效果,如果不建立一個相機(Camera
),是什麼也看不到的,因為預設的觀察點在座標軸原點,它處於幾何體的內部。
相機(Camera
)指定了我們在什麼位置觀察這個三維場景,以及以什麼樣的角度進行觀察。
2.4.1 兩種相機的區別
目前Three.js
提供了幾種不同的相機,最常用的,也是下面外掛中使用的兩種相機是:PerspectiveCamera
(透視相機)、 OrthographicCamera
(正交投影相機)。
上面的圖很清楚的解釋了兩種相機的區別:
右側是 OrthographicCamera
(正交投影相機)他不具有透視效果,即物體的大小不受遠近距離的影響,對應的是投影中的正交投影。我們數學課本上所畫的幾何體大多數都採用這種投影。
左側是PerspectiveCamera
(透視相機),這符合我們正常人的視野,近大遠小,對應的是投影中的透視投影。
如果你想讓場景看起來更真實,更具有立體感,那麼採用透視相機最合適,如果場景中有一些元素你不想讓他隨著遠近放大縮小,那麼採用正交投影相機最合適。
2.4.2 構造引數
我們再分別來看看兩個建立兩個相機需要什麼引數:
_camera = new OrthographicCamera(left, right, top, bottom, near, far);
複製程式碼
OrthographicCamera
接收六個引數,left, right, top, bottom
分別對應上、下、左、右、遠、近的一個距離,超過這些距離的元素將不會出現在視野範圍內,也不會被瀏覽器繪製。實際上,這六個距離就構成了一個立方體,所以OrthographicCamera
的可視範圍永遠在這個立方體內。
_camera = new PerspectiveCamera(fov, aspect, near, far);
複製程式碼
PerspectiveCamera
接收四個引數,near
、far
和上面的相同,分別對應相機可觀測的最遠和最近距離;fov
代表水平範圍可觀測的角度,fov
越大,水平範圍能觀測到的範圍越廣;aspect
代表水平方向和豎直方向可觀測距離的比值,所以fov
和aspect
就可以確定垂直範圍內能觀測到的範圍。
2.4.3 position、lookAt
關於相機還有兩個必須要知道的點,一個是position
屬性,一個是lookAt
函式:
position
屬性指定了相機所處的位置。
lookAt
函式指定相機觀察的方向。
實際上position
的值和lookAt
接收的引數都是一個型別為Vector3
的物件,這個物件用來表示三維空間中的座標,它有三個屬性:x、y、z
分別代表距離x
軸、距離y
軸、距離z
軸的距離。
下面,我們讓相機觀察的方向指向原點,另外分別讓x、y、z
為0,另外兩個引數不為0,看一下視野會發生什麼變化:
_camera = new OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.1, 1000);
_camera.lookAt(new THREE.Vector3(0, 0, 0))
_camera.position.set(0, 300, 600); // 1 - x為0
_camera.position.set(500, 0, 600); // 2 - y為0
_camera.position.set(500, 300, 0); // 3 - z為0
複製程式碼
很清楚的看到position
決定了我們視野的出發點,但是鏡頭指向的方向是不變的。
下面我們將position
固定,改變相機觀察的方向:
_camera = new OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.1, 1000);
_camera.position.set(500, 300, 600);
_camera.lookAt(new THREE.Vector3(0, 0, 0)) // 1 - 視野指向原點
_camera.lookAt(new THREE.Vector3(200, 0, 0)) // 2 - 視野偏向x軸
複製程式碼
可見:我們視野的出發點是相同的,但是視野看向的方向發生了改變。
2.4.4 兩種相機對比
好,有了上面的基礎,我們再來寫兩個例子看一看兩個相機的視角對比,為了方便觀看,我們建立兩個位置不同的幾何體:
var geometry = new THREE.BoxGeometry(200, 100, 100);
var material = new THREE.MeshStandardMaterial({ color: 0x645d50 });
var mesh = new THREE.Mesh(geometry, material);
_scene.add(mesh);
var geometry = new THREE.SphereGeometry(50, 100, 100);
var ball = new THREE.Mesh(geometry, material);
ball.position.set(200, 0, -200);
_scene.add(ball);
複製程式碼
正交投影相機視野:
_camera = new OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.1, 1000);
_camera.position.set(0, 300, 600);
_camera.lookAt(new THREE.Vector3(0, 0, 0))
複製程式碼
透視相機視野:
_camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1100);
_camera.position.set(0, 300, 600);
_camera.lookAt(new THREE.Vector3(0, 0, 0))
複製程式碼
可見,這印證了我們上面關於兩種相機的理論
2.5 渲染器
上面我們建立了場景、元素和相機,下面我們要告訴瀏覽器將這些東西渲染到瀏覽器上。
Three.js
也為我們提供了幾種不同的渲染器,這裡我們主要看WebGL
渲染器(WebGLRenderer
)。顧名思義:WebGL
渲染器使用WebGL
來繪製場景,其夠利用GPU
硬體加速從而提高渲染效能。
_renderer = new THREE.WebGLRenderer();
複製程式碼
你需要將你使用Three.js
繪製的元素新增到瀏覽器上,這個過程需要一個載體,上面我們介紹,這個載體就是Canvas
,你可以通過_renderer.domElement
獲取到這個Canvas
,並將它給定到真實DOM
中。
_container = document.getElementById('conianer');
_container.appendChild(_renderer.domElement);
複製程式碼
使用setSize
函式設定你要渲染的範圍,實際上它改變的就是上面Canvas
的範圍:
_renderer.setSize(window.innerWidth, window.innerHeight);
複製程式碼
現在,你已經指定了一個渲染的載體和載體的範圍,你可以通過render
函式渲染上面指定的場景和相機:
_renderer.render(_scene, _camera);
複製程式碼
實際上,你如果依次執行上面的程式碼,可能螢幕上還是黑漆漆的一片,並沒有任何元素渲染出來。
這是因為上面你要渲染的元素可能並未被載入完,你就執行了渲染,並且只執行了一次,這時我們需要一種方法,讓場景和相機進行實時渲染,我們需要用到下面的方法:
2.6 requestAnimationFrame
window.requestAnimationFrame()
告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前呼叫指定的回撥函式更新動畫。
該方法需要傳入一個回撥函式作為引數,該回撥函式會在瀏覽器下一次重繪之前執行。
window.requestAnimationFrame(callback);
複製程式碼
若你想在瀏覽器下次重繪之前繼續更新下一幀動畫,那麼回撥函式自身必須再次呼叫window.requestAnimationFrame()
。
使用者韓函式就意味著,你可以在requestAnimationFrame
不停的執行繪製操作,瀏覽器就實時的知道它需要渲染的內容。
當然,某些時候你已經不需要實時繪製了,你也可以使用cancelAnimationFrame
立即停止這個繪製:
window.cancelAnimationFrame(myReq);
複製程式碼
來看一個簡單的例子:
var i = 0;
var animateName;
animate();
function animate() {
animateName = requestAnimationFrame(animate);
console.log(i++);
if (i > 100) {
cancelAnimationFrame(animateName);
}
}
複製程式碼
來看一下執行效果:
我們使用requestAnimationFrame
和Three.js
的渲染器結合使用,這樣就能實時繪製三維動畫了:
function animate() {
requestAnimationFrame(animate);
_renderer.render(_scene, _camera);
}
複製程式碼
藉助上面的程式碼,我們可以簡單實現一些動畫效果:
var y = 100;
var option = 'down';
function animateIn() {
animateName = requestAnimationFrame(animateIn);
mesh.rotateX(Math.PI / 40);
if (option == 'up') {
ball.position.set(200, y += 8, 0);
} else {
ball.position.set(200, y -= 8, 0);
}
if (y < 1) { option = 'up'; }
if (y > 100) { option = 'down' }
}
複製程式碼
2.7 總結
上面的知識是Three.js
中最基礎的知識,也是最重要的和最主幹的。
這些知識能夠讓你在看到一個複雜的三維效果時有一定的思路,當然,要實現還需要非常多的細節。這些細節你可以去官方文件中查閱。
下面的章節即告訴你如何使用Three.js
進行實戰 — 實現一個360度全景外掛。
這個外掛包括兩部分,第一部分是對全景圖進行預覽。
第二部分是對全景圖的標記進行配置,並關聯預覽的座標。
我們首先來看看全景預覽部分:
三、全景預覽
3.1 基本邏輯
-
將一張全景圖包裹在球體的內壁
-
設定一個觀察點,在球的圓心
-
使用滑鼠可以拖動球體,從而改變我們看到全景的視野
-
滑鼠滾輪可以縮放,和放大,改變觀察全景的遠近
-
根據座標在全景圖上掛載一些標記,如文字、圖示等,並且可以增加事件,如點選事件
3.2 初始化
我們先把必要的基礎設施搭建起來:
場景、相機(選擇遠景相機,這樣可以讓全景看起來更真實)、渲染器:
_scene = new THREE.Scene();
initCamera();
initRenderer();
animate();
// 初始化相機
function initCamera() {
_camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1100);
_camera.position.set(0, 0, 2000);
_camera.lookAt(new THREE.Vector3(0, 0, 0));
}
// 初始化渲染器
function initRenderer() {
_renderer = new THREE.WebGLRenderer();
_renderer.setSize(window.innerWidth, window.innerHeight);
_container = document.getElementById('panoramaConianer');
_container.appendChild(_renderer.domElement);
}
// 實時渲染
function animate() {
requestAnimationFrame(animate);
_renderer.render(_scene, _camera);
}
複製程式碼
下面我們在場景內新增一個球體,並把全景圖作為材料包裹在球體上面:
var mesh = new THREE.Mesh(new THREE.SphereGeometry(1000, 100, 100),
new THREE.MeshBasicMaterial(
{ map: ImageUtils.loadTexture('img/p3.png') }
));
_scene.add(mesh);
複製程式碼
然後我們看到的場景應該是這樣的:
這不是我們想要的效果,我們想要的是從球的內部觀察全景,並且全景圖是附著外球的內壁的,而不是鋪在外面:
我們只要需將Material
的scale
的一個屬性設定為負值,材料即可附著在幾何體的內部:
mesh.scale.x = -1;
複製程式碼
然後我們將相機的中心點移動到球的中心:
_camera.position.set(0, 0, 0);
複製程式碼
現在我們已經在全景球的內部啦:
3.3 事件處理
全景圖已經可以瀏覽了,但是你只能看到你眼前的這一塊,並不能拖動它看到其他部分,為了精確的控制拖動的速度和縮放、放大等場景,我們手動為它增加一些事件:
監聽滑鼠的mousedown
事件,在此時將開始拖動標記_isUserInteracting
設定為true
,並且記錄起始的螢幕座標,以及起始的相機lookAt
的座標。
_container.addEventListener('mousedown', (event)=>{
event.preventDefault();
_isUserInteracting = true;
_onPointerDownPointerX = event.clientX;
_onPointerDownPointerY = event.clientY;
_onPointerDownLon = _lon;
_onPointerDownLat = _lat;
});
複製程式碼
監聽滑鼠的mousemove
事件,當_isUserInteracting
為true
時,實時計算當前相機lookAt
的真實座標。
_container.addEventListener('mousemove', (event)=>{
if (_isUserInteracting) {
_lon = (_onPointerDownPointerX - event.clientX) * 0.1 + _onPointerDownLon;
_lat = (event.clientY - _onPointerDownPointerY) * 0.1 + _onPointerDownLat;
}
});
複製程式碼
監聽滑鼠的mouseup
事件,將_isUserInteracting
設定為false
。
_container.addEventListener('mouseup', (event)=>{
_isUserInteracting = false;
});
複製程式碼
當然,上面我們只是改變了座標,並沒有告訴相機它改變了,我們在animate
函式中來做這件事:
function animate() {
requestAnimationFrame(animate);
calPosition();
_renderer.render(_scene, _camera);
_renderer.render(_sceneOrtho, _cameraOrtho);
}
function calPosition() {
_lat = Math.max(-85, Math.min(85, _lat));
var phi = tMath.degToRad(90 - _lat);
var theta = tMath.degToRad(_lon);
_camera.target.x = _pRadius * Math.sin(phi) * Math.cos(theta);
_camera.target.y = _pRadius * Math.cos(phi);
_camera.target.z = _pRadius * Math.sin(phi) * Math.sin(theta);
_camera.lookAt(_camera.target);
}
複製程式碼
監聽mousewheel
事件,對全景圖進行放大和縮小,注意這裡指定了最大縮放範圍maxFocalLength
和最小縮放範圍minFocalLength
。
_container.addEventListener('mousewheel', (event)=>{
var ev = ev || window.event;
var down = true;
var m = _camera.getFocalLength();
down = ev.wheelDelta ? ev.wheelDelta < 0 : ev.detail > 0;
if (down) {
if (m > minFocalLength) {
m -= m * 0.05
_camera.setFocalLength(m);
}
} else {
if (m < maxFocalLength) {
m += m * 0.05
_camera.setFocalLength(m);
}
}
});
複製程式碼
來看一下效果吧:
3.4 增加標記
在瀏覽全景圖的時候,我們往往需要對某些特殊的位置進行一些標記,並且這些標記可能附帶一些事件,比如你需要點選一個標記才能到達下一張全景圖。
下面我們來看看如何在全景中增加標記,以及如何為這些標記新增事件。
我們可能不需要讓這些標記隨著視野的變化而放大和縮小,基於此,我們使用正交投影相機來展現標記,只需給它一個固定的觀察高度:
_cameraOrtho = new THREE.OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 1, 10);
_cameraOrtho.position.z = 10;
_sceneOrtho = new Scene();
複製程式碼
利用精靈材料(SpriteMaterial
)來實現文字標記,或者圖片標記:
// 建立文字標記
function createLableSprite(name) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const metrics = context.measureText(name);
const width = metrics.width * 1.5;
context.font = "10px 宋體";
context.fillStyle = "rgba(0,0,0,0.95)";
context.fillRect(2, 2, width + 4, 20 + 4);
context.fillText(name, 4, 20);
const texture = new Texture(canvas);
const spriteMaterial = new SpriteMaterial({ map: texture });
const sprite = new Sprite(spriteMaterial);
sprite.name = name;
const lable = {
name: name,
canvas: canvas,
context: context,
texture: texture,
sprite: sprite
};
_sceneOrtho.add(lable.sprite);
return lable;
}
// 建立圖片標記
function createSprite(position, url, name) {
const textureLoader = new TextureLoader();
const ballMaterial = new SpriteMaterial({
map: textureLoader.load(url)
});
const sp = {
pos: position,
name: name,
sprite: new Sprite(ballMaterial)
};
sp.sprite.scale.set(32, 32, 1.0);
sp.sprite.name = name;
_sceneOrtho.add(sp.sprite);
return sp;
}
複製程式碼
建立好這些標記,我們把它渲染到場景中。
我們必須告訴場景這些標記的位置,為了直觀的理解,我們需要給這些標記賦予一種座標,這種座標很類似於經緯度,我們叫它lon
和lat
,具體是如何給定的我們在下面的章節:全景標記中會詳細介紹。
在這個過程中,一共經歷了兩次座標轉換:
第一次轉換:將“經緯度”轉換為三維空間座標,即我們上面講的那種x、y、z
形式的座標。
使用geoPosition2World
函式進行轉換,得到一個Vector3
物件,我們可以將當前相機_camera
作為引數傳入這個物件的project
方法,這會得到一個標準化後的座標,基於這個座標可以幫我們判斷標記是否在視野範圍內,如下面的程式碼,若標準化座標在-1
和1
的範圍內,則它會出現在我們的視野中,我們將它進行準確渲染。
第二次轉換:將三維空間座標轉換為螢幕座標。
如果我們直接講上面的三維空間座標座標應用到標記中,我們會發現無論視野如何移動,標記的位置是不會有任何變化的,因為這樣算出來的座標永遠是一個常量。
所以我們需要藉助上面的標準化座標,將標記的三維空間座標轉換為真實的螢幕座標,這個過程是worldPostion2Screen
函式來實現的。
關於geoPosition2World
和worldPostion2Screen
兩個函式的實現,大家有興趣可以去我的github
原始碼中檢視,這裡就不多做解釋了,因為這又要牽扯到一大堆專業知識啦。?
var wp = geoPosition2World(_sprites.lon, _sprites.lat);
var sp = worldPostion2Screen(wp, _camera);
var test = wp.clone();
test.project(_camera);
if (test.x > -1 && test.x < 1 && test.y > -1 && test.y < 1 && test.z > -1 && test.z < 1) {
_sprites[i].sprite.scale.set(32, 32, 32);
_sprites[i].sprite.position.set(sp.x, sp.y, 1);
}else {
_sprites[i].sprite.scale.set(1.0, 1.0, 1.0);
_sprites[i].sprite.position.set(0, 0, 0);
}
複製程式碼
現在,標記已經新增到全景上面了,我們來為它新增一個點選事件:
Three.js
並沒有單獨提供為Sprite
新增事件的方法,我們可以藉助光線投射器(Raycaster
)來實現。
Raycaster
提供了滑鼠拾取的能力:
通過setFromCamera
函式來建立當前點選的座標(經過歸一化處理)和相機的繫結關係。
通過intersectObjects
來判定一組物件中有哪些被命中(點選),得到被命中的物件陣列。
這樣,我們就可以獲取到點選的物件,並基於它做一些處理:
_container.addEventListener('click', (event)=>{
_mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
_mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
_raycaster.setFromCamera(_mouse, _cameraOrtho);
var intersects = _raycaster.intersectObjects(_clickableObjects);
intersects.forEach(function (element) {
alert("點選到了: " + element.object.name);
});
});
複製程式碼
點選到一個標記,進入到下一張全景圖:
四、全景標記
為了讓全景圖知道,我要把標記標註在什麼地方,我需要一個工具來把原圖和全景圖上的位置關聯起來:
由於這部分程式碼和Three.js
關係不大,這裡我只說一下基本的實現邏輯,有興趣可以去我的github
倉庫檢視。
4.1 要求
-
建立座標和全景的對映關係,為全景賦予一套虛擬座標
-
在一張平鋪的全景圖上,可以在任意位置增加標記,並獲取標記的座標
-
使用座標在預覽全景增加標記,看到的標記位置和平鋪全景中的位置相同
4.2 座標
在2D
平面上,我們能監聽螢幕的滑鼠事件,我們可以獲取的也只是當前的滑鼠座標,我們要做的是將滑鼠座標轉換成三維空間座標。
看起來好像是不可能的,二維座標怎麼能轉換成三維座標呢?
但是,我們可以藉助一種中間座標來轉換,可以把它稱之為“經緯度”。
在這之前,我們先來看看我們常說的經緯度到底是什麼。
4.3 經緯度
使用經緯度,可以精確的定位到地球上任意一個點,它的計算規則是這樣的:
通常把連線南極到北極的線叫做子午線也叫經線,其所對應的面叫做子午面,規定英國倫敦格林尼治天文臺原址的那條經線稱為0°經線,也叫本初子午線其對應的面即本初子午面。
經度:球面上某店對應的子午面與本初子午面間的夾角。東正西負。
緯度 :球面上某點的法線(以該店作為切點與球面相切的面的法線)與赤道平面的夾角。北正南負。
由此,地球上每一個點都能被對應到一個經度和緯度,想對應的,也能對應到某條經線和緯線上。
這樣,即使把球面展開稱平面,我們仍然能用經緯度表示某店點的位置:
4.4 座標轉換
基於上面的分析,我們完全可以給平面的全景圖賦予一個虛擬的“經緯度”。我們使用Canvas
為它繪製一張"經緯網":
將滑鼠座標轉換為"經緯度":
function calLonLat(e) {
var h = _setContainer.style.height.split("px")[0];
var w = _setContainer.style.width.split("px")[0];
var ix = _setContainer.offsetLeft;
var iy = _setContainer.offsetTop;
iy = iy + h;
var x = e.clientX;
var y = e.clientY;
var lonS = (x - ix) / w;
var lon = 0;
if (lonS > 0.5) {
lon = -(1 - lonS) * 360;
} else {
lon = 1 * 360 * lonS;
}
var latS = (iy - y) / h;
var lat = 0;
if (latS > 0.5) {
lat = (latS - 0.5) * 180;
} else {
lat = (0.5 - latS) * 180 * -1
}
lon = lon.toFixed(2);
lat = lat.toFixed(2);
return { lon: lon, lat: lat };
}
複製程式碼
這樣平面地圖上的某點就可以和三維座標關聯起來了,當然,這還需要一定的轉換,有興趣可以去原始碼研究下geoPosition2World
和worldPostion2Screen
兩個函式。
五、外掛封裝
上面的程式碼中,我們實現了全景預覽和全景標記的功能,下面,我們要把這些功能封裝成外掛。
所謂外掛,即可以直接引用你寫的程式碼,並新增少量的配置就可以實現想要的功能。
5.1 全景預覽封裝
我們來看看,究竟哪些配置是可以抽取出來的:
var options = {
container: 'panoramaConianer',
url: 'resources/img/panorama/pano-7.jpg',
lables: [],
widthSegments: 60,
heightSegments: 40,
pRadius: 1000,
minFocalLength: 1,
maxFocalLength: 100,
sprite: 'label',
onClick: () => { }
}
複製程式碼
container
:dom
容器的id
url
:圖片路徑lables
:全景中的標記陣列,格式為{position:{lon:114,lat:38},logoUrl:'lableLogo.png',text:'name'}
widthSegments
:水平切段數heightSegments
:垂直切段數(值小粗糙速度快,值大精細速度慢)pRadius
:全景球的半徑,推薦使用預設值minFocalLength
:鏡頭最小拉近距離maxFocalLength
:鏡頭最大拉近距離sprite
:展示的標記型別label,icon
onClick
:標記的點選事件
上面的配置是可以使用者配置的,那麼使用者該如何傳入外掛呢?
我們可以在外掛中宣告一些預設配置options
,使用者使用建構函式傳入引數,然後使用Object.assign
將傳入配置覆蓋到預設配置。
接下來,你就可以使用this.def
來訪問這些變數了,然後只需要把寫死的程式碼改成這些配置即可。
options = {
// 預設配置...
}
function tpanorama(opt) {
this.render(opt);
}
tpanorama.prototype = {
constructor: this,
def: {},
render: function (opt) {
this.def = Object.assign(options, opt);
// 初始化操作...
}
}
複製程式碼
5.2 全景標記封裝
基本邏輯和上面的類似,下面是提取出來的一些引數。
var setOpt = {
container: 'myDiv',//setting容器
imgUrl: 'resources/img/panorama/3.jpg',
width: '',//指定寬度,高度自適應
showGrid: true,//是否顯示格網
showPosition: true,//是否顯示經緯度提示
lableColor: '#9400D3',//標記顏色
gridColor: '#48D1CC',//格網顏色
lables: [],//標記 {lon:114,lat:38,text:'標記一'}
addLable: true,//開啟後雙擊新增標記 (必須開啟經緯度提示)
getLable: true,//開啟後右鍵查詢標記 (必須開啟經緯度提示)
deleteLbale: true,//開啟預設中鍵刪除 (必須開啟經緯度提示)
}
複製程式碼
六、釋出
接下來,我們就好考慮如何將寫好的外掛讓使用者使用了。
我們主要考慮兩種場景,直接引用和npm install
6.1 直接引用JS
為了不汙染全域性變數,我們使用一個自執行函式(function(){}())
將程式碼包起來,然後將我們寫好的外掛暴露給全域性變數window
。
我把它放在originSrc
目錄下。
(function (global, undefined) {
function tpanorama(opt) {
// ...
}
tpanorama.prototype = {
// ...
}
function tpanoramaSetting(opt) {
// ...
}
tpanoramaSetting.prototype = {
// ...
}
global.tpanorama = tpanorama;
global.tpanoramaSetting = panoramaSetting;
}(window))
複製程式碼
6.2 使用npm install
直接將寫好的外掛匯出:
module.exports = tpanorama;
module.exports = panoramaSetting;
複製程式碼
我把它放在src
目錄下。
同時,我們要把package.json
中的main
屬性指向我們要匯出的檔案:"main": "lib/index.js"
,然後將name
、description
、version
等資訊補充完整。
下面,我們就可以開始釋出了,首先你要有一個npm
賬號,並且登陸,如果你沒有賬號,使用下面的命令建立一個賬號。
npm adduser --registry http://registry.npmjs.org
複製程式碼
如果你已經有賬號了,那麼可以直接使用下面的命令進行登陸。
npm login --registry http://registry.npmjs.org
複製程式碼
登陸成功之後,就可以釋出了:
npm publish --registry http://registry.npmjs.org
複製程式碼
注意,上面每個命令我都手動指定了registry
,這是因為當前你使用的npm
源可能已經被更換了,可能使用的是淘寶源或者公司源,這時不手動指定會導致釋出失敗。
釋出成功後直接在npm官網
上看到你的包了。
然後,你可以直接使用npm install tpanorama
進行安裝,然後進行使用:
var { tpanorama,tpanoramaSetting } = require('tpanorama');
複製程式碼
6.3 babel編譯
最後不要忘了,無論使用以上哪種方式,我們都要使用babel
編譯後才能暴露給使用者。
在scripts
中建立一個build
命令,將原始檔進行編譯,最終暴露給使用者使用的將是lib
和origin
。
"build": "babel src --out-dir lib && babel originSrc --out-dir origin",
複製程式碼
你還可以指定一些其他的命令來供使用者測試,如我將寫好的例子全部放在examples
中,然後在scripts
定義了expamle
命令:
"example": "npm run webpack && node ./server/www"
複製程式碼
這樣,使用者將程式碼克隆後直接在本地執行npm run example
就可以進行除錯了。
七、小結
本專案的github
地址:github.com/ConardLi/tp…
文中如有錯誤,歡迎在評論區指正,如果這篇文章幫助到了你,歡迎點贊和關注。
想閱讀更多優質文章、可關注我的github部落格,你的star✨、點贊和關注是我持續創作的動力!
推薦關注我的微信公眾號【code祕密花園】,每天推送高質量文章,我們一起交流成長。