Cesium中的体渲染
上篇介绍了Cesium中的BoxGeometry的本地坐标获取方法,获取了本地坐标后,我们就可以开始做体渲染相关的东西了。将相机坐标也换算到模型本地坐标,即可计算得到以相机为起点的到立方体的射线。体渲染相关的内容参看这篇文章,里面说明了ThreeJS中体渲染的相关内容。
先上图
模型本地坐标
这次模型使用自定义的primitive
来实现,直接通过Cesium内置的position
来获取本地坐标,不再通过编码后的变量计算。
相机本地坐标
Cesium中内置的变量中提供相机对于模型的本地坐标,czm_encodedCameraPositionMCHigh
,czm_encodedCameraPositionMCLow
,两个为编码后的相机模型坐标。射线起点即为相机的模型坐标
vOrigin=czm_encodedCameraPositionMCHigh+czm_encodedCameraPositionMCLow;
每条射线朝向
有了模型本地坐标和相机本地坐标,即可计算,相机到每个顶点的朝向。
vDirection=position-vOrigin;
体渲染,正式开始
核心思想和在ThreeJS中实现体渲染是一样的,通过射线计算同射线相交的体数据,这里采用ThreeJS官方例子中的体数据生成的方法,柏林噪声的体渲染例子。在ThreeJS中我们可以通过3D纹理的方式直接提交体数据,然后通过采样函数直接采样3D纹理。
但是在Cesium里面,我目前没有发现可以直接使用sampler3D
方法,查看了Texture.js里面的代码发现没有支持构造3D纹理的函数,仅支持2D纹理和纹理数组。
不支持sampler3D
虽然目前不支持使用smapler3D
,但是,看到常量文件里面已经列出了WebGL2的一些内容,应该在不久之后就可以支持直接使用3D纹理了。
粗略看了一下代码,Cesium是支持通过requestWebgl2
来使用glsl 3.0的。其中modernizeShader.js中实现了从glsl 1.0迁移到3.0的方法。这一部分还没看完,后续看完后,试试支持一下3D纹理。先埋个坑,有时间再填。
既然不支持3D纹理,只能将生成的3D纹理通过2D纹理传入GPU,来进行采样。有如下两个方法:
- 使用纹理数组,将体数据以切片方式传入GPU中,如128 * 128 * 128的体数据,构造128张128 * 128 的纹理传入显卡中
- 将体数据存入单张二维纹理中
以上两种方法,第一种方法,很容易就遇到纹理单元不够用的情况,除非数据量非常小,不然没法使用。考虑使用第二种方法,直接构造一张二维纹理,存储所有体数据。
体数据 --> 2D纹理
三维数据存储到二维中,我选择逐各方式存储到纹理中,即逐切片遍历,将每个切片依次逐行存储。如下伪代码。
for z to size
for y to size
for z to size
data[i++]=volumn[x,y,z]
二维纹理的尺寸通过三维数据的个数开根号来计算得到,保证宽高一样,简化计算。
ceil(sqrt(size * size *size))
数据准备好后,只要在shader中实现采样即可在Cesium中实现体渲染。
代理几何体
代理几何体通过自定义primitive
方式实现。自定义primitive
的内容可以参看[这篇文章](Cesium 高性能扩展之DrawCommand(一):入门 - 知乎 (zhihu.com)),文章也介绍了drawcommand
相关的东西,我这里就不在啰嗦了。
这里重点说一下shader中的采样方法。顶点着色器中没什么好说的了,计算射线起点,射线朝向,就没了。重点看片元着色器。
几何体提供的纹理一定不要开mipmap,我们只使用原始的像素值,因为我们将三维数据放到二维纹理中,GPU对纹理多级缓存后,混合后的像素会和我们预期的效果差很多。因此一定要设置Filter
为NEAREST, LINEAR
这两种,也不要提供miplevel
,防止使用mipmap。
FragmentShader中采样体数据
这里简化计算,代理几何体采用的边长为1,且为轴对齐立方体,同样采用AABB,计算射线进入射出的位置。
迭代步长计算,和采样后着色都使用ThreeJS里面的代码不做修改。
射线相交可得到立方体上某一个点的三维坐标,这里需要通过三维坐标从二维纹理中取到对应的体数据。下面代码中``p为射线相交的立方体上某点,
slice_size`为体数据的一个维度的尺寸。
- 第一步先将[0,1]之间的数据放大到同体数据相同的维度,所以这里乘以
slice_size
,尤其要注意**clamp
**,这个方法不能忽略,对于0附近的值,其真实值可能是各负数,导致计算得到的数值不在[0,slice_size) 范围内,对应的就会出现类似z-filghting一样的现象,同一个平面,有些是0,有些是负数,如下类似雪花一样的结果。所以一定要用clamp
将计算后的坐标限制到[0,slice_size-1]范围内。
vec3 p=clamp(floor(pos*slice_size),0.,slice_size-1.);
- 接下来需要计算三维中这个点对应二维纹理中的第几个像素,直接乘一乘就好了。
float idx=p.x+p.y*slice_size+p.z*slice_size*slice_size;
- 知道了是第几个像素,就可以计算这个像素所在的行列,取模就是一行中的第几个,除一下就是第几行,然后再归一化一下,下面
st
即为二维的纹理坐标。
vec2 st=vec2(0.0);
st.s=floor(mod(idx,lxs_tex_size));
st.t=floor(idx / lxs_tex_size);
st/=(lxs_tex_size-1.);
到这里所有的工作基本就做完了,复用ThreeJS中的采样方法,这里还有个坑,WebGL中循环必须是固定次数的,即循环上限必须是一个常量。这里我固定写了500,然后循环中判断射线是否出了立方体,来结束循环。代码中的normal
为ThreeJS中的着色方法。
//循环最多执行500次
for(float i=0.;i<500.0;i+=1.){
float d=getData(p+0.5);
if(d>0.6){
color.rgb=normal(p+0.5)*0.5+(p*1.5+0.25);
color.a=1.;
break;
}
p+=rayDir*delta;
bounds.x+=delta;
if(bounds.x>bounds.y) break; //当射线出立方体时,结束循环,停止采样
}
到此体渲染就介绍完成了。我们会发现,这里渲染结果和ThreeJS中有很大的不同,不够平滑。因为我们直接采样体数据,相当于找最近的体数据来表达一片范围,导致出现明显的马赛克,实际上做一次三线性插值就会好很多了,这里就懒得写了,后续能用sampler3D
后直接用GPU内置的插值就好了。
控制代理几何体维度
上面代码中使用的代理几何体为单位立方体,使用单位立方体可以简化很多计算。实际的体数据长宽高三个维度需要各自指定。
在fs中增加halfdim
uniform变量通过参数方式传入立方体维度即可。
第一步,修改hitBox
中的box_min
和box_max
分别为最大最小维度。
vec3 box_min = vec3( -halfdim );
vec3 box_max = vec3( halfdim );
第二步,main
方法中在开始步进计算射线相交的体素时,对立方体相交位置p
加了0.5,用来将坐标计算到大于0。这里要加一半的维度。
float d = getData(p+halfdim);
第三步,在getData
中为了简化取体素的计算,还是将p
归一化。
vec3 pos = pox_lxs/(halfdim*2.);
最后,在自定义primitive
中的uniformmap
中增加对应的halfdim
即可。
代码库
本文转自 https://blog.csdn.net/yue1241630499/article/details/123562142,如有侵权,请联系删除。