看完這篇,你也可以實現一個360度全景外掛

ConardLi發表於2019-05-05

導讀

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

我們先來看一下外掛的效果:

如果你對Three.js已經很熟悉了,或者你想跳過基礎理論,那麼你可以直接從全景預覽開始看起。

本專案的github地址:https://github.com/ConardLi/t...

一、理清關係

1.1 OpenGL

OpenGL是用於渲染2D、3D量圖形的跨語言、跨平臺的應用程式程式設計介面(API)

這個介面由近350個不同的函式呼叫組成,用來從簡單的圖形位元繪製複雜的三維景象。

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

基於OpenGL,一般使用CCpp開發,對前端開發者來說不是很友好。

1.2 WebGL

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

WebGLHTML5 Canvas提供硬體3D加速渲染,這樣Web開發人員就可以藉助系統顯示卡來在瀏覽器裡更流暢地展示3D場景和模型了,還能建立複雜的導航和資料視覺化。

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是使用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)決定了幾何圖形具體是以什麼形式展現的。它包括了一個幾何體如何形狀以外的其他屬性,例如色彩、紋理、透明度等等,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.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);
  });
});

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

四、全景標記

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


由於這部分程式碼和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.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",然後將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就可以進行除錯了。

七、小結

本專案的github地址:https://github.com/ConardLi/t...

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

想閱讀更多優質文章、可關注我的github部落格,你的star✨、點贊和關注是我持續創作的動力!

推薦關注我的微信公眾號【code祕密花園】,每天推送高質量文章,我們一起交流成長。

相關文章