分享 HT 实用技巧:实现指南针和 3D 魔方导航

2024-08-30 10:58

本文主要是介绍分享 HT 实用技巧:实现指南针和 3D 魔方导航,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

三维场景时常需要一个导航标识,用来确定场景所处的方位。

一般有两种表现形式:指南针、小方盒(方位魔方)。

参考一下百度百科中的 maya 界面,可以看到右上角有一个标识方位的小盒子,说的就是它:

HightopoHT for Web 产品可以很方便地构造轻量化的 3D 可视化场景,在 web 端 我们可以利用 HT 2D 引擎3D 渲染引擎 来实现这个功能,搭建一个简易的类 maya 操作界面。

预览地址: https://www.hightopo.com/demo/compass-and-directionbox/

界面简介及效果预览

在这个界面里面我们用到了一个二维场景和两个三维场景,具体效果如下:

功能实现

先来描述一下页面布局:

指南针 通过在 ht.graph.GraphView 中给一个图元设置一个事先绘制好的图标来实现,只需把它放在图纸的左上角(即下图中的位置 1)即可。

方位魔方 通过在一个小场景 (ht.graph3d.Graph3dView)中放置一个魔方 obj 模型来实现,然后把这个小场景放置在图纸的右上角(即下图中的位置 2) 即可。

主三维场景(ht.graph3d.Graph3dView)作为背景放置在整个二维页面的下方(即下图中的位置 3)。

代码示例:

const g3d = new ht.graph3d.Graph3dView();
g3d.setOriginAxisVisible(true);
g3d.setGridVisible(true);
g3d.addToDOM();
const g2d = new ht.graph.GraphView();
g2d.deserialize('displays/test.json', json => {g2d.addToDOM(g3d.getView());
});

位置关系:

指南针同步

先约定一下方位,我们将 Z 轴的负半轴的方向作为北方,Z 轴正半轴作为南方,X 轴的正半轴作为东方,X 轴的负半轴作为西方。

由于 指南针 的目的是用于指示鸟瞰图中的方位,所以与 Y 轴并没有什么关系,我们可以将整个计算过程放在二维空间中进行。

代码示例:

const eye = this.g3d.getEye();
const center = this.g3d.getCenter();
const v = new ht.Math.Vector2(eye[0], eye[2]);
const v2 = new ht.Math.Vector2(center[0], center[2]);
const angle = v.sub(v2).angle() - Math.PI / 2;
compass.setRotation(-angle);
compass.a('angle', angle);
compass.a('angle2', angle);

在这段代码中,我们用** eye (相机) 和 center** (观测点)来构建两个二维向量 (ht.Math.Vector2),舍弃掉 Y 轴上的分量。

利用向量减法,求得由 center 指向 eye 的向量并存入变量 v 中,利用 angle() 方法可以获取到当前向量与 x 正半轴 (即正东方向)的夹角(弧度制),为什么要减去 Math.PI / 2 呢,因为我们计算求得的是与 x 轴的夹角,而指南针的正方向(北方)是对应着 z 轴的负半轴。

求得了旋转角度后,通过 setRotation() 方法我们可以设置 指南针 图元的旋转角度,为什么要取一个负值(- angle)?因为当视线逆时针转动的时候,坐标轴 和 指南针 相对于人眼是沿反方向运动的,也就是顺时针旋转。

利用 **HT 2D **引擎提供的 数据绑定 的功能,轮盘图标 和 角度图标 的旋转角度可以通过给 compass 这个节点设置属性值来实时动态改变。

每一次视线发生改变都需要进行如上的计算和设置,我们可以通过给三维场景组件增加一个属性监听器来实现:

graph3dView.addPropertyChangeListener(e=>{if(e.property === 'eye' || e.property === 'center'){changeCompass();//...}
});

图例参考:

方位魔方同步

先约定一下方位,X 正半轴为右,负半轴为左; Y 正半轴为顶,负半轴为底;Z 正半轴为前,负半轴为后。

方位魔方不同于指南针,它用于呈现三维空间中的视线方位。

与此同时,它也是一个可以交互的方位操纵杆,可以方便快捷的将当前视角变为顶视图、侧视图等。

视线改变触发魔方变换

代码示例:

graph3dView.addPropertyChangeListener(e => {if (e.property === 'eye') {const newValue = e.newValue;const vEye = new ht.Math.Vector3(newValue[0], newValue[1], newValue[2]).normalize();graph3dView2.setEye([300 * vEye.x, 300 * vEye.y, 300 * vEye.z]);}
});

在上述代码中我们通过监听主三维场景(graph3dView) 中** eye **属性的变化来动态改变小场景(graph3dView2) 中的 eye 的位置, 来达到联动的效果。

其中,e.newValue 会获取到场景视点改变后的值,我们用这个值构建一个三维向量(ht.Math.Vector3)并调用 **normalize() ** 方法进行归一化,这样可以使得任何角度、位置求得的距离都保持一致。

将求得的分量乘以 300 的原因在于这个距离观测小方块不大不小刚合适,当然也可以根据需要改成别的值。

效果示例:

点击魔方改变场景视角

要想实现点击魔方来改变主场景中的视线,需要一个非常关键的信息,那就是鼠标究竟点击了小魔方的哪一个面。

在这里我们需要用到一个求交点的方法:** graph3dView.intersectObject(event, data)**,该方法会返回一个对象,该对象用于描述点击的位置信息, 其中 **world **属性用来表示点击位置的世界坐标。

代码示例:

graph3dView2.addInteractorListener(event => {if (event.kind === 'clickData') {const obj = graph3dView2.intersectObject(event.event, event.data);if(obj) {const world = obj.world;//...}}
});

拿到了这个描述点击位置的 world 属性我们就可以比较轻松地算出点击了哪个面,因为我们的小方块是放置在原点处,并且它是规则的六面体,这两个关键信息决定了无论点击它的哪一个面,所点击的那个面它所对应的轴的分量的值一定会大于它在另外两个轴的分量,因此我们可以简单的判断三分量中哪个值较大就能确定视线更靠近哪个轴,然后通过判断分量的正负号来判断是在正半轴还是负半轴。

判断了出了点击的哪个面之后,只需要在两个三维场景中分别设置各自视点(eye) 的位置即可。

代码示例:

const world = obj.world;
const x = world.x;
const y = world.y;
const z = world.z;
if (Math.abs(x) - Math.abs(y) > 0 && Math.abs(x) - Math.abs(z) > 0) {if (x > 0) {graph3dView2.setEye([300, 0, 0]);graph3dView.setEye([this._distance, 0, 0]);graph3dView2.setCenter([0, 0, 0]);this._g3d.setCenter([0, 0, 0]);} else {graph3dView2.setEye([-300, 0, 0]);graph3dView.setEye([-this._distance, 0, 0]);graph3dView2.setCenter([0, 0, 0]);graph3dView.setCenter([0, 0, 0]);}
} else if (Math.abs(y) - Math.abs(x) > 0 && Math.abs(y) - Math.abs(z) > 0) {//...
}

其中,this._distance 是用来描述主场景中视线与原点的距离,可根据需要来调整,300 与之前的描述一致,是小场景中一个比较合适的视角位置,也可以根据需要调整。

最后我们还需要处理一下小方块点击变色的问题(这也不见得是个问题,视需求而定),可以在点击事件监听器的最后做如下设置:

1 const sm = graph3dView2.dm().getSelectionModel();
2 sm.setSelection(null);

点击魔方各个面效果演示:

总结

直观的方位指示在室内定位、GIS、车站、机场等诸多场景中有着广泛的应用,利用 HT 提供的二三维引擎可以轻松地实现。

web 3D 有无限的想象空间,有着非常丰富的数据呈现方式,更有着诸多让人眼前一亮的可视化效果,等着我们去将这些数据呈现方式在各个行业中落地,HT 在这方面做了大量的探索和尝试,例如这个好玩儿的太阳系监控系统:https://www.hightopo.com/demo/solar-system/

2019 我们也更新了数百个工业互联网 2D/3D 可视化案例集,在这里你能发现许多新奇的实例,也能发掘出不一样的工业互联网:《分享数百个 HT 工业互联网 2D 3D 可视化应用案例之 2019 篇》,更多行业应用实例可以参考官网案例链接:
https://www.hightopo.com/demos/index.html

这篇关于分享 HT 实用技巧:实现指南针和 3D 魔方导航的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1120630

相关文章

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分