数据制作
模型数据制作包括模型制作和属性编辑两个部分,使用的工具和版本如下:
名称 | 版本 |
SuperMap iDesktopX | v11.1,即11i(2023) |
Blender | 3.4.1 |
Substance Painter | 2021.1.1 (7.1.1) build 954 |
数据制作流程参照下图:
图:模型管线制作流程图 |
制作模型
(1)编辑模型
在Blender中处理管线模型的UV,处理完成后,导出obj格式的管线模型。
(2)制作贴图
使用材质编辑软件Substance Painter编写材质贴图,将贴图导出为png、jpg等常用图片格式,本例中使用的是png。
(3)模型贴图
在Blender中将贴图加载在模型中,修改管线材质等。将管线导出为glb模型,用于管线显示,如果项目需要分段管线模型,则将模型分开导出为单独的glb。同时,将管线导出为fbx模型,用于下一步在SuperMap iDesktopX中添加管线属性。
以上步骤建议专业UI人员制作,确保管线数据美观性。
添加属性
将Blender中导出的管线模型,导入到SuperMap iDesktopX。
在“工作空间管理器”中,右键【新建文件型数据源...】,创建数据源。
图:新建文件型数据源 |
在新数据源,右键【导入数据集...】,在“数据导入”窗口,点击【添加】按钮,选择从Blender中导出的fbx格式管线模型,导入数据集。
图:导入数据集 |
在新导入的数据集,右键【添加到新场景...】—【添加到新平面场景】,调整场景的角度、方向,查看模型。
图:添加到新平面场景 |
图:模型显示效果 |
(1)添加爆管分析属性
如果需要对管线进行流向分析、爆管分析,需要为管线数据添加对应属性。
通过【数据】-【类型转换】-【模型->二维面】,将模型生成二维面数据。
图:模型转二维面 |
图:转换为二维面效果 |
新建线数据集,在数据源右键,选择【新建数据集...】—【线】,新建一个线图层。
图:新建线数据集 |
将新建的线数据集拖到二维面窗口,在“图层管理器”中,点击编辑图标,设置图层可编辑。通过【对象操作】-【线】-【直线】/【折线】,手动绘制线对象。注意:每一个模型对应一条线段。
图:基于模型绘制线对象 |
线段绘制完成后,为线段添加属性字段。在线段数据级上单击右键,选择【属性】,在打开的属性表中,选择【属性结构】选项卡,通过【添加】按钮,依次添加属性值LineName、LineHeight、Direction、Position。
图:添加属性值 |
属性 | 含义 | 注意事项 |
---|---|---|
Direction | 管线流向 0 代表以线方向为正向 1 代表以线方向反向为正向 |
|
LineName | 线段对应的管线的名称 | 线段需要进行打断等处理,线段要与管线、弯头等一一对应 有时可能一个线段代表多个模型对象,具体情况具体处理 |
Position | 管线位置 0 表示地上管线 1 表示墙上管线 2 表示顶部管线 |
|
LineHeight | 管线高度 | 在Blender中查看,参照下图 |
图:查看管线高度 |
为每个管线段添加属性。
图:添加管线属性 |
(2)添加阀门点数据集
新建点数据集,在模型阀门位置处绘制点数据。注意:该点需要在线数据之上。
图:添加阀门点 |
为点数据添加属性字段value和height。
属性 | 含义 | 注意事项 |
---|---|---|
value | 阀门点 true:阀门点 |
|
height | 阀门点高度 | 在Blender中查看,参照下图 设置模型原点时需要将点设置在相应高度 |
图:查看阀门高度 |
图:点数据集属性表 |
构建路网数据,用于爆管分析。选择【交通分析】—【拓扑构网】—【构建二维网络】,选择线数据集和阀门点数据集,其他设置默认,构建网络数据。
图:构建网络数据 |
(3)添加管线其它属性
管线的其它属性,如编号、权属、类型、地址、建设日期等,也可以根据需求添加。添加方式同添加爆管属性一致。选择【属性】,在打开的属性表中,选择【属性结构】选项卡,通过【添加】按钮,依次添加属性值。
功能实现
(1)必备类库
进行AR管线功能开发必需的类库为com.supermap.data.jar、com.supermap.ar.jar、sceneform-sm-11.1.0.aar,必需的so库为libimb2d.so。
(2)功能开发
扫码加载管线
public void startImageScan(AREffectView arEffectView,ScanCallback callback){
ImageScanner instance = ImageScanner.getInstance(arEffectView);
instance.addImageListener(images -> {
J:
for (ARAugmentedImage e : images) {
if (e.getTrackingState() != TrackingState.TRACKING){
continue;//需确保图片在Tracking状态
}
Iterator<Map.Entry<String, Marker>> iterator = markerMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Marker> next = iterator.next();
if (next.getKey().equals(e.getName())){
Marker value = next.getValue();
//marker的位置信息不能为null
if (value.getLocation() == null){
throw new NullPointerException("The Location of marker was null.");
}
//通过marker的地理坐标,去校正场景的启动坐标和启动时方位角
ImageScanner.DeviceInfo info = ImageScanner.getInstance(arEffectView).calculateDeviceInfo(e,value.getLocation());
callback.callback(info.getDeviceLocation(),info.getAzimuth());
//已获得位置,后续不再使用,此处销毁监听
stopScan(arEffectView);
break J;
}
}
}
});
}
private void stopScan(AREffectView arEffectView) {
ImageScanner.getInstance(arEffectView).disposeImageListener();
for (ObjectAnimator e: objectAnimators) {
e.cancel();
}
VibrateHelper.vSimple(context,100);
}
public void addPipeScene(AREffectElement parent,String dataPath,boolean onClickEnabled) {
if (parent == null){
return;
}
ModelGroupScene modelGroupScene = new ModelGroupScene(parent);
modelGroupScene.setOnClickEnabled(onClickEnabled);
modelGroupScene.setOnClickListener(new ModelGroupScene.OnClickListener() {
@Override
public void onClick(AREffectElement element, TouchResult touchResult) {
com.eqgis.eqtool.tmp.VibrateHelper.vSimple(parent.getContext(),75);
element.select();
if (dataManagerCallback != null){
dataManagerCallback.onClick(element, touchResult);
}
}
});
modelGroupScene.loadModelFolder(dataPath, "GLB", AREffectElement.VisualizerType.EMISSIVE_FACTOR, new ErrorCallback() {
@Override
public void onError(Error error) {
if (error == null){
Log.i("DataManager-LoadData-successful",dataPath);
}else {
Log.i("DataManager-LoadData-failed",dataPath);
}
}
});
sceneLoaders.add(modelGroupScene);
}
坑洞开挖参考代码:
//遮挡设置
occlusionHelper = arView.getOcclusionHelper();
occlusionHelper.init(0.36f).setRenderMode(OcclusionHelper.RenderMode.NORMAL);
List roomBounds = Arrays.asList(
new Point3D(-1, -1, -2),
new Point3D(-1, 6, -2),
new Point3D(6, 6, -2),
new Point3D(6, -1, -2),
new Point3D(-1, -1, -2)
);
//采用ARGeoPrism,构建“检测墙”
ARGeoPrism geoVerticalRegion = new ARGeoPrism();
geoVerticalRegion.setParentNode(arView);
//仅用作射线检测,渲染状态设置为false
geoVerticalRegion.setRenderable(false);
geoVerticalRegion.addPart(roomBounds,6.0f);
//创建开挖工具,在这之前,需确认AREffectView开启了遮挡设置
//Excavator所有子类使用方法一致
excavatorWall = new WallExcavator(geoVerticalRegion);
//坑洞纹理
Bitmap bitmap=null;
Bitmap bitmap2=null;
try {
InputStream is = getApplicationContext().getAssets().open("brown_mud_dry2.png");
bitmap= BitmapFactory.decodeStream(is);
InputStream is2 = getApplicationContext().getAssets().open("wall_texture.png");
bitmap2= BitmapFactory.decodeStream(is2);
is.close();
is2.close();
} catch (IOException e) {
}
//创建坑洞渲染对象
pitWall = new PitObject(excavatorWall).setTexture(bitmap,bitmap2);
//在每一帧刷新时调用(通常使用EffectView.addOnUpdateListener(EffectView.OnUpdateListener)添加帧监听事件)
arView.addOnUpdateListener(()->{
//开挖计算墙面碰撞点 arView为AREffectView、screenPointX/Y为对应的屏幕坐标
//以屏幕中心计算碰撞点
Point3D hitPoint = excavatorWall.generateHitPoint(arView, screenPointX, screenPointY);
if (hitPoint!=null){
//desc-执行开挖的顶点计算(开挖参数)
excavatorWall.calculate(ExcavationParameter.builder()
.setRadius(radius)
.setOffset(offset)
.setInnerMargin(0)
.setCenterPoint(hitPoint)
.build());
}
//渲染坑洞结果
pitWall.updateMesh();
if (occlusionHelper.isEnabled()){
//desc-执行画面裁剪
ArrayList screenPoint = null;
if (excavatorWall !=null){
//计算屏幕坐标
screenPoint = excavatorWall.getScreenPoint(null);
if (screenPoint!=null){
//根据屏幕坐标刷新裁剪范围
occlusionHelper.setUniquePointList(screenPoint).refresh();
}
}
}
})