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...

相关推荐
用户7617363540115 小时前
CSS重点知识-样式计算
前端
yoyoma15 小时前
object 、 map 、weakmap区别
前端·javascript
shyshi15 小时前
vercel 部署 node 服务和解决 vercel 不可访问的问题
前端·javascript
.生产的驴15 小时前
React 模块化Axios封装请求 统一响应格式 请求统一处理
前端·javascript·react.js·前端框架·json·ecmascript·html5
前端大神之路15 小时前
vue2 响应式原理
前端
一个努力的小码农15 小时前
Rust中if let与while let语法糖的工程哲学
前端·rust
雾岛听风来15 小时前
Android开发中常用高效数据结构
前端·javascript·后端
IT_陈寒15 小时前
Vue 3性能优化实战:这5个Composition API技巧让你的应用快30%
前端·人工智能·后端
IT_陈寒15 小时前
Vue3性能翻倍的5个秘密:从Composition API到Tree Shaking实战指南
前端·人工智能·后端
IT_陈寒16 小时前
JavaScript 性能优化:3个V8引擎隐藏技巧让你的代码提速50%
前端·人工智能·后端