最近和一家公司在谈一个项目合作,他们公司主要是做油田相关设备的,比如油罐车、压力车、泵车等。

我的印象中只要和石油相关的企业,就感觉和钱挨得好近,😄 。

他们老板看了我们公司的三维产品后,大为赞叹。 惊呼,我们油田的管理最好也能上一套这样的三维系统。

油田行业的三维可视化项目,我们之前没有做过相关的行业,但是在三维可视化方面,我们经验还是挺多的,比如数据中心、医院、学校等三维可视化项目,还包括智慧园区、智慧城市、智慧小镇的方向的等三维可视化。

下面先上几张三维可视化的图瞅瞅:

园区

数据中心

智慧园区

虽然我们没有直接做过油田的三维可视化,但是有了以上三维方案的技术积累,这事做起来就不会太难。

其实客户的需求,并不是就某个油田场景进行三维可视化的场景搭建。而是要做一个油田三维的布局工具,通过布局工具,可以自由搭建不同的油田场景。

这比直接搭建一个三维的场景要难许多。

所谓万事开头难,难在不开头。 天下事有难易乎,干就完了。

在商务人员和客户确立合同,正式立项后, 我们的设计小姐姐,开发小哥哥,都各司其职,下边就讲一下项目的大概内容。

搭建模型库

第一步要做的就是建模,设计组使用3D建模工具 3d max或者c4d 进行油田设备模型的建模。建模后,导出后缀为obj或者gltf格式文件,这两种格式是我们三维渲染引擎支持的最好的文件格式。

建模后的所有模型文件,最终会放到后端的模型库,模型库的管理目录如下图所示:

模型列表

加载模型

加载模型可使用引擎模型的加载函数进行模型加载,比如obj模型加载,示例代码如下:

new mono.OBJMTLLoader().load( 'yaliche.obj', 'yaliche.mtl', '',  (node)=> {
    node.type = 'obj';
    box.addByDescendant(node);
  },
);

上面加载了一个压力车的模型,加载模型是一个异步的过程,所以会有一个回调函数,加载完成之后,在回调函数中,把模型文件生成的三维对象加入到场景容器box之中,加入之后,场景中就会显示我们的三维对象,如下图所示:

压力车

搭建编辑器框架

在和设计组、开发组一起探讨之后,我们编辑器的框架和视图初步设计出来了,大致样子如下:

编辑器框架

视图左上角是我们的logo,上方是工具栏。左侧分为场景区和组件区,场景区是创建三维场景的列表,组件区主要是模型列表,同时还有些echarts图表组件。

中间部分是三维场景呈现区。

对于这个页面布局,我想不用做太多技术上的阐述,基本上会一点前端开发的人员都可以实现类似的效果。

<div class="layui-layout layui-layout-admin">
      <div class="layui-side layui-bg-black" id="leftTreeWrap">
        <div class="layui-side-scroll">
          <div class="layui-collapse" id="leftTree">
            <div class="sceneTreeWrap">
              <div class="groupTitle">
                场景
              </div>
              
            </div>
            <div class="groupTitle">
              组件
            </div>

            <div id="modelGroupWrap">
              <!-- <div class="layui-colla-item modelGroup not-select" id="groundWrap">
                <h2 class="layui-colla-title"><span>场景模型</span></h2>
                <div class="layui-colla-content" id="groundTree">
                  <div class="tree-wrap"></div>
                </div>
              </div> -->
            </div>
          </div>
        </div>
      </div>
      <div class="layui-body">
        <div class="toolbar">
          <div class="temporaryTool"></div>
        </div>
        <div class="app" tabindex="0">
          <canvas id="monoCanvas"></canvas>
        </div>
      </div>

左侧边栏包括两个部分,一个是场景列表,一个是模型列表。场景列表是树组件,模型列表是手风琴组件,如下图所示:
列表

模型列表的创建过程是这样的,首先从后端获取所有的模型:

 getComponentTree({ params: { owner: user } }, '同步云端组件树失败').then((res) => {
      if (res) {
        const treeData = res.data.data;
        treeData.forEach(({ data }) => {
          this.appendModelBtn(data, true);
        });
        // this.renderTreeDom(res.data.data);
      }
    });

通过每个模型创建模型对于的button,函数是appendModelBtn,如下:

 appendModelBtn(modelData, isNew) {
    const domWrap = this.groupDom[modelData.group];
    if (!domWrap) {
      console.log('缺少该类型对应的组', modelData.group);
      if (modelData.category === 'skyBox') {
        modelData.isNew = true;
        skyData.push({ modelData });
      }
    } else {
      domWrap.querySelector('.tree-wrap').appendChild(this.createModelBtnDom(modelData, isNew));
    }
  }

需要注意的,每个模型按钮都需要有drag and drop的功能。在模型按钮上需要监听drag 或者dragstart事件,这个被封装到一个独立的类Dragger.js里面,在该类中专门处理了dragstart事件:

 addDragger(parent, subClass, option) {
    parent.addEventListener('dragstart', (e) => {
      let target = null;
      //  拿到冒泡的所有元素
      const path = eventPath(e);
      for (let i = 0; i < path.length; i += 1) {
        if (path[i].classList && path[i].classList.contains(subClass)) {
          target = path[i];
          break;
        }
      }
...
}

中间区域是三维呈现区域。 首先创建一个Network3D对象,Network3D对象是封装的三维呈现页面,其底层是由canvas组成的,并使用webgl技术进行三维渲染。下面是创建Network3D的代码:

 const network = new mono.Network3D(box, null, 'monoCanvas');
    network.mode = 'editor';
    window.network = network; // todo
    this.network = network;
    network.bindApp(this);
    network.setRenderSelectFunction(() => false);
    make.Default.path = './static/myModellib/';

    network.setClearColor(0, 0, 0);
    network.setClearAlpha(0);

创建对象之后,让network可以和中间区域的大小自适应:

 mono.Utils.autoAdjustNetworkBounds(
      network,
      document.querySelector('.app'),
      'clientWidth',
      'clientHeight',
    );

其中network上的box对象用于管理要加载的三维对象模型。前面说过在模型列表上增加了drag事件,模型列表上的模型,通过拖拽可以添加到network对象上去,因此在network上面也需要添加对应的事件来添加对象:

 onup: (e) => {
        if (!this.sceneTree.senceId && !window.debug) {
          layui.layer.msg('请先创建或选择场景', {
            time: 2000,
          });
          return;
        }
        //  鼠标不在画布内的时候不创建
        if (isPosInCanvas(network, e)) {
          network.createElement({
            e,
            configString,
            senceId: this.sceneTree.senceId,
          });
        }
      },

当模型从左侧模型列表拖拽到network对象后,鼠标mouseup事件后,创建模型实例:

 network.createElement({
            e,
            configString,
            senceId: this.sceneTree.senceId,
          });

到目前为止,已经完成了整个模型列表加载,模型拖拽创建模型实例的过程。 比如最终通过拖拽的油田场景如下所示:

拖拽的油田场景

在3d场景中,需要调整三维模型的位置、旋转角度和缩放比例,可以通过属性面板来调整:
属性面板

也可以通过三维编辑功能直接在三维场景中对模型进行调整标记,要使用调整编辑功能,只需要加入如下这行代码即可:

 const editInteraction = new mono.EditInteraction(network);
    editInteraction.setScaleable(false);
    editInteraction.setRotateable(false);
    editInteraction.setTranslateable(true);
    editInteraction.setDefaultMode('');

    network.setInteractions([...network.getInteractions(), editInteraction]);

EditInteraction类 用于调整模型的位置、旋转角度和缩放比例。 通过键盘可以调整EditInteraction当前要调整的是那个属性:

case 82: // r 在有选中element时,切换操作为旋转
          if (this.network.getIsMonoElement(this.network.currComponent)) {
            const editInteraction = this.network.getInteractions()[2];
            editInteraction.setScaleable(false);
            editInteraction.setRotateable(true);
            editInteraction.setTranslateable(false);
          }
          break;
        case 84: // t 在有选中element时,切换操作为移动
          if (this.network.getIsMonoElement(this.network.currComponent)) {
            const editInteraction = this.network.getInteractions()[2];
            editInteraction.setScaleable(false);
            editInteraction.setRotateable(false);
            editInteraction.setTranslateable(true);
          }
          break;
        case 89: // y 在有选中element时,切换操作为缩放
          if (this.network.getIsMonoElement(this.network.currComponent)) {
            const editInteraction = this.network.getInteractions()[2];
            editInteraction.setScaleable(true);
            editInteraction.setRotateable(false);
            editInteraction.setTranslateable(false);
          }
          break;

r键切换为旋转角度的调整:
旋转

t键切换为位置的调整:
平移

y键切换为缩放的调整:
缩放

拖拽创造场景之后,每个对象还可以进行实时数据的对接,对接后呈现的效果如下:
实时数据

在完成场景的创建和数据的对接之后,便可以发布场景,点击工具栏的预览按钮,即可以完成场景的发布和预览。上一张最终发布的效果图如下:

最终效果

有兴趣获取编辑器的,请发邮件到:
terry.tan@servasoft.com
欢迎关注公众号“ITman彪叔”。彪叔,拥有10多年开发经验,现任公司系统架构师、技术总监、技术培训师、职业规划师。在计算机图形学、WebGL、前端可视化方面有深入研究。对程序员思维能力训练和培训、程序员职业规划有浓厚兴趣。
ITman彪叔公众号


netcy
204 声望120 粉丝

欢迎对canvas、webgl、图形学感兴趣的读者订阅专栏。