M3D – 一个想突破又还有很多包袱的新生三维数据格式杂谈

都说 “一流企业定标准,二流企业卖品牌,三流企业卖产品,四流企业做项目”,在面向类 WebGL 技术的三维浏览器端渲染的大规模流式数据加载技术中,等到了 MapGIS 主导的 m3d 标准。

1 门派

这一份浓浓的武汉味似乎是想在继 Cesium 3d-Tiles、Esri i3s、超图 S3M 后再在三维市场分一杯羹。

在前言中,吴老爷子带路,洋洋洒洒的一队人。

2 几个术语简析

  • m3d:Model of 3D,在原文第 4 节有指出,但是翻译过来不太对的样子。
  • glbx:glTF 数据规范中,对于 glb 格式的一种变种文件。原文指的是 “gltf数据的扩展格式”,我不清楚 m3d 标准对 gltf 扩展了什么顶层属性,或者是没有好好研究 glTF 规范,因为 glTF 规范本身在几乎所有的属性中都有 extensionsextras 的预留属性,用于扩展各种不同逻辑的业务功能或渲染特征。

3 结构简析

image

结构的设计与 Esri i3s 略相类似,使用 Node 充当空间剖分的术语。

  • M3DDataInfo.mcj:一种 JSON 文本文件,后缀名是 mcj,且此文件的文件名可任意设置,用于描述整份 m3d 数据
  • RootNodeInfo.jsonNodeInfo<i>.json:对 node 的描述文件,文件名可以自定义
  • *.m3d:m3d 文件,一种压缩文件,可选压缩格式有:,后缀名是m3d,文件名可以自定义

每份 m3d 数据,其根目录下有

  • 一个 mcj 文件描述整份数据
  • 一个 Shared 目录存放 Shared.m3d 文件
  • 一个 根节点(RootNode)json 描述文件
  • 一个 Data 目录用于存放子一级的节点目录
    • 每个节点目录下仍能拥有更下一级的节点目录,可递归
    • 每个节点目录下有一个对当前 node 的 json 描述文件
    • 每个节点目录下可拥有 n 个 m3d 文件,这些被称作 “瓦片”,用于管理不同行业的数据(点云.m3d、倾斜摄影模型.m3d、bim.m3d、地质体.m3d)
      • 每个 m3d 文件下压缩了三个目录:GeometryTextureAttribute,分别存放 *.glb/glbx*.jpg/png/webp*.json/bin 文件用于存储几何、纹理、属性数据

3.1 空间剖分方式

规范中的术语是 “数据树形结构”,个人认为使用 空间剖分方式 更好,直观。

使用了三种常规三维数据结构,即空间八叉树、四叉树、KD树。

3.2 空间范围表示方式

  • BoundingBox
  • BoundingSphere

以上两种常见空间范围体二选一。

其中,BoundingBox 使用的定义是 6 元素的 AABB 包围盒,即经纬高三轴的最值。

4 M3DDataInfo.mcj 类定义

描述整个 m3d 文件的 json 文件,它拥有如下顶层属性:

  • asset: string,基本信息,简单的字符串
  • version: string,这个令我十分无语,UML 图中是 string,在文下是 float,我认为 string 更合适。
  • dataName: string,数据名
  • guid: string,数据唯一标识符
  • compressType: string,压缩类型,可选 "zip""7z""rar"
  • spatialReference: string,空间参考,可选 "WGS84""CGCS2000"
  • treeType: string,空间剖分结构,可选 "QuadTree""OCTree""K-DTree""RTree",这点我也有点无语,在前文中并未提及 R树,虽然 R树在空间搜索中还是很常见的。
  • lodType: string,层次细节类型,可选 "ADD""REPLACE"
  • boundingVolume: BoundingVolume
  • position: Point
  • rootNode: Uri
  • fieldInfo: Array<Field>

其中,BoundingVolume 在上文已提及,连同 PointField,在此简单使用 Rust 结构体定义:

struct BoundingVolume {
  boundingBox: BoundingBox,
  boundingSphere: BoundingSphere,
}

struct BoundingBox {
  left: f64,
  top: f64,
  right: f64,
  bottom: f64,
  minHeight: f64,
  maxHeight: f64,
}

struct BoundingSphere {
  center: Point,
  radius: f64,
}

struct Point {
  x: f64, 
  y: f64,
  z: f64,
}

struct Field {
  name: String,
  type: String,
  alias: String,
  size: i32,
}

其中,Field 的 type 字段可选值为 "bool""int16""uint16""int32""uint32""int64""uint64""float""double""wchar""text""date""time""timestamp"

5 NodeInfo.json 类定义

每个节点(node)的描述文件是一个 json 文件,它的顶层属性有:

  • name: string
  • lodLevel: string,用字符串是一个留余量的设计,因为不一定 level 必须是数字
  • boundingVolume: BoundingVolume
  • lodMode: string,可选值 "distance""pixel"
  • lodType: string,可选值 "ADD""REPLACE"
  • lodError: string,切换误差值,单位根据 lodMode 决定
  • transform: float[16],转换矩阵
  • parentNode: NodeInfo
  • childrenNode: Array<NodeInfo>
  • uri: string
  • shared: Uri
  • tileDataInfoIndex: int,M3D 瓦片索引
  • tileDataInfoList: Array<TileDataInfo>,瓦片数据列表

其中,TileDataInfo 的类定义用 rust 表示可为:

struct TileDataInfo {
  tileData: Uri,
  geometry: Geometry,
  attribute: Attribute,
  texture: Uri,
  dataType: string, // 可选值 "Vector"、"TiltPhotography"、"Model"、"BIM"、"PointCloud"、"PipeLine"、"GeoModel"、"GeoGrid"、"GeoDrill"、"GeoSection",分别表示:矢量、倾斜、模型、BIM、点云、管线、地质体、地质体网格、地址钻孔、地质剖面
}

struct Geometry {
  blobType: string, // 可选值 "glb"、"glbx"
  url: string,
  geoCompressType: string, // 只能是 "draco"
  geometryType: string, // 可选值 "Point"、"Line"、"Polygon"、"Surface"、"Entity"
}

struct Attribute {
  attType: string, // 可选值 "json"、"bin"
  uri: string,
}

6 *.m3d 文件

文中对 m3d 的介绍在 7.4 节,就不复述了,主要提一点:

  • glbx:基于 glb 数据扩展了单体化信息及地质体数据信息的几何要素文件,后缀名为.glbx,文件名可自定义。

我觉得这个格式的提出是多余的,槽点最后再谈。

7. 地质模型几何结构定义

由于本部分(原文位于7.4.3节)在原文中较为详细,只捡几个点提一提。

7.1 钻孔模型几何结构定义

略。

7.2 地质剖面模型几何结构定义

剖面使用的是一个点列表表示的简单封闭三维线,这是有缺点的,因为不能表达多剖面的情况。

7.3 地质体模型几何结构定义

地质体在此处使用了多个索引三角面来定义,其实可以完全使用 glTF 来定义,只不过这里简化成了索引+顶点的形式。

7.4 网格模型几何结构

这部分的设计略显冗杂,有重复设计的嫌疑,没有优化。

8 属性文件的设计

属性文件的类型在 M3DDataInfo 对象中定义,见本文第 4 部分。

它使用 “属性记录(Record)” 来描述一个属性,又分了下列两个类定义:

struct Record {
  oid: long,
  rcdValues: Vec<RcdValue>,
}

struct RcdValue<T> {
  fldName: String,
  rcdValue: T
}

关于此部分的吐槽留到最后。

9 单体化的定义

m3d 标准使用一个叫 oidTable 的表,记录每个 “物件” 的单体信息。

具体做法是,对每个顶点进行设定值,例如房屋A设定每个顶点的值是1,那么就记录在 oidTable 中即可。

并由此扩展 glb 文件,构成 glbx 文件。

单开一小节吐槽

在 Cesium 提出的 glTF 扩展中,有一个更为接近此设计的扩充:glTF 中 Primitive 的定义增加一个 _BATCHID,使得顶点属性新增一个,与 POSITIONCOLOR 等同级别,使用此 _BATCHID 来区分顶点归属于哪个 batch。

利用 glTF 强大的扩展能力完全没必要 “扩展 glb 文件成 glbx”。

另外,单体化也是一个伪命题。因为倾斜摄影模型仅仅是有外观的三角网模型,不具备精细模型和 GIS 要素的独立性,所以才提出 “单体化” 这种词。

在 GIS 数据、BIM 数据中就很少听说过这词,因为这两种数据本身就有着完备的物件独立信息。

如果模型建模时,有完整的地理要素的定义,其实就具备了单体化的信息,使用 GIS 中的 “要素” 完全可以区分开不同的物体。

10 Shared 文件夹

关于此文件夹没有过多的描述,只有不到 1/4 页纸。

它似乎是设计来存放公共数据的,用来提供 instance 能力。

11 RESTful 接口设计

11.1 数据信息获取服务:M3DData

url模板:<base-url>/services/<service-name>/M3dServer

例:

//igserver.mapgis.com/igs/rest/services/wuhan-3d/M3dServer

调取方法是 HTTP GET,返回类型是 application/json

11.2 公共资源获取服务:M3DSharedResources

url模板:<m3d-data-url>/shared-resources

例:

//igserver.mapgis.com/igs/rest/services/wuhan-3d/M3dServer/shared-resource

调取方法是 HTTP GET,返回类型是 application/octet-stream

11.3 根节点信息获取服务:M3DRootNodeInfo

11.4 其他服务

为什么写两个就不写了?

因为原文定义已经很详细了,无需复述。

12 优点

  • 几何部分使用 glTF 定义
  • 能使用 webp 作为纹理贴图
  • 容纳了各行各与的业务数据
  • 规范定义较为完备,文档废话不多,但是不失详尽性
  • 参考了i3s的结构定义,设计上把几何、纹理、属性数据解耦
  • 在几何部分能重视 draco 压缩算法

13 缺点

① 对 glTF 规范认知不全

没有充分阅读 glTF 规范

  • glTF 本身就含有纹理贴图、采样器、材质的定义,此处再将纹理贴图单独定义,但未指出究竟是几何部分的 glb/glbx 独立存储几何 + 纹理单独存放在纹理目录,亦或者是混用,这一点十分疑惑。

没有充分利用 glTF 强大的扩展能力

  • glTF 本身就带了十几种世界各大组织提供的扩展能力,包括但不限于纹理压缩、几何压缩、材质扩展

如无必要,勿增实体。glbx 是一种多余的设计。

② 文档定义缺陷

定义歧义

对于路径的定义,在原文见过 Uriurlstring 三种写法。

对于空间剖分的定义,在文件组织上,使用 Node 文件夹来组织,但是其下的各部分数据又使用 Tile 来定义,在原文第 3.2 节还专门说明了瓦片数据,却不见 Node 的详细定义;

在图1和图2中,对 Node 和 m3d 文件的组织十分困惑,Node<i> 目录应该是一个 Node,下面能挂载 N 个 m3d 文件;但是在原文 3.3 节中,每个瓦片下面挂 0 或 1 个 m3d 文件,却不见瓦片的定义,只见 tile data(瓦片数据) 的定义。

asset 属性按理说可以囊括 nameversionguiddataName 等属性的,却将他们分开,不知道设计上是否经过足够的考虑。

类型错误

在 M3DDataInfo 的类定义中,对于 version 字段的类型,表格里是 float,表格上面的 UML 类图又是 string,这种低级错误应该在校对的时候修正。

UML 描述不够完整

属性的默认值、属性的可选性未指出。

属性拼写不规范

例如 geoCompressType,此属性是 Geometry 类下的,按理说就不需要 geo 前缀,只需保留 compressType 即可。

还有 attType,该简写的地方简写,此处就不该简写,它是 Attribute 对象的一个属性,所以它应该是 type

RcdValue 对象应该完整的命名为 RecordValue,且其 rcdValue 应简单命名为 value,它的类型还出现了模糊不清的定义,因为没有人知道 value 是什么值类型。

驼峰命名法、下划线命名法混用就不说了。

③ 标准里太多 “专有” 词汇

  • 单体化:这个就不适用于全体行业数据,在本文第 9 节已详尽提及。
  • 其他见 本文 14.②

④ 对一些属性未完全解释

  • lodType:对 "ADD""REPLACE" 未完全解释
  • lodError:未解释其详细的值含义

14 槽点

① ArcGIS Server REST接口风格味道太重

在原文的第 8 部分体现的十分明显,RESTful 风格其实完全没有必要设计如此冗杂的 URL 模板。

② 对数据的行业层级和结构层级混淆

数据的结构层级是与行业无关的,例如 “矢量” 是一种概念,矢量数据可以囊括二维图形、三维图形数据,完全可以描述几何部分。

行业层级必须基于结构层级去定义,才有物质基础,这一点马克思老爷子早就总结好了。如果一个行业的数据对其底层的结构不清楚,例如单提 “BIM数据”,不提是什么格式,不提它的结构,那也只能是谈判桌上的词,落实不下来,谈不了什么自主创新。

③ 使用商业压缩格式 rar,使用过多的压缩格式

若为开放标准,而使用半封闭的 rar 格式,则必须在服务端多准备一个 rar 解压的程序。

zip、7z 作为存储时能有效减少体积,但是文章中并未提及这两种压缩方式的压缩等级、压缩与解压缩的性能开销,如果因为这些延长了文件 IO 的时间,那是得不偿失的。

这一点是 i3s 的资源 gzip 法则占优,因为浏览器天生支持 gzip,并且开销不算很大。

④ 扩展性不佳

若将来要新增气象数据或时态数据,没有留有冗余的设计。

15 个人总结

定义这种类型的数据规范,不仅要有足够充足的行业一线实践经历,还要在 Web 可视化领域有足够的底层技术积累。

从技术和数据的视角做定义,才具备完整的可实施性和一般适配性。

M3D 的提出丰富了我国对三维数据规范的标准库,但是现在缺实现,生态不足,推广几乎没有,如何让各行各业买账,还需要走相当长的路,务必脚踏实地,方可行百里路。

Tags: