Cesium中的体渲染

上篇介绍了Cesium中的BoxGeometry的本地坐标获取方法,获取了本地坐标后,我们就可以开始做体渲染相关的东西了。将相机坐标也换算到模型本地坐标,即可计算得到以相机为起点的到立方体的射线。体渲染相关的内容参看这篇文章,里面说明了ThreeJS中体渲染的相关内容。

先上图

在这里插入图片描述

模型本地坐标

Cesium中的BoxGeometry渲染流程,及模型本地坐标

这次模型使用自定义的primitive来实现,直接通过Cesium内置的position来获取本地坐标,不再通过编码后的变量计算。

相机本地坐标

Cesium中内置的变量中提供相机对于模型的本地坐标,czm_encodedCameraPositionMCHighczm_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对纹理多级缓存后,混合后的像素会和我们预期的效果差很多。因此一定要设置FilterNEAREST, 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_minbox_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即可。

在这里插入图片描述

代码库

代码库地址

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐