最近和一家公司在谈一个项目合作,他们公司主要是做油田相关设备的,比如油罐车、压力车、泵车等。
我的印象中只要和石油相关的企业,就感觉和钱挨得好近,皮一下 。
他们老板看了我们公司的三维产品后,大为赞叹。 惊呼,我们油田的管理最好也能上一套这样的三维系统。
油田行业的三维可视化项目,我们之前没有做过相关的行业,但是在三维可视化方面,我们经验还是挺多的,比如数据中心、医院、学校等三维可视化项目,还包括智慧园区、智慧城市、智慧小镇的方向的等三维可视化。
下面先上几张三维可视化的图瞅瞅:
虽然我们没有直接做过油田的三维可视化,但是有了以上三维方案的技术积累,这事做起来就不会太难。
其实客户的需求,并不是就某个油田场景进行三维可视化的场景搭建。而是要做一个油田三维的布局工具,通过布局工具,可以自由搭建不同的油田场景。
这比直接搭建一个三维的场景要难许多。
所谓万事开头难,难在不开头。 天下事有难易乎,干就完了。
在商务人员和客户确立合同,正式立项后, 我们的设计小姐姐,开发小哥哥,都各司其职,下边就讲一下项目的大概内容。
搭建模型库
第一步要做的就是建模,设计组使用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键切换为缩放的调整:
拖拽创造场景之后,每个对象还可以进行实时数据的对接,对接后呈现的效果如下:
在完成场景的创建和数据的对接之后,便可以发布场景,点击工具栏的预览按钮,即可以完成场景的发布和预览。上一张最终发布的效果图如下: