CesiumJS PrimitiveAPI 高級着色入門 - 從參數化幾何與 Fabric 材質到着色器 - 上篇


Primitive API 還包括 Appearance APIGeometry API 兩個主要部分,是 CesiumJS 擋在原生 WebGL 接口之前的最底層圖形封裝接口(公開的),不公開的最底層接口是 DrawCommand 爲主的 Renderer API,DC 對實時渲染管線的技術要求略高,可定製性也高,這篇還是以 Primitive API 爲側重點。

0. 基礎

0.1. 座標系基礎

這裏的“座標系”特指 WebGL 圖形渲染的座標系。Primitive API 收到的幾何數據,默認沒有任何座標系(即最基本的空間直角座標),想要移動到地表感興趣的地方,需要藉助 ENU 轉換矩陣,或者把幾何頂點的座標直接設爲 EPSG:4978 座標(即所謂通俗的“世界座標”)。

ENU 轉換矩陣,用道家八卦的說法類似“定中宮”。它能將座標轉換到這樣一個 ENU 地表局部座標系上:

  • 指定一處地表點(經緯度)爲座標原點

  • 以貼地正東方(ENU 中的 E)爲正 X 軸

  • 以貼地正北方(ENU 中的 N)爲正 Y 軸

  • 以地心到座標原點的方向(即 ENU 中的 U,up)爲正 Z 軸

這樣一個 ENU 座標系上的局部座標左乘 ENU 轉換矩陣後,就能得到標準的 EPSG:4978 世界座標。

image

GIS 中的投影座標、經緯座標不太適用,需要轉換。

0.2. 合併批次

雖然 WebGL 支持實例繪製技術,但是 Primitive API 減少繪製調用並不是通過這個思路來的,而是儘可能地把 Vertex 數據合併,這個叫做 Batch,也就是“合併批次(並批)”。

在 CesiumJS 的 API 文檔中能看到 new Primitive() 時,可以傳遞一個 GeometryInstance 或者 GeometryInstance 數組,而 GeometryInstance 對象又能複用具體的某個Geometry 對象,僅在幾何的變換位置(通過矩陣表達)、頂點屬性(Vertex Attribute)上做差異化。

CesiumJS 會在 WebWorker 中異步地拼裝這些幾何數據,儘可能一次性發送給底層的 Renderer,以達到儘可能少的 DC。

我沒有十分精確地去確認這個並批的概念和 CesiumJS 源碼中合併的過程,如有錯誤請指出。

1. 參數化幾何

這是公開 API 的最常規用法了,你可以在官方指引文檔中學習如何使用參數化幾何來創建內置的幾何對象:Custom Geometry and Appearance

1.1. 幾何類清單

CesiumJS 內置的參數幾何有如下數種:

  • 立方體(盒) - BoxGeometry & BoxOutlineGeometry

  • 矩形 - RectangleGeometry & RectangleOutlineGeometry

  • 圓形 - CircleGeometry & CircleOutlineGeometry

  • 線的緩衝區(可設定轉角類型和擠出高度) - CorridorGeometry & CorridorOutlineGeometry

  • 圓柱、圓臺、圓錐 - CylinderGeometry & CylinderOutlineGeometry

  • 橢圓、橢圓柱 - EllipseGeometry & EllipseOutlineGeometry

  • 橢球面 - EllipsoidGeometry & EllipsoidOutlineGeometry

  • 多邊形(可擠出高度) - PolygonGeometry & PolygonOutlineGeometry

  • 多段線 - PolylineGeometry & SimplePolylineGeometry

  • 多段線等徑柱體 - PolylineVolumeGeometry & PolylineVolumeOutlineGeometry

  • 球面 - SphereGeometry & SphereOutlineGeometry

  • 牆體 - WallGeometry & WallOutlineGeometry

  • 四棱臺(視錐截頭體) - FrustumGeometry & FrustumOutlineGeometry

  • 平面 - PlaneGeometry & PlaneOutlineGeometry

  • 共面多邊形 - CoplanarPolygonGeometry & CoplanarPolygonOutlineGeometry

  • Esri I3S 專用的幾何 - I3SGeometry

這裏有兩個特別說明:

  • 除了 I3SGeometry 比較特殊外,其它的幾何對象都有其對應的邊線幾何對象(邊線不是三角網格)

  • CoplanarPolygonGeometryPolygonGeometry 兩個 API 很像,但是前者是 2018 年 1.48 後來添加的 API,適用於頂點共面的多邊形;不共面的頂點在 PolygonGeometry 中可能會引起崩潰,但在這個共面多邊形 API 不會(儘管可能會產生一些不可預測的三角形)。在 PolygonGeometry 出現三角形顯示不正常、不完整的情況,可考慮用這個共面多邊形 API;也支持挖洞。

可見 CesiumJS 對參數幾何的支持是比較豐富的。

1.2. 舉例

以下即兩個橢球體的實例繪製示例代碼:

import {
  EllipsoidGeometry,
  GeometryInstance,
  Matrix4,
  Cartesian3,
  Transforms,
  PerInstanceColorAppearance,
  Color,
  ColorGeometryInstanceAttribute,
  Primitive,
} from 'cesium'


// 只創建一個橢球體幾何對象,下面會複用
const ellipsoidGeometry = new EllipsoidGeometry({
  vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
  radii: new Cartesian3(300000.0, 200000.0, 150000.0),
})

// 亮藍色橢球體繪製實例
const cyanEllipsoidInstance = new GeometryInstance({
  geometry: ellipsoidGeometry,
  modelMatrix: Matrix4.multiplyByTranslation(
    Transforms.eastNorthUpToFixedFrame(
      Cartesian3.fromDegrees(-100.0, 40.0)
    ),
    new Cartesian3(0.0, 0.0, 150000.0),
    new Matrix4()
  ),
  attributes: {
    color: ColorGeometryInstanceAttribute.fromColor(Color.CYAN),
  },
})

// 橙色橢球體繪製實例
const orangeEllipsoidInstance = new GeometryInstance({
  geometry: ellipsoidGeometry,
  modelMatrix: Matrix4.multiplyByTranslation(
    Transforms.eastNorthUpToFixedFrame(
      Cartesian3.fromDegrees(-100.0, 40.0)
    ),
    new Cartesian3(0.0, 0.0, 450000.0),
    new Matrix4()
  ),
  attributes: {
    color: ColorGeometryInstanceAttribute.fromColor(Color.ORANGE),
  },
})

scene.primitives.add(
  new Primitive({
    geometryInstances: [cyanEllipsoidInstance, orangeEllipsoidInstance],
    appearance: new PerInstanceColorAppearance({
      translucent: false,
      closed: true,
    }),
  })
)

代碼就不詳細解釋了,需要有一定的 WebGL 基礎,否則對 vertexFormatattributes 等字段會有些陌生。

如下圖所示:

image

1.3. 純手搓幾何

CesiumJS 的封裝能力和 API 設計能力可謂一絕,它給開發者留下了非常多層級的調用方法。除了 1.1、1.2 提到的內置幾何體,假如你對 WebGL 的數據格式(VertexBuffer)能熟練應用的話,你可以使用 Geometry + GeometryAttribute 類自己創建幾何體對象,查閱 Geometry 的文檔,它提供了一個很簡單的例子:

import { Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere } from 'cesium'

const positions = new Float64Array([
  0.0, 0.0, 0.0,
  7500000.0, 0.0, 0.0,
  0.0, 7500000.0, 0.0
])

const geometry = new Geometry({
  attributes: {
    position: new GeometryAttribute({
      componentDatatype: ComponentDatatype.DOUBLE,
      componentsPerAttribute: 3,
      values: positions
    })
  },
  indices: new Uint16Array([0, 1, 1, 2, 2, 0]),
  primitiveType: PrimitiveType.LINES,
  boundingSphere: BoundingSphere.fromVertices(positions)
})

然後就可以繼續創建 GeometryInstance,搭配外觀、材質對象創建 Primitive 了。

這一個屬於高階用法,適用於有自定義二進制 3D 數據格式能力的讀者。

這一步還沒有觸及 CesiumJS 的最底層,擋在 WebGL 之前的是一層非公開的 API,叫 DrawCommand,有興趣可以自己研究。

1.4. *子線程異步生成幾何

有部分參數化幾何對象經過一系列邏輯運送後,是要在 WebWorker 內三角化、生成頂點緩衝的。

這小節內容比較接近源碼解析,不會講太詳細。從 Primitive.prototype.update 方法中模塊內函數 loadAsynchronous 看起:

Primitive.prototype.update = function (frameState) {
  /* ... */
  if (
    this._state !== PrimitiveState.COMPLETE &&
    this._state !== PrimitiveState.COMBINED
  ) {
    if (this.asynchronous) {
      loadAsynchronous(this, frameState);
    } else { /* ... */ }
  }
  /* ... */
}

在這個 loadAsynchronous 函數內,會調度一些 TaskProcessor 對象,這些 TaskProcessor 會通過 WebWorker 的消息傳遞來完成 Geometry 的 Vertex 創建。這個過程很複雜,就不展開了。

如果你感興趣,打開瀏覽器的開發者工具,在 “源代碼” 選項卡左側的“頁面”中,能看到一堆 “cesiumWorkerBootstrapper” 在運行。每一個,背後都是一個內嵌的 requirejs 在調度額外的異步模塊,這些異步模塊在默默地爲主頁面生成數據。

2. 使用材質

這一節講 Primitive API 配套的第二個大類,Appearance + Material API,也叫外觀材質 API,它允許開發者爲自己的 Primitive 編寫着色器。

2.1. 外觀 API

CesiumJS 提供瞭如下幾個具體的 Appearance 類:

  • MaterialAppearance - 材質外觀,通用型,適用於第 1 節中大部分 Geometry

  • EllipsoidSurfaceAppearance - 上一個的子類,允許用在橢球面上的一些幾何,例如 Polygon、Rectangle 等幾何類型,這個外觀類使用算法來表達部分頂點屬性以節約數據大小

  • PerInstanceColorAppearance - 如果每個 GeometryInstance 用的是單獨的顏色,可以用這個外觀類,在 1.2 的例子中就用到這個類

  • PolylineMaterialAppearance - 使用材質(下一小節)來給有寬度的折線着色

  • PolylineColorAppearance - 使用逐頂點或逐線段來給有寬度的折線着色

外觀類有一個抽象父類 Appearance(JavaScript 中沒有抽象類,CesiumJS 也沒有繼承,大致意思,理解記可),上述 5 個均爲它的實現類。

通常,爲 Primitive 幾何着色的主要職責在材質類,但是即使沒有材質類,完全通過 GLSL 代碼,設定外觀類的頂點着色器和片元着色器(當然,要合規)也是可以完成渲染的。

下面就演示一下用 MaterialAppearance 與着色器代碼實現立方體幾何對象(BoxGeometry)的着色案例:

import {
  MaterialAppearance,
  Material,
  BoxGeometry,
  Matrix4,
  Cartesian3,
  Transforms,
  GeometryInstance,
  Primitive,
  VertexFormat,
} from 'cesium'

const scene = viewer.scene

// 創建 ENU 轉換矩陣後,再基於 ENU 轉換矩陣作 Z 軸平移 500000 * 0.5 個單位
const boxModelMatrix = Matrix4.multiplyByTranslation(
  Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(112.0, 23.0)),
  new Cartesian3(0.0, 0.0, 500000 * 0.5),
  new Matrix4()
)
// 創建 Geometry 和 Instance
const boxGeometry = BoxGeometry.fromDimensions({
  vertexFormat: VertexFormat.POSITION_NORMAL_AND_ST, // 注意這裏,下面要細說
  dimensions: new Cartesian3(400000.0, 300000.0, 500000.0),
})
const boxGeometryInstance = new GeometryInstance({
  geometry: boxGeometry,
  modelMatrix: boxModelMatrix, // 應用 ENU + 平移矩陣
})

// 準備 fabric shader 材質和外觀對象
const shader = `czm_material czm_getMaterial(czm_materialInput materialInput) {
  czm_material material = czm_getDefaultMaterial(materialInput);
  material.diffuse = vec3(0.8, 0.2, 0.1);
  material.specular = 3.0;
  material.shininess = 0.8;
  material.alpha = 0.6;
  return material;
}`
const appearance = new MaterialAppearance({
  material: new Material({
    fabric: {
      source: shader
    }
  }),
})

scene.primitives.add(
  new Primitive({
    geometryInstances: boxGeometryInstance,
    appearance: appearance,
  })
)

然後你就能獲得一個 blingbling 的立方塊:

image

注意我在創建 BoxGeometry 時,留了一行註釋:

vertexFormat: VertexFormat.POSITION_NORMAL_AND_ST,

使用 WebGL 原生接口的朋友應該知道這個,這個 VertexFormat 是指定要爲參數幾何體生成什麼 頂點屬性(VertexAttribute)。這裏指定的是 POSITION_NORMAL_AND_ST,即生成的 VertexBuffer 中會包含頂點的座標、法線、紋理座標三個頂點屬性。CesiumJS 的教程資料上說過,這個頂點格式參數,幾何和外觀對象要一一匹配才能兼容。

默認的,所有的 Geometry 對象都不需要傳遞這個,默認都是 VertexFormat.DEFAULT,也即 VertexFormat.POSITION_NORMAL_AND_ST。不妨設置成這個 POSITION_AND_NORMAL

vertexFormat: VertexFormat.POSITION_AND_NORMAL,

雖然法線影響光照,但是這裏只是缺少了紋理座標,盒子就沒有 blingbling 的效果了:

image

具體的着色邏輯不深究,但是足夠說明問題:這個 vertexFormat 會影響幾何體的着色效果。

還有一個與外觀有關的參數,那就是 new Primitive 時的構造參數 compressVertices,這個值默認是 true,即會根據幾何體的 vertexFormat 參數來決定是否壓縮 VertexBuffer。

如果設爲:

// ...
const boxGeometry = BoxGeometry.fromDimensions({
  vertexFormat: VertexFormat.POSITION_AND_NORMAL,
  dimensions: new Cartesian3(400000.0, 300000.0, 500000.0),
})

// ...

new Primitive({
  geometryInstances: boxGeometryInstance,
  appearance: appearance,
  compressVertices: false
})

即不壓縮頂點緩衝,但是 vertexFormat 設置的格式缺少了其中某一個,比如這裏就缺少了紋理座標,那麼就會出現頂點緩衝和頂點格式不匹配的情況,會出現報錯:

image

通常,使用 MaterialAppearance 能搭配大多數幾何類了,也可以自己使用 Geometry + GeometryAttribute 這兩個最基礎的類創建出自定義的 Geometry,搭配使用。

只有極少數的情況,需要去動外觀對象的兩個着色器,這裏先不展開,高階用法會在第 3 節講解。

2.2. 材質 API

CesiumJS 有自己的材質規則,叫做 Fabric 材質,全文參考文檔 Fabric,在 2.3、2.4 小節會展開。

先看看直接實例化的參數。使用 new Material({}) 創建一個材質對象,除了 fabric 參數外,還需要這幾個參數(有些是可選的):

  • strict: boolean,默認 false,即是否嚴格檢查材質與 uniform、嵌套材質的匹配問題

  • translucent: boolean | (m: Material) => boolean,默認 true,爲真則使用此材質的幾何體允許有半透明

  • minificationFilter: TextureMinificationFilter,默認 TextureMinificationFilter.LINEAR,採樣參數

  • magnificationFilter: TextureMagnificationFilter,默認 TextureMagnificationFilter.LINEAR,採樣參數

fabric 參數,則是 Fabric 材質的全部內容,如果不使用內置材質類型要自己寫材質的話,就需要認真研究這個 fabric 對象的參數規則了。

2.3. Fabric 材質初步 - 內置材質、材質緩存與 uniform

如幾何、外觀 API 一樣,Material 類也給予了開發者一定的內置材質,略像簡單工廠模式。只需要使用 Material.fromType() 就可以使用內置的十幾種寫好着色器的材質。

內置材質也是通過正經的 Fabric 對象創建的,有興趣的可以看源碼,所以內置材質也歸爲 Fabric 內容

列舉幾種基礎材質和幾種常見材質:

  • 常見材質 Material.fromType('Color') - 純顏色

  • 常見材質 Material.fromType('Image') - 普通貼圖

  • 基礎材質 Material.fromType('DiffuseMap') - 漫反射貼圖

  • 基礎材質 Material.fromType('NormalMap') - 法線貼圖

  • 基礎材質 Material.fromType('SpecularMap') - 高光貼圖

  • ...

具體的可以查看 Material 類的 API 文檔,文檔頁面的最頂部就列舉了若干種 type 對應的內置材質。fromType() 方法還可以傳遞第二個參數,第二個參數是這個材質所需要的 uniforms,會應用到着色器對應的 uniform 變量上。

例如,文檔中對透明度貼圖的 uniform 描述是這樣的:

image

你就可以通過傳遞這些 uniform 值,來決定着色器使用傳入的 image 的哪個 channel,以及要 repeat 的程度:

const alphaMapMaterial = Material.fromType('AlphaMap', {
  image: '相對於網頁運行時的圖片路徑;網絡地址絕對路徑;base64圖片', // 對多種圖片地址有兼容
  channel: 'a', // 使用圖片的 alpha 通道,根據圖片的通道數量來填寫 glsl 的值,可以是 r、g、b、a 等
  repeat: {
    x: 1,
    y: 1, // 透明度貼圖在 x、y 方向的重複次數  
  }
})

當然,Material 類也可以自己創建材質對象,分緩存和一次性使用兩種創建方法。

new Material({
  fabric: {
    type: 'MyOwnMaterial',
    // fabric 材質對象的其它參數  
  }
  // ... 其它參數
})
// 緩存後就可以這樣用:
Material.fromType('MyOwnMaterial', /* uniforms */)

new Material({
  fabric: {
    // fabric 材質對象的其它參數  
  }
  // ... 其它參數
})

區別就在 fabric.type 參數,只要有 fabric.type,第一次創建就會緩存這個 fabric 材質,第二次就可以使用 fromType() 來訪問緩存的材質了,並且不再需要傳遞完整的 fabric 對象,只需傳遞 type 和新的 uniforms 參數(如果需要更新)即可。

如果不傳遞 fabric.type 參數,那麼創建的材質對象只能在生命週期內使用,CesiumJS 不會緩存,適合一次性使用。

創建好材質對象後,可以直接修改 uniform 的值完成動態更新效果,例如:

// 賦予一個新材質
primitive.appearance.material = Material.fromType('Image')
// 在某一處動態更新貼圖
primitive.appearance.material.uniforms.image = '新貼圖的地址'

2.4. Fabric 材質中級(GLSL表達式、嵌套材質)

Fabric 材質規範允許在創建材質對象時,使用更細緻的規則。當然可以使用完整的着色器函數代碼,但是爲了簡單易用,CesiumJS 在“完整着色器函數”和“JavaScript API” 之間還設計了一層“GLSL表達式”來定製各個 成分組件(components,下文簡稱成分)

舉例:

new Material({
  fabric: {
    type: 'MyComponentsMaterial',
    components: {
      diffuse: 'vec3(1.0, 0.0, 0.0)',
      specular: '0.1',
      alpha: '0.6',
    }  
  }
})

從這個 components 對象可以看出,這一個材質對象設定了三個成分:

  • diffuse,漫反射顏色,設爲了 GLSL 表達式 vec3(1.0, 0.0, 0.0),即純紅色

  • specular,高光強度,設爲了 0.1

  • alpha,透明度,設爲了 0.6

這些都會合成到完整的着色器代碼的對應分量上。

那麼,這個 components 對象允許擁有哪些成分呢?這受限制於內置的 GLSL 結構體的成員:

struct czm_material {
  vec3 diffuse;
  float specular;
  float shininess;
  vec3 normal;
  vec3 emission;
  float alpha;
}

也就是說,diffuse(漫反射顏色)、specular(高光強度)、shininess(鏡面反射強度)、normal(相機或眼座標中的法線)、emission(自發光顏色)、alpha(透明度)這 6 個都可以出現在 components 對象中,其值是字符串,必須是可以賦予給 GLSL 結構體對應成員的表達式。

什麼意思呢?除了上面的舉例 diffuse: 'vec3(1.0, 0.0, 0.0)' 外,任意的 GLSL 內置類型、內置函數均可使用,只要是表達式均可,例如 mixcossintantexture2D(GLSL100)、texture(GLSL300)。

舉例,如果你在 uniforms 中傳遞了一個自定義的 image 作爲紋理,那麼你可以在 components.diffuse 中調用 texture2D 函數對這個 image 變量進行紋理採樣:

const someMaterialFabric = {
  type: 'OurDiffuseMap',
  uniforms: {
    image: 'czm_defaultImage' // 'czm_defaultImage' 是一個內置的 1x1 貼圖
  },
  components: {
    diffuse: 'texture2D(image, materialInput.st).rgb'
  }
}

其中,texture(image, materialInput.st).rgbimage 就是 uniforms.imagematerialInput.st 是來自輸入變量 materialInput 的紋理座標。至於 materialInput,之後講解 fabric.source 完整版着色器代碼的用法時會介紹。

我覺得如果要寫一些更復雜的表達式,不如直接進階用法,寫完整的着色器更靈活,components 適合最簡單的表達式。

fabric 對象上已經介紹了 3 個成員了,即 fabric.typefabric.uniformsfabric.components,那麼現在介紹第四個 —— 允許材質組合的 fabric.materials 成員。

幸運的是,官方的文檔有舉簡單的例子,我就直接抄過來說明了:

const combineFabric = {
  type: 'MyCombineMaterial',
  materials: {
    diffuseMaterial: {
      type: 'DiffuseMap'
    },
    specularMaterial: {
      type: 'SpecularMap'
    }
  },
  components: {
    diffuse: 'diffuseMaterial.diffuse',
    specular: 'specularMaterial.specular'
  }
}

materials 中定義的兩個子材質 diffuseMaterialspecularMaterial 也是滿足 Fabric 規範的,這裏直接用了兩個內置材質(漫反射貼圖材質、高光貼圖材質)。定義在 materials 中,然後在 components 和將來要介紹的 fabric.source 着色器完整代碼中都能用了。

例如,這裏的 components.diffuse 設爲了 diffuseMaterial.diffuse,實際上 diffuseMaterial 就是一個 CesiumJS 內置的 GLSL 結構體變量,在上文提過,結構體爲 czm_material

子材質的 uniforms 也和普通材質的一樣可以更新:

const m = Material.fromType('MyCombineMaterial')
primitive.appearance.material = m

m.materials.diffuseMaterial.uniforms.image = 'diffuseMap.png'
m.materials.specularMaterial.uniforms.image = 'specularMap.png'

通常不建議嵌套太深,容易造成性能問題。

中段小結

至此,已經介紹了 Primitive API 中的兩大 API —— Geometry APIAppearance + Material API 的入門和中階使用,並使用一些簡單的代碼實例輔助說明。到這裏爲止已經可以運用內置的幾何、材質外觀來做一些入門的高性能渲染了,但是未來的你一定不滿足於此,那就需要更進階的用法 —— 完整的着色器編寫,去控制幾何體在頂點和片元着色階段的細節。

受限於篇幅,進階內容於下一篇講解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章