看完這篇,你也能夠實現一個360度全景插件

導讀

本文從繪圖基礎開始講起,詳細介紹瞭如何使用Three.js開發一個功能齊全的全景插件。html

咱們先來看一下插件的效果:前端

若是你對Three.js已經很熟悉了,或者你想跳過基礎理論,那麼你能夠直接從全景預覽開始看起。node

本項目的github地址:github.com/ConardLi/tp…webpack

1、理清關係

1.1 OpenGL

OpenGL是用於渲染2D、3D量圖形的跨語言、跨平臺的應用程序編程接口(API)git

這個接口由近350個不一樣的函數調用組成,用來從簡單的圖形比特繪製複雜的三維景象。github

OpenGL ESOpenGL三維圖形API的子集,針對手機、PDA和遊戲主機等嵌入式設備而設計。web

基於OpenGL,通常使用CCpp開發,對前端開發者來講不是很友好。npm

1.2 WebGL

WebGLJavaScriptOpenGL ES 2.0結合在一塊兒,從而爲前端開發者提供了使用JavaScript編寫3D效果的能力。編程

WebGLHTML5 Canvas提供硬件3D加速渲染,這樣Web開發人員就能夠藉助系統顯卡來在瀏覽器裏更流暢地展現3D場景和模型了,還能建立複雜的導航和數據視覺化。json

1.3 Canvas

Canvas是一個能夠自由制定大小的矩形區域,能夠經過JavaScript能夠對矩形區域進行操做,能夠自由的繪製圖形,文字等。

通常使用Canvas都是使用它的2dcontext功能,進行2d繪圖,這是其自己的能力。

和這個相對的,WebGL是三維,能夠描畫3D圖形,WebGL,想要在瀏覽器上進行呈現,它必須須要一個載體,這個載體就是Canvas,區別於以前的2dcontext,還能夠從Canvas中獲取webglcontext

1.4 Three.js

咱們先來從字面意思理解下:Three表明3Djs表明JavaScript,即便用JavaScript來開發3D效果。

Three.js是使用JavaScriptWebGL接口進行封裝與簡化而造成的一個易用的3D庫。

直接使用WebGL進行開發對於開發者來講成本相對來講是比較高的,它須要你掌握較多的計算機圖形學知識。

Three.js在必定程度上簡化了一些規範和難以理解的概念,對不少API進行了簡化,這大大下降了學習和開發三維效果成本。

下面咱們來具體看一下使用Three.js必需要知道的知識。

2、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)決定了幾何圖形具體是以什麼形式展示的。它包括了一個幾何體如何形狀之外的其餘屬性,例如色彩、紋理、透明度等等,MaterialGeometry是相輔相成的,必須結合使用。

下面的代碼咱們建立了一個長方體體,賦予它基礎網孔材料(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接收四個參數,nearfar和上面的相同,分別對應相機可觀測的最遠和最近距離;fov表明水平範圍可觀測的角度,fov越大,水平範圍能觀測到的範圍越廣;aspect表明水平方向和豎直方向可觀測距離的比值,因此fovaspect就能夠肯定垂直範圍內能觀測到的範圍。

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);
            }
        }
複製代碼

來看一下執行效果:

咱們使用requestAnimationFrameThree.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、全景預覽

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);
複製代碼

而後咱們看到的場景應該是這樣的:

這不是咱們想要的效果,咱們想要的是從球的內部觀察全景,而且全景圖是附着外球的內壁的,而不是鋪在外面:

咱們只要需將Materialscale的一個屬性設置爲負值,材料便可附着在幾何體的內部:

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事件,當_isUserInteractingtrue時,實時計算當前相機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;
}
複製代碼

建立好這些標記,咱們把它渲染到場景中。

咱們必須告訴場景這些標記的位置,爲了直觀的理解,咱們須要給這些標記賦予一種座標,這種座標很相似於經緯度,咱們叫它lonlat,具體是如何給定的咱們在下面的章節:全景標記中會詳細介紹。

在這個過程當中,一共經歷了兩次座標轉換:

第一次轉換:將「經緯度」轉換爲三維空間座標,即咱們上面講的那種x、y、z形式的座標。

使用geoPosition2World函數進行轉換,獲得一個Vector3對象,咱們能夠將當前相機_camera做爲參數傳入這個對象的project方法,這會獲得一個標準化後的座標,基於這個座標能夠幫咱們判斷標記是否在視野範圍內,以下面的代碼,若標準化座標在-11的範圍內,則它會出如今咱們的視野中,咱們將它進行準確渲染。

第二次轉換:將三維空間座標轉換爲屏幕座標。

若是咱們直接講上面的三維空間座標座標應用到標記中,咱們會發現不管視野如何移動,標記的位置是不會有任何變化的,由於這樣算出來的座標永遠是一個常量。

因此咱們須要藉助上面的標準化座標,將標記的三維空間座標轉換爲真實的屏幕座標,這個過程是worldPostion2Screen函數來實現的。

關於geoPosition2WorldworldPostion2Screen兩個函數的實現,你們有興趣能夠去個人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);
  });
});
複製代碼

點擊到一個標記,進入到下一張全景圖:

4、全景標記

爲了讓全景圖知道,我要把標記標註在什麼地方,我須要一個工具來把原圖和全景圖上的位置關聯起來:

因爲這部分代碼和 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 };
}

複製代碼

這樣平面地圖上的某點就能夠和三維座標關聯起來了,固然,這還須要必定的轉換,有興趣能夠去源碼研究下geoPosition2WorldworldPostion2Screen兩個函數。

5、插件封裝

上面的代碼中,咱們實現了全景預覽和全景標記的功能,下面,咱們要把這些功能封裝成插件。

所謂插件,便可以直接引用你寫的代碼,並添加少許的配置就能夠實現想要的功能。

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,//開啓默認中鍵刪除 (必須開啓經緯度提示)
}
複製代碼

6、發佈

接下來,咱們就好考慮如何將寫好的插件讓用戶使用了。

咱們主要考慮兩種場景,直接引用和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",而後將namedescriptionversion等信息補充完整。

下面,咱們就能夠開始發佈了,首先你要有一個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命令,將源文件進行編譯,最終暴露給用戶使用的將是liborigin

"build": "babel src --out-dir lib && babel originSrc --out-dir origin",
複製代碼

你還能夠指定一些其餘的命令來供用戶測試,如我將寫好的例子所有放在examples中,而後在scripts定義了expamle命令:

"example": "npm run webpack && node ./server/www"
複製代碼

這樣,用戶將代碼克隆後直接在本地運行npm run example就能夠進行調試了。

7、小結

本項目的github地址:github.com/ConardLi/tp…

文中若有錯誤,歡迎在評論區指正,若是這篇文章幫助到了你,歡迎點贊和關注。

想閱讀更多優質文章、可關注個人github博客,你的star✨、點贊和關注是我持續創做的動力!

關注公衆號後回覆【加羣】拉你進入優質前端交流羣。

相關文章
相關標籤/搜索