Skip to content

上一节我们介绍了 glTF 的主要数据结构以及 Mars3D 是如何对其进行加载的,这一节我们来介绍一下 glTF 的升级版 3D Tiles ,也是目前 Mars3D 在加载海量三维模型数据方面必须采用的一种数据格式。

1. 3DTiles 介绍

2016 年,Cesium 团队借鉴传统 2DGIS 的地图规范:WMTS,借鉴图形学中的层次细节模型,打造出大规模的三维数据标准:3d-Tiles,中文译名:三维瓦片。

它在模型上利用了 gltf 渲染快的特点,对大规模的三维数据进行组织,包括层次细节模型、模型的属性数据、模型的层级数据等。

3D Tiles 是在 glTF 的基础上,加入了分层 LOD 的概念(可以把 3D Tiles 简单地理解为带有 LOD 的 glTF ),专门为流式传输和渲染海量 3D 地理空间数据而设计的,例如倾斜摄影、3D 建筑、BIM/CAD、实例化要素集和点云。它定义了一种数据分层结构和一组切片格式,用于渲染数据内容。3D Tiles 没有为数据的可视化定义明确的规则,客户可以按照自己合适的方式来可视化 3D 空间数据。同时,3D Tiles 也是 OGC 标准规范成员之一,可用于在台式机、Web 端和移动应用程序中实现与海量异构 3D 地理空间数据的共享、可视化、融合以及交互功能。

2. 3dTiles 数据示例及其加载

数据处理相关的,可以参看:各类三维模型转为 3DTiles 格式

2.1 3dTiles 数据示例

首先,我从一个简单的 3D Tiles 数据示例说起,请看下面目录: image

上图是一份 3dTiles 数据集在文件夹内的样子,层层打开可得以下特点:

  • 入口文件是tileset.json
  • 各级瓦片用文件夹来组织(类似套娃),目录中有零散的*.json文件
  • 叶子节点有*.b3dm、*.i3dm等格式

对这些数据的结构和详细介绍,我们下一节再详细解说,第一步,我们先学会怎么加载使用这个模型。

2.2 在平台中加载模型

平台内加载 3dtiles 是使用 TilesetLayer 类

3dTiles 至少有一个 tileset.json 文件,作为整个数据集的入口,在代码层面,我们拿到这个主瓦片集 JSON 文件(tileset.json)的 url 地址即可使用加载三维模型了。 如: http://data.mars3d.cn/3dtiles/qx-dyt/tileset.json

当我们拿到这个模型服务地址后:

  • (1)浏览器输入模型 url 地址验证下是否可以正常访问。
  • (2)打开模型参数调试编辑页面,在这个页面调试的模型 URL 输入框内输入模型 url 地址,并单击加载模型按钮。

image

  • (3)可以勾选深度检测来方便测试和调试模型高度 image

  • (4)如果是人工模型,可以勾选“鼠标拖拽编辑”来手动拖拽模型的位置与地图上匹配。

  • (5)在该页面调整好所有参数后,单击“保存参数”按钮,保存的参数 json,会自动下载一个 json 文件。
    image

  • (6) 如果模型是初始化就加载的,可以打开项目的config.json文件,拷贝刚下载的 json 到 config.json 文件的 layers 参数中即可。

json
{
  "scene": {},
  "terrain": {},
  "basemaps": [], //已忽略其他参数
  "layers": [
    {
      "name": "模型名称",
      "type": "3dtiles",
      "url": "http://data.mars3d.cn/3dtiles/qx-dyt/tileset.json",
      "maximumScreenSpaceError": 1,
      "maximumMemoryUsage": 1024,
      "position": { "alt": 452.9 },
      "center": {
        "lat": 34.216894,
        "lng": 108.959834,
        "alt": 591,
        "heading": 4,
        "pitch": -37
      },
      "show": true
    }
  ]
}
{
  "scene": {},
  "terrain": {},
  "basemaps": [], //已忽略其他参数
  "layers": [
    {
      "name": "模型名称",
      "type": "3dtiles",
      "url": "http://data.mars3d.cn/3dtiles/qx-dyt/tileset.json",
      "maximumScreenSpaceError": 1,
      "maximumMemoryUsage": 1024,
      "position": { "alt": 452.9 },
      "center": {
        "lat": 34.216894,
        "lng": 108.959834,
        "alt": 591,
        "heading": 4,
        "pitch": -37
      },
      "show": true
    }
  ]
}
  • (7)如果代码中直接使用TilesetLayer 类构造三维模型,可以将 json 中的参数拷贝到类参数中。
js
var tiles3dLayer = new mars3d.layer.TilesetLayer({
  url: 'http://data.mars3d.cn/3dtiles/qx-simiao/tileset.json',
  maximumScreenSpaceError: 16,
  maximumMemoryUsage: 1024,
  position: { alt: 452.9 },
  center: { lat: 34.216894, lng: 108.959834, alt: 591, heading: 4, pitch: -37 },
  flyTo: true
});
map.addLayer(tiles3dLayer);
var tiles3dLayer = new mars3d.layer.TilesetLayer({
  url: 'http://data.mars3d.cn/3dtiles/qx-simiao/tileset.json',
  maximumScreenSpaceError: 16,
  maximumMemoryUsage: 1024,
  position: { alt: 452.9 },
  center: { lat: 34.216894, lng: 108.959834, alt: 591, heading: 4, pitch: -37 },
  flyTo: true
});
map.addLayer(tiles3dLayer);

运行效果

  • (8) 最后再说几个影响加载效率的关键参数:
  1. maximumScreenSpaceError

控制加载的精度,这个参数默认是 16,数值加大,能让最终成像变模糊

  1. maximumMemoryUsage

这个参数默认是 512,也即是当几何体和纹理资源大于 512MB 的时候,Cesium 就会淘汰掉当前帧中没有 visited 的所有块,这个值其实很小,也是 cesium 为了避免资源占用过高的一个保障,不过上述我们也估算过最差情况下,没有做纹理 crn 压缩的情况下,这个值很容易被超过,导致很多人误以为 cesium 的淘汰没有效果。这个值如果设置的过小,导致 cesium 几乎每帧都在尝试淘汰数据,增加了遍历的时间,也同时增加了崩溃的风险。这个值如果设置的过大,cesium 的淘汰机制失效,那么容易导致显存超过显卡内存,也会导致崩溃。这个值应该处于最差视角下资源占用 和 显存最大量之间。建议显存大小的 50%左右,内存分配变小有利于倾斜摄影数据回收,提升性能体验。

  1. skipLevelOfDetail

这个参数默认值是 true,是 Cesium 在 1.5x 引入的一个优化参数,这个参数在金字塔数据加载中,可以跳过一些级别,这样整体的效率会高一些,数据占用也会小一些。 但是带来的异常是:

  1. 加载过程中闪烁,看起来像是透过去了,数据载入完成后正常。
  2. 有些异常的面片,这个还是因为两级 LOD 之间数据差异较大,导致的。 当这个参数设置 false,两级之间的变化更平滑,不会跳跃穿透,但是清晰的数据需要更长,而且还有个致命问题,一旦某一个 tile 数据无法请求到或者失败,导致一直不清晰。 所以我们建议:对于网络条件好,并且数据总量较小的情况下,可以设置 false,提升数据显示质量。
  1. preferLeaves

这个参数默认是 false,同等条件下,叶子节点会优先加载。但是 Cesium 的 tile 加载优先级有很多考虑条件,这个只是其中之一,如果 skipLevelOfDetail=false,这个参数几乎无意义。所以要配合 skipLevelOfDetail=true 来使用,此时设置 preferLeaves=true。这样我们就能最快的看见符合当前视觉精度的块,对于提升大数据以及网络环境不好的前提下有一点点改善意义。

3. 3D Tiles 数据结构

3dTiles 是一种规范(要区分规范和实现的概念),在规范的指导下,各种资源文件可以是独立存在于硬盘中的目录、文件,也可以以二进制形式写入数据库中。

3dTiles 还有一个特点:那就是不记录模型数据(指三维模型的顶点、贴图材质、法线、颜色等信息),只记录各级“Tile”的逻辑关系(指 LOD 是如何组织的),以及“Tile”自己的属性信息(如房子的名称、建设年限、面积等)。

image

3.1 3D Tiles 支持的模型

  • 城市建筑白膜:在拥有如 shp、kml 等格式的建筑物二维面边界坐标数据,和高度或楼层数属性信息,再通过工具转换为三维立体的白膜建筑物 3DTiles 模型。
  • 倾斜摄影:一般是无人机拍摄,拍摄的数据通过建模工具 ContextCapture Cente 等建模软件可以直接导出 3DTiles 格式。也可以通过 osgb 通用格式转 3DTiles 格式后在平台中使用。
  • 点云数据:一般是激光扫描后生产的数据,有 las、pts、ply 等格式。
  • 人工建模:数据来源于 3dmax、Maya 等建模软件建模,建好的三维模型导出为 dae 和 obj 数据后,再转换为 3DTiles 数据格式。
  • BIM 模型:数据来源于专业的 BIM 软件,常见的有 rvt 和 dgn 格式。

TIP

这些数据的原始格式都需要转换为 3dtiles 格式后,才能在平台加载使用。

3.2 LOD 树结构

通过上面介绍,3dTiles 数据的入口文件是一个名叫tileset.json的文件,在这个 json 文件中 root 属性下通过 children 属性来关联子级节点*.json ,这样一层层关联到叶子节点(如*.b3dm等格式)。

image

树结构对于三维空间数据的组织有很大的优势。3dTiles 在空间上允许数据集使用如下几种树结构:

  • 四叉树:允许使用传统的均匀四叉树,也允许使用松散四叉树等变种(例如,允许子节点,即子瓦片允许存在空间范围重叠),常用于 建筑物(存在不可能严格切分的特点)。
  • 八叉树: 四叉树对在高度上不太好切分的数据比较适合,而如果追求极致的空间分割和分级(例如点云数据),那么八叉树更合适。八叉树也允许使用各种变种。
  • KD 树:
  • 格网结构:允许瓦片存在多个子瓦片,通常出现在倾斜摄影数据上,但是这会导致网络请求过多的问题。

八叉树 image

3.3 构成 3dtiles 的成员:Tile 瓦片

瓦片对象会引用一个二进制的瓦片数据文件,目前这些文件有以下类型:

文件后缀名名称英文名称对应实际数据
b3dm批量三维模型Batch 3D Model传统三维建模数据、BIM 数据、倾斜摄影数据
i3dm实例三维模型Instance 3D Model一个模型多次渲染的数据,灯塔、树木、椅子等
pnts点云PointCloud点云数据
cmpt复合模型Component前三种数据的复合(允许一个 cmpt 文件内嵌多个其他类型的瓦片)

image

上面表格中的 b3dm 和 i3dm 格式是基于 glTF(一种专为高效传输 3D 内容而设计的开放性规范)构建的,它们的瓦片内容在二进制体中嵌入了 glTF 资源,包含模型的几何和纹理信息,而 pnts 格式却没有嵌入 glTF 资源。

3.4 单个 Tile 瓦片的内部构成

在 3dTiles 中,模型数据是以 glb 的形式嵌入在瓦片文件中的(点云直接就写 xyz 和颜色信息了),模糊了二维中“要素”的概念,而且 gltf 规范看起来并没有所谓的“要素”的概念,仅仅是对 GPU 友好的 vertex、normal、texture 等信息。

瓦片引用的二进制文件有 4 种,即:b3dm、i3dm、pnts、cmpt。 除去 cmpt 这个复合类型不谈,前三种的瓦片二进制数据文件的大致字节布局结构见下图: image

每一种瓦片二进制数据文件都有一个记录该瓦片的文件头信息,文件头包括若干个因瓦片不同而不太一致的数据信息,紧随其后的是两大数据表:FeatureTable(我翻译其为:要素表)、BatchTable(我翻译其为:批量表)。

这两个表既然是二进制的数据,尽管它名字里带“表”,但是却不是二维表格,它更多的是一些 键值 信息。

(1)FeatureTable 要素表: 记录渲染相关的数据

在 b3dm 瓦片中,要素表记录这个批量模型瓦片中模型的个数,这个模型单体在人类逻辑上不可再分。

(在房屋级别来看,房子并不是单体,构造它的门、门把、窗户、屋顶、墙等才是模型单体;但是在模型壳子的普通表面建模数据中,房子就是一个简单的模型)

要素表还可以记录当前瓦片的中心坐标,以便 gltf 使用相对坐标,压缩顶点坐标数字的数据量。

在一个瓦片中,一个三维要素(GIS 中的通常叫法)= 一个模型(图形学、工业建模叫法) = 一个 BATCH(3dtiles 叫法)

要素表的结构:JSON 描述信息+要素表数据体 image

(2)BatchTable 批量表: 记录属性数据

批量表就是所谓的模型属性表,批量表中每个属性数组的个数,就等于模型的个数,因为有多少个模型就对应多少个属性嘛!如果把批量表删除,那么 3dTiles 数据还能正常渲染。

批量表的结构:JSON 描述信息+批量表数据本体,与 要素表 很像,批量表也是由: 二进制的 JSON 文本头 + 二进制的数据体 构成的。如下图所示: image

(3)四类类型对应的具体结构

b3dm: image

i3dm:与 b3dm 一致,文件头多了个 gltfFormat 属性。 image

pnts 不存在 gltf 模型,故结构如下: image

cmpt:这是前三种的一种更灵活的组织,允许一个瓦片使用 cmpt 形式,组合多种瓦片,cmpt 瓦片可以内嵌任意个、任意类型的瓦片,b3dm、i3dm、pnts 均可。 image

(4)代码中如何查询瓦片的批量表

我们通常在平台中使用 点击 事件,来获取一个 BATCH,即三维要素。在 API 中,这个被叫做:Cesium3DTileFeature。那么,这个 Cesium3DTileFeature 就能访问到它自己的批量表中的属性数据:

js
map.on(mars3d.EventType.click, function (event) {
  let feature = event.pickedObject;
  //下面只是演示解释底层实现,在平台中直接通过event.graphic.attr可以获取属性
  if (feature instanceof Cesium.Cesium3DTileFeature) {
    let propertyNames = feature.getPropertyNames();
    let length = propertyNames.length;
    for (var i = 0; i < length; ++i) {
      let propertyName = propertyNames[i];
      console.log(propertyName + ': ' + feature.getProperty(propertyName));
    }
  }
});
map.on(mars3d.EventType.click, function (event) {
  let feature = event.pickedObject;
  //下面只是演示解释底层实现,在平台中直接通过event.graphic.attr可以获取属性
  if (feature instanceof Cesium.Cesium3DTileFeature) {
    let propertyNames = feature.getPropertyNames();
    let length = propertyNames.length;
    for (var i = 0; i < length; ++i) {
      let propertyName = propertyNames[i];
      console.log(propertyName + ': ' + feature.getProperty(propertyName));
    }
  }
});

用到了 Cesium3DTileFeature.getPropertyNames() 方法获取批量表中所有属性名,用了 Cesium3DTileFeature.getProperty(string Name) 来获取对应属性名的属性值。更多 API 见官方文档。

获取更多关于 3D Tiles 的格式信息可访问:

官方 github官方格式说明秋意正寒博客详解