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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Cesium进阶学习

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

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

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

打赏作者

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

抵扣说明:

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

余额充值