体积云渲染实战:ray marching,体积云与体积云光照 您所在的位置:网站首页 苹果电脑qq远程控制在哪 体积云渲染实战:ray marching,体积云与体积云光照

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

2023-11-04 08:41| 来源: 网络整理| 查看: 265

目录 写在前面 ray marching 算法 通过噪声图生成云朵 云朵光照效果的绘制 优化与改进 着色器代码 总结

写在前面

今天来搞了赛艇的特效 ---- 体积云。第一次看见体积云还是在 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.0−cvColor.a)+cvColor

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

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

当前片元的世界坐标:worldPos 摄像机在世界空间下的位置: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 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 point += direction * (abs(cameraPos.y - top) / abs(direction.y)); }

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

在这里插入图片描述

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

在这里插入图片描述

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

// 如果目标像素遮挡了云层则放弃测试 float len1 = length(point - cameraPos


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有