原文地址: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不同,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)。這樣可以實現諸如將每個城市保存在一個砌塊集中,這些砌塊集再構成一個全局砌塊集的情況:
當一個砌塊指向了一個外部砌塊集,這個砌塊應該:
- 不能有子節點,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時是紅色,其他則是白色。
更多細節參看聲明式樣式部分。
答疑
普通問題
技術問題