分享 HT 实用技巧:实现指南针和 3D 魔方导航
前言
三維場(chǎng)景時(shí)常需要一個(gè)導(dǎo)航標(biāo)識(shí),用來(lái)確定場(chǎng)景所處的方位。
一般有兩種表現(xiàn)形式:指南針、小方盒(方位魔方)。
參考一下百度百科中的 maya 界面,可以看到右上角有一個(gè)標(biāo)識(shí)方位的小盒子,說(shuō)的就是它:
Hightopo 的 HT for Web 產(chǎn)品可以很方便地構(gòu)造輕量化的 3D 可視化場(chǎng)景,在 web 端我們可以利用 HT 2D 引擎和3D渲染引擎來(lái)實(shí)現(xiàn)這個(gè)功能,搭建一個(gè)簡(jiǎn)易的類 maya 操作界面。
預(yù)覽地址:https://www.hightopo.com/demo/compass-and-directionbox/
界面簡(jiǎn)介及效果預(yù)覽
在這個(gè)界面里面我們用到了一個(gè)二維場(chǎng)景和兩個(gè)三維場(chǎng)景,具體效果如下:
功能實(shí)現(xiàn)
先來(lái)描述一下頁(yè)面布局:
指南針通過(guò)在 ht.graph.GraphView 中給一個(gè)圖元設(shè)置一個(gè)事先繪制好的圖標(biāo)來(lái)實(shí)現(xiàn),只需把它放在圖紙的左上角(即下圖中的位置 1)即可。
方位魔方 通過(guò)在一個(gè)小場(chǎng)景 (ht.graph3d.Graph3dView)中放置一個(gè)魔方 obj 模型來(lái)實(shí)現(xiàn),然后把這個(gè)小場(chǎng)景放置在圖紙的右上角(即下圖中的位置 2) 即可。
主三維場(chǎng)景(ht.graph3d.Graph3dView)作為背景放置在整個(gè)二維頁(yè)面的下方(即下圖中的位置 3)。
代碼示例:
1 const g3d = new ht.graph3d.Graph3dView();
2 g3d.setOriginAxisVisible(true);
3 g3d.setGridVisible(true);
4 g3d.addToDOM();
5 const g2d = new ht.graph.GraphView();
6 g2d.deserialize('displays/test.json', json => {
7 g2d.addToDOM(g3d.getView());
8 });
位置關(guān)系:
指南針同步
先約定一下方位,我們將 Z 軸的負(fù)半軸的方向作為北方,Z 軸正半軸作為南方,X 軸的正半軸作為東方,X 軸的負(fù)半軸作為西方。
由于 指南針 的目的是用于指示鳥(niǎo)瞰圖中的方位,所以與 Y 軸并沒(méi)有什么關(guān)系,我們可以將整個(gè)計(jì)算過(guò)程放在二維空間中進(jìn)行。
代碼示例:
1 const eye = this.g3d.getEye();
2 const center = this.g3d.getCenter();
3 const v = new ht.Math.Vector2(eye[0], eye[2]);
4 const v2 = new ht.Math.Vector2(center[0], center[2]);
5 const angle = v.sub(v2).angle() - Math.PI / 2;
6 compass.setRotation(-angle);
7 compass.a('angle', angle);
8 compass.a('angle2', angle);
在這段代碼中,我們用 eye (相機(jī)) 和 center (觀測(cè)點(diǎn))來(lái)構(gòu)建兩個(gè)二維向量 (ht.Math.Vector2),舍棄掉 Y 軸上的分量。
利用向量減法,求得由 center 指向 eye 的向量并存入變量 v 中,利用 angle() 方法可以獲取到當(dāng)前向量與 x 正半軸 (即正東方向)的夾角(弧度制),為什么要減去 Math.PI / 2 呢,因?yàn)槲覀冇?jì)算求得的是與 x 軸的夾角,而指南針的正方向(北方)是對(duì)應(yīng)著 z 軸的負(fù)半軸。
求得了旋轉(zhuǎn)角度后,通過(guò) setRotation() 方法我們可以設(shè)置 指南針 圖元的旋轉(zhuǎn)角度,為什么要取一個(gè)負(fù)值(- angle)?因?yàn)楫?dāng)視線逆時(shí)針轉(zhuǎn)動(dòng)的時(shí)候,坐標(biāo)軸和 指南針相對(duì)于人眼是沿反方向運(yùn)動(dòng)的,也就是順時(shí)針旋轉(zhuǎn)。
利用 HT 2D引擎提供的數(shù)據(jù)綁定的功能,輪盤圖標(biāo)和 角度圖標(biāo)的旋轉(zhuǎn)角度可以通過(guò)給 compass 這個(gè)節(jié)點(diǎn)設(shè)置屬性值來(lái)實(shí)時(shí)動(dòng)態(tài)改變。
每一次視線發(fā)生改變都需要進(jìn)行如上的計(jì)算和設(shè)置,我們可以通過(guò)給三維場(chǎng)景組件增加一個(gè)屬性監(jiān)聽(tīng)器來(lái)實(shí)現(xiàn):
1 graph3dView.addPropertyChangeListener(e=>{
2 if(e.property === 'eye' || e.property === 'center'){
3 changeCompass();
4 //...
5 }
6 });
圖例參考:
方位魔方同步
先約定一下方位,X 正半軸為右,負(fù)半軸為左; Y 正半軸為頂,負(fù)半軸為底;Z 正半軸為前,負(fù)半軸為后。
方位魔方不同于指南針,它用于呈現(xiàn)三維空間中的視線方位。
與此同時(shí),它也是一個(gè)可以交互的方位操縱桿,可以方便快捷的將當(dāng)前視角變?yōu)轫斠晥D、側(cè)視圖等。
視線改變觸發(fā)魔方變換
代碼示例:
1 graph3dView.addPropertyChangeListener(e => {
2 if (e.property === 'eye') {
3 const newValue = e.newValue;
4 const vEye = new ht.Math.Vector3(newValue[0], newValue[1], newValue[2]).normalize();
5 graph3dView2.setEye([300 * vEye.x, 300 * vEye.y, 300 * vEye.z]);
6 }
7 });
在上述代碼中我們通過(guò)監(jiān)聽(tīng)主三維場(chǎng)景(graph3dView) 中 eye 屬性的變化來(lái)動(dòng)態(tài)改變小場(chǎng)景(graph3dView2) 中的 eye 的位置, 來(lái)達(dá)到聯(lián)動(dòng)的效果。
其中,e.newValue 會(huì)獲取到場(chǎng)景視點(diǎn)改變后的值,我們用這個(gè)值構(gòu)建一個(gè)三維向量(ht.Math.Vector3)并調(diào)用 normalize() 方法進(jìn)行歸一化,這樣可以使得任何角度、位置求得的距離都保持一致。
將求得的分量乘以 300 的原因在于這個(gè)距離觀測(cè)小方塊不大不小剛合適,當(dāng)然也可以根據(jù)需要改成別的值。
效果示例:
點(diǎn)擊魔方改變場(chǎng)景視角
要想實(shí)現(xiàn)點(diǎn)擊魔方來(lái)改變主場(chǎng)景中的視線,需要一個(gè)非常關(guān)鍵的信息,那就是鼠標(biāo)究竟點(diǎn)擊了小魔方的哪一個(gè)面。
在這里我們需要用到一個(gè)求交點(diǎn)的方法: graph3dView.intersectObject(event, data),該方法會(huì)返回一個(gè)對(duì)象,該對(duì)象用于描述點(diǎn)擊的位置信息, 其中 world 屬性用來(lái)表示點(diǎn)擊位置的世界坐標(biāo)。
代碼示例:
1 graph3dView2.addInteractorListener(event => {
2 if (event.kind === 'clickData') {
3 const obj = graph3dView2.intersectObject(event.event, event.data);
4 if(obj) {
5 const world = obj.world;
6 //...
7 }
8 }
9 });
拿到了這個(gè)描述點(diǎn)擊位置的 world 屬性我們就可以比較輕松地算出點(diǎn)擊了哪個(gè)面,因?yàn)槲覀兊男》綁K是放置在原點(diǎn)處,并且它是規(guī)則的六面體,這兩個(gè)關(guān)鍵信息決定了無(wú)論點(diǎn)擊它的哪一個(gè)面,所點(diǎn)擊的那個(gè)面它所對(duì)應(yīng)的軸的分量的值一定會(huì)大于它在另外兩個(gè)軸的分量,因此我們可以簡(jiǎn)單的判斷三分量中哪個(gè)值較大就能確定視線更靠近哪個(gè)軸,然后通過(guò)判斷分量的正負(fù)號(hào)來(lái)判斷是在正半軸還是負(fù)半軸。
判斷了出了點(diǎn)擊的哪個(gè)面之后,只需要在兩個(gè)三維場(chǎng)景中分別設(shè)置各自視點(diǎn)(eye) 的位置即可。
代碼示例:
1 const world = obj.world;
2 const x = world.x;
3 const y = world.y;
4 const z = world.z;
5 if (Math.abs(x) - Math.abs(y) > 0 && Math.abs(x) - Math.abs(z) > 0) {
6 if (x > 0) {
7 graph3dView2.setEye([300, 0, 0]);
8 graph3dView.setEye([this._distance, 0, 0]);
9 graph3dView2.setCenter([0, 0, 0]);
10 this._g3d.setCenter([0, 0, 0]);
11 } else {
12 graph3dView2.setEye([-300, 0, 0]);
13 graph3dView.setEye([-this._distance, 0, 0]);
14 graph3dView2.setCenter([0, 0, 0]);
15 graph3dView.setCenter([0, 0, 0]);
16 }
17 } else if (Math.abs(y) - Math.abs(x) > 0 && Math.abs(y) - Math.abs(z) > 0) {
18 //...
19 }
其中,this._distance 是用來(lái)描述主場(chǎng)景中視線與原點(diǎn)的距離,可根據(jù)需要來(lái)調(diào)整,300 與之前的描述一致,是小場(chǎng)景中一個(gè)比較合適的視角位置,也可以根據(jù)需要調(diào)整。
最后我們還需要處理一下小方塊點(diǎn)擊變色的問(wèn)題(這也不見(jiàn)得是個(gè)問(wèn)題,視需求而定),可以在點(diǎn)擊事件監(jiān)聽(tīng)器的最后做如下設(shè)置:
1 const sm = graph3dView2.dm().getSelectionModel(); 2 sm.setSelection(null);
點(diǎn)擊魔方各個(gè)面效果演示:
總結(jié)
直觀的方位指示在室內(nèi)定位、GIS、車站、機(jī)場(chǎng)等諸多場(chǎng)景中有著廣泛的應(yīng)用,利用 HT 提供的二三維引擎可以輕松地實(shí)現(xiàn)。
web 3D 有無(wú)限的想象空間,有著非常豐富的數(shù)據(jù)呈現(xiàn)方式,更有著諸多讓人眼前一亮的可視化效果,等著我們?nèi)⑦@些數(shù)據(jù)呈現(xiàn)方式在各個(gè)行業(yè)中落地,HT 在這方面做了大量的探索和嘗試,例如這個(gè)好玩兒的太陽(yáng)系監(jiān)控系統(tǒng):https://www.hightopo.com/demo/solar-system/
2019我們也更新了數(shù)百個(gè)工業(yè)互聯(lián)網(wǎng)2D/3D可視化案例集,在這里你能發(fā)現(xiàn)許多新奇的實(shí)例,也能發(fā)掘出不一樣的工業(yè)互聯(lián)網(wǎng):《分享數(shù)百個(gè)HT工業(yè)互聯(lián)網(wǎng)2D3D可視化應(yīng)用案例之2019篇》,更多行業(yè)應(yīng)用實(shí)例可以參考官網(wǎng)案例鏈接:
https://www.hightopo.com/demos/index.html
總結(jié)
以上是生活随笔為你收集整理的分享 HT 实用技巧:实现指南针和 3D 魔方导航的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Invoia 推出狗狗智能项圈:可跟踪心
- 下一篇: 用于芯片生产的氖气开始降价