前端3D引擎-Cesium自定義動態材質

夜盡丶發表於2022-01-20

本文程式碼基於Vue-cli4和使用WebGL的地圖引擎Cesium,主要內容為三維場景下不同物件的動態材質構建。

參考了很多文章,連結附在文末。

為不同的幾何物件新增動態材質

不知道這一小節的名稱概況是否準確,在我的理解中Cesium中的集合實體分成兩類:Primitive和Entity,一般翻譯成圖元和實體,圖元更接近底層,實體是封裝後的高階物件,使用更加簡便,這裡不對使用場景進行分析,但會介紹如果為這兩種集合物件新增材質。

使用Primitive生成泛光牆

一般來說,Primitive的使用相對繁瑣,相比Entity需要使用者自己初始化更多物件,包括外觀、地理資訊等,但正因為如此,為Primitive新增材質要方便很多,因為可以直接操作幾何體的外觀部分。首先我們初始化一個牆幾何體:

var wallInstance = new Cesium.GeometryInstance({
  geometry: Cesium.WallGeometry.fromConstantHeights({
    positions: Cesium.Cartesian3.fromDegreesArray([
      97.0,
      43.0,
      107.0,
      43.0,
      107.0,
      40.0,
      97.0,
      40.0,
      97.0,
      43.0,
    ]),
    maximumHeight: 100000.0,
    vertexFormat: Cesium.MaterialAppearance.VERTEX_FORMAT,
  }),
})

viewer.scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: [wallInstance],
    appearance: new Cesium.MaterialAppearance(),
  })
)

上文程式碼所建立的是一個只有基礎外觀的“牆”, 例項化 Cesium.MaterialAppearance() 這個類不傳引數所得到的的就是下圖的樣子:

官方文件中我們可以查到有如下引數:

這裡我們關注的是 material ,也就是所謂的“材質”,接下來補充上材質的程式碼:

let image = '/static/images/colors1.png', //選擇自己的動態材質圖片
  color = new Cesium.Color.fromCssColorString('rgba(0, 255, 255, 1)'),
  speed = 0,
  source =
  'czm_material czm_getMaterial(czm_materialInput materialInput)\n\
{\n\
    czm_material material = czm_getDefaultMaterial(materialInput);\n\
    vec2 st = materialInput.st;\n\
    vec4 colorImage = texture2D(image, vec2(fract((st.t - speed*czm_frameNumber*0.005)), st.t));\n\
    vec4 fragColor;\n\
    fragColor.rgb = color.rgb / 1.0;\n\
    fragColor = czm_gammaCorrect(fragColor);\n\
    material.alpha = colorImage.a * color.a;\n\
    material.diffuse = (colorImage.rgb+color.rgb)/2.0;\n\
    material.emission = fragColor.rgb;\n\
    return material;\n\
}'

let material = new Cesium.Material({
  fabric: {
    type: 'PolylinePulseLink',
    uniforms: {
      color: color,
      image: image,
      speed: speed,
    },
    source: source,
  },
  translucent: function () {
    return true
  },
})

viewer.scene.primitives.add(
  new Cesium.Primitive({
    geometryInstances: [greenWallInstance],
    appearance: new Cesium.MaterialAppearance({
      material: material,
    }),
  })
)

應用材質的效果圖:

材質檔案colros1.png:

colros1.png

通過上面的程式碼和截圖可以看出材質檔案和最後表現出的結果還是有很大差異的,而且變數source中的程式碼看起來似乎很難理解,這些內容會在下文進行說明,本節還是針對材質如何應用進行說明。

使用Entity生成流動線

參考Primitive的流程,我們先建立一個實體物件,這次我們換一個場景,改為建立流動線,參考的文章 流動線紋理

create(viewer) {
  //建立射線
  var data = {
    center: {
      id: 0,
      lon: 114.302312702,
      lat: 30.598026044,
      size: 20,
      color: Cesium.Color.PURPLE,
    },
    points: [
      {
        id: 1,
        lon: 115.028495718,
        lat: 30.200814617,
        color: Cesium.Color.YELLOW,
        size: 15,
      },
      {
        id: 2,
        lon: 110.795000473,
        lat: 32.638540762,
        color: Cesium.Color.RED,
        size: 15,
      },
      {
        id: 3,
        lon: 111.267729446,
        lat: 30.698151246,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 4,
        lon: 112.126643144,
        lat: 32.058588576,
        color: Cesium.Color.GREEN,
        size: 15,
      },
      {
        id: 5,
        lon: 114.885884938,
        lat: 30.395401912,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 6,
        lon: 112.190419415,
        lat: 31.043949588,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 7,
        lon: 113.903569642,
        lat: 30.93205405,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 8,
        lon: 112.226648859,
        lat: 30.367904255,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 9,
        lon: 114.86171677,
        lat: 30.468634833,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 10,
        lon: 114.317846048,
        lat: 29.848946148,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 11,
        lon: 113.371985426,
        lat: 31.70498833,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 12,
        lon: 109.468884533,
        lat: 30.289012191,
        color: Cesium.Color.BLUE,
        size: 15,
      },
      {
        id: 13,
        lon: 113.414585069,
        lat: 30.368350431,
        color: Cesium.Color.SALMON,
        size: 15,
      },
      {
        id: 14,
        lon: 112.892742589,
        lat: 30.409306203,
        color: Cesium.Color.WHITE,
        size: 15,
      },
      {
        id: 15,
        lon: 113.16085371,
        lat: 30.667483468,
        color: Cesium.Color.SALMON,
        size: 15,
      },
      {
        id: 16,
        lon: 110.670643354,
        lat: 31.74854078,
        color: Cesium.Color.PINK,
        size: 15,
      },
    ],
    options: {
      name: '',
      polyline: {
        width: 2,
        material: [Cesium.Color.GREEN, 3000],
      },
    },
  }

  this.createFlyLines(data, viewer)
},

createFlyLines(data, viewer) {
  const center = data.center
  const cities = data.points
  const startPoint = Cesium.Cartesian3.fromDegrees(
    center.lon,
    center.lat,
    0
  )
  //中心點
  viewer.entities.add({
    position: startPoint,
    point: {
      pixelSize: center.size,
      color: center.color,
    },
  })
  //大批量操作時,臨時禁用事件可以提高效能
  viewer.entities.suspendEvents() //散點
  cities.map((city) => {
    let material = new Cesium.PolylineTrailMaterialProperty({
      color: Cesium.Color.RED,
      duration: 3000,
      trailImage: '/static/images/colors1.png',
    })

    const endPoint = Cesium.Cartesian3.fromDegrees(city.lon, city.lat, 0)
    viewer.entities.add({
      position: endPoint,
      point: {
        pixelSize: city.size - 10,
        color: city.color,
      },
    })

    viewer.entities.add({
      polyline: {
        positions: this.generateCurve(startPoint, endPoint),
        width: 2,
        material: material,
      },
    })
  })
  viewer.entities.resumeEvents()
  viewer.flyTo(viewer.entities)
},

/**
 * 生成流動曲線
 * @param startPoint 起點
 * @param endPoint 終點
 * @returns {Array}
 */
generateCurve(startPoint, endPoint) {
  let addPointCartesian = new Cesium.Cartesian3()
  Cesium.Cartesian3.add(startPoint, endPoint, addPointCartesian)
  let midPointCartesian = new Cesium.Cartesian3()
  Cesium.Cartesian3.divideByScalar(addPointCartesian, 2, midPointCartesian)
  let midPointCartographic = Cesium.Cartographic.fromCartesian(
    midPointCartesian
  )
  midPointCartographic.height =
    Cesium.Cartesian3.distance(startPoint, endPoint) / 5
  let midPoint = new Cesium.Cartesian3()
  Cesium.Ellipsoid.WGS84.cartographicToCartesian(
    midPointCartographic,
    midPoint
  )
  let spline = new Cesium.CatmullRomSpline({
    times: [0.0, 0.5, 1.0],
    points: [startPoint, midPoint, endPoint],
  })
  let curvePoints = []
  for (let i = 0, len = 200; i < len; i++) {
    curvePoints.push(spline.evaluate(i / len))
  }

  return curvePoints
},

程式碼要多很多,但主要是點位資訊,並且曲線實體的建立要更加複雜,我們著重關注 createFlyLines 這個方法就可以了,可以看到其中建立 material 的部分與Primitive略有不同,例項化的是一個自定義的類 PolylineTrailMaterialProperty

let material = new Cesium.PolylineTrailMaterialProperty({
  color: Cesium.Color.WHITE,
  duration: 3000,
  trailImage: '/static/images/colors1.png',
})

具體原因我們還是從官方文件來看,可以發現與Primitive不同,傳入material的不再是Material類,而是MaterialProperty類。

Cesium提供了一些MaterialProperty,分別代表不同的效果,但顯然不能滿足我們多樣的業務需求,所以這時需要我們自定義這個MaterialProperty。

MaterialProperty

建立 PolylineTrailMaterialProperty.js 檔案並匯入:

import PolylineTrailMaterialProperty from '@/utils/cesium/PolylineTrailMaterialProperty'
import * as Cesium from 'cesium/Cesium'

export class PolylineTrailMaterialProperty {
  constructor(options) {
    options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT)

    this._definitionChanged = new Cesium.Event()
    this._color = undefined
    this._colorSubscription = undefined
    this._time = performance.now()

    this.color = options.color
    this.duration = options.duration
    this.trailImage = options.trailImage
  }
}

Object.defineProperties(PolylineTrailMaterialProperty.prototype, {
  isConstant: {
    get: function () {
      return false
    },
  },

  definitionChanged: {
    get: function () {
      return this._definitionChanged
    },
  },

  color: Cesium.createPropertyDescriptor('color'),
})

PolylineTrailMaterialProperty.prototype.getType = function () {
  return 'PolylineTrail'
}

PolylineTrailMaterialProperty.prototype.getValue = function (time, result) {
  if (!Cesium.defined(result)) {
    result = {}
  }

  result.color = Cesium.Property.getValueOrClonedDefault(
    this._color,
    time,
    Cesium.Color.WHITE,
    result.color
  )
  result.image = this.trailImage
  result.time =
    ((performance.now() - this._time) % this.duration) / this.duration

  return result
}

PolylineTrailMaterialProperty.prototype.equals = function (other) {
  return (
    this === other ||
    (other instanceof PolylineTrailMaterialProperty &&
      Cesium.Property.equals(this._color, other._color))
  )
}

Cesium.Material.PolylineTrailType = 'PolylineTrail'
Cesium.Material.PolylineTrailImage = '/static/images/colors1.png'
Cesium.Material.PolylineTrailSource =
  'czm_material czm_getMaterial(czm_materialInput materialInput)\n\
                           {\n\
                             czm_material material = czm_getDefaultMaterial(materialInput);\n\
                             vec2 st = materialInput.st;\n\
                             vec4 colorImage = texture2D(image, vec2(fract(st.s - time), st.t));\n\
                             material.alpha = colorImage.a * color.a;\n\
                             material.diffuse = (colorImage.rgb+color.rgb)/2.0;\n\
                             return material;\n\
                           }'

Cesium.Material._materialCache.addMaterial(Cesium.Material.PolylineTrailType, {
  fabric: {
    type: Cesium.Material.PolylineTrailType,
    uniforms: {
      color: new Cesium.Color(1.0, 0.0, 0.0, 0.5),
      image: Cesium.Material.PolylineTrailImage,
      time: 0,
    },
    source: Cesium.Material.PolylineTrailSource,
  },
  translucent: function () {
    return true
  },
})

Cesium.PolylineTrailMaterialProperty = PolylineTrailMaterialProperty

在定義自己的 MaterialProperty 時,需要設定幾個公共的方法,分別是:getValueisConstantdefinitionChangedequals

  1. getValue:用來獲取某個時間點的特定屬性值,包括兩個引數:type和result,分別是用於傳遞時間點和儲存屬性值。
  2. isConstant:用來判斷該屬性是否會隨時間變化,是一個bool型別。Cesium會通過這個變數來決定是否需要在場景更新的每一幀中都獲取該屬性的數值,從而來更新三維場景中的物體。如果isConstant為true,則只會獲取一次數值,除非definitionChanged事件被觸發。
  3. definitionChanged:是一個事件,可以通過該事件,來監聽該Property自身所發生的變化,比如數值發生修改。
  4. equals:用來檢測屬性值是否相等。

具體參考 Cesium自定義材質

實際效果見下圖:

材質是如何“煉”成的

Fabric

MaterialProperty 之前的程式碼主要是構建弧線,然後為弧線新增動態效果,至於效果什麼樣,就全部取決於MaterialProperty這個類如何運作,可以看到與上文為Primitive新增的 Material 相似,它們都包含一個 fabric 物件,這個單詞直譯為“織物”,也就是說它是覆蓋在模型上的一層衣服,它有顏色(color)和圖片(image),如果想讓這層效果動起來,還需要一個時間變數,比如在泛光牆的例子中的“speed”,流動線例子中的“time”。Cesium的官方文件中定義它為一個用於生成材質的JSON物件。

WebGL的shader(著色器)

那如何將我們定義的幾個變數變成想象中的材質呢?這裡就涉及到上文程式碼中最晦澀難懂的那兩段字串,這是一種從屬於WebGL技術的獨立語法,叫做shader(著色器),從這裡開始已經步入了圖形學的領域,我雖然很有興趣但是暫時還沒打算入坑,可以看下這篇 【Cesium 顏狗初步】fabric 材質定義與自定義著色器實踐,比較通俗易懂,也有示例。

以我的粗淺理解來看,WebGL作為HTML5的API為我們提供了一個更好的與客戶端硬體圖形部分的互動,來實現更好的視覺效果。

動態牆和流動線shader的簡單說明

如果不想看長篇大論,還是希望能直接實現效果可以參考上面的程式碼,如果只是想微調效果也不需要細緻學習shader的用法,我們仔細看下就能看出規律。Fabric 一節中我們提到時間變數,但是泛光牆的例子中似乎與時間無關,實際上這也是個動態材質,將speed設定為正數後,牆面會有上下移動的效果。

其實看下引數就明白,它們都有在shader中出場:

uniforms: {
  color: color,
  image: image,
  speed: speed,
},

只不過shader會以它獨特的方法來解析這些我們傳入的圖形和引數,比如這裡修改speed後面乘的數值,可以改變動態影像每一單位的變化速度,修改下面fragColor的部分可以改變顏色的處理方式,在原本的例子中這裡其實是混合了傳入的顏色和一個基本的顏色,我這裡改成了除1,也就是傳入什麼顏色就用什麼顏色,不過還要考慮與圖片素材的混合,另外,shader的數字基本都是使用浮點型,比如這裡要用“1.0”,用“1”就會報錯。流動線的shader類似,其實差別不大,因為我這裡也是隻用了兩個簡單的例子。

輕鬆應用複雜特效:EarthSDK

如果既不想研究WebGL又不想要5毛錢的特效,有沒有辦法呢?也不是沒有,涉及到比較垂直的領域,就需要有樂於奉獻的大佬提供開發包了,這裡我推薦EarthSDK,簡單展示一下實現的效果:

出於圖片大小的考慮,這裡限制了幀數,實際效果要更好,這裡使用的是測試用的shp檔案,根據座標繪製形狀,再根據層高拉昇模型的高度,之後只要應用SDK中的效果就可以了,這裡貼下部分配置,url做了代理,這裡用的是我自己部署的shp檔案:

{
  ref: 'tileset1',
  czmObject: {
    xbsjType: 'Tileset',
    xbsjGuid: '739ae1b9-bf61-455a-89d4-280668ed6f3c',
    name: '白模測試1',
    url: '/tileset/tileset.json',
    xbsjStyle: 'var style = {\n    color: "vec4(0, 0.5, 1.0,1)"\n}',
    xbsjClippingPlanes: {},
    xbsjCustomShader: {
      fsBody: this.fsBody,
    },
  },
},
{
  ref: 'scaneline',
  czmObject: {
    xbsjType: 'Scanline',
    xbsjGuid: 'c827bdc1-c14b-4452-81de-7b2ce427b786',
    name: '掃描線',
    position: [2.1201528021066163, 0.5451706303633767, 0],
    show: true,
    radius: 1000,
    timeDuration: '3',
    currentTime: 0,
    color: [0.5, 0.8, 1.0, 1.0],
  },
},

配置並不算難,詳細內容看文件就可以EarthSDK官網

另外他們家的另一個產品CesiumLab在GIS開發中也很實用。

參考資料

流動線紋理

Cesium動態立體牆效果

Cesium自定義材質

【Cesium 顏狗初步】fabric 材質定義與自定義著色器實踐

除了上面提到的,還有一些不錯的參考

Cesium參考資源

Cesium官方教程12--材質(Fabric)

【三維GIS視覺化】基於Vue+Cesium+Supermap實現智慧城市(四)
這個系列作者寫得很詳細,學到了很多

相關文章