官方雖然說文章已過時,且說 2018 年會更新文章的代碼,但是咕咕咕到了現在都沒更新。
Geometry and Appearances · CesiumGS/cesium Wiki (github.com)
創建自定義幾何圖形和外觀
Cesium支持許多常見的圖形,開箱即用。但是,有時候這些預置的圖形不一定能滿足需求。
由於 幾何 和 外觀 在 Primitive API 中是分離的,所以可以在外觀相同的時候添加幾何形狀,反之亦然。
這么做需要一定的圖形學知識,在這個教程中,將創建一個簡單的三棱錐。
如果你有更好的幾何圖形,可以參考官方 “貢獻說明” 來提交你的成果。
幾何圖形
Cesium 中的 Geometry
支持索引三角形、未索引三角形、線框渲染和點渲染。開始,我們先為四面體(三棱錐)創建它的幾何形狀,即四個等邊三角形。首先,在 Source/Core/
下創建一個 TetrahedronGeometry.js
:
官方代碼是 AMD 格式的,我改為和 1.63 之后類似的風格。原文的代碼可以直接到文章頂部的原文鏈接找。
import Cartesian3 from './Cartesian3'
import ComponentDatatype from './ComponentDatatype'
import PrimitiveType from './PrimitiveType'
import BoundingSphere from './BoundingSphere'
import GeometryAttribute from './GeometryAttribute'
import GeometryAttributes from './GeometryAttributes'
import GeometryPipeline from './GeometryPipeline'
import VertexFormat from './VertexFormat'
import Geometry from './Geometry'
function TetrahedronGeometry() {
const negativeRootTwoOverThree = -Math.sqrt(2.0) / 3.0;
const negativeOneThird = -1.0 / 3.0;
const rootSixOverThree = Math.sqrt(6.0) / 3.0;
const positions = new Float64Array(4 * 3);
// 四面體有4個三角形,共計12個點,但是由於重合的關系,可以只記錄4個點
// 點0 坐標
positions[0] = 0.0;
positions[1] = 0.0;
positions[2] = 1.0;
// 點1 坐標
positions[3] = 0.0;
positions[4] = (2.0 * Math.sqrt(2.0)) / 3.0;
positions[5] = negativeOneThird;
// 點2 坐標
positions[6] = -rootSixOverThree;
positions[7] = negativeRootTwoOverThree;
positions[8] = negativeOneThird;
// 點3 坐標
positions[9] = rootSixOverThree;
positions[10] = negativeRootTwoOverThree;
positions[11] = negativeOneThird;
// 創建頂點屬性中的坐標
const attributes = new GeometryAttributes({
position : new GeometryAttribute({
componentDatatype : ComponentDatatype.DOUBLE,
componentsPerAttribute : 3,
values : positions
})
});
const indices = new Uint16Array(4 * 3);
// 后面的三角形用到 0、1、2 號點坐標
indices[0] = 0;
indices[1] = 1;
indices[2] = 2;
// 左邊的三角形用到 0、2、3 號點坐標
indices[3] = 0;
indices[4] = 2;
indices[5] = 3;
// 右邊的三角形用到 0、3、1 號點坐標
indices[6] = 0;
indices[7] = 3;
indices[8] = 1;
// 下面的三角形用到 2、1、3 號點坐標
indices[9] = 2;
indices[10] = 1;
indices[11] = 3;
// 指定此四面體的各種屬性
this.attributes = attributes;
this.indices = indices;
this.primitiveType = PrimitiveType.TRIANGLES;
this.boundingSphere = undefined;
}
export default TetrahedronGeometry
譯者注:很明顯,這是一個死數據的幾何形狀,不具備任何參數化建模(例如我能外部傳入數據)的能力,但是這作為演示足夠了
四面體由四個頂點組成,其所有點都在半徑為1的球面上,為了表達精度,坐標存儲在 Float64Array
這個類型數組中。下圖是示意圖(局部坐標)
四面體的有三個三角形面,每個三角形的點有三個,使用坐標數組中的序號來表達,稱之為索引值,來節約內存。
對於這個四面體,每個頂點均被索引了三次(三個三角形共用一個頂點)。索引數組可以存在 Uint16Array
中,也可以存在 Uint32Array
中,如果頂點數量超過 64K 個,可考慮用后者。
“Back” 三角形這兒有個藍箭頭,表示的是頂點的順序,現在是逆時針。使用右手定則,可以判斷它的這個三角形的“外面”是朝向屏幕的。Cesium 使用逆時針順序來定義三角形的面朝向。
四面體類需要分配四個屬性,這是 Geometry 必須的:
this.attributes = attributes;
this.indices = indices; // 可選
this.primitiveType = Cesium.PrimitiveType.TRIANGLES;
this.boundingSphere = undefined; // 可選
attributes
- 一個GeometryAttributes
對象,每個頂點屬性都存儲在這個對象中,頂點屬性包括但不限於:坐標、法線、顏色值、uv等indices
- 索引數組,使用索引可以減少坐標個數,節約內存primitiveType
- 幾何圖元的類型,這里使用了Cesium.PrimitiveType.TRIANGLES
,即每三個頂點被解析為一個三角形渲染。boundingSphere
- 可選參數,包圍整個幾何形狀的球體,可以輔助剔除來提高性能。
① 包圍球
可以通過計算 boundingSphere
來提高繪制四面體的性能。
this.boundingSphere = BoundingSphere.fromVertices(positions);
BoundingSphere 有一個 fromVertices()
函數來計算緊密包圍幾何形狀所有坐標的包圍球。不過,通常在其他情況下,可以用簡單的幾何知識來更快創建它。由於預先知道這個四面體的頂點坐標位於半徑為1的球體上,所以可以用半徑為1的球面作為邊界球:
this.boundingSphere = new BoundingSphere(new Cartesian3(0.0, 0.0, 0.0), 1.0);
② 使用 Primitive API 繪制一個四面體
這個四面體的幾何中心作為局部坐標原點。為了讓它得以可視化,需要計算一個模型矩陣,用以定位和放縮它。此外,因為它只有坐標,且暫時只打算使用平面陰影,所以暫時不需要法線。
首先,構建源代碼並啟動本地 Cesium 開發環境
譯者注:直接把上面這個類寫在沙盒中更快且省事,方便修改
隨后,打開沙盒,寫一些代碼:
const widget = new Cesium.CesiumWidget('cesiumContainer');
const scene = widget.scene;
const ellipsoid = widget.centralBody.ellipsoid;
// 看似復雜,其實只是對經緯度 (-100, 40) 這個點做垂直地表向上平移200km的計算,並將幾何體放大50w倍(即變成500km那么大),返回矩陣而已
const modelMatrix = Cesium.Matrix4.multiplyByUniformScale(
Cesium.Matrix4.multiplyByTranslation(
Cesium.Transforms.eastNorthUpToFixedFrame(ellipsoid.cartographicToCartesian(
Cesium.Cartographic.fromDegrees(-100.0, 40.0))), // e-n-u計算,返回局部到世界坐標的轉換矩陣
new Cesium.Cartesian3(0.0, 0.0, 200000.0)), // 平移計算,矩陣·平移向量
500000.0); // 縮放計算,矩陣·50w
const instance = new Cesium.GeometryInstance({
geometry : new Cesium.TetrahedronGeometry(), // 如果直接寫在代碼而不是構建出來的,可以直接 new TetrahedronGeometry()
modelMatrix : modelMatrix,
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.WHITE) // 快捷計算頂點顏色
}
});
/* 使用 Primitive API 繪制幾何 */
scene.primitives.add(new Cesium.Primitive({
geometryInstances : instance,
appearance : new Cesium.PerInstanceColorAppearance({
flat : true,
translucent : false
})
}));
然后,它大概會長這樣:
因為設置了 appearance 中的 flat: true
,沒有陰影,所以很難觀察三角面。如果想看到線框圖,可以改 TetrahedronGeometry 對象的 primitiveType 為 LINE。
使用 GeometryPipeline
類做這個轉換工作會比較方便。
GeometryPipeline.toWireframe
方法將幾何圖形轉換為 primitiveType 為 LINE 的模式。
稍作修改創建 GeometryInstance 的代碼:
const instance = new Cesium.GeometryInstance({
geometry : Cesium.GeometryPipeline.toWireframe(new Cesium.TetrahedronGeometry()),
modelMatrix : modelMatrix,
attributes : {
color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.WHITE)
}
});
如圖所示:
③ 添加法線出現陰影
要想外觀看起來有陰影,Geometry 的頂點屬性中必須擁有法線。三角形的法線是垂直於三角面的單位向量:
在圖形學中,通常法線是指每個頂點的法線。頂點的法線是這么定義的:頂點的法線是所有與它有關的面的法線的加和,並且必須是單位向量。下圖所示:
修改 TetrahedronGeometry 類,使用 GeometryPipeline.computeNormal
方法可以計算它的每個頂點的法線:
// ... 上面省略
const boundingSphere = new BoundingSphere(new Cartesian3(0.0, 0.0, 0.0), 1.0);
// 主要是改這里
const geometry = GeometryPipeline.computeNormal(new Geometry({
attributes: attributes,
indices: indices,
primitiveType: PrimitiveType.TRIANGLES,
boundingSphere: boundingSphere
}));
this.attributes = geometry.attributes;
this.indices = geometry.indices;
this.primitiveType = geometry.primitiveType;
this.boundingSphere = geometry.boundingSphere;
繼續執行代碼,圖形就變成了這樣:
這並不是想象中的樣子。
為了更好地觀察為什么會這樣,不如把頂點的法線可視化出來,使用 createTangentSpaceDebugPrimitive
這個全局函數即可,修改沙盒中的代碼:
scene.primitives.add(Cesium.createTangentSpaceDebugPrimitive({
geometry: tetrahedron,
modelMatrix: modelMatrix,
length: 0.2
}));
你就會看到頂點的法線了:
可以看到,這些頂點的法線因為是各個三角面的法線的矢量和,看起來就不是那么”法線“了。
通常,一個頂點附近的面大致接近平行的話,用這些平面的法線矢量和作為頂點的法線,會使得頂點附近的顏色看起來很光滑過渡。但是,像這種十分尖銳的形狀,就不應該使用索引式頂點了,而是每個面的頂點單獨使用,每個頂點的法線自己獨立。
所以為了使得陰影看起來常規一些,必須使用獨立頂點的三角形,即四面體一共4個三角形,一共12個頂點。如圖:
再次修改 TetrahedronGeometry
類:
var positions = new Float64Array(4 * 3 * 3); // 4個三角形,每個有3個坐標,一個坐標有3個float分量
// back triangle
positions[0] = 0.0;
positions[1] = 0.0;
positions[2] = 1.0;
positions[3] = 0.0;
positions[4] = (2.0 * Math.sqrt(2.0)) / 3.0;
positions[5] = negativeOneThird;
positions[6] = -rootSixOverThree;
positions[7] = negativeRootTwoOverThree;
positions[8] = negativeOneThird;
// left triangle
positions[9] = 0.0;
positions[10] = 0.0;
positions[11] = 1.0;
positions[12] = -rootSixOverThree;
positions[13] = negativeRootTwoOverThree;
positions[14] = negativeOneThird;
positions[15] = rootSixOverThree;
positions[16] = negativeRootTwoOverThree;
positions[17] = negativeOneThird;
// right triangle
positions[18] = 0.0;
positions[19] = 0.0;
positions[20] = 1.0;
positions[21] = rootSixOverThree;
positions[22] = negativeRootTwoOverThree;
positions[23] = negativeOneThird;
positions[24] = 0.0;
positions[25] = (2.0 * Math.sqrt(2.0)) / 3.0;
positions[26] = negativeOneThird;
// bottom triangle
positions[27] = -rootSixOverThree;
positions[28] = negativeRootTwoOverThree;
positions[29] = negativeOneThird;
positions[30] = 0.0;
positions[31] = (2.0 * Math.sqrt(2.0)) / 3.0;
positions[32] = negativeOneThird;
positions[33] = rootSixOverThree;
positions[34] = negativeRootTwoOverThree;
positions[35] = negativeOneThird;
var indices = new Uint16Array(4 * 3); // 12個頂點索引,各自獨立
// back triangle
indices[0] = 0;
indices[1] = 1;
indices[2] = 2;
// left triangle
indices[3] = 3;
indices[4] = 4;
indices[5] = 5;
// right triangle
indices[6] = 6;
indices[7] = 7;
indices[8] = 8;
// bottom triangle
indices[9] = 9;
indices[10] = 10;
indices[11] = 11;
現在,仍舊使用 GeometryPipeline.computeNormal
來計算法線省的自己燒腦。最終,繪制的新四面體如下:
現在,每個三角形的陰影看起來就像正常的了,而不是在頂點附近”生硬“地”光滑過渡“。也看到了頂點的法線,同一個三角形的三個頂點法線是跟面的法線平行的。
至此,繪制算結束了。
④ 使用 WebWorker
對幾何圖形的計算可以使用 WebWorker 技術異步進行,使得界面保持響應。四面體可能比較簡單,計算量少,但是對於復雜的幾何體就很難說了,可以使用 Cesium 內置的 WebWorker。
首先,你得在 Source/Workers
目錄下創建一個 createTetrahedronGeometry.js
文件,寫一個函數來創建 Geometry。
譯者注:已改為 es6 風格
import TetrahedronGeometry from '../Core/TetrahedronGeometry' // 這個看你喜好咯
import PrimitivePipeline from '../Scene/PrimitivePipeline'
import createTaskProcessWorker from './createTaskProcessWorker' // 使用 Cesium 內置的工具函數
function createTetrahedronGeometry(parameters, transferableObjects) {
const geometry = TetrahedronGeometry.createGeometry();
PrimitivePipeline.transferGeometry(geometry, transferableObjects);
return {
geometry : geometry,
index : parameters.index
};
}
export default createTaskProcessWorker(createTetrahedronGeometry)
此時,TetrahedronGeometry 類還沒有 createGeometry() 方法,修改使其擁有:
TetrahedronGeometry.createGeometry = function() {
return new Geometry({
attributes : attributes,
indices : indices,
primitiveType : PrimitiveType.TRIANGLES,
boundingSphere : new BoundingSphere(new Cartesian3(0.0, 0.0, 0.0), 1.0)
});
};
最后,加上一句代碼使得內置的 WebWorker 可以識別:
this._workerName = 'createTetrahedronGeometry';
// this 指的是 TetrahedronGeometry
這些修改就能使用上面的代碼異步生成四面體了。當然,你依舊可以使用 createGeometry() 方法同步生成四面體。
至此,自定義幾何圖形的生成教程結束。