AR模型管线

数据制作

模型数据制作包括模型制作和属性编辑两个部分,使用的工具和版本如下:

名称 版本
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();
    }
   }
 }
})