Cesium创建局部山体等高线

693 阅读6分钟

引言

在地理信息系统(GIS)和地球科学领域,3D可视化技术正在迅速发展。它不仅为研究人员提供了更直观的数据展示方式,还使得普通用户能够以全新的视角探索世界。本文将介绍如何使用CesiumJS库来创建一个基于Web的3D地球视图,并实现特定区域的等高线显示功能。我们将通过一段HTML代码实例,演示如何加载地形数据、添加自定义材质以及生成等高线。

项目设置

为了开始我们的项目,首先需要确保页面中包含了CesiumJS库及其样式文件。我们可以通过CDN链接引入这些资源:

<script src="https://cesium.com/downloads/cesiumjs/releases/1.121/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.121/Build/Cesium/Widgets/widgets.css" rel="stylesheet">

初始化Cesium Viewer

接下来是初始化Cesium Viewer,这是CesiumJS的核心组件,负责渲染3D场景。在初始化时,我们可以配置各种参数,如是否启用光照效果、大气层显示等。此外,还需要指定地形数据源,这里我们选择了WorldTerrain作为默认地形提供商:

Cesium.Ion.defaultAccessToken = 'YOUR_ACCESS_TOKEN'; // 替换为您的访问令牌

let viewer = new Cesium.Viewer('cesiumContainer', {
    terrain: Cesium.Terrain.fromWorldTerrain(),
    enableLighting: true,
    showGroundAtmosphere: true,
    dynamicAtmosphereLightingFromSun: true,
});

添加等高线

要添加等高线,我们需要编写自定义材质,并将其应用于地球表面。在这个例子中,我们将根据给定的一系列坐标点计算矩形范围,并使用GLSL着色器语言定义等高线的绘制逻辑

1、编写shader
定义Uniform变量
uniform vec4 color;
uniform float spacing;
uniform float width;  
uniform vec4 rect;  
uniform vec4 m_0;  
uniform vec4 m_1;  
uniform vec4 m_2;  
uniform vec4 m_3;  
  • color:设置等高线的颜色。
  • spacing:设定等高线之间的间隔,即每隔多少高度绘制一条等高线。
  • width:设定等高线的宽度。
  • rect:一个矩形范围,用来限定等高线显示的区域。
  • m_0m_3:这些是转换矩阵的四个行向量,用来将世界坐标转换为局部坐标系统,以便根据矩形范围来确定是否绘制等高线。
计算距离最近的等高线
float distanceToContour = mod(materialInput.height, spacing);
  • 这一行计算了当前像素的高度与最接近的等高线之间的差值。mod函数返回的是当前高度除以spacing后的余数,也就是到下一个等高线的距离。
检查是否应该绘制等高线
#if (__VERSION__ == 300 || defined(GL_OES_standard_derivatives))
float dxc = abs(dFdx(materialInput.height));
float dyc = abs(dFdy(materialInput.height));
float dF = max(dxc, dyc) * czm_pixelRatio * width;
float alpha = (distanceToContour < dF) ? 1.0 : 0.0;
#else
// If no derivatives available (IE 10?), use pixel ratio
float alpha = (distanceToContour < (czm_pixelRatio * width)) ? 1.0 : 0.0;
#endif
  • 这一部分代码首先检查是否支持标准导数(GL_OES_standard_derivatives),如果支持,则使用dFdxdFdy函数来计算相邻像素高度变化的最大值,并结合像素比例和等高线宽度来决定是否绘制等高线。如果不支持导数计算(例如在较老的浏览器或硬件上),则直接使用像素比例和宽度来判断。
  • alpha值决定了该像素的透明度。当distanceToContour小于等于dF时,alpha设为1.0,表示完全不透明;否则设为0.0,表示完全透明,这样就只有靠近等高线的像素会被绘制出来。
应用颜色和透明度
vec4 outColor = czm_gammaCorrect(vec4(color.rgb, alpha * color.a));
material.diffuse = outColor.rgb; 
material.alpha =0.;    
if(local.x>rect.x&&local.x<rect.z&&local.y<rect.w&&local.y>rect.y){  
    material.alpha = outColor.a;     
} 
定义Material函数
czm_material czm_getMaterial(czm_materialInput materialInput)
    {
        czm_material material = czm_getDefaultMaterial(materialInput); 
        float distanceToContour = mod(materialInput.height, spacing);
        
        #if (__VERSION__ == 300 || defined(GL_OES_standard_derivatives))
        float dxc = abs(dFdx(materialInput.height));
        float dyc = abs(dFdy(materialInput.height));
        float dF = max(dxc, dyc) * czm_pixelRatio * width;
        float alpha = (distanceToContour < dF) ? 1.0 : 0.0;
        #else
        // If no derivatives available (IE 10?), use pixel ratio
        float alpha = (distanceToContour < (czm_pixelRatio * width)) ? 1.0 : 0.0;
        #endif
        
        vec4 outColor = czm_gammaCorrect(vec4(color.rgb, alpha * color.a));
        material.diffuse = outColor.rgb; 
        mat4 m=mat4(m_0[0],m_0[1],m_0[2],m_0[3],m_1[0],m_1[1],m_1[2],m_1[3],m_2[0],m_2[1],m_2[2],m_2[3],m_3[0],m_3[1],m_3[2],m_3[3]);
         
        vec4 eyeCoordinate =vec4(-materialInput.positionToEyeEC,1.0); 
        vec4 worldCoordinate4 =  czm_inverseView * eyeCoordinate;
        vec3 worldCoordinate = worldCoordinate4.xyz ;// worldCoordinate4.w;
        vec4 local=m * vec4(worldCoordinate,1.);  
        material.alpha =0.;    
        if(local.x>rect.x&&local.x<rect.z&&local.y<rect.w&&local.y>rect.y){  
        material.alpha = outColor.a;     
        } 
        return material;
    }
  • czm_getMaterial是CesiumJS提供的一个钩子函数,允许我们自定义材质属性。在这个函数里,我们可以修改材质的颜色、透明度等属性。

2、处理传参数据

将地理坐标转换为笛卡尔坐标
let positionsHieght = Cesium.Cartesian3.fromDegreesArrayHeights(
    [].concat.apply([], positions)
);
  • positions 是一个二维数组,其中每个子数组包含三个元素:经度、纬度和高度。
  • fromDegreesArrayHeights 方法将这些地理坐标(以度为单位的经纬度和米为单位的高度)转换为笛卡尔坐标系中的三维点(Cartesian3),这些点可以直接用于CesiumJS的3D场景中。
  • [].concat.apply([], positions) 将二维数组展平为一维数组,以便符合fromDegreesArrayHeights方法的参数要求。
创建东-北-上(ENU)固定框架
let m = Cesium.Transforms.eastNorthUpToFixedFrame(positionsHieght[0]);
  • eastNorthUpToFixedFrame 方法创建了一个从世界坐标系到东-北-上(ENU)局部坐标系的转换矩阵。这个矩阵基于positionsHieght[0](即第一个点的位置)来定义局部坐标系的原点和方向。

  • ENU坐标系是一种常用的局部坐标系,其中:

    • 东方向为X轴正方向,
    • 北方向为Y轴正方向,
    • 上方向为Z轴正方向。
计算逆矩阵
let inverse = Cesium.Matrix4.inverse(m, new Cesium.Matrix4());
  • Matrix4.inverse 方法计算上述转换矩阵的逆矩阵。逆矩阵的作用是将局部坐标系中的点转换回世界坐标系。
将所有点转换到局部坐标系
let localPositions = [];
positionsHieght.forEach((position) => {
    localPositions.push(
        Cesium.Matrix4.multiplyByPoint(
            inverse,
            position,
            new Cesium.Cartesian3()
        )
    );
});
  • 这段代码遍历所有转换后的笛卡尔坐标点,并使用逆矩阵将它们从世界坐标系转换到局部坐标系。
  • multiplyByPoint 方法执行矩阵乘法,将点从世界坐标系转换到局部坐标系。
计算包围矩形
//计算矩形范围
rect = Cesium.BoundingRectangle.fromPoints(
    localPositions,
    new Cesium.BoundingRectangle()
);

rect = new Cesium.Cartesian4(
    rect.x,
    rect.y,
    rect.x + rect.width,
    rect.y + rect.height
);
  • BoundingRectangle.fromPoints 方法计算所有局部坐标点所围成的最小矩形范围。这个矩形是在局部坐标系中的。
  • new Cesium.Cartesian4(...) 将这个矩形的四个角点(左下角和右上角)封装为一个Cartesian4对象,方便后续在着色器中使用。
添加Material材质
  let material = new Cesium.Material({
          fabric: {
            type: "ElevationContour",
            uniforms: {
              width: 1,
              spacing: 50,
              color: Cesium.Color.YELLOW,
              rect: rect,
              m_0: new Cesium.Cartesian4(
                inverse[0],
                inverse[1],
                inverse[2],
                inverse[3]
              ),
              m_1: new Cesium.Cartesian4(
                inverse[4],
                inverse[5],
                inverse[6],
                inverse[7]
              ),
              m_2: new Cesium.Cartesian4(
                inverse[8],
                inverse[9],
                inverse[10],
                inverse[11]
              ),
              m_3: new Cesium.Cartesian4(
                inverse[12],
                inverse[13],
                inverse[14],
                inverse[15]
              ),
            },
          },
完整代码
     // 设置访问令牌
      Cesium.Ion.defaultAccessToken =
        "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjMzU2ZTQyYy1iOTU5LTQ5MDQtOGNkNC0yYzcxMTI1ZDJiZGQiLCJpZCI6NzY1OTcsImlhdCI6MTYzOTU2MDcwOH0.kbWigipGD6l2OPBGpnkkN6dzp8NuNjoHNNM1NF4gaIo";
      let viewer = "";
      const init = () => {
        viewer = new Cesium.Viewer("cesiumContainer", {
          terrain: Cesium.Terrain.fromWorldTerrain(), // 地形数据
          enableLighting: true,
          showGroundAtmosphere: true,
          dynamicAtmosphereLightingFromSun: true,
        });
        viewer.scene.skyAtmosphere.show = true;
        viewer.shadows = true;
        addLine([
          [110.88106709929548, 30.10536702913351, 1187.5288999303148],
          [110.8834848758976, 30.04803830643123, 923.651454152295],
          [110.93732277846644, 30.048178667944597, 597.5883761989865],
          [110.95818944973519, 30.08307354872316, 1011.2606325887339],
          [110.92957034551483, 30.09950535924442, 1180.6223600558253],
        ]);
      };
      const addLine = (positions) => {
        let positionsHieght = Cesium.Cartesian3.fromDegreesArrayHeights(
          [].concat.apply([], positions)
        );
        let m = Cesium.Transforms.eastNorthUpToFixedFrame(positionsHieght[0]);
        let inverse = Cesium.Matrix4.inverse(m, new Cesium.Matrix4());
        let localPositions = [];
        positionsHieght.forEach((position) => {
          localPositions.push(
            Cesium.Matrix4.multiplyByPoint(
              inverse,
              position,
              new Cesium.Cartesian3()
            )
          );
        });

        //计算矩形范围
        rect = Cesium.BoundingRectangle.fromPoints(
          localPositions,
          new Cesium.BoundingRectangle()
        );

        rect = new Cesium.Cartesian4(
          rect.x,
          rect.y,
          rect.x + rect.width,
          rect.y + rect.height
        );
        Cesium.Material._materialCache._materials.ElevationContour.fabric.source = `
    uniform vec4 color;
    uniform float spacing;
    uniform float width;  
    uniform vec4 rect;  
    uniform vec4 m_0;  
    uniform vec4 m_1;  
    uniform vec4 m_2;  
    uniform vec4 m_3;  

    czm_material czm_getMaterial(czm_materialInput materialInput)
    {
        czm_material material = czm_getDefaultMaterial(materialInput); 
        float distanceToContour = mod(materialInput.height, spacing);
        
        #if (__VERSION__ == 300 || defined(GL_OES_standard_derivatives))
        float dxc = abs(dFdx(materialInput.height));
        float dyc = abs(dFdy(materialInput.height));
        float dF = max(dxc, dyc) * czm_pixelRatio * width;
        float alpha = (distanceToContour < dF) ? 1.0 : 0.0;
        #else
        // If no derivatives available (IE 10?), use pixel ratio
        float alpha = (distanceToContour < (czm_pixelRatio * width)) ? 1.0 : 0.0;
        #endif
        
        vec4 outColor = czm_gammaCorrect(vec4(color.rgb, alpha * color.a));
        material.diffuse = outColor.rgb; 
        mat4 m=mat4(m_0[0],m_0[1],m_0[2],m_0[3],m_1[0],m_1[1],m_1[2],m_1[3],m_2[0],m_2[1],m_2[2],m_2[3],m_3[0],m_3[1],m_3[2],m_3[3]);
         
        vec4 eyeCoordinate =vec4(-materialInput.positionToEyeEC,1.0); 
        vec4 worldCoordinate4 =  czm_inverseView * eyeCoordinate;
        vec3 worldCoordinate = worldCoordinate4.xyz ;// worldCoordinate4.w;
        vec4 local=m * vec4(worldCoordinate,1.);  
        material.alpha =0.;    
        if(local.x>rect.x&&local.x<rect.z&&local.y<rect.w&&local.y>rect.y){  
        material.alpha = outColor.a;     
        } 
        return material;
    }
    `;
        let material = new Cesium.Material({
          fabric: {
            type: "ElevationContour",
            uniforms: {
              width: 1,
              spacing: 50,
              color: Cesium.Color.YELLOW,
              rect: rect,
              m_0: new Cesium.Cartesian4(
                inverse[0],
                inverse[1],
                inverse[2],
                inverse[3]
              ),
              m_1: new Cesium.Cartesian4(
                inverse[4],
                inverse[5],
                inverse[6],
                inverse[7]
              ),
              m_2: new Cesium.Cartesian4(
                inverse[8],
                inverse[9],
                inverse[10],
                inverse[11]
              ),
              m_3: new Cesium.Cartesian4(
                inverse[12],
                inverse[13],
                inverse[14],
                inverse[15]
              ),
            },
          },
        });
        viewer.scene.globe.material = material;
        viewer.camera.flyTo({
          destination: Cesium.Cartesian3.fromDegrees(
            positions[0][0],
            positions[0][1],
            1000
          ),
          orientation: {},
        });
      };
      init();

最终效果

image.png