体积云渲染实战:ray marching,体积云与体积云光照

本文介绍了使用Ray Marching算法实现体积云渲染的过程,包括通过噪声图生成云朵、处理云朵光照效果以及优化与改进。文章详细阐述了Ray Marching算法在无规则形状求交中的应用,展示了如何使用GLSL着色器进行体积云的渲染,并探讨了云层移动、正确的遮挡关系、可变步长采样等优化技术。最后,文章提供了着色器代码示例,帮助读者理解和实现体积云特效。
摘要由CSDN通过智能技术生成

写在前面

今天来搞了赛艇的特效 ---- 体积云。第一次看见体积云还是在 Minecraft 的光影包里面,好像也是 SE 大大写的。。。当时因为硬件条件(买不起显卡)而没能享受到,今天重新在 OpenGL 中再自己做一次!先上效果图:

注:本篇博客的代码几乎都在 GLSL 中完成,与前面的博客的 c++ 代码无关,可以放心食用!

上一篇博客回顾:OpenGL学习(十一):延迟渲染管线 本来想写 OpenGL学习(十二)的,可是一想体积云都是在 shader 里面写的,和 OpenGL 这套 API 没啥关系了,就改了标题。


ray marching 算法

与一般的实体绘制不同,体积云是一种无中生有的特效。因为体积云不是 cpu 传递三角面片信息给 GPU 而绘制的,相反,体积云是在 shader 中由算法生成的。

注意:体积云不是实体,也没有顶点信息,我们在片元着色器中进行渲染。此外,我们渲染体积云,其实是对云后面的像素颜色做计算

渲染体积云的思路十分简单:在片元着色器中,我们负责对场景的每一个像素进行上色。如果一个像素被云遮挡,那么我们应该把它涂上云的颜色。如图描述了体积云的渲染流程:

在这里插入图片描述

于是问题变为求解视线方向和云朵有无相交。如果有,那么我们绘制上对应的颜色:

在这里插入图片描述

如果云朵是一个三角形,或者是其他规则的几何图形,比如球形,那么我们通过数学几何的方法,就能很好进行求交,可是偏偏云朵是不规则的,无法确定形状的 “体”,我们无法通过几何方法进行求交。ray marching 算法帮助我们解决了不规则体的求交问题。


ray marching 算法又名光线行进,在 之前的博客 中我简单讲过这种算法,并且用它来生成了一个体积光的特效作为大作业。今天来详细讲解。

ray marching 算法从摄像机开始,向世界空间投射光线,并且逐步行进,记录沿途的信息。比如我们沿途不断判断当前点是否在云层中,如果沿途至少有一点在云层中,我们认为视线和云层相交。下图描述了 ray marching 算法的步进过程:

在这里插入图片描述

以上的思路是针对具有明确边界的【固体】进行的,但是云朵通常是用一种没有具体边界的【密度函数】来描述的。密度函数的输入是三维的坐标,输出是当前坐标的云朵的密度。于是每次采样我们累积云朵的密度,就可以知道当前光线穿越了多厚的云。二维下的算法图示如下:

在这里插入图片描述
两条光线穿越厚度不同的云层,于是累积了不同的云密度。我们根据积累的密度,将云层的颜色和背景的颜色进行混合(时刻记得在任何 “体积” 特效中,我们都是针对背景的像素进行着色!),这里需要用到透明混合的技巧。


在 RGBA 色彩空间中,RGB 通道存储了颜色,而 A 通道则是透明度。已知背景的颜色为 bgColor,透明覆盖物的颜色为 cvColor,最终的颜色为 c,那么可以用如下的公式进行透明物体的颜色混合:

c = b g C o l o r ∗ ( 1.0 − c v C o l o r . a ) + c v C o l o r c =bgColor * (1.0 - cvColor.a) + cvColor c=bgColor(1.0cvColor.a)+cvColor

此处 1.0 - color.a 为透明物体的 “不透明度”,比如透明度是 0.4,不透明度就是 0.6 。我们将不透明度乘以背景色,然后叠加透明物体的颜色即可!


至此,我们知晓了 ray marching 的整个流程,下面我们来实现一个简单的 ray marching 以绘制带体积的物体,我们在指定范围内生成一个立方体。因为最基础的 ray marching 需要两个变量:

  1. 当前片元的世界坐标:worldPos
  2. 摄像机在世界空间下的位置:cameraPos

这里 cameraPos 不是眼坐标,不要搞混了。然后我们编写一个函数,执行 ray marching 算法:

#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);   // 与累积的颜色混合
    }

    return colorSum;
}

首先朝视线方向 direction 投射光线,然后沿途记录光线是否在指定的盒子中,如果在,那么我们积累颜色,并且进行颜色混合。注意这里我们的 ray marching 是从摄像机出发,在代公式的时候我们要注意:

  • 当前点的颜色 color,是背景色 bgColor
  • 累积的颜色 colorSum,是覆盖物的颜色 cvColor

于是有:

colorSum = colorSum + color * (1.0 - colorSum.a);   // 与累积的颜色混合

这样的混合公式。不要搞错了。。。

最后我们在片元着色器的 main 中添加如下的调用,其中 fColor 是片元着色器输出的颜色。同样,云的颜色是公式中的 cvColor,而背景色是公式中的 bgColor,于是有:

vec4 cloud = getCloud(worldPos, cameraPos); // 云颜色
fColor.rgb = fColor.rgb*(1.0 - cloud.a) + cloud.rgb;    // 混色

我们可以看到,一个立方体被绘制了出来:

在这里插入图片描述

注意到每次采样,我们都认为当前点的密度为 0.1,然后均匀地叠加颜色,所以我们渲染出来的 “体” 是一个规则的立方体。如果我们随即地改变每次采样的密度,就可以得到形状不规则的云了!


这里还要引入两个小优化。因为我们的云层是在有效范围 [bottom, top] 内才会生成,而测试却从相机原点开始投射光线。假设摄像机在云层下方,那么从相机开始到云层底部这一段路绝对不会有云,我们可以直接 pass。我们将采样原点移动至云层底部即可:

在这里插入图片描述


根据相似三角形法则,我们可以这么挪:

在这里插入图片描述
于是在计算完起始点 point 之后,我们马上执行如下代码:

// 如果相机在云层下,将测试起始点移动到云层底部 bottom
if(point.y<bottom) {
   
    point += direction * (abs(bottom - cameraPos.y) / abs(direction.y));
}
// 如果相机在云层上,将测试起始点移动到云层顶部 top
if(top<point.y) {
   
    point += direction * (abs(cameraPos.y - top) / abs(direction.y));
}

此外,还有一个问题,就是目前的云层会不正确地遮挡本应该遮挡云层的物体,比如树明明在云层之下,却被云层遮挡了。尤其是我们增大云层的范围 width 的时候:

在这里插入图片描述

出现这个问题的原因是我们没有判断当前片元和云层之间的关系。解决方案也很简单,通过距离来判断:

在这里插入图片描述

知道原理就可以进行操作了。在平移采样点之后,我们加入:

// 如果目标像素遮挡了云层则放弃测试
float len1 = length(point - cameraPos);     // 云层到眼距离
float len2 = length(worldPos - cameraPos);  // 目标像素到眼距离
if(len2<len1
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值