Cesium 体积云实践总结
先上结果图
基础知识学习和参考
实现体积云有三个重要的知识点:自定义primitive、体渲染、perlin+worley 分形噪声混合。
自定义primitive和体渲染
感谢大佬 Bro_Of_Nagi 的文章,关于自定义primitive和体渲染参考此文章,大佬不止给出了思路还有git源码。
由于我使用的cesium版本不便于修改源码,所以使用的是2d纹理存储体数据的方法,使用cesium源码的可以参考大佬的新文章直接修改为使用Texture3D。
源码中比较关键的是这个/texture3D/src/lxs_volumn.js,要理解几个关键部分:
- 体数据生成(205行),其实就是三维数组用一个Uint8Array存储,每个位置都存放一个0-255的数表示噪声大小,通常表示透明度。
- 射线碰撞取样(51行),计算相机到几何box的射线后,采用步进的方式获取每步的体数据进行叠加,从而实现立体效果。
成功应用起来的话,可以得到如文章截图的有很多立体彩色方块的box。
然后参考threejs的体积云源码webgl2_volume_cloud,参考(227行)开始的内容重写关于采样累加计算的部分逻辑。
重写后如下,我将参数都直接写成了数字,这些参数不同得到的效果也不同,可以在threejs的example webgl2_volume_cloud中调节查看。
gl_FragColor = vec4(0.8,0.8,0.8,0.);
for ( float t = bounds.x; t < bounds.y; t += delta ){
float d = getData(p + halfdim);
d = smoothstep( 0.25 - 0.05, 0.25 + 0.05, d ) * 0.1;
float col = shading(p + halfdim) * 3.0 +(( p.x + p.z ) * 0.25 ) + 0.2 ;//threejs与cesium的轴方向不同所以是z
gl_FragColor.rgb += ( 1.0 - gl_FragColor.a ) * d * col;
gl_FragColor.a += ( 1.0 - gl_FragColor.a ) * d;
if ( gl_FragColor.a >= 0.95 ) break;
p+=rayDir*delta;
}
这样修改后,可以看到cesium中渲染出来的和threejs example中的效果是一样的一团云。
perlin+worley 分形噪声混合
看源码中使用的noise方法其实就是threejs源码中用到的perlin噪声算法,这在生成一团云时看起来还行,但是如果要生成一大片的云区还是不太够的。
通过查询资料(参考这篇实时体积云渲染(地平线):二.Perlin噪声和Worley噪声,直接看FBM、Worley噪声、Perlin-Worley噪声),采用 Perlin-Worley噪声得到的云区图还是比较仿真的。开整!
既然是Perlin-Worley噪声,那么Perlin噪声的算法threejs源码中提供了,还需要Worley噪声。在网上搜索了很多资料后找到了一段glsl语法实现的Worley噪声二维算法,我将其改成了三维算法,后续又转换为了js以节省算力,各位可以自行转一下。
// 这个方法里的常量不同得到的结果就不同
vec3 random(vec3 st){
return fract(
sin(
vec3(
dot(st, vec3(127.1,311.7, 51.1)),
dot(st, vec3(269.5,183.3, 23.)),
dot(st, vec3(305.2,250.3, 113.))
)
) * 43758.5453
);
}
float getWorleyNoise(vec3 pos_lxs){
vec3 pos = pos_lxs/(halfdim*2.);
vec3 p = clamp(pos,0.,1. );
p *= 4.;
vec3 i = floor(p); // 获取当前网格索引i
vec3 f = fract(p); // 获取当前片元在网格内的相对位置
float F1 = 1.;
// 遍历当前像素点相邻的9个网格特征点
for (int j = -1; j <= 1; j++) {
for (int k = -1; k <= 1; k++) {
for (int l = -1; l <= 1; l++) {
vec3 neighbor = vec3(float(j), float(k), float(l));
vec3 point = random(i + neighbor);
float d = length(point + neighbor - f);
F1 = min(F1,d);
}
}
}
return F1;
}
获得Worley噪声算法后,修改生成体数据的算法,加入Worley噪声的fbm,同时将原有Texture参数中pixelFormat的alpha通道改为RGBA通道,以便传输新增的噪声数据。
/**
* 生成体数据
*/
const size = 128;
//data在0~255之间
const data = new Uint8Array(size * size * size * 4);//增加到4通道
let dx, dy, dz;
let i = 0;
for (let z = 0; z < size; z++) {
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
/* 实际代码并非如此,本段只是示意 */
dx = x * 1.0 / size;
dy = y * 1.0 / size;
dz = z * 1.0 / size;
const d = noise(dx * 6.5, dy * 6.5, dz * 6.5);
data[i] = d * 128 + 128;//r通道存储Perlin噪声,此处可对Perlin噪声也进行fbm混合,生成更混乱的图案
// 之后3个通道分别存储3个不同波高的worley噪声,在shader里再进行混合。
data[i+1] = 1 - worley.noise(p[0] % 1, p[1] % 1, p[2] % 1)//对1求余避免超界
data[i+2] = 1 - worley.noise(p[0]*2 % 1, p[1]*2 % 1, p[2]*2 % 1)
data[i+3] = 1 - worley.noise(p[0]*4 % 1, p[1]*4 % 1, p[2]*4 % 1)
i++
}
}
}
this.texture=new Texture3D({
width:size,
height:size,
depth:size,
context: context,
flipY: false,
pixelFormat: Cesium.PixelFormat.RGBA,//改成RGBA以传输4个通道
pixelDataType: Cesium.ComponentDatatype.fromTypedArray(
this.data
),
source: {
width: texture_size,
height: texture_size,
arrayBufferView: this.data,
},
sampler: new Cesium.Sampler({
minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
}),
})
之后在shader中使用这个材质时,就可以通过读取不同通道来获取对应的噪声图了。关于混合比例,我采用的是 ( texture.g * 0.625 ) + ( texture.b * 0.25 ) + ( texture.a * 0.125 ); 混合的fbm,再用texture.r减去fbm,并重映射到0-1的范围。
总结
我目前仅做到了一片区域的云区,范围设太大的话性能也不好,看到有其他大佬做了全球云区,根据实际云图生成的,可惜没有透露做法。不过目前这个效果暂时可用,就先这样吧。