Cesium高阶学习十二、体积云

一、原理
体积云与普通材质云最大的区别就是体积云有厚度(体积感),能够进入云中

图为普通材质云 效果可见进阶篇中shadertory着色器一节
在这里插入图片描述

图为体积云 本章需要实现的效果
在这里插入图片描述

关于体积云更多的知识及原理可以参考以下文章,大部分都是基于游戏引擎的,因为体积云最早就是出现在游戏引擎中。
https://zhuanlan.zhihu.com/p/501039307
https://zhuanlan.zhihu.com/p/645281439
https://zhuanlan.zhihu.com/p/622654876
https://zhuanlan.zhihu.com/p/640248737
https://juejin.cn/post/6844904054955458573

也可以百度搜索 体积云 查看更多信息。目前实现体积云最常用的方式是云图噪声+光线步进。
在这里插入图片描述

通过云图噪声生成云的基本形状,然后通过光线步进采样云的密度

1、云图噪声:云图噪声主要用来生成云的形状,噪声的生成可以通过glsl实时生成,也可以通过工具将其生成为2D/3D纹理数据,方便加载使用。比如 Shadertoy柏林+沃利噪声。

2、光线步进:因为云的形状是不规则的,所以无法使用简单的几何算法判断交点,而光线步进则可以很好地解决云的求交问题。因为光线步进的原理是模拟光的前进,所以光从哪儿前进,每次前进多少,最多前进多远这些参数的设置对程序的性能有很大影响。

对于局部的体积云,一般使用一个Box来表示云的范围,为了提高程序性能,光线步进一般只在该Box中进行,所以对于每条光线,首先需要获取其在Box上的起点和终点。
在这里插入图片描述

Cesium实现体积云的方式:体积云不是实体,所以是没有顶点信息的,因此我们只能通过片元着色器来实现,在Cesium中实现体积云有两种方式:

1、基于Primitive:基于Primitive的方式可见“体渲染”相关章节的体渲染实现,此种方式是通过将体内的计算结果显示到Geometry表面上来,所以虽然看起来像体,其实还是Geometry表面渲染。

2、基于后处理:基于后处理实现体渲染,更接近真实的体渲染效果,但是此方式需要在片元着色器中还原世界坐标,相对麻烦一点,不过在后处理中还原世界坐标的相关知识,已经在进阶篇中介绍过,假设您还没有Cesium后处理相关知识,可以先参考进阶篇。

本章优先使用后处理进行体积云的实现,学完后您也可以使用Primitive方法进行实现。要实现体积云,我们可以先参考游戏引擎的相关代码,作者实现体积云也是参考的其他引擎的代码,所以本章的重点在于讲解在Cesium的整体实现思路,至于涉及到的着色器里面的计算原理,作者也是shader菜鸡,也只能看懂大概的执行过程,至于里面的一些数学计算方法也是懵逼得很,所以涉及到的着色器计算不会一一讲解。

二、后处理实现
要在Cesium后处理实现体积云(局部),因为前面我们说了,局部体积云一般是在一个Box内进行渲染,所以我们要先知道如何在后处理中绘制一个Box。首先我们回想一下使用Geometry绘制一个Box,一般需要知道Box的坐标原点,然后是Box的大小信息(长、宽、高),我们先假定Box的坐标为-75.59670696331766, 40.0387958759308, 90.62678445553416,长宽高都为20。

let entity = new Cesium.Entity({
   
   
        position:position, 
        box:{
   
   
            dimensions: new Cesium.Cartesian3(20.0, 20.0, 20.0),
            material:Cesium.Color.BLUE,
        }
    })
viewer.entities.add(entity);

在这里插入图片描述

在后处理中要绘制一个同样的Box,我的思路是这样的:首先以Box中心点建立一个局部坐标系,每个片元还原到世界坐标,然后转到这个局部坐标系,通过判断坐标值的大小就可以判断这个片元是否在该Box内,如果在内,就设置颜色为蓝色。我们按照思路编写代码,因为要转坐标系,首先我们通过Box的原点坐标建立一个局部坐标系:

//矩阵
    let transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
    //逆矩阵
    let inverse = Cesium.Matrix4.inverse(transform, new Cesium.Matrix4());

然后将次坐标系信息传入后处理着色器,着色器中每个片元坐标先还原为世界坐标,然后转到该坐标系下,最后进行坐标数值比较。

let shader=`
          uniform sampler2D colorTexture;
          uniform sampler2D depthTexture;
          in vec2 v_textureCoordinates;  
          uniform mat4 inverse; 
          void main(){
              out_FragColor = texture(colorTexture, v_textureCoordinates); 
              vec4 rawDepthColor = texture(czm_globeDepthTexture, v_textureCoordinates);
              float depth = czm_unpackDepth(rawDepthColor);
              if (depth == 0.0) {
                depth = 1.0;  
              }  
              vec4 eyeCoordinate4 = czm_windowToEyeCoordinates(gl_FragCoord.xy, depth); 
              vec3 eyeCoordinate3 = eyeCoordinate4.xyz/eyeCoordinate4.w; 
              vec4 worldCoordinate4 = czm_inverseView * vec4(eyeCoordinate3,1.) ;  
              vec3 worldCoordinate = worldCoordinate4.xyz / worldCoordinate4.w;

              vec4 local= inverse * vec4(worldCoordinate,1.);       
              if(local.x>-20.&&local.x<20.&&local.y>-20.&&local.y<20.&&local.z>-20.&&local.z<20.){
                out_FragColor=vec4(0.,0.,1.,1);
              }    
            }
    `; 
    let stage = new Cesium.PostProcessStage({
   
   
      fragmentShader: shader,
      uniforms: {
   
    
        inverse: inverse
      }
    });

在代码中,我们将位于Box内的片元都设置为蓝色,运行代码看看结果
在这里插入图片描述

从结果中,我们可以看到,虽然在Box范围内的片元被设置成了蓝色,但是并没有出现一个像上面那样立体的Box,这是因为后处理就是一副画,这是一个二维的概念,所以肯定没有立体的效果。但是如果又想要立体的效果呢?是不是没有办法呢?您也可以先试想以下,先不捉急看下面。要实现立体效果,其实我们只需要将这个Box所遮蔽的片元设置为蓝色即可,这里就涉及到一个重要的知识,Box所遮蔽的片元如何求取?
在这里插入图片描述

求取方式为:相机到片元(世界坐标)的射线与Box如果有交点,那么该片元就被遮蔽,所以问题转为相机到片元的射线和Box求交,射线和Box求交有很多算法,这里采用AABB的方式,shader代码如下:

//边界框最小值       边界框最大值         
            float2 rayBoxDst(float3 boundsMin, float3 boundsMax, 
                            //世界相机位置      光线方向倒数
                            float3 rayOrigin, float3 invRaydir) 
            {
   
   
                float3 t0 = (boundsMin - rayOrigin) * invRaydir;
                float3 t1 = (boundsMax - rayOrigin) * invRaydir;
                float3 tmin = min(t0, t1);
                float3 tmax = max(t0, t1);

                float dstA = max(max(tmin.x, tmin.y), tmin.z); //进入点
                float dstB = min(tmax.x, min(tmax.y, tmax.z)); //出去点

                float dstToBox = max(0, dstA);
                float dstInsideBox = max(0, dstB - dstToBox);
                return float2(dstToBox, dstInsideBox);
            }

如果distA&&distA<distB则有交点,否则没有交点,加入代码测试

let shader=`
          uniform sampler2D colorTexture;
          uniform sampler2D depthTexture;
          in vec2 v_textureCoordinates;  
          uniform mat4 inverse; 


          vec4 rayBoxDst(vec3 boundsMin, vec3 boundsMax,  vec3 rayOrigin, vec3 invRaydir) 
          {
              vec3 t0 = (boundsMin - rayOrigin) * invRaydir;
              vec3 t1 = (boundsMax - rayOrigin) * invRaydir;
              vec3 tmin = min(t0, t1);
              vec3 tmax = max(t0, t1);

              float dstA = max(max(tmin.x, tmin.y), tmin.z); //进入点
              float dstB = min(tmax.x, min(tmax.y, tmax.z)); //出去点

              float dstToBox = max(0., dstA);
              float dstInsideBox = max(0., dstB - dstToBox);
              return vec4(dstToBox, dstInsideBox,dstA,dstB);
          } 

          void main(){
              out_FragColor = texture(colorTexture, v_textureCoordinates); 
              vec4 rawDepthColor = texture(czm_globeDepthTexture, v_textureCoordinates);
              float depth = czm_unpackDepth(rawDepthColor);
              if (depth == 0.0) {
                depth = 1.0;  
              }  
              vec4 eyeCoordinate4 = czm_windowToEyeCoordinates(gl_FragCoord.xy, depth); 
              vec3 eyeCoordinate3 = eyeCoordinate4.xyz/eyeCoordinate4.w; 
              vec4 worldCoordinate4 = czm_inverseView * vec4(eyeCoordinate3,1.) ;  
              vec3 worldCoordinate = worldCoordinate4.xyz / worldCoordinate4.w;  

              vec4 worldPos= inverse * vec4(worldCoordinate,1.);  
              vec4 cameraPos= inverse * vec4(czm_viewerPositionWC,1.);


              vec3 vDirection=worldPos.xyz-cameraPos.xyz;//方向
              vec3 rayDir = normalize( vDirection ); 

              vec3 dim= vec3(20.,20.,20.);//盒子长宽高
              vec3 box_min = vec3(0.) - dim / 2.;
              vec3 box_max = vec3(0.) + dim / 2.;

              vec4 bounds =rayBoxDst(box_min,box_max,cameraPos.xyz,1.0 / rayDir);
              bounds.x = max( bounds.x, 0.0 );
                  if ( bounds.z > bounds.w ) return; //盒子外   
              out_FragColor=vec4(0.,0.,1.,1.);
            }
    `;

在这里插入图片描述

三、简单的体积云
实现简单的体积云,我们参考这篇博客 体积云渲染实战:ray marching,体积云与体积云光照,这是一篇基于opengl的,为什么选择此示例参考呢?因为该示例一是相对简单,并且流程比较完善,二是涉及到的引擎代码比较少,不像其他示例有很多c#或者c++代码
在这里插入图片描述

1、光线步进创建Box
需要注意他的盒子中心应该就是世界坐标的中心点,所以他直接使用相关坐标进行计算,而我们的盒子中心并不在世界坐标的中心,所以不能直接算,需要像上一节那样建立一个局部坐标系当做世界坐标系。

#define bottom 13   // 云层底部
#define top 20      // 云层顶部
#define width 5     // 云层 xz 坐标范围 [-width, width]

// 获取体积云颜色
vec4 getCloud(vec3 worldPos, vec3 cameraPos) {
   
   
    vec3 direction = normalize(worldPos - cameraPos);   // 视线射线方向
    vec3 step = direction * 0.25;   // 步长
    vec4 colorSum = vec4(0);        // 积累的颜色
    vec3 point = cameraPos;         // 从相机出发开始测试

    // ray marching
    for(int i=0; i<100; i++) {
   
   
        point += step;
        if(bottom>point.y || point.y>top || -width>point.x || point.x>width || -width>point.z || point.z>width) {
   
   
            continue;
        }

        float density = 0.1;
        vec4 color = vec4(0.9, 0.8, 0.7, 1.0) * density;    // 当前点的颜色
        colorSum = colorSum + color * (1.0 - colorSum.a);   // 与累积的颜色混合
    }

    return colorSum;
}

l
<think>我们正在开发一个基于Cesium的水利一张图解决方案。根据用户需求,需要支持以下功能: 1. 多图层可视化展示与资源目录管理(对应二维图层、三维模型的加载显示或隐藏管理) 2. 二维和三维空间分析及量测功能(对应数学计算及图形绘制表达GIS分析结果) 3. 地图场景打印输出PNG或PDF(对应相机视角管理?但打印功能需要额外实现) 参考引用[1]中提到的核心点,我们可以将项目分解为以下几个技术模块: ### 1. 图层管理(资源目录管理) - 实现一个图层树控件,用于控制二维图层(如影像、矢量)和三维模型(如倾斜摄影、BIM)的加载、显示和隐藏。 - 使用Cesium的`DataSourceCollection`、`ImageryLayerCollection`等API管理图层。 ### 2. 相机视角管理 - 提供视点保存和路径漫游功能,使用`Viewer.camera`相关API。 ### 3. POI点的展示与点选查询 - 使用`Entity`或`Billboard`展示兴趣点,并通过`ScreenSpaceEventHandler`实现点选查询。 ### 4. 空间分析与量测 - 二维量测:距离、面积(在二维地图模式下) - 三维量测:空间距离、水平距离、垂直距离、面积、体积(在三维场景下) - 使用Cesium的测量插件或自行实现(如`Cesium.Measure`)。 ### 5. 简易图形绘制及保存 - 提供绘制点、线、面、矩形、圆等工具,并可将绘制的图形保存为GeoJSON等格式。 ### 6. GIS分析功能(高阶) - 如洪水淹没分析、通视分析等,可能需要使用Cesium的地形和几何运算能力。 ### 7. 地图打印输出 - 将当前场景输出为PNG或PDF。注意:Cesium直接截图可以使用`viewer.scene.canvas.toDataURL()`得到PNG,但PDF需要借助第三方库(如jsPDF)将图片转为PDF。 ### 8. Shader应用(可选高阶功能) - 如自定义材质表现水流、水质等。 ## 具实现步骤 ### 1. 搭建基础Cesium环境 ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>水利一张图</title> <script src="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Cesium.js"></script> <link href="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Widgets/widgets.css" rel="stylesheet"> </head> <body> <div id="cesiumContainer"></div> <script> Cesium.Ion.defaultAccessToken = 'your_access_token'; const viewer = new Cesium.Viewer('cesiumContainer', { terrainProvider: Cesium.createWorldTerrain() }); </script> </body> </html> ``` ### 2. 图层管理 - 使用Cesium的`ImageryLayer`加载影像图层(如天地图、ArcGIS等)。 - 使用`Cesium3DTileset`加载三维模型。 - 实现一个UI控件(如使用jQuery或Vue等)来切换图层的显隐。 ### 3. 相机视角管理 - 保存视点:存储当前相机的位置(`viewer.camera.position`)、方向(`viewer.camera.heading/pitch/roll`)等。 - 路径漫游:使用`viewer.camera.flyTo`或`flyAlong`。 ### 4. POI点展示与查询 - 添加点: ```javascript const entity = viewer.entities.add({ name: 'POI点', position: Cesium.Cartesian3.fromDegrees(116.39, 39.9), point: { pixelSize: 10, color: Cesium.Color.RED } }); ``` - 点选查询:监听点击事件,通过`viewer.scene.pick`获取实。 ### 5. 空间量测 - 使用官方提供的测量工具(如`CesiumMeasure`)或自行实现: - 距离测量:监听鼠标点击,计算点与点之间的距离(使用`Cesium.Cartesian3.distance`)。 - 面积测量:使用多边形面积计算(`Cesium.PolygonGeometry`)。 ### 6. 图形绘制 - 使用`Cesium.DrawHelper`或`cesium-draw`等插件。 ### 7. GIS分析 - 例如淹没分析:通过修改地形高度或使用自定义shader模拟水面上升。 ### 8. 地图打印 - 截图:`viewer.scene.canvas.toDataURL('image/png')` - 生成PDF:使用`jsPDF`库将图片添加到PDF中。 ## 注意事项 - 性能优化:对于大量数据,使用3D Tiles规范加载。 - 跨域问题:部署时注意服务端配置。 ## 相关问题 1. 如何优化Cesium加载大量三维模型的性能? 2. 在Cesium中如何实现淹没分析? 3. 如何将Cesium的测量工具集成到项目中? 4. 如何实现自定义的Shader材质表现水流效果?</think>### 基于Cesium的水利水务一张图解决方案设计 #### 一、整架构设计 采用分层架构实现: 1. **数据层**:水利专题数据库(河道、水库、管网等)+ 空间数据库(PostGIS) 2. **服务层**:GeoServer发布WMTS/WMS服务 + Cesium离子平台托管3D模型 3. **应用层**: ```javascript const viewer = new Cesium.Viewer('cesiumContainer', { terrainProvider: Cesium.createWorldTerrain(), baseLayerPicker: false, imageryProvider: new Cesium.UrlTemplateImageryProvider({ url: "http://map.geoq.cn/ArcGIS/rest/services/ChinaOnlineStreetPurplishBlue/MapServer/tile/{z}/{y}/{x}" }) }); ``` #### 二、核心功能实现 ##### 1. 多图层可视化与目录管理 - **图层树控件**: ```javascript const layerManager = new Cesium.LayerManager(viewer, { region: 'water-resources', // 水利专题 baseLayers: [{ id: 'pipe_network', name: '供水管网', type: 'geojson', url: './data/pipes.geojson' }], overlays: [{ id: 'rain_sensors', name: '雨量监测点', type: 'czml', url: './data/sensors.czml' }] }); ``` - **动态图层控制**: ```javascript viewer.scene.layers.addImageryProvider(new Cesium.SingleTileImageryProvider({ url: 'flood_risk.png', rectangle: Cesium.Rectangle.fromDegrees(110, 30, 120, 40) })); ``` ##### 2. 空间分析与量测 - **三维量测工具**: ```javascript const measureTool = new Cesium.Measure(viewer); measureTool.measureLine(); // 距离测量 measureTool.measureArea(); // 面积测量 measureTool.measureVolume(viewer.terrainProvider); // 库容计算 ``` - **洪水淹没分析**: ```javascript const floodAnalysis = new Cesium.FloodAnalysis(viewer, { waterHeight: 50, // 水位高程 color: Cesium.Color.BLUE.withAlpha(0.5) }); ``` ##### 3. 地图打印输出 - **场景导出PNG/PDF**: ```javascript function exportMap(format) { Cesium.screenshot(viewer.scene, { filename: `水利一张图_${new Date().toISOString()}.${format}`, width: 1920, height: 1080 }); } // 使用jsPDF生成带坐标的PDF const pdf = new jsPDF('landscape'); pdf.addImage(canvasData, 'PNG', 10, 10, 280, 150); ``` ##### 4. 水利专题功能 - **水位动态模拟**: ```javascript const waterSurface = viewer.entities.add({ polygon: { hierarchy: reservoirBoundary, material: new Cesium.WaterMaterial({ baseWaterColor: Cesium.Color.AQUA, specularMap: './textures/water_waves.jpg' }), extrudedHeight: new Cesium.CallbackProperty(updateWaterLevel, false) } }); ``` #### 三、性能优化策略 1. **3D Tiles分级加载**: ```json { "asset": { "version": "1.0" }, "geometricError": 128, "root": { "boundingVolume": { "region": [1.24, 0.62, 1.25, 0.63] }, "geometricError": 64, "content": { "uri": "reservoir_L1/tileset.json" } } } ``` 2. **Web Worker并行计算**:将水文分析计算移入Worker线程 3. **视锥裁剪**:启用`viewer.scene.camera.frustumCulling` #### 四、部署建议 1. **微服务架构**: - 地图服务:GeoServer集群 - 三维服务:Cesium Ion + Kubernetes - 业务系统:Spring Boot微服务 2. **安全控制**:OAuth2.0认证 + CORS配置 [^1]: 水利可视化项目的核心在于图层管理、空间分析表达和输出功能实现。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cesium进阶学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值