揭秘Cesium.js:打造炫酷行政区与自定义材质的魔法!

1,003 阅读4分钟

什么是Cesium.js?

Cesium.js是一个用于创建3D地球和地图的JavaScript库,它支持全球范围内的地形、影像、矢量数据和3D模型的可视化。Cesium利用了现代浏览器中的WebGL技术,使得开发者可以构建高性能、高保真度的应用程序。此外,Cesium还提供了丰富的API和插件系统,极大地扩展了其功能。

自定义材质的实现

在Cesium中,材质可以通过Cesium.Material类进行定义。我们可以通过编写GLSL代码来自定义材质的外观,例如改变颜色、添加动画效果或模拟不同的表面属性。为了实现这一点,我们需要定义一个新的材质类型,并提供相应的着色器代码。

定义自定义材质

首先,我们定义了一个名为CustomMaterialProperty的类,该类继承了Cesium的材质属性接口。这个类负责设置材质的颜色、持续时间等属性,并根据当前时间动态计算材质的值。通过重写getValue方法,我们可以让材质随时间变化,从而产生动画效果。

class CustomMaterialProperty {
  /**
   * @param {Object=} options 配置项
   */
  constructor(options = {}) {
    this._definitionChanged = new Cesium.Event();
    this._color = undefined;
    this._colorSubscription = undefined;

    this.color = options.color || Cesium.Color.RED;
    this.duration = options.duration || 2000;
    this._time = performance.now();
  }

  /**
   * @return {boolean}
   */
  get isConstant() {
    return false;
  }

  /**
   * @return {Cesium.Event}
   */
  get definitionChanged() {
    return this._definitionChanged;
  }

  /**
   * @return {string}
   */
  getType() {
    return MATERIAL_TYPE;
  }

  /**
   * @param {Cesium.JulianDate} time
   * @param {Object=} result
   * @return {Object}
   */
  getValue(time, result = {}) {
    result.color = Cesium.Property.getValueOrUndefined(this.color, time);
    result.time =
      ((performance.now() - this._time) % this.duration) / this.duration;
    return result;
  }

  /**
   * @param {CustomMaterialProperty} other
   * @return {boolean}
   */
  equals(other) {
    return (
      this === other ||
      (other instanceof CustomMaterialProperty && this._color === other._color)
    );
  }
}

接着,我们使用Cesium.Material._materialCache.addMaterial注册我们的自定义材质。这里,我们指定了材质的类型名称MATERIAL_TYPE,以及包含着色器代码的fabric对象。着色器代码定义了如何根据输入的参数(如颜色和时间)计算材质的最终显示效果。

Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
  fabric: {
    type: MATERIAL_TYPE,
    uniforms: {
      color: new Cesium.Color(1, 1, 0, 1),
      time: 1,
      spacing: 40,
      width: 1,
    },
    source: `
      uniform vec4 color;
      czm_material czm_getMaterial(czm_materialInput materialInput) {
        czm_material material = czm_getDefaultMaterial(materialInput);
        vec2 st = materialInput.st;
        float alpha = distance(st, vec2(.5));
        material.alpha = color.a * alpha * 1.5;
        material.diffuse = color.rgb * 1.3;
        return material;
      }`,
  },
  translucent: () => true,
});

初始化Viewer并加载GeoJSON数据

为了展示自定义材质的效果,我们需要初始化一个Cesium.Viewer实例,并加载一些地理数据。在这个例子中,我们使用了ArcGIS提供的世界影像图层作为底图,并从GitHub上加载了一个描述广东省边界的GeoJSON文件。


const viewer = new Cesium.Viewer("box");

async function addMaterial() {
  const dataSource = await Cesium.GeoJsonDataSource.load('url');

  // 设置材质...
  viewer.dataSources.add(dataSource);

  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(113.280637, 23.125178, 20000),
    duration: 3,
  });
}

addMaterial();

动态设置材质颜色

为了让每个区域都有独特的颜色,我们预定义了一系列颜色配置,并将它们应用到加载的GeoJSON实体上。我们遍历所有实体,并根据索引分配颜色,确保不同区域之间有明显的区分。

const COLOR_CONFIGS = [
  [15, 176, 255],
  [18, 76, 154],
  [64, 196, 228],
  [66, 178, 190],
  [51, 176, 204],
  [140, 183, 229],
  [0, 244, 188],
  [19, 159, 240],
];

// 在addMaterial函数内...
entities.forEach((entity, index) => {
  entity.polygon.extrudedHeight = 10000;
  entity.polygon.outline = false;
  entity.polygon.material = new CustomMaterialProperty({
    color: colors[index % colors.length],
  });
});

完整代码

import * as Cesium from "cesium";

/**
 * 自定义材质类型名称
 * @const {string}
 */
const MATERIAL_TYPE = "Custom";

/**
 * 自定义材质属性类
 * @class
 */
class CustomMaterialProperty {
  /**
   * @param {Object=} options 配置项
   */
  constructor(options = {}) {
    this._definitionChanged = new Cesium.Event();
    this._color = undefined;
    this._colorSubscription = undefined;

    this.color = options.color || Cesium.Color.RED;
    this.duration = options.duration || 2000;
    this._time = performance.now();
  }

  /**
   * @return {boolean}
   */
  get isConstant() {
    return false;
  }

  /**
   * @return {Cesium.Event}
   */
  get definitionChanged() {
    return this._definitionChanged;
  }

  /**
   * @return {string}
   */
  getType() {
    return MATERIAL_TYPE;
  }

  /**
   * @param {Cesium.JulianDate} time
   * @param {Object=} result
   * @return {Object}
   */
  getValue(time, result = {}) {
    result.color = Cesium.Property.getValueOrUndefined(this.color, time);
    result.time =
      ((performance.now() - this._time) % this.duration) / this.duration;
    return result;
  }

  /**
   * @param {CustomMaterialProperty} other
   * @return {boolean}
   */
  equals(other) {
    return (
      this === other ||
      (other instanceof CustomMaterialProperty && this._color === other._color)
    );
  }
}

// 定义颜色属性
Object.defineProperty(
  CustomMaterialProperty.prototype,
  "color",
  Cesium.createPropertyDescriptor("color")
);

// 注册自定义材质
Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, {
  fabric: {
    type: MATERIAL_TYPE,
    uniforms: {
      color: new Cesium.Color(1, 1, 0, 1),
      time: 1,
      spacing: 40,
      width: 1,
    },
    source: `
      uniform vec4 color;
      czm_material czm_getMaterial(czm_materialInput materialInput) {
        czm_material material = czm_getDefaultMaterial(materialInput);
        vec2 st = materialInput.st;
        float alpha = distance(st, vec2(.5));
        material.alpha = color.a * alpha * 1.5;
        material.diffuse = color.rgb * 1.3;
        return material;
      }`,
  },
  translucent: () => true,
});

/**
 * 初始化 viewer
 * @type {Cesium.Viewer}
 */
const viewer = new Cesium.Viewer("box", {
  animation: false,
  baseLayerPicker: false,
  baseLayer: Cesium.ImageryLayer.fromProviderAsync(
    Cesium.ArcGisMapServerImageryProvider.fromUrl(
      "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"
    )
  ),
  fullscreenButton: false,
  timeline: false,
  infoBox: false,
});

/**
 * 预定义的颜色配置
 * @type {Array<Array<number>>}
 */
const COLOR_CONFIGS = [
  [15, 176, 255],
  [18, 76, 154],
  [64, 196, 228],
  [66, 178, 190],
  [51, 176, 204],
  [140, 183, 229],
  [0, 244, 188],
  [19, 159, 240],
];

/**
 * 添加材质到地图
 * @async
 */
async function addMaterial() {
  const dataSource = await Cesium.GeoJsonDataSource.load(
    "https://z2586300277.github.io/three-editor/dist/files/font/guangdong.json"
  );

  const entities = dataSource.entities.values;
  const colors = COLOR_CONFIGS.map(
    ([r, g, b]) => new Cesium.Color(r / 255, g / 255, b / 255, 1)
  );

  entities.forEach((entity, index) => {
    entity.polygon.extrudedHeight = 10000;
    entity.polygon.outline = false;
    entity.polygon.material = new CustomMaterialProperty({
      color: colors[index % colors.length],
    });
  });

  viewer.dataSources.add(dataSource);

  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(113.280637, 23.125178, 20000),
    orientation: {},
    duration: 3,
  });
}

addMaterial();

实现效果

image.png

结论

通过上述步骤,我们成功地创建了一个带有自定义材质行政区。Cesium.js不仅让我们能够以极高的精度和细节展示地理信息,还为我们提供了灵活的工具来增强用户的视觉体验。无论是开发教育工具、城市规划软件还是娱乐应用,Cesium都是一个非常有价值的选择。