cesium 地形压平

在cesium中实现地形的压平功能

1.功能分析

cesium中有地形功能,能看到凹凸不平的山丘,所以有些时候模型和地形会存在重叠的情况,这时为了突出模型需要把地形中多余的部分进行压平处理。

实现步骤一般为选择一块区域,设置压平高度,实现压平

2.基本架构设计

先封装实现区域选择的操作

ts 复制代码
import {
  CallbackProperty,
  Cartesian3,
  Cartographic,
  Color,
  defined,
  Entity,
  Math,
  PolygonHierarchy,
  ScreenSpaceEventHandler,
  ScreenSpaceEventType
} from 'cesium';

class PlanishOperate {
  private app: any;
  private viewerCesium: any;

  private handler: ScreenSpaceEventHandler | undefined;

  private pointArray: Array<number>;
  private pointEntityArray: Array<any>;
  private polygonEntity: Entity | null;

  public planishHeight: number | null;

  constructor(app: any) {
    this.app = app;
    this.viewerCesium = this.app.getViewerCesium();

    this.pointArray = [];
    this.pointEntityArray = [];
    this.polygonEntity = null;

    this.planishHeight = null;
  }

  /**
   * 设置压平高度
   * @param height 高度值
   */
  public setPlanishHeight(height: number) {
    this.planishHeight = height;
  }

  /**
   * 开启绘制区域
   * @param successCallback 绘制成功后的回调函数 
   */
  public startDrawArea(successCallback: Function) {
    if (!this.handler) {
      // 构造屏幕空间事件句柄,用于后面的鼠标交互
      this.handler = new ScreenSpaceEventHandler(this.viewerCesium.scene.canvas);
    }
    // 绑定鼠标左键点击事件
    this.handler.setInputAction((e: any) => this.innerLeftClickAction(e), ScreenSpaceEventType.LEFT_CLICK);
    // 绑定鼠标移动事件
    this.handler.setInputAction((e: any) => this.innerMouseMoveAction(e), ScreenSpaceEventType.MOUSE_MOVE);
    // 绑定鼠标右键点击事件
    this.handler.setInputAction(
      (e: any) => this.innerRightClickAction(e, successCallback),
      ScreenSpaceEventType.RIGHT_CLICK
    );
  }

  /**
   * 关闭绘制区域
   */
  public endDrawArea() {
    this.handler?.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
    this.handler?.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE);
    this.handler?.removeInputAction(ScreenSpaceEventType.RIGHT_CLICK);
  }

  // 右键结束区域绘制
  public innerRightClickAction(e: any, successCallback: Function) {
    this.pointArray.pop();
    this.pointArray.pop();
    this.pointArray.push(this.pointArray[0]);
    this.pointArray.push(this.pointArray[1]);

    if (successCallback) successCallback(this.pointArray, this.planishHeight);

    this.cancelDrawArea();
  }

  // 左键开始区域绘制
  private innerLeftClickAction(movement: any) {
    let ray = this.viewerCesium.camera.getPickRay(movement.position);
    let cartesian = this.viewerCesium.scene.globe.pick(ray, this.viewerCesium.scene);
    // 是否获取到空间坐标
    if (defined(cartesian)) {
      this.drawPoint(cartesian);

      let cartographic = Cartographic.fromCartesian(cartesian);
      // 弧度转为经纬度
      let lon = Math.toDegrees(cartographic.longitude);
      let lat = Math.toDegrees(cartographic.latitude);

      this.pointArray.push(lon);
      this.pointArray.push(lat);
    }

    if (this.pointArray.length === 6) {
      let polygonEntity = new Entity({
        name: '拍平区域面对象',
        polygon: {
          hierarchy: new CallbackProperty(() => {
            return new PolygonHierarchy(Cartesian3.fromDegreesArray(this.pointArray));
          }, false),
          material: Color.RED.withAlpha(0.5)
        }
      });
      this.viewerCesium.entities.add(polygonEntity);

      this.polygonEntity = polygonEntity;
    }
  }

  // 绘制点
  private drawPoint(position: Cartesian3) {
    let pointEntity = new Entity({
      name: '拍平区域点对象',
      position: position,
      point: {
        color: Color.SKYBLUE,
        pixelSize: 10,
        outlineColor: Color.YELLOW,
        outlineWidth: 3,
        disableDepthTestDistance: Number.POSITIVE_INFINITY
      }
    });
    this.viewerCesium.entities.add(pointEntity);

    this.pointEntityArray.push(pointEntity);
  }

  // 鼠标移动绘制区域面
  private innerMouseMoveAction(movement: any) {
    let ray = this.viewerCesium.camera.getPickRay(movement.endPosition);
    let cartesian = this.viewerCesium.scene.globe.pick(ray, this.viewerCesium.scene);
    // 是否获取到空间坐标
    if (defined(cartesian)) {
      let cartographic = Cartographic.fromCartesian(cartesian);
      // 弧度转为经纬度
      let lon = Math.toDegrees(cartographic.longitude);
      let lat = Math.toDegrees(cartographic.latitude);

      this.pointArray.pop();
      this.pointArray.pop();
      this.pointArray.push(lon);
      this.pointArray.push(lat);
    }
  }

  /**
   * 取消区域绘制
   */
  public cancelDrawArea() {
    this.viewerCesium.entities.remove(this.polygonEntity);
    this.polygonEntity = null;

    this.pointEntityArray.forEach((item) => {
      this.viewerCesium.entities.remove(item);
    });
    this.pointEntityArray = [];

    this.pointArray = [];

    this.endDrawArea();
  }
}

export { PlanishOperate };

封装地形压平的数据项

ts 复制代码
class PlanishArea {
  // 平整区域名称
  name: string;

  /** uuid */
  uuid: string;

  // 平整区域点数组
  area: Array<number>;

  // 平整高度
  height: number | null;

  show: boolean;

  constructor() {
    this.name = '区域';
    this.uuid = this.createUUID();
    this.area = [];
    this.height = null;
    this.show = true;
  }

  private createUUID() {
    function S4() {
      return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    }
    return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
  }
}

class PlanishOptions {
  // 版本号
  version: string;

  // 自定义场地平整数据
  customPlanishArr: PlanishArea[];

  constructor() {
    this.version = '1.0';
    this.customPlanishArr = [];
  }

  public copyData(PlanishOptions: PlanishOptions) {
    if (!PlanishOptions) return;

    this.version = PlanishOptions.version;
    this.customPlanishArr = PlanishOptions.customPlanishArr;
  }
}

export { PlanishOptions, PlanishArea };

封装业务代码操作

ts 复制代码
import { PlanishOperate } from './PlanishOperate';
import { PlanishArea, PlanishOptions } from './PlanishOptions';

class PlanishManager {
  planishOperate: PlanishOperate;
  planishOptions: PlanishOptions;
  viewerCesium: any;

  constructor(app: any) {
    super(app);
    this.planishOperate = new PlanishOperate(app);
    this.planishOptions = new PlanishOptions();
    this.viewerCesium = this.app.getViewerCesium();
  }

  protected doGetSceneData(): any {
    return this.planishOptions;
  }

  protected doSetSceneData(sceneData: PlanishOptions): void {
    this.planishOptions.copyData(sceneData);

    this.addTerrainEditsDataArr(this.planishOptions.customPlanishArr);
  }

  /**
   * 开启绘制区域
   * @param successCallback 绘制成功后的回调函数 
   */
  public startDrawArea(successCallback: Function) {
    // 调用PlanishOperate操作器里的绘制区域
    this.planishOperate.startDrawArea((areaData: Array<any>, height: number) => {
      // 绘制区域成功后在回调里进行业务操作
      let num = this.planishOptions.customPlanishArr.length + 1;
      let name = '区域' + num;

      let planishArea = new PlanishArea();
      planishArea.name = name;

      planishArea.area = areaData;

      planishArea.height = height;

      this.planishOptions.customPlanishArr.push(planishArea);

      this.savePlanish(planishArea.uuid, areaData, height);

      if (successCallback) successCallback(planishArea);
    });
  }

  /**
   * 进行地形压平处理
   * @param uuid 唯一标识符
   * @param areaData 压平区域数据
   * @param height 压平高度
   */
  public savePlanish(uuid: string, areaData: Array<any>, height: number) {
    let terrainProvider = this.viewerCesium.scene.terrainProvider;
    // 进行压平处理
    terrainProvider.addTerrainEditsData(uuid, areaData, height);
    this.viewerCesium.scene.globe._surface.invalidateAllTiles();
  }

  /**
   * 地形压平批量处理
   * @param arr PlanishArea类型的数组
   */
  public addTerrainEditsDataArr(arr: PlanishArea[]) {
    let terrainProvider = this.viewerCesium.scene.terrainProvider;
    // 进行压平处理
    terrainProvider.addTerrainEditsDataArr(arr);
    this.viewerCesium.scene.globe._surface.invalidateAllTiles();
  }

  /**
   * 删除压平区域
   * @param uuid 压平区域的uuid
   * @param successFunc 删除成功后的回调
   */
  public removePlanish(uuid: string, successFunc: Function) {
    for (let i = 0; i < this.planishOptions.customPlanishArr.length; i++) {
      let item = this.planishOptions.customPlanishArr[i];
      if (item.uuid === uuid) {
        this.planishOptions.customPlanishArr.splice(i, 1);

        let terrainProvider = this.viewerCesium.scene.terrainProvider;
        // 删除对应的压平区域
        terrainProvider.removeTerrainEditsData(uuid);
        this.viewerCesium.scene.globe._surface.invalidateAllTiles();

        successFunc();
      }
    }
  }

  /**
   * 编辑压平区域高度
   * @param uuid 压平区域的uuid
   * @param heightValue 高度值
   * @param successFunc 编辑成功后的回调
   */
  public editPlanishHeight(uuid: string, heightValue: number, successFunc: Function) {
    for (let i = 0; i < this.planishOptions.customPlanishArr.length; i++) {
      let item = this.planishOptions.customPlanishArr[i];
      if (item.uuid === uuid) {
        item.height = heightValue;

        let terrainProvider = this.viewerCesium.scene.terrainProvider;
        // 设置对应的压平区域高度
        terrainProvider.setTerrainEditsHeight(item.uuid, heightValue);
        this.viewerCesium.scene.globe._surface.invalidateAllTiles();

        successFunc();
      }
    }
  }

  /**
   * 编辑压平区域的显隐
   * @param uuid 压平区域的uuid
   * @param visible 显隐值
   * @param successFunc 编辑成功后的回调
   */
  public editPlanishVisible(uuid: string, visible: boolean, successFunc: Function) {
    for (let i = 0; i < this.planishOptions.customPlanishArr.length; i++) {
      let item = this.planishOptions.customPlanishArr[i];
      if (item.uuid === uuid) {
        item.show = visible;

        let terrainProvider = this.viewerCesium.scene.terrainProvider;
        // 设置对应的压平区域显隐
        terrainProvider.setTerrainEditsVisible(item.uuid, visible);
        this.viewerCesium.scene.globe._surface.invalidateAllTiles();

        successFunc();
      }
    }
  }

  /**
   * 取消区域绘制
   */
  public cancelPlanish() {
    this.planishOperate.cancelDrawArea();
  }

  /**
   * 设置压平区域高度
   * @param height 压平高度
   */
  public setPlanishHeight(height: number) {
    this.planishOperate.setPlanishHeight(height);
  }
}

export { PlanishManager };

3.核心代码实现

压平的核心实现在terrainProvider

ts 复制代码
this.viewerCesium.scene.terrainProvider = createWorldTerrain(); //cesium默认的世界地形图

要实现地形压平就需要在地形加载的时候执行到特殊区域时,对这块区域的地形顶点数据进行处理,设置顶点高度到指定的压平高度

所以修改一下createWorldTerrain.js的源码,修改内部new CesiumTerrainProvider为new CesiumTerrainProviderEdit

实现CesiumTerrainProviderEdit

ts 复制代码
import { CesiumTerrainProvider, Math as CesiumMath } from 'cesium';

import * as turf from '@turf/turf';

import { LoopSubdivision } from '../../gis-modules/gisCommon/loopSubdivision.js';

/**
 * 对CesiumTerrainProvider进行扩展,修改地形数据,实现场地平整功能
 */

const MAX_SHORT = 32767;

class CesiumTerrainProviderEdit extends CesiumTerrainProvider {
  constructor({ url, modelEdits }) {
    super({ url });

    // 存储多边形压平区域的数组
    this._polygonEdits = [];
  }

  /**
   * 过滤地形瓦片,如果这块地形瓦片在压平区域内则进行压平处理
   * @param {*} x 瓦片的x
   * @param {*} y 瓦片的y
   * @param {*} level 瓦片的层级
   * @param {*} terrainData 瓦片的地形数据
   * @returns
   */
  filterTerrainTile(x, y, level, terrainData) {
    if (this._polygonEdits.length === 0) {
      return terrainData;
    }

    // 获取对应瓦片的矩形数据
    const tileRectangle = this.tilingScheme.tileXYToRectangle(x, y, level);

    const tilePolygon = rectangleToPolygon(tileRectangle);

    // 通过truf提供的算法来筛选压平区域(booleanOverlap 判断是否重叠;booleanContains 判断是否包含)
    const relevantEdits = this._polygonEdits.filter(
      (edit) =>
        edit.show && (turf.booleanOverlap(edit.polygon, tilePolygon) || turf.booleanContains(edit.polygon, tilePolygon))
    );

    if (relevantEdits.length === 0) {
      return terrainData;
    }

    if (level >= 18) {
      subdivisionTerrainTile(terrainData);
    }

    // 地形瓦片细分好后,修改地形
    let modifyData = modifyTerrainTile(terrainData, tileRectangle, relevantEdits);

    return modifyData;
  }

  /**
   * 添加地形压平区域数据
   * @param {*} uuid 压平区域的uuid
   * @param {*} area 压平区域数据
   * @param {*} height 压平高度
   * @returns
   */
  addTerrainEditsData(uuid, area, height) {
    let arr = [];
    for (let i = 0; i < area.length; i += 2) {
      if (area[i]) {
        arr.push([area[i], area[i + 1]]);
      }
    }

    for (let p = 0; p < this._polygonEdits.length; p++) {
      if (this._polygonEdits[p].uuid === uuid) {
        return;
      }
    }

    if (arr.length === 0) return;

    // 把多边形压平区域的数据对象存储起来,用于后续地形处理
    this._polygonEdits.push({
      uuid: uuid,
      show: true,
      polygon: turf.polygon([arr]),
      height: height
    });
  }

  /**
   * 业务需要封装根据压平区域数据的数组添加(内部实现与addTerrainEditsData类似)
   * @param {*} dataArr 压平区域数据的对象数组
   * @returns
   */
  addTerrainEditsDataArr(dataArr) {
    for (let d = 0; d < dataArr.length; d++) {
      let item = dataArr[d];
      let arr = [];
      for (let i = 0; i < dataArr[d].area.length; i += 2) {
        if (dataArr[d].area[i]) {
          arr.push([item.area[i], item.area[i + 1]]);
        }
      }

      for (let p = 0; p < this._polygonEdits.length; p++) {
        if (this._polygonEdits[p].uuid === item.uuid) {
          return;
        }
      }

      if (arr.length === 0) return;

      this._polygonEdits.push({
        uuid: item.uuid,
        show: item.show,
        polygon: turf.polygon([arr]),
        height: item.height
      });
    }
  }

  /**
   * 删除压平区域
   * @param {*} uuid 压平区域的uuid
   */
  removeTerrainEditsData(uuid) {
    for (let i = 0; i < this._polygonEdits.length; i++) {
      if (this._polygonEdits[i].uuid === uuid) {
        this._polygonEdits.splice(i, 1);
      }
    }
  }

  /**
   * 设置压平区域的显隐
   * @param {*} uuid 压平区域的uuid
   * @param {*} visible 显隐值
   */
  setTerrainEditsVisible(uuid, visible) {
    for (let i = 0; i < this._polygonEdits.length; i++) {
      if (this._polygonEdits[i].uuid === uuid) {
        this._polygonEdits[i].show = visible;
      }
    }
  }

  /**
   * 设置压平区域高度
   * @param {*} uuid 压平区域的uuid
   * @param {*} height 压平高度
   */
  setTerrainEditsHeight(uuid, height) {
    for (let i = 0; i < this._polygonEdits.length; i++) {
      if (this._polygonEdits[i].uuid === uuid) {
        this._polygonEdits[i].height = height;
      }
    }
  }
}

/**
 *
 * @param {*} tileRectangle 经度和纬度坐标的二维区域
 * @returns
 */
function rectangleToPolygon(tileRectangle) {
  // 将弧度转换为度数
  let westD = CesiumMath.toDegrees(tileRectangle.west);
  let southD = CesiumMath.toDegrees(tileRectangle.south);
  let eastD = CesiumMath.toDegrees(tileRectangle.east);
  let northD = CesiumMath.toDegrees(tileRectangle.north);
  // 使用turf.js进行构造多边形
  let polygon = turf.polygon([
    [
      [westD, northD],
      [eastD, northD],
      [eastD, southD],
      [westD, southD],
      [westD, northD]
    ]
  ]);
  return polygon;
}

/**
 * 细分地形瓦片(进行细分的目的是解决压平的多边形区域的边缘处粗糙问题)
 * @param {*} terrainData 瓦片的地形数据
 */
function subdivisionTerrainTile(terrainData) {
  // 对地形数据中的position和index进行处理,改成便于网格化细分的数据
  positionToNonIndexed(terrainData);

  // 进行网格化细分(LoopSubdivision是我封装好的网格化细分算法)
  let quantizedVerticesNew = LoopSubdivision.modify(terrainData._quantizedVertices, 4, 2500);
  terrainData._quantizedVertices = quantizedVerticesNew;

  let indicesLength = quantizedVerticesNew.length / 3;
  let newIndices = new Uint16Array(indicesLength);
  for (let i = 0; i < indicesLength; i++) {
    newIndices[i] = i;
  }
  terrainData._indices = newIndices;
}

/**
 * 将postion与index对应的这种优化方式改为position与index无索引优化的方式
 * @param {*} terrainData 瓦片的地形数据
 */
function positionToNonIndexed(terrainData) {
  // position与index的这种索引优化可以复用position数据,减少内存,但是在后续进行网格化细分的时候不好计算处理,所以先进行这步处理。
  let quantizedVertices = terrainData._quantizedVertices;
  let indices = terrainData._indices;

  let itemSize = 3;
  let indicesLength = indices.length;
  let quanLength = quantizedVertices.length / 3;

  const quantizedVerticesNew = new Uint16Array(indicesLength * itemSize);
  const indicesNew = new Uint16Array(indicesLength);

  for (let i = 0, len = indicesLength; i < len; i++) {
    let index = indices[i];

    quantizedVerticesNew[i] = quantizedVertices[index];
    quantizedVerticesNew[i + indicesLength] = quantizedVertices[index + quanLength];
    quantizedVerticesNew[i + indicesLength * 2] = quantizedVertices[index + quanLength * 2];

    indicesNew[i] = i;
  }

  let westIndices = changeWsenIndices(terrainData._westIndices, quantizedVertices, quantizedVerticesNew);
  let southIndices = changeWsenIndices(terrainData._southIndices, quantizedVertices, quantizedVerticesNew);
  let eastIndices = changeWsenIndices(terrainData._eastIndices, quantizedVertices, quantizedVerticesNew);
  let northIndices = changeWsenIndices(terrainData._northIndices, quantizedVertices, quantizedVerticesNew);

  terrainData._quantizedVertices = quantizedVerticesNew;
  terrainData._indices = indicesNew;
  terrainData._westIndices = westIndices;
  terrainData._southIndices = southIndices;
  terrainData._eastIndices = eastIndices;
  terrainData._northIndices = northIndices;
}

function changeWsenIndices(indices, quantizedVertices, quantizedVerticesNew) {
  let quanLength = quantizedVertices.length / 3;
  let quanNewLength = quantizedVerticesNew.length / 3;

  let indicesNew = [];
  for (let w = 0, wl = indices.length; w < wl; w++) {
    let index = indices[w];

    let p1 = quantizedVertices[index];
    let p2 = quantizedVertices[index + quanLength];
    let p3 = quantizedVertices[index + quanLength * 2];

    for (let p = 0; p < quanNewLength; p++) {
      if (
        p1 === quantizedVerticesNew[p] &&
        p2 === quantizedVerticesNew[p + quanNewLength] &&
        p3 === quantizedVerticesNew[p + quanNewLength * 2]
      ) {
        indicesNew.push(p);
        break;
      }
    }
  }

  return indicesNew;
}

/**
 * 对瓦片数据进行修改,实现压平(对瓦片和压平区域进行范围判断处理,范围内的顶点修改高度值)
 * @param {*} terrainData 瓦片的地形数据
 * @param {*} tileRectangle 经度和纬度坐标的二维区域
 * @param {*} relevantEdits 要压平的区域
 * @returns
 */
function modifyTerrainTile(terrainData, tileRectangle, relevantEdits) {
  const data = terrainData;
  const minimumHeight = data._minimumHeight;
  const maximumHeight = data._maximumHeight;

  const quantizedVertices = data._quantizedVertices;
  const vertexCount = quantizedVertices.length / 3;

  const positions = [];
  for (let i = 0; i < vertexCount; i++) {
    const rawU = quantizedVertices[i];
    const rawV = quantizedVertices[i + vertexCount];
    const rawH = quantizedVertices[i + vertexCount * 2];

    const u = rawU / MAX_SHORT;
    const v = rawV / MAX_SHORT;
    const longitude = CesiumMath.toDegrees(CesiumMath.lerp(tileRectangle.west, tileRectangle.east, u));
    const latitude = CesiumMath.toDegrees(CesiumMath.lerp(tileRectangle.south, tileRectangle.north, v));

    let height = CesiumMath.lerp(minimumHeight, maximumHeight, rawH / MAX_SHORT);

    const currentPoint = turf.point([longitude, latitude]);

    const relevantEdit = relevantEdits.find((edit) => turf.booleanPointInPolygon(currentPoint, edit.polygon));
    if (relevantEdit) {
      if (relevantEdit.height !== null) {
        height = relevantEdit.height;
      }
    }
    positions.push([longitude, latitude, height]);
  }

  const heights = positions.map((p) => p[2]);
  const newMinHeight = Math.min(...heights);
  const newMaxHeight = Math.max(...heights);

  if (newMaxHeight - newMinHeight === 0) {
    positions.forEach((p, i) => {
      const relativeHeight = Math.round(CesiumMath.lerp(0, MAX_SHORT, (p[2] - newMinHeight) / 1));
      quantizedVertices[i + positions.length * 2] = relativeHeight;
    });
  } else {
    positions.forEach((p, i) => {
      const relativeHeight = Math.round(
        CesiumMath.lerp(0, MAX_SHORT, (p[2] - newMinHeight) / (newMaxHeight - newMinHeight))
      );
      quantizedVertices[i + positions.length * 2] = relativeHeight;
    });
  }

  data._minimumHeight = newMinHeight;
  data._maximumHeight = newMaxHeight;

  return data;
}

export default CesiumTerrainProviderEdit;

// https://community.cesium.com/t/terrain-editing-on-the-fly/9695/6

由于地形瓦片上的顶点数据比较粗糙,压平的时候在范围的边缘处锯齿化严重,所以需要对地形上的顶点进行细分,分成更多的顶点来减少误差

实现地形网格化细分算法LoopSubdivision

ts 复制代码
class Vector3 {
  constructor(x, y, z) {
    this.x = x || 0;
    this.y = y || 0;
    this.z = z || 0;
  }

  set(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
    return this;
  }

  copy(v) {
    this.x = v.x;
    this.y = v.y;
    this.z = v.z;
    return this;
  }

  add(v) {
    this.x += v.x;
    this.y += v.y;
    this.z += v.z;
    return this;
  }

  divideScalar(scalar) {
    return this.multiplyScalar(1 / scalar);
  }

  multiplyScalar(scalar) {
    this.x *= scalar;
    this.y *= scalar;
    this.z *= scalar;
    return this;
  }
}

const _vector0 = new Vector3();
const _vector1 = new Vector3();
const _vector2 = new Vector3();
const _vec0to1 = new Vector3();
const _vec1to2 = new Vector3();
const _vec2to0 = new Vector3();

class LoopSubdivision {
  /**
   * 对顶点的三角形进行细分
   * @param {*} positionArray 顶点数据
   * @param {*} iterations 递归细分次数
   * @param {*} maxTriangles 细分的最大三角形数量
   * @returns 
   */
  static modify(positionArray, iterations = 1, maxTriangles = Infinity) {
    let modifiedPosition = [...positionArray];

    for (let i = 0; i < iterations; i++) {
      let currentTriangles = modifiedPosition.length / 3;
      if (currentTriangles < maxTriangles) {
        let subdividedPosition = LoopSubdivision.flatAttribute(modifiedPosition);

        modifiedPosition = [];
        modifiedPosition = subdividedPosition;
      }
    }

    return modifiedPosition;
  }

  /**
   * 一个三角形细分成四个三角形
   * @param {*} positionArray 
   * @returns 
   */
  static flatAttribute(positionArray) {
    const vertexCount = positionArray.length / 3;

    const arrayLength = vertexCount * 3 * 4;
    const newPositionArray = new Float32Array(arrayLength);

    let index = 0;
    for (let i = 0; i < vertexCount; i += 3) {
      let x0 = i + 0;
      let y0 = i + 0 + vertexCount;
      let z0 = i + 0 + vertexCount * 2;
      _vector0.set(positionArray[x0], positionArray[y0], positionArray[z0]);

      let x1 = i + 1;
      let y1 = i + 1 + vertexCount;
      let z1 = i + 1 + vertexCount * 2;
      _vector1.set(positionArray[x1], positionArray[y1], positionArray[z1]);

      let x2 = i + 2;
      let y2 = i + 2 + vertexCount;
      let z2 = i + 2 + vertexCount * 2;
      _vector2.set(positionArray[x2], positionArray[y2], positionArray[z2]);

      //                 /\ 
      //                /  \
      //               /_ _ \
      //              /\    /\
      //             /  \  /  \
      //            /_ _ \/_ _ \

      // 取出三个顶点后,每两个顶点之间取中间点
      _vec0to1.copy(_vector0).add(_vector1).divideScalar(2.0);
      _vec1to2.copy(_vector1).add(_vector2).divideScalar(2.0);
      _vec2to0.copy(_vector2).add(_vector0).divideScalar(2.0);

      // 构造三角形
      setTriangle(newPositionArray, index, _vector0, _vec0to1, _vec2to0);
      index += 3;
      setTriangle(newPositionArray, index, _vector1, _vec1to2, _vec0to1);
      index += 3;
      setTriangle(newPositionArray, index, _vector2, _vec2to0, _vec1to2);
      index += 3;
      setTriangle(newPositionArray, index, _vec0to1, _vec1to2, _vec2to0);
      index += 3;
    }

    return newPositionArray;
  }
}

function setTriangle(positions, index, vec0, vec1, vec2) {
  let vertexCount = positions.length / 3;

  positions[index + 0] = vec0.x;
  positions[index + 0 + vertexCount] = vec0.y;
  positions[index + 0 + vertexCount * 2] = vec0.z;

  positions[index + 1] = vec1.x;
  positions[index + 1 + vertexCount] = vec1.y;
  positions[index + 1 + vertexCount * 2] = vec1.z;

  positions[index + 2] = vec2.x;
  positions[index + 2 + vertexCount] = vec2.y;
  positions[index + 2 + vertexCount * 2] = vec2.z;
}

export { LoopSubdivision };

最后还需修改一处位置,在GlobeSurfaceTile.js中加载地形时,调用我们新增的函数,对地形数据进行过滤压平

4.效果

参考学习文章 community.cesium.com/t/terrain-e...

相关推荐
开心工作室_kaic13 分钟前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿32 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
枝上棉蛮2 小时前
GISBox VS ArcGIS:分别适用于大型和小型项目的两款GIS软件
arcgis·gis·数据可视化·数据处理·地理信息系统·gis工具箱·gisbox
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx