原文地址:https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md
前言
在3D Tiles中,一个砌块集是一系列以树型空间数据结构组织起来的砌块。每一个砌块都有一个完全包裹它全部内容的包围体。树型空间数据结构具有空间关系;所有子砌块的内容都完全包含在父砌块的包围体中。为了保证灵活性,树可以是任何具有空间关系的空间数据结构,例如K-D树、四叉树、八叉树、格网。
为了支持对从规则划分的地形到零散分布的城市再到无序点云等各种各样的数据集的紧密包裹,包围体可能是个定向的包围盒或包围球或者由最大和最小经度、纬度、高程所定义的地理区域。
一个砌块索引一个或一组要素,例如以建筑物或绿化为主的三维模型,点云中的点,多边形,折线,还有矢量数据集中的点。这些要素可以分批组合成一个对象以减少客户端的加载时间和WebGL绘制函数的调用开销。
砌块元数据
每个砌块的元数据(不是数据本身)以JSON格式定义。例如:
{
"boundingVolume": {
"region": [
-1.2419052957251926,
0.7395016240301894,
-1.2415404171917719,
0.7396563300150859,
0,
20.4
]
},
"geometricError": 43.88464075650763,
"refine" : "add",
"content": {
"boundingVolume": {
"region": [
-1.2418882438584018,
0.7395016240301894,
-1.2415422846940714,
0.7396461198389616,
0,
19.4
]
},
"url": "2/0/0.b3dm"
},
"children": [...]
}
boundingVolume.region这个属性是个包含六个数的数组,以 [最西、最南、最东、最北、最小高程、最大高程] 的顺序定义所包围的地理区域。其中经度和纬度以弧度为单位,高程是高于(或低于)WGS84椭球体的米数。除了区域外,其他包围体例如盒子和球体也可能会用到。
geometricError这个属性以一个以米为单位的非负数字定义了“尺度(error)”。引进这一参数用于界定当一个砌块已经被渲染而它的子砌块未被渲染。在调度过程中,几何尺度参与计算以像素为单位计量的屏幕空间尺度(SSE)。屏幕空间尺度决定着分层层次细节模型(HLOD)的更新,也就是在当前视野下是否成功加载精细的砌块或者这个砌块的子砌块是否需要预取。
viewerRequestVolume是个可选的属性(这个例子中没有),使用和boundingVolume同样的结构定义了一个体。在砌块内容将要被请求和砌块将要根据geometricError更新之前,viewer(可视空间)必须在这个体中。参看Viewer request volume(可视空间请求体)部分。
refine属性是一个字符串,当值为“replace”时为替换型更新,值为“add”时为累加型更新。对于一个砌块数据集的根节点来说这个属性是必须的,而对于其他砌块来说这是个可选属性。refine属性默认将从砌块的父节点继承。
content属性是一个对象,对象中包含砌块数据的元数据和数据的地址。content.url的值是一个字符串,这个字符串指向砌块数据的绝对或相对url。在上面的示例中,2/0/0.b3dm这个url具有瓦片地图服务的命名规则即“{z}/{y}/{x}.扩展名”,这并不是必须的。参看答疑“如何请求第n级瓦片?”。
content.url属性中文件的扩展名定义了砌块格式,这一url还可以通过一个tileset.json文件创建一个砌块集的子集,参看 外部砌块集 部分。
content.boundingVolume属性定义了与顶级boundingVolume属性类似的可选的包围体,但与其不同的是content.boundingVolume是一个仅包含砌块内容的紧密贴合的包围体,是用于取代更新部分的。boundingVolume属性提供了空间关系 ,content.boundingVolume属性实现了严格的视锥体裁剪。下面的截图展示了金丝雀码头示例中根节点的包围体。boundingVolume以红色线框表示,包裹砌块集的整个区域;content.boundingVolume以蓝色线框表示,仅包裹根节点中的四个对象(模型)。
content属性是可选的,当其未定义时,砌块的包围体仍会被用于裁剪。(参看 格网 部分)
transform属性也是可选的(本例中没有),它定义了一个4x4的仿射变换矩阵将砌块的scontent
、boundingVolume
、
viewerRequestVolume
转换成“砌块变换”部分所描述的形式。
children属性是一个对象数组,其中定义了子节点,参看tileset.json部分
坐标系与单位
3D Tiles中采用了右手笛卡尔坐标系,即x与y的向量积是z。3D Tiles中定义z轴为局部笛卡尔坐标系中向上的方向(参看 砌块变换 部分)。一个砌块集的全局坐标系通常是WGS84的,但这并不是必须的,比如使用了没有地理空间参考的建模工具后一座电厂可能完全定义在它的地方坐标系下。
所有的直线距离单位都是米,所有的角度单位都是弧度。
3D Tiles中并不明文存储地理坐标(精度、纬度、高程),地理坐标可以由WGS84坐标计算得到,因为WGS84坐标不需要非仿射坐标转换,使用WGS84坐标可以提高GPU的渲染效率。3D Tiles砌块集可以包含专用的元数据,例如地理坐标,但这并不是3D Tiles规格的一部分。
砌块转换
砌块转换目的是支持地方坐标系,比如使得城市砌块集内的某个建筑物砌块集可以定义在它自己的坐标系统下,再比如建筑物点云中的点云块也可以定义在它自己的坐标系统下,每一个砌块都有可选的transform属性。
transform属性是个按照列顺序存储的4x4的仿射变换矩阵,这一变换将砌块的地方坐标系转换到它的父节点或根节点的坐标系。
transform属性应用于如下情形(对象):
- tile.content
- 每个对象的位置。
- 每个对象的法线应被
transform
的逆转置矩阵左上角的3x3矩阵转换,参看 尺度变化时纠正矢量旋转。 - content.boundingVolume (除WGS84坐标系下的content.boundingVolume.region被定义的情形)
- tile.boundingVolume(除当WGS84坐标系下的tile.boundingVolume.region被定义的情形)
- tile.viewerRequestVolume(除当WGS84坐标系下的tile.viewerRequestVolume.region被定义的情形)
transform属性与geometricError属性没有关系,比如Transform属性中定义的尺度并不会决定几何尺度的大小;几何尺度始终以米为单位。
当transform属性未定义时,它的默认值是单位矩阵:
[
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
每一个砌块的局部坐标向砌块集全局坐标系的转换都是由砌块集从上到下的遍历计算,正如计算机图形中传统的场景图或者节点层次那样,将砌块的transform矩阵乘在它的父节点的transform矩阵右边。
下面的JavaScript代码展示了如何使用Cesium的Matrix4和 Matrix3类实现这一计算过程。
function computeTransforms(tileset) {
var t = tileset.root;
var transformToRoot = defined(t.transform) ? Matrix4.fromArray(t.transform) : Matrix4.IDENTITY;
computeTransform(t, transformToRoot);
}
function computeTransform(tile, transformToRoot) {
// Apply 4x4 transformToRoot to this tile's positions and bounding volumes
var inverseTransform = Matrix4.inverse(transformToRoot, new Matrix4());
var normalTransform = Matrix4.getRotation(inverseTransform, new Matrix3());
normalTransform = Matrix3.transpose(normalTransform, normalTransform);
// Apply 3x3 normalTransform to this tile's normals
var children = tile.children;
var length = children.length;
for (var k = 0; k < length; ++k) {
var child = children[k];
var childToRoot = defined(child.transform) ? Matrix4.fromArray(child.transform) : Matrix4.clone(Matrix4.IDENTITY);
childToRoot = Matrix4.multiplyTransformation(transformToRoot, childToRoot, childToRoot);
computeTransform(child, childToRoot);
}
}
如下是一个砌块集的转换矩阵计算的例子(上面代码中的transformToRoot):
每个砌块变换矩阵的计算式为:
l T0:[T0]
l T1:[T0][T1]
l T2:[T0][T2]
l T3:[T0][T1][T3]
l T4:[T0][T1][T4]
在定义变换矩阵或仿射变换右乘之前,砌块内容里或许已经有砌块专属的适用于位置和法线的变换,示例如下:
l 由于glTF中定义了自己的节点层级关系,对于内嵌glTF的b3dm和i3dm砌块,每一个节点都有变换矩阵,这会优先于tile.transform中的定义。
l i3dm的要素表中定义了位置、法线和缩放尺度的实例,这些数据可以为每个实例生成4x4的仿射变换矩阵,这会优于tile.transform属性应用于每个实例。
l 像POSITION_QUANTIZED这种在i3dm、 pnts和vctr的要素表中被压缩的属性应该在做任何变换之前解压,pnts中的NORMAL_OCT16P也是这样。
因此,上面例子的完整变换矩阵的计算式应为:
l T0:[T0]
l T1:[T0][T1]
l T2:[T0][T2][源自ptn专有要素表属性派生的变换矩阵]
l T3:[T0][T1][T3][b3dm专有变换矩阵(含glTF节点层级关系)]
l T4:[T0][T1][T4][i3dm专有变换矩阵(含每个属性要素表属性派生的变换矩阵和glTF节点层级关系)]
可视空间请求体
一个砌块的viewerRequestVolume可用于与异构数据和外部砌块集的结合。
下面的示例中有一个在b3dm砌块中的建筑物,建筑物中有一块pnts砌块中的点云。点云砌块的boundingVolume是个半径为1.25的球体,它还有个较大的球体作为ViewerRequestVolume,球体的半径是15。因为geometricError的值是0,在可视空间进入viewerRequestVolume定义的较大的球体时,点云砌块的数据会从开始一直被渲染。
"children": [{
"transform": [
4.843178171884396, 1.2424271388626869, 0, 0,
-0.7993325488216595, 3.1159251367235608, 3.8278032889280675, 0,
0.9511533376784163, -3.7077466670407433, 3.2168186118075526, 0,
1215001.7612985559, -4736269.697480114, 4081650.708604793, 1
],
"boundingVolume": {
"box": [
0, 0, 6.701,
3.738, 0, 0,
0, 3.72, 0,
0, 0, 13.402
]
},
"geometricError": 32,
"content": {
"url": "building.b3dm"
}
}, {
"transform": [
0.968635634376879, 0.24848542777253732, 0, 0,
-0.15986650990768783, 0.6231850279035362, 0.7655606573007809, 0,
0.19023066741520941, -0.7415493329385225, 0.6433637229384295, 0,
1215002.0371330238, -4736270.772726648, 4081651.6414821907, 1
],
"viewerRequestVolume": {
"sphere": [0, 0, 0, 15]
},
"boundingVolume": {
"sphere": [0, 0, 0, 1.25]
},
"geometricError": 0,
"content": {
"url": "points.pnts"
}
}]
备忘:插入数据请求体与包围体对比的截图
tileset.json
tileset.json定义了切片集,下面是金丝雀码头中使用的tileset.json的片段(查看完整版tileset.json):
{
"asset" : {
"version": "0.0",
"tilesetVersion": "e575c6f1-a45b-420a-b172-6449fa6e0a59"
},
"properties": {
"Height": {
"minimum": 1,
"maximum": 241.6
}
},
"geometricError": 494.50961650991815,
"root": {
"boundingVolume": {
"region": [
-0.0005682966577418737,
0.8987233516605286,
0.00011646582098558159,
0.8990603398325034,
0,
241.6
]
},
"geometricError": 268.37878244706053,
"content": {
"url": "0/0/0.b3dm",
"boundingVolume": {
"region": [
-0.0004001690908972599,
0.8988700116775743,
0.00010096729722787196,
0.8989625664878067,
0,
241.6
]
}
},
"children": [..]
}
}
tileset.json的顶级对象有四个属性:asset、properties、geometricError和root。
asset是一个包含整个切片集元数据属性的对象。其中的version属性以字符串形式定义了3D Tiles的版本。版本定义了tileset.json的JSON模式和砌块格式的基本集。tilesetVersion属性是可选的,它定义了一个专用的版本号,用于类似当前砌块集升级这样的情况。
properties是一个包含每一个原始要素属性对象的对象。上面的tileset.json片段是针对三维建筑物的,所以每个砌块都含有建筑物模型,每个建筑物模型都有Height属性(参看[TileFormats/BatchTable/README.md]中的“Batch Table”)。properties属性中每一个对象的名字与原始对象中的名字相对应并定义了它的minimum和
maximum值,当在为样式生成色带这样的应用时这个属性是有用的。
geometricError以一个以米为单位的非负数字定义了尺度,在这个尺度下切片集不会被渲染。
root是一个定义了在砌块元数据中描述的JSON所定义的根砌块的对象。root.geometricError与 tileset.json中顶层的geometricError不同,ileset.json的geometricError是整个砌块集不被渲染的尺度,root.geometricError是只有根节点砌块被渲染的尺度。
root.children是一个定义了子砌块的对象数组。每一个子砌块都有被其父砌块包围体所完全包裹的boundingVolume,而且在通常情况下一个砌块的geometricError要小于其父砌块的geometricError。对于叶子砌块,root.children数组的长度是0,children可能未定义。
关于tileset.json的详细JSON数据模式请参看“3D Tiles的JSON数据模式”。
参看问题“tileset.json是否会加入3D Tiles细则?”了解tileset.json 如何扩展海量砌块。
外部砌块集
为实现在树的分支下创建子树,砌块的content.url属性可以指向一个外部砌块集(另一个tileset.json)。这样可以实现诸如将每个城市保存在一个砌块集中,这些砌块集再构成一个全局砌块集的情况:
当一个砌块指向了一个外部砌块集,这个砌块应该:
- 不能有子节点,tile.children必须是未定义或空数组。
- 有数个与外部砌块集的根节点相符合的属性:
-
- root.geometricError === tile.geometricError,
- root.refine === tile.refine,
- root.boundingVolume === tile.content.boundingVolume (或当 tile.content.boundingVolume未定义时 root.boundingVolume === tile.boundingVolume),
- root.viewerRequestVolume === tile.viewerRequestVolume 或root.viewerRequestVolum未定义。
- 不能形成闭环, 例如指向同一个包含砌块本身的tileset.json或指向另一个tileset.json之后又指回了包含这个砌块的tileset.json。
- 这个砌块的transform属性和根节点砌块的transform都会生效,向下面的示例中这样,切片集引用了一个外部砌块集,T3的变换矩阵计算式是[T0][T1][T2][T3]。
包围体空间关系
正如上面描述的那样,树结构具有空间相关性;每个砌块都有包围体完全包裹它的内容,而且子砌块的内容完全在父砌块包围体内部。这并不是说子砌块的包围体要完全在父砌块的包围体内部,例如下图:
地形瓦片的包围球
四个子瓦片的包围球。子瓦片的内容完全在父瓦片的包围体内,但子瓦片的包围体并不在父瓦片的包围体内,它们并不是严密贴合的。
创建空间数据结构
tileset.json中定义的树由root和它的children递归构成,树可以定义不同种类的空间数据结构。除此之外,任何砌块格式和更新策略(替换或增加)的组合都可以使用,这给对异构数据的支持提供了很多便利。
生成tileset.json的转换工具将为数据集定义一种理想的树。一个像Cesium这样的实时运行引擎可以渲染任何由tileset.json定义的树。以下是一个关于3D Tiles如何表达各种各样的空间数据结构的简要说明。
K-d 树
当每个砌块有两个被平行于x、y或z轴(或精度、纬度、高程)的分割面分开的子节点时,k-d树就可以被创建。分割轴通常随着树的深度的增加循环旋转,分割面可以用取中点划分、表面启发式划分或其他途径选出。
k-d树示例。注意分割是不均匀的。
需要注意的是k-d树并不像典型的二维地理空间切片算法那样规则分割,因此k-d树可以为稀疏和不均匀分布的数据集创建更加和谐的树型结构。
3D Tiles还支持k-d树的变种,例如多路k-d树,在树的每一个叶子节点上有沿着坐标轴的多个分割,每一个砌块有n个子节点而不是两个。
四叉树
当一个砌块可以分割成统一的四个子节点,四叉树就可以被创建(例如使用中央经纬度分割)。空的子砌块会像典型的二维空间切片算法中那样被忽略。
经典四叉树分割
3D Tiles支持四叉树的变种,例如不均匀分割和紧密包围体(与包围框相反,例如,对稀疏数据集来说包围框父节点有25%的浪费)。
每个子节点都有紧密包围体的四叉树
下面的例子中是金丝雀码头的根砌块和它的子砌块。注意左下角的包围体中并不包含左侧的水域,因为那个区域并没有建筑物。
3D Tiles 还支持其他的四叉树变种,比如“松散四叉树”,树的子树重叠但空间关系得以保留,也就是父砌块完全包裹它所有的子砌块。这可以避免分割跨砌块的要素,例如三维模型。
有不一致分割且重叠砌块的四叉树
下图中,绿色的建筑物处于左子砌块中,紫色的建筑物处于右子砌块中。注意砌块重叠的部分,中部两个绿色的建筑物和一个紫色的建筑物并没有分割。
八叉树
八叉树通过使用三个正交的分割面将一个砌块分成八个子砌块扩展了四叉树。像四叉树一样,3D Tiles支持八叉树的变种,例如不规则分割、紧密包围体和重叠子树。
传统八叉树分割
累加式更新的点云不规则八叉树分割。法国沙佩的圣玛丽教堂。
格网
3D Tiles 通过支持任意数量的子切片支持规则格网、不规则格网、重叠格网。下图是剑桥市不规则重叠格网的俯视图:
3D Tiles会利用那些有包围体但没有内容空砌块。既然空砌块的content没有必要定义,空的非叶子节点通过层次剔除被用于加速不规则格网。这在本质上创建了一个没有分层层次细节的八叉树或四叉树。
砌块格式
每一个砌块的content.url属性指向的另一个砌块的格式都在上面的格式支持表中列了出来。
一个砌块集可以包含任意砌块格式的组合。3D Tiles 可能也会在同一个砌块中通过使用复合砌块支持不同的格式。
声明式样式
使用声明式样式以高度值为建筑物着色
3D Tiles包含简洁的以JSON格式定义的声明式样式,表达式使用样式扩展的JavaScript的一个小子集书写。
样式通过一个基于要素属性的表达式决定要素的show和color(RGB值和透明度),例如:
{
"color" : "(${Temperature} > 90) ? color('red') : color('white')"
}
这个颜色特征在温度高于90时是红色,其他则是白色。
更多细节参看声明式样式部分。
答疑
普通问题
技术问题