基于 HTML5 WebGL 与 WebVR 3D 虚拟现实的可视化培训系统
<p><span style="font-size: 18pt"><strong>前言</strong></span></p><p>2019 年 <strong>VR</strong>, <strong>AR</strong>, <strong>XR</strong>, <strong>5G</strong>, <strong>工业互联网</strong>等名词频繁出现在我们的视野中,信息的分享与虚实的结合已经成为大势所趋,<strong>5G</strong> 是新一代信息通信技术升级的重要方向,工业互联网是制造业转型升级的发展趋势。本文所讲的 <strong>VR</strong> 是机械制造业与设备的又一次交流,当技术新星遇上制造潮流,无疑将成为制造业,工控业等行业数字化转型的重要驱动力。<strong>“5G + VR + 工业互联网”</strong>必将成为新的一年不变的话题,如何将当前工业中遇到的问题通过<strong>虚拟现实</strong>结合起来,让我们可以更近的去交流,去感受技术带给我们的变化。在今年苹果的发布会上,相信大家都知道苹果的 <strong>5G</strong> 手机没有问世,说明 <strong>5G</strong> 的应用和发展还处在快速发展的阶段,但是手机结合 <strong>AR</strong> 功能的 APP 已经早就问世,<strong>5G</strong> 的速度加上 <strong>AR, VR</strong> 的身临其境,让我们感受到的不仅仅是技术的革新,更是让我们感受到技术在不同领域的实际应用场景,我相信 2020 年新的一年必定是 <strong>“5G + VR + 工业互联网”</strong> 应用的又一个新的开始,本文接下来所讲的就是 <strong>HT for Web </strong>结合 <strong>WebVR</strong> 开发的具体应用案例。</p>
<p><span style="font-size: 18pt"><strong>系统预览</strong></span></p>
<p>预览地址:<strong>基于 HTML5 WebGL 与 WebVR 3D 虚拟现实的可视化培训系统 </strong><strong>http://www.hightopo.com/demo/vr-training/</strong></p>
<p><strong><span style="font-size: 14pt">VR 拆解还原</span></strong></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105142740798-574522666.gif"></p>
<p><span style="font-size: 14pt"><strong>VR 操作</strong></span></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105142752415-1341192767.gif"></p>
<p><span style="font-size: 14pt"><strong>VR 场景切换</strong></span></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105142800168-40154469.gif"></p>
<p><span style="font-size: 14pt"><strong>PC 端拆解还原</strong></span></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105142811234-1780951321.gif"></p>
<p><span style="font-size: 14pt"><strong>PC 端考试</strong></span></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105142823944-1537887484.gif"></p>
<p><span style="font-size: 18pt"><strong>系统介绍</strong></span></p>
<p>该系统共分为三个实际应用层面:</p>
<ul>
<li><strong>三维培训:</strong> 用户通过 mb 端手指触摸或者 pc 端鼠标拖拽可以将设备拆解开来,之后可以通过一键还原来将设备还原到最初的状态,或者可以通过拆解 or 还原按钮查看设备自动拆解的过程以及拆解之后自动还原的过程。</li>
<li><strong>考试系统:</strong> 这部分是考验你对设备拆解的熟悉程度,在第一步的三维培训之后,可以在该系统中考核你对拆解过程的了解。</li>
<li><strong>VR 模式:</strong> 该部分便是三维场景结合 WebVR 的具体实现应用,在进入 VR 之后可以通过操作 VR 手柄,进行设备的拆解还原。</li>
</ul>
<p>文章主要讲解第三部分的 <strong>VR</strong> 模式,让我们了解如何结合 <strong>HT</strong> 来搭建 <strong>VR</strong> 场景。下面描述了 <strong>VR</strong> 中的主要操作,没有进入 <strong>VR</strong> 的时候不会出现如下所说的六个按钮操作,在点击进入 <strong>WebVR</strong> 时,系统自动显示出 <strong>VR</strong> 场景里的六个操作按钮,反之退出 <strong>VR</strong> 时,系统也会自动隐藏三维中的六个操作按钮,<strong>VR</strong> 中的主要操作如下:</p>
<ul>
<li><strong>设备切换:</strong> 顾名思义,可以通过手柄射线对准场景中左侧列表,按动板机进行场景设备切换。</li>
<li><strong>操作切换:</strong> VR 中对设备有如下两种操作,可以通过右下角的模式按钮点击切换。</li>
<li><strong>平移模式:</strong> 该模式下,用户可以对准设备并且按动板机将设备从一个位置移动到另一个位置,并且可以通过触摸触摸板来拉近和拉远设备零件。</li>
<li><strong>抓取模式:</strong> 该模式下,用户可以对准设备并且按动板机将设备抓取过来,抓取过来之后,可以通过触摸触摸板来旋转以及放大或者缩小零件。</li>
<li><strong>一键还原:</strong> 将设备各部分零件还原到最初始的位置。</li>
<li><strong>拆解动画:</strong> 将设备的各部分零件通过之前预定好的位置按步骤一步一步拆解开来。</li>
<li><strong>还原动画:</strong> 该操作可以理解为拆解动画的倒放,即将拆解的过程逆序还原。</li>
<li><strong>线框切换:</strong> HT 支持将设备节点的三角面表示出来,可以具体的看到该设备的线框轮廓。</li>
</ul>
<p><span style="font-size: 18pt"><strong>系统开发</strong></span></p>
<p><span style="font-size: 14pt"><strong>三维场景</strong></span><br><strong>HT</strong> 支持 <strong>obj</strong> 模型的导入,<strong>VR</strong> 场景所出现的设备零件均为 obj 模型,由于需要在之后进行设备的拆解,所以建模的时候需要分别对设备的各部分零件进行建模,而不是对设备整体进行建模,如果对设备整体建模那么在 HT 的场景中就是一个 <strong>Data</strong> 节点,从而不能对零件进行拆解,如果拆解开来,那么在 HT 中可以加载多个 obj 则就有多个 Data 节点,有多个零件的 Data 节点之后就可以对设备零件进行移动或者其它旋转操作,具体的 Data 在 HT 的含义可以参考<strong> HT for Web 数据模型手册</strong></p>
<p>如下为导入场景中的 <strong>obj</strong> 模型:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105143425677-1331933726.png"></p>
<p>从上图可以看出我们导入 <strong>obj</strong> 之后零件之间是分散的,所以需要对零件的初始位置进行调整,从而调整出一个由许多零件构成的完整设备,当然调整不可能通过代码来调整,对应的有三维编辑器可以调整,进行拖拖拽拽将不同零件拼凑起来,如下为组合之后的设备整体:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105143443083-1142217726.png"></p>
<p><span style="font-size: 14pt"><strong>VR 搭建</strong></span><br><strong>VR</strong> 场景的搭建是在第一步的基础上进行搭建,上面所说的只在 VR 场景中显示的按钮也是在场景中进行搭建,在正常的场景时候我们可以隐藏掉对应的节点,<strong>node.s('3d.visible', false)</strong> 上面的代码就是 HT 中在三维下面隐藏三维节点的代码,因为进入 VR 和离开 VR 的时候,HT 内部会派发出对应的状态告诉用户此时已经进入 VR 或者此时已经离开 VR,相应伪代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> graph3dView 为 HT 中的三维场景视图容器</span>
<span style="color: rgba(0, 128, 128, 1)"> 2</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> vr 获取挂载在 graph3dView 上的 vr 对象</span>
<span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 0, 255, 1)">var</span> vr =<span style="color: rgba(0, 0, 0, 1)"> graph3dView.vr;
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> vr.mp(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(e) {
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> property 对应的 vr 事件类型,detail 此时事件的状态</span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 0, 255, 1)">var</span> property =<span style="color: rgba(0, 0, 0, 1)"> e.property;
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span> <span style="color: rgba(0, 0, 255, 1)">var</span> detail =<span style="color: rgba(0, 0, 0, 1)"> e.newValue;
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> present 代表此时进入或者离开 VR 场景</span>
<span style="color: rgba(0, 128, 128, 1)"> 9</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (property === 'present'<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 此时 detail 为 true 表示进入 vr,false 表示离开 vr</span>
<span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (detail) {
</span><span style="color: rgba(0, 128, 128, 1)">12</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 执行显示 vr 场景中需要显示的节点操作</span>
<span style="color: rgba(0, 128, 128, 1)">13</span> } <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 执行隐藏 vr 场景中需要隐藏的节点操作</span>
<span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">16</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">17</span> });</pre>
</div>
<p>上面 property 在 <strong>HT</strong> 总共会派发出以下几种类型,主要是包括 <strong>VR</strong> 的状态和手柄的操作类型:</p>
<ul>
<li><strong>enable:</strong> vr 的 enable 信息发生变化</li>
<li><strong>present:</strong> vr 的 present 信息发生变化,表明进出 vr 世界</li>
<li><strong>gamepad.pose:</strong> 手柄位置或旋转发生变动,参数 id,position,rotation</li>
<li><strong>gamepad.axes:</strong> 手柄中间的转盘触摸点位变动,参数 id,axes;其中 axes 格式形如:[ 0.2, 0.7 ],分辨表示横纵百分比</li>
<li><strong>gamepad.button.thumbpad:</strong> thumbpad 按键被按下,参数 id,state,其中 state 包含 down 跟 up 两种</li>
<li><strong>gamepad.button.trigger:</strong> trigger 按键被按下,参数 id,state,其中 state 包含 down 跟 up 两种</li>
<li><strong>gamepad.button.grips:</strong> grips 按键被按下,参数 id,state,其中 state 包含 down 跟 up 两种</li>
<li><strong>gamepad.button.menu:</strong> menu 按键被按下,参数 id,state,其中 state 包含 down 跟 up 两种</li>
</ul>
<p>VR 中有一个关键的配置就是<strong>比例尺</strong>,因为 VR 里面的单位是和现实中的长度单位是一致的,我们戴着头盔往前走 1m 那么对应在 HT 三维场景中需要往前走多远这需要一个对应关系,HT 提供的 VR 插件中会提供一个 <strong>measureOflength</strong> 的配置项,如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 255, 1)">var</span> vr_config =<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">2</span> measureOflength: 0.01<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">3</span> }</pre>
</div>
<p>上面 0.01 代表的意思就是 HT 场景中的单位长度 1 代表现实场景的 0.01 米,所以如果此时现实场景你戴着头盔往前移动 1m,那么 HT 中对应的视角会往前移动 100 个单位,所以如果需要搭建 VR 场景要注意场景的模型建模比例和现实世界是相差多少,按照统一的比例来建模,不然在 VR 场景中会出现设备大小不一的问题,导致出现错觉,如下对比图,左侧是 0.01 的比例,射线的小点很小,右侧是是 0.001 的比例导致射线的小点变大。</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105143842424-724429196.png"></p>
<p><strong>HT</strong> 中已经对浏览器提供的 <strong>WebVR</strong> 相关接口的 API 进行了封装,包括获取设备 <strong>navigator.getVRDisplays()</strong> 这是进入 VR 世界的第一步,如果此时执行此代码返回的结果为空代表获取 VR 设备失败,那么之后更不用说了,以及获取手柄信息 <strong>navigator.getGamepads()</strong>,用户可以通过在浏览器控制台敲入上面两行代码,查看浏览器是否已经获取到了 VR 设备信息和 VR 手柄信息,如果返回为空则说明获取失败。HT 只要通过执行 <strong>graph3dView.vr.enable = true</strong> 就可以开启 <strong>VR</strong>,当然用户不用执行该代码,HT 提供的 <strong>VR 插件</strong>也会提供对应的配置项 <strong>vrEnable: true</strong> 来代表开启 VR,对应的配置也挂在在上面的 <strong>vr_config</strong> 对象内,如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 255, 1)">var</span> vr_config =<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">2</span> measureOflength: 0.01<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">3</span> vrEnable: <span style="color: rgba(0, 0, 255, 1)">true</span>, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 代表开启 VR</span>
<span style="color: rgba(0, 128, 128, 1)">4</span> }</pre>
</div>
<p>在该展示的系统中有直接在 <strong>VR 中切换场景</strong>的功能,由于每个场景的 <strong>vr_config</strong> 中的<strong>配置项值</strong>可能会有差别,例如第一个场景的 <strong>measureOflength</strong> 比例尺的大小为 0.01,可能第二个场景的比例尺大小 <strong>measureOflength</strong> 就变成了 0.02,所以 VR 插件提供一个<strong>销毁的功能</strong>,用来销毁上一个场景的资源,销毁场景的资源<strong>包括清空上一个场景的所有节点</strong>,所以在加载新的场景时,<strong>不需要再执行清空场景节点的操作,即不需要执行 dataModel.clear()</strong>,因为 VR 提供的销毁功能已经都清空了,手柄和射线都是场景中的一个 Data 节点,所以在新的场景不需要额外的清除手柄和射线这两个节点,故插件帮你管理场景的节点。在调用销毁功能之后,可以调用 <strong>graph3dView</strong> 的序列化函数 <strong>graph3dView.deserialize('场景资源json地址')</strong> 来序列化新的场景 json 文件,在序列化完成的回调函数中,可以根据新的场景修改此时 <strong>vr_config</strong> 的值,然后再次调用 <strong>graph3dView.initVRForScene()</strong> 来再次初始化 <strong>VR</strong> 场景。相关的步骤伪代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> window.GVR 是在调用 graph3dView.initVRForScene() 之后初始化的一个全局 VR 插件变量 用于用户获取插件对象</span>
<span style="color: rgba(0, 128, 128, 1)"> 2</span> <span style="color: rgba(0, 0, 0, 1)">window.GVR.destory();
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 执行新的场景序列化操作</span>
<span style="color: rgba(0, 128, 128, 1)"> 4</span> graph3dView.deserialize('场景资源json地址'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(json, dm, g3d, datas) {
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 修改新的场景比例尺为 0.02</span>
<span style="color: rgba(0, 128, 128, 1)"> 7</span> window.vr_config.measureOflength = 0.02<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 修改新的 VR 场景初始化视角</span>
<span style="color: rgba(0, 128, 128, 1)"> 9</span> window.vr_config.vrEye =<span style="color: rgba(0, 0, 0, 1)"> ht.Default.clone(g3d.getEye());;
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 再次初始化 VR 场景</span>
<span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 0, 1)"> graph3dView.initVRForScene()
</span><span style="color: rgba(0, 128, 128, 1)">12</span> });</pre>
</div>
<p>当然 HT 提供的 VR 插件还有很多的配置项,方便用户更好的调整 VR 场景,包括刷地形,场景移动方式,场景操作方式都可以通过配置进行配置,利用 HT 进行 VR 搭建主要流程如下流程图所示:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105144250023-798470602.png"></p>
<p>通过上面的流程图,我们大体可以了解配合 HT 提供的 VR 插件如何进行快速的搭建 VR 场景。</p>
<p>目前谷歌浏览器和火狐浏览器都很友好的支持 VR,可以通过火狐官网提供的 <strong>WebVR Demo</strong> 在线感受下官方提供的 VR 场景。</p>
<p><strong><span style="font-size: 14pt">拆解规则</span></strong><br>从文章前面的部分效果图可以看到我们每个场景的设备都有拆解,并且每个设备的零件数量,零件位置,零件拆解的方向,偏移的长短都是不一致的,所以不可能通过代码来将上面的偏移长短,偏移方向写死,需要制定一套拆解规则来帮助我们可以更方便制作每个场景的拆解动画,这样只需要设计师根据与程序约定好的拆解规则进行配置就可以配置出不同场景不同设备的拆解动画。该系统的拆解分为两种情况:</p>
<ul>
<li><strong>单体移动:</strong> 单个设备零件沿着父节点位置和该节点位置的连接线方向移动</li>
<li><strong>组合移动:</strong> 多个设备零件的组合沿着某个方向移动,组合移动之后,设备零件可以在组合移动之后的位置进行再沿着某个方向进行移动,可以无限进行嵌套,即组合之后还可以组合移动,或者单体移动</li>
</ul>
<p>单体移动示意图如下:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105144416446-2116016256.gif"></p>
<p>组合移动示意图如下:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105144424686-1193050562.gif"></p>
<p>在 HT 中可以通过 <strong>data.setDisplayName('名称')</strong> 来给节点设置名称,这里约定通过不同设备的名称,来获取到不同设备的偏移信息,例如 <strong>data.setDisplayName('1-0.5-1000')</strong> 该名称就是和设计师约定好的配置规则,<strong>1</strong> 代表拆解步骤的第一步执行,当然场景中可以有多个 1,即第一步同时拆解这些零件 <strong>0.5</strong> 代表朝着父节点的方向偏移自己位置和父节点位置连接线长度的 <strong>50%</strong>。<strong>1000</strong> 代表偏移的过程持续 <strong>1000</strong> 毫秒,当然之后可以约定旋转以及旋转的角度等信息。设计师知道这些配置规则之后便可以通过可视化编辑器进行不同零件的配置,这样程序方面只需要写一套通用的逻辑就可以对不同的设备进行拆解和还原。</p>
<p>系统中维护了一个<strong>队列</strong>和一个<strong>栈</strong>,<strong>队列</strong>用来记录<strong>拆解顺序</strong>,<strong>栈</strong>用来记录<strong>还原顺序</strong>。拆解的过程通过配置的序号,按顺序推进队列,采用队列的数据结构便是因为队列<strong>先进先出</strong>的特点,第一个压入队列的零件则第一个执行,最后压入队列的零件最后一个执行拆解顺序。拆解出队列的零件则同时压入栈,采用<strong>栈</strong>记录还原顺序是因为<strong>先进后出</strong>的特点,即第一个执行完拆解的零件,在还原的时候却是最后一个执行还原的动作。所以上述采用的不同数据结构便是为了更好的记录数据。以下为相关 js 伪代码:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 记录拆解顺序</span>
<span style="color: rgba(0, 128, 128, 1)"> 2</span> const queue =<span style="color: rgba(0, 0, 0, 1)"> [];
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 记录还原顺序</span>
<span style="color: rgba(0, 128, 128, 1)"> 4</span> const stack =<span style="color: rgba(0, 0, 0, 1)"> [];
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> each 循环中用来记录拆解队列 queue 顺序</span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span> dataModel.each((node) = ><span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span> const displayName =<span style="color: rgba(0, 0, 0, 1)"> node.getDisplayName();
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (displayName) {
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> const = displayName.split('-'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (index !== <span style="color: rgba(0, 0, 255, 1)">void</span> 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (queue) {
</span><span style="color: rgba(0, 128, 128, 1)">12</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (queue <span style="color: rgba(0, 0, 255, 1)">instanceof</span><span style="color: rgba(0, 0, 0, 1)"> Array) {
</span><span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 0, 1)"> queue.push(node);
</span><span style="color: rgba(0, 128, 128, 1)">14</span> } <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">15</span> const tempNode =<span style="color: rgba(0, 0, 0, 1)"> queue;
</span><span style="color: rgba(0, 128, 128, 1)">16</span> queue =<span style="color: rgba(0, 0, 0, 1)"> ;
</span><span style="color: rgba(0, 128, 128, 1)">17</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">18</span> } <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">19</span> queue =<span style="color: rgba(0, 0, 0, 1)"> node;
</span><span style="color: rgba(0, 128, 128, 1)">20</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">21</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">22</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">23</span> });</pre>
</div>
<p>相关逻辑如下流程图:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105144657640-773804926.png"></p>
<p>通过上面的约定,设计师可以使用可视化编辑器来配置不同零件的移动规则,大大提高了动画的制作效率。</p>
<p><span style="font-size: 14pt"><strong>代码分析</strong></span><br>该部分主要对拆解还原动画的代码进行分析,主要采用<strong>向量</strong>和部分<strong>三角函数</strong>的概念来计算不同零件在三维空间的位置,初始的时候需要记录下每个零件在前面所有组合移动之后的初始移动位置向量,以及零件没有组合移动之前的初始位置向量,获取这两个位置向量目的是一是为了零件拆解在前面所说组合之后移动,和零件在拆解之后恢复到一整个设备形态的初始位置,两个位置向量都有重要的作用,以下为相关伪代码:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Vector3 为 HT 封装的三维向量</span>
<span style="color: rgba(0, 128, 128, 1)"> 2</span> const Vector3 =<span style="color: rgba(0, 0, 0, 1)"> ht.Math.Vector3;
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 记录第一个重要位置向量</span>
<span style="color: rgba(0, 128, 128, 1)"> 4</span> node.a('relativeP3Vec', <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vector3(node.p3()));
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> node 当前零件节点</span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> moveQueue 为移动顺序在 node 之前的,并且为 node 节点的祖先节点</span>
<span style="color: rgba(0, 128, 128, 1)"> 7</span> <span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0, l = moveQueue.length; i < l; i++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> const moveNode =<span style="color: rgba(0, 0, 0, 1)"> moveQueue,
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> parentMoveNode =<span style="color: rgba(0, 0, 0, 1)"> moveNode.getParent();
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (parentMoveNode) {
</span><span style="color: rgba(0, 128, 128, 1)">11</span> const[, distancePer] = moveNode.getDisplayName().split('-'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 128, 1)">12</span> moveNode.a('defP3', moveNode.p3()) moveNode.p3(<span style="color: rgba(0, 0, 255, 1)">new</span> Vector3().lerpVectors(<span style="color: rgba(0, 0, 255, 1)">new</span> Vector3(moveNode.p3()), <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vector3(parentMoveNode.p3()), distancePer).toArray());
</span><span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 记录组合节点移动之后的第二个重要相对位置向量</span>
<span style="color: rgba(0, 128, 128, 1)">16</span> node.a('relativeP3Vec', <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Vector3(node.p3()));
</span><span style="color: rgba(0, 128, 128, 1)">17</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 逆序还原组合的父节点位置</span>
<span style="color: rgba(0, 128, 128, 1)">18</span> <span style="color: rgba(0, 0, 255, 1)">for</span> (let i = moveQueue.length - 1; i >= 0; i--<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">19</span> const moveNode =<span style="color: rgba(0, 0, 0, 1)"> moveQueue;
</span><span style="color: rgba(0, 128, 128, 1)">20</span> moveNode.p3(moveNode.a('defP3'<span style="color: rgba(0, 0, 0, 1)">));
</span><span style="color: rgba(0, 128, 128, 1)">21</span> moveNode.a('defP3'<span style="color: rgba(0, 0, 0, 1)">, undefined);
</span><span style="color: rgba(0, 128, 128, 1)">22</span> }</pre>
</div>
<p>由于在场景拆解过程中需要设置设备零件节点不可选择,所以需要记录下不可选择之前的零件是否可选择状态,用来恢复节点初始状态,相关伪代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> dm3d.each((node) = ><span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 128, 128, 1)">2</span> node.a('defSelectable', node.s('3d.selectable'<span style="color: rgba(0, 0, 0, 1)">));
</span><span style="color: rgba(0, 128, 128, 1)">3</span> });</pre>
</div>
<p>文中所示的线框效果为 HT 核心包支持的线框模式,可以通过如下代码进行配置:</p>
<div class="cnblogs_code">
<pre>dm3d.each((data) = ><span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (data.s('shape3d') && data.s('shape3d').startsWith('models/'<span style="color: rgba(0, 0, 0, 1)">)) {
data.s({
</span>'shape3d.transparent': <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
</span>'shape3d.opacity': 0, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 目的为隐藏原本的模型</span>
'wf.geometry': <span style="color: rgba(0, 0, 255, 1)">true</span>, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 开启线框模式</span>
'wf.combineTriangle': 2, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 线框三角面合并类型</span>
'wf.color': 'rgba(96,172,252,0.3)' <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 线框颜色</span>
<span style="color: rgba(0, 0, 0, 1)"> });
}
});</span></pre>
</div>
<p>上述 <strong>wf.combineTriangle</strong> 主要包括</p>
<ul>
<li><strong>false,0:</strong> 不合并三角形</li>
<li><strong>true,1:</strong> 合并相邻三角为四边面,原来的效果</li>
<li><strong>2:</strong> 融合所有联通的共面三角面</li>
<li><strong>3:</strong> 根据法线信息融合所有平滑三角面</li>
</ul>
<p><span style="font-size: 18pt"><strong>VR 软件以及硬件安装</strong></span></p>
<p>本系统采用的 <strong>VR</strong> 硬件设备为 <strong>HTC VIVE</strong> 接下来讲的是<strong>安装 HTC VIVE</strong> 的过程和步骤。</p>
<p><span style="font-size: 18px"><strong>第一步:撮合 HTC VIVE 和电脑主机</strong></span></p>
<p>到 <strong>HTC</strong> 官网找到连接指南,然后按照步骤安装即可,我们只需看以下截图部分的目录即可。</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145036715-2070027276.png"></p>
<p><span style="font-size: 18px"><strong>第二步:下载软件</strong></span></p>
<p>到 <strong>Steam</strong> 官网下载 Steam,下载完 Steam 可以在 Steam 中下载 Stream VR。</p>
<p><span style="font-size: 18px"><strong>第三步:打开 Stream VR 检查设备状态</strong></span></p>
<p>打开 Stream VR,会出现以下画面,这是用来表示 HTC VIVE 头显的工作状态的,通过图标我们即可查看头显、手柄控制器和定位器等配件的工作情况。</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145114732-1920946188.png"></p>
<p><span style="font-size: 18px"><strong>第四步:选择房间设置模式</strong></span></p>
<p>如果您的房间位置比较大可以选择第一项,我选择的模式为第二项,站立模式。建议选择一种房间规模,可以完整的进行设置。</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145129413-365702223.png"></p>
<p><span style="font-size: 18px"><strong>第五步:将头盔、两个手柄控制器放置在两个定位器可视范围内,建立定位</strong></span></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145145432-1249286427.png"></p>
<p><span style="font-size: 18px"><strong>第六步:校准头盔中心点</strong></span></p>
<p>该步为设置头盔默认的朝向。</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145159214-1567347883.png"></p>
<p><span style="font-size: 18px"><strong>第七步:定位地面</strong></span></p>
<p>将两个手柄控制器放置在定位器可视范围内,然后点击电脑屏幕上的按钮“校准地面”,等待系统校准</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145213015-1912931312.png"></p>
<p><span style="font-size: 18px"><strong>第八步:进入 Steam VR 自带房间进行测试</strong></span></p>
<p>设置完毕之后可以进入 Steam VR 自带的房间进行体验。</p>
<p><span style="font-size: 18pt"><strong>总结</strong></span><br>当人们谈起 <strong>5G</strong> 时代的新应用,<strong>VR、AR</strong> 总是一大热门话题。4G 时代移动网络已经足以承载起高清视频,那么 5G 时代理所当然就能传输数据量更大的沉浸式 VR、AR 影像。因此,不少人将 5G 视为 VR、AR 崛起的踏板,随时随地<strong>身临</strong>天涯海角,似乎并非是遥不可及的梦。当前 4G 网络应用在 VR/AR 上会带来大约 70ms 的时延,这个时延会导致体验者存在眩晕感,而 5G 数据传输的延迟可达到毫秒级,可以有效解决数据时延带来的眩晕感,有助于 VR/AR 的大规模应用。目前随着 5G 网络的逐渐普及,VR/AR 产业正逐步走向复苏,市场热情在逐渐升温,虚拟现实游戏、虚拟现实现场直播等都是 5G 在 VR/AR 上的具体应用。在科技进步的今天,安全也是一个重要的话题,VR 结合<strong>仿真</strong>的应用也是大势所趋,<strong>仿真</strong>可以让用户真实切身感受,例如<strong>消防预警</strong>,<strong>管道预警</strong>,可以让用户在 VR 世界中体验消防灭火等消防员的操作,让用户沉浸在 VR 世界中感受到火灾来临时怎么进行实际操作。所以 VR 带来的应用远远不止仿真,模拟等体验,更多带来的是能为人们提供真实的实际作用,而不是噱头。</p>
<p>程序手机端运行截图:</p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145334221-547944883.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/591709/202001/591709-20200105145343983-1500045311.png"></p><br><br>
来源:https://www.cnblogs.com/xhload3d/p/12154649.html
頁:
[1]