原文地址:https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.mdhtml
前言git
在3D Tiles中,一个砌块集是一系列以树型空间数据结构组织起来的砌块。每个砌块都有一个彻底包裹它所有内容的包围体。树型空间数据结构具备空间关系;全部子砌块的内容都彻底包含在父砌块的包围体中。为了保证灵活性,树能够是任何具备空间关系的空间数据结构,例如K-D树、四叉树、八叉树、格网。github
为了支持对从规则划分的地形到零散分布的城市再到无序点云等各类各样的数据集的紧密包裹,包围体多是个定向的包围盒或包围球或者由最大和最小经度、纬度、高程所定义的地理区域。算法
一个砌块索引一个或一组要素,例如以建筑物或绿化为主的三维模型,点云中的点,多边形,折线,还有矢量数据集中的点。这些要素能够分批组合成一个对象以减小客户端的加载时间和WebGL绘制函数的调用开销。json
砌块元数据数组
每一个砌块的元数据(不是数据自己)以JSON格式定义。例如:数据结构
{app
"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属性应用于以下情形(对象):
transform
的逆转置矩阵左上角的3x3矩阵转换,参看 尺度变化时纠正矢量旋转。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不一样,tileset.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)。这样能够实现诸如将每一个城市保存在一个砌块集中,这些砌块集再构成一个全局砌块集的状况:
当一个砌块指向了一个外部砌块集,这个砌块应该:
包围体空间关系
正如上面描述的那样,树结构具备空间相关性;每一个砌块都有包围体彻底包裹它的内容,并且子砌块的内容彻底在父砌块包围体内部。这并非说子砌块的包围体要彻底在父砌块的包围体内部,例以下图:
地形瓦片的包围球
四个子瓦片的包围球。子瓦片的内容彻底在父瓦片的包围体内,但子瓦片的包围体并不在父瓦片的包围体内,它们并非严密贴合的。
建立空间数据结构
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时是红色,其余则是白色。
更多细节参看声明式样式部分。
答疑
普通问题
技术问题