Cesium中实现全球体积云效果的一种方案

原生 Cesium 提供了一种积云的效果,云的物理特征和渲染性能都还不错,这种方案适合表达小范围相对离散的云朵,但是用来实现全球范围下相对连续、柔和渐变的云层比较困难。本文在体渲染的基础上,参考了开源社区中 shadertoy 和 three.js 关于体积云的一些例子,提出一种在 Cesium 上实现全球体积云效果的方案。

Cesium 上进行体渲染可以参考《Cesium中使用Sampler3D,3D纹理,实现体渲染》。体渲染有相对固定的实现流程,关键在于以三维纹理表达的体数据的构建。由于云没有统一的形状和密度,因此我们在构建体数据时,需要引入噪声来表达这种随机性。本文参考的 shadertoy 效果 用到了perlin 噪声和 worley 噪声的组合,并且在 perlin 噪声和 worley 噪声的基础上都加上了分形布朗运动(Fractal Brownian Motion),将不同振幅(amplitude)和频率(frequency)的多个噪声叠加起来,让噪声有更多细节,使得云层的形态更加自然。

体数据的计算我基本照搬了上面链接中 shadertoy 的代码。计算过程放在 CPU 或者 GPU 上都可以。我最开始是在 CPU 上做的,但是发现实在是太慢了,光计算体数据就要花两三分钟;后来转到 GPU 上,用渲染到纹理的方式,依次把三维纹理每一层(三维纹理可以看成是由一系列二维纹理叠放组成的)的数值绘制到二维纹理上;每绘制一层就把那一层的数据读取(gl.readPixels)到一个 Uint8Array 中,最后合并成一个大的 Uint8Array 用于构建存储体数据的三维纹理,基本流程如下面的代码片段所示。这种方式非常快,几乎感觉不到计算的耗时。这一步创建好的体数据在光线步进(RayMarching)的时候采样。我把计算好的体数据存在了一个 json 文件里,如果有同学想直接拿到一份可用的体积云数据,可以到这里下载

  const slice = 128; // 体数据是一张 128 * 128 * 128的三维纹理
  const data = new Uint8Array(slice * slice * slice * 4);

  for (let i = 0; i < slice; ++i) {
	// 清空上一次绘制的纹理
	renderClearCommand.execute(viewer.scene.frameState.context, viewer.scene.view.passState);
	// 离屏渲染三维纹理的一张二维切片数据
	renderColorCommand.execute(viewer.scene.frameState.context, viewer.scene.view.passState);
	// 读取一层二维切片的像数值
	const pixels = viewer.scene.context.readPixels({ 
	  framebuffer: renderFbo,
	  x: 0,
	  y: 0,
	  width: slice,
	  height: slice
	});
	
	// 存储像数值到体数据的 Uint8Array
	data.set(pixels, i * slice * slice * 4);
  }

 用GPGPU的方式生成体数据

光线步进(RayMarching)一般不会从相机位置就开始迭代,为了提升性能,都是将射线和体渲染的范围几何体求一个近处的交点、一个远处的交点,缩短步进的距离。我们要渲染全球范围的体积云,云层的最小高度和最大高度分别确定了两个球体,射线需要和这两个球体求交。下面代码参考自《Unity URP RayMarching 体积云》,我补充了一些注释方便大家理解,其中 raySphereDst 函数用于计算射线和球体的相交情况,rayCloudLayerDst 函数计算从相机发出的射线和云层最小高度和最大高度分别确定的两个球体的相交情况,从而得到体积云渲染光线步进的起点和终点。

	/*
		计算射线和球体的相交情况

		sphereCenter 球体中心
		sphereRadius 球体半径
		rayOrigin 步进起点
		rayDir 步进方向
		返回值:
			dstToSphere  射线起点到球体的距离
			dstInSphere  射线穿过球体的距离
	*/
	vec2 raySphereDst(vec3 sphereCenter, float sphereRadius, vec3 rayOrigin, vec3 rayDir)
	{
		vec3 oc = rayOrigin - sphereCenter;
		float b = dot(rayDir, oc);
		float c = dot(oc, oc) - sphereRadius * sphereRadius;
		float t = b * b - c; // t > 0有两个交点, = 0 相切, < 0 不相交
		
		float delta = sqrt(max(t, 0.0));
		float dstToSphere = max(-b - delta, 0.0);
		float dstInSphere = max(-b + delta - dstToSphere, 0.0);
		return vec2(dstToSphere, dstInSphere);
	}

	/*
		计算相机发出的射线与云层范围的相交情况

		返回值:
			dstToCloudLayer  到云层的最近距离
			dstInCloudLayer  在云层中穿过的距离
	*/
	vec2 rayCloudLayerDst(vec3 rayOrigin, vec3 rayDir)
	{
		vec3 sphereCenter = vec3(0.0);
		vec2 cloudDstMin = raySphereDst(sphereCenter, (minCloudHeight + earthRadius) / (maxCloudHeight + earthRadius), rayOrigin, rayDir);
		vec2 cloudDstMax = raySphereDst(sphereCenter, 1.0, rayOrigin, rayDir);
		
		float cameraHeight = czm_eyeHeight;

		// 射线到云层的最近距离
		float dstToCloudLayer = 0.0;
		// 射线穿过云层的距离
		float dstInCloudLayer = 0.0;
		
		// 在地表上
		if (cameraHeight <= minCloudHeight)
		{
			vec3 startPos = rayOrigin + rayDir * cloudDstMin.y;
			dstToCloudLayer = cloudDstMin.y;
			dstInCloudLayer = cloudDstMax.y - cloudDstMin.y;
			return vec2(dstToCloudLayer, dstInCloudLayer);
		}
		
		// 在云层内
		if (cameraHeight > minCloudHeight && cameraHeight <= maxCloudHeight)
		{
			dstToCloudLayer = 0.0;
			dstInCloudLayer = cloudDstMin.y > 0.0 ? cloudDstMin.x: cloudDstMax.y;
			return vec2(dstToCloudLayer, dstInCloudLayer);
		}
		
		// 在云层外
		dstToCloudLayer = cloudDstMax.x;
		dstInCloudLayer = cloudDstMin.y > 0.0 ? cloudDstMin.x - cloudDstMax.x: cloudDstMax.y;
		
		return vec2(dstToCloudLayer, dstInCloudLayer);
	}

 计算光线步进的起点和终点(非原创)

上面的步骤分别是体数据的生成和光线步进起止点的计算,体积云的正式渲染可以参考 three.js 官方给出的体积云示例,这个示例也只适合小场景离散的云朵渲染,结合上面的步骤可以在 Cesium 上拓展为全球体积云效果。该示例的实现比较简单,主要工作量是在通过噪声生成体数据以及片元着色器中相关的着色代码。

本文介绍的全球体积云实现方案主要是把现有的一些开源方案做了组合,它不需要依赖外部的噪声纹理,也不用再从片元向光源步进去计算云层的漫反射光颜色,优势是实现步骤简单明了,在仿真要求不是特别高的场景是够用的。最终可以得到如下图1和2所示的体积云效果。

图1 全球视角下的体积云效果

图2 近地面体积云效果 

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cesium是一个用于创建三维地球和宇宙可视化的开源JavaScript库。它可以通过Web浏览器无缝地展示具有高精细度的大规模地理和时空数据。要实现内嵌体积效果,我们可以借助Cesium的功能和一些额外的技术来完成。 首先,我们需要获取体积的数据。体积数据通常是由大量离散点或网格表示的,表示不同位置上的密度或的各种属性。可以将这些数据存储在后端服务器上,并使用网络请求将数据传输到前端。Cesium提供了支持加载和渲染此类体积数据的功能,我们可以使用这些功能来加载数据。 然后,我们需要将加载的数据与Cesium的地球模型进行融合。通过使用Cesium图形接口,我们可以将数据绑定到地球的场景,并使用适当的纹理、材质和光照效果来呈现体积。可以调整的透明度、密度和颜色等属性,以便实现更真实的效果。 此外,为了实现交互性和动态效果,我们可以结合Cesium的动画和用户交互功能。通过使用动画功能,可以使的运动和变化更加流畅自然。例如,可以根据气候数据或预设的模型对的位置、形状和密度进行动态调整。而通过用户交互功能,用户可以自由浏览场景,并与进行交互,例如旋转视角、缩放和漫游。 总结起来,要实现内嵌体积,我们需要使用Cesium加载和渲染数据,将与地球模型融合,并在需要时实现动态和交互效果。这样,我们就可以通过Cesium创建一个逼真的体积效果,提供更加沉浸和交互的三维场景体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值