手把手教你构建Cesium可视域分析工具!深度揭秘阴影映射黑科技(附完整源码)

403 阅读7分钟

大家好,我是日拱一卒的攻城师不浪,致力于前沿科技探索,摸索小而美工作室,这是2025年输出的第15/100篇文章。

三维GIS应用中,可视域分析是一项非常实用的功能,它能直观地展示从特定观测点可以看到和看不到的区域

效果预览

接下来,我们将在Cesium中实现这一功能,并且开放完整源码,请文末领取

什么是可视域分析?

可视域分析(Viewshed Analysis)是一种空间分析技术,用于确定从特定观测点可以看见的地理区域。

它通过计算从观测点发出的视线与地形建筑物等障碍物的交互关系,将空间划分为可见区域和不可见区域。在三维场景中,可视域分析通常以不同颜色区分这些区域,例如绿色表示可见区域,红色表示不可见区域

应用场景

可视域分析在许多领域有着广泛的应用:

  1. 军事领域:评估观察哨雷达站的覆盖范围,识别视野死角和最佳观测位置

  2. 城市规划:分析新建筑对周围景观视野的影响,确保重要景点的可见性

  3. 通信网络规划:优化信号塔的位置,确保最大的信号覆盖范围

  4. 安防监控:评估监控摄像头的覆盖范围,消除监控盲区

  5. 旅游景点设计:确定最佳观景平台位置,提升游客体验。

  6. 环境影响评估:分析新项目对周围环境视觉影响的范围。

可视域分析的技术原理

Cesium中实现可视域分析的核心原理主要是利用阴影映射(Shadow Mapping)技术。该技术原本用于渲染阴影,但我们可以巧妙地应用于可视域分析中:

  1. 创建虚拟光源相机:将观测点作为光源,设置相机的视锥体参数(如视距、水平视角、垂直视角等)。

  2. 生成深度图:从虚拟光源相机角度渲染场景,生成深度图(Shadow Map)

  3. 可见性判断:对于每个像素点,比较其到观测点的实际距离与深度图中记录的距离,判断该点是否可见。

  4. 着色处理:根据可见性结果,对可见区域和不可见区域分别使用不同颜色进行着色。

代码解析

1. 基本架构设计

export default class Viewshed extends Analyser {
  constructor(viewer, options) {
    super(viewer);
    this.viewer = viewer;
    this.viewPosition = options.viewPosition;
    this.viewPositionEnd = options.viewPositionEnd;
    this.viewDistance = this.viewPositionEnd
      ? Cesium.Cartesian3.distance(this.viewPosition, this.viewPositionEnd)
      : options.viewDistance || 100.0;
    // 其他参数初始化...
    
    this.action();
  }
  
  // 各种方法实现...
}

Viewshed类继承自Analyser基类,构造函数接收Cesium的viewer对象和一系列配置选项,包括观测点位置最远观测点位置观测距离等参数。

2. 交互式设置观测点

action方法实现了交互式设置观测点和观测目标点的功能:

action() {
  let _self = this;
  var ellipsoid = this.viewer.scene.globe.ellipsoid;
  _self.handler.setInputAction(function (movement) {
    var cartesian = latlng.getCurrentMousePosition(
      _self.viewer.scene,
      movement.position
    );
    
    // 处理点击事件,添加标记点
    if (_self._markers.length == 0) {
      // 添加起点标记
      // ...
      _self.state = _self.BEYONANALYSER_STATE.OPERATING;
    } else if (_self._markers.length == 1) {
      // 添加终点标记并计算视线参数
      // ...
      _self.state = _self.BEYONANALYSER_STATE.END;
      _self.handler.destroy();
      _self.handler = null;

      _self.remove();
      _self.update();
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  
  // 鼠标移动提示
  // ...
}

这段代码处理用户的点击事件,分别设置观测起点终点,并在两点都设置完成后,计算视线的方向距离等参数,然后调用update方法进行可视域分析。

3. 创建虚拟光源相机

createLightCamera() {
  this.lightCamera = new Cesium.Camera(this.viewer.scene);
  this.lightCamera.position = this.viewPosition;
  this.lightCamera.frustum.near = this.viewDistance * 0.001;
  this.lightCamera.frustum.far = this.viewDistance;
  const hr = Cesium.Math.toRadians(this.horizontalViewAngle);
  const vr = Cesium.Math.toRadians(this.verticalViewAngle);
  const aspectRatio =
    (this.viewDistance * Math.tan(hr / 2) * 2) /
    (this.viewDistance * Math.tan(vr / 2) * 2);
  this.lightCamera.frustum.aspectRatio = aspectRatio;
  if (hr > vr) {
    this.lightCamera.frustum.fov = hr;
  } else {
    this.lightCamera.frustum.fov = vr;
  }
  this.lightCamera.setView({
    destination: this.viewPosition,
    orientation: {
      heading: Cesium.Math.toRadians(this.viewHeading || 0),
      pitch: Cesium.Math.toRadians(this.viewPitch || 0),
      roll: 0,
    },
  });
}

这个方法创建一个虚拟相机作为光源,位于观测点位置,朝向观测方向,并设置视锥体参数(近平面、远平面、视场角、纵横比等)。这个相机将用于生成深度图。

4. 创建阴影贴图

createShadowMap() {
  this.shadowMap = new Cesium.ShadowMap({
    context: this.viewer.scene.context,
    lightCamera: this.lightCamera,
    enabled: this.enabled,
    isPointLight: true,
    pointLightRadius: this.viewDistance,
    cascadesEnabled: false,
    size: this.size,
    softShadows: this.softShadows,
    normalOffset: false,
    fromLightSource: false,
  });
  this.viewer.scene.shadowMap = this.shadowMap;
}

这个方法创建了一个ShadowMap对象,用于从光源(观测点)角度生成场景的深度信息。设置isPointLighttrue表示从点光源出发,可以向各个方向投射视线。

5. 创建后处理

后处理是可视域分析中最关键的部分,注意:着色器需要根据Cesium版本进行语法变更,webgl1webgl2的语法略有不同。 通过自定义着色器来渲染可视域分析的结果;

createPostStage() {
  const fs = `
      #define USE_CUBE_MAP_SHADOW true
      uniform sampler2D colorTexture;
      uniform sampler2D depthTexture;
      in vec2 v_textureCoordinates;
      // ...省略,详细请参考源码
      `;
  
  const postStage = new Cesium.PostProcessStage({
    fragmentShader: fs,
    uniforms: {
      // 各种参数传递到着色器
      // ...
      helsing_viewDistance: () => {
        return this.viewDistance;
      },
      helsing_visibleAreaColor: this.visibleAreaColor,
      helsing_invisibleAreaColor: this.invisibleAreaColor,
    },
  });
  this.postStage = this.viewer.scene.postProcessStages.add(postStage);
}

这个方法创建了一个后处理,其中包含自定义的片元着色器。着色器通过比较每个像素点到观测点的距离与深度图中记录的距离,判断该点是否在视线范围内,然后对可见区域和不可见区域分别着色

6. 绘制视锥体和视网

drawFrustumOutline() {
  // 创建视锥体轮廓
  // ...
}

drawSketch() {
  this.sketch = this.viewer.entities.add({
    name: "sketch",
    position: this.viewPosition,
    orientation: Cesium.Transforms.headingPitchRollQuaternion(
      this.viewPosition,
      Cesium.HeadingPitchRoll.fromDegrees(
        this.viewHeading - this.horizontalViewAngle,
        this.viewPitch,
        0.0
      )
    ),
    ellipsoid: {
      // 椭球体参数设置
      // ...
    },
  });
}

这两个方法分别绘制视锥体轮廓视网,用于直观地展示观测范围。

着色器核心实现原理

片元着色器的核心代码实现了可视域的判断和着色:

void main(){
    // 获取当前像素的颜色和深度信息
    out_FragColor = texture(colorTexture, v_textureCoordinates);
    float depth = getDepth(texture(depthTexture, v_textureCoordinates));
    
    // 转换到视角坐标系和世界坐标系
    vec4 viewPos = toEye(v_textureCoordinates, depth);
    vec4 wordPos = czm_inverseView * viewPos;
    
    // 转换到虚拟相机坐标系
    vec4 vcPos = camera_view_matrix * wordPos;
    float near = .001 * helsing_viewDistance;
    float dis = length(vcPos.xyz);
    
    // 在有效观测距离范围内进行可视性判断
    if(dis > near && dis < helsing_viewDistance){
        vec4 posInEye = camera_projection_matrix * vcPos;
        if(visible(posInEye)){
            float vis = shadow(viewPos);
            if(vis > 0.3){
                // 可见区域着色
                out_FragColor = mix(out_FragColor,helsing_visibleAreaColor,.5);
            } else{
                // 不可见区域着色
                out_FragColor = mix(out_FragColor,helsing_invisibleAreaColor,.5);
            }
        }
    }
}

这段着色器代码的工作流程是:

  1. 获取当前像素的颜色深度信息

  2. 转换到相机坐标系和世界坐标系

  3. 转换到观测点(虚拟相机)坐标系

  4. 判断点是否在观测距离范围内

  5. 判断点是否在视锥体内

  6. 通过shadow函数判断点是否可见

  7. 根据可见性结果进行着色

实用工具函数

代码中还包含了一些实用的工具函数:

// 获取偏航角
function getHeading(fromPosition, toPosition) {
  // ...
}

// 获取俯仰角
function getPitch(fromPosition, toPosition) {
  // ...
}

这些函数用于计算从观测点到目标点的方向角度,帮助设置虚拟相机的朝向。

总结

OK,这样我们就实现了一个功能完善的可视域分析工具。

这个工具支持交互式设置观测点和观测方向,能够直观地展示可见区域和不可见区域,对于城市规划、军事分析、通信网络规划等领域有着重要的应用价值。

实现可视域分析的关键在于理解阴影映射技术并将其应用于可见性判断

在实际应用中,还可以进一步优化性能或增加功能,例如增加动态更新,使可视域分析能够随着观测点的移动而实时更新。

源码】:github.com/jiawanlong/…

不浪的Cesium案例集合】:github.com/tingyuxuan2…

如果开源对您有帮助,也欢迎帮我们点一个免费的star,以鼓励和支持我们开源更多案例!

如有任何问题或需要进一步探讨,欢迎在评论区留言交流!

想系统学习Cesium的小伙伴儿,可以了解下不浪的教程《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程最近也更新了不少新内容,想了解+作者:brown_7778(备注来意)。

有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意)。