程序化天空盒过程记录01:日月 天空渐变 大气散射

Posted 九九345

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序化天空盒过程记录01:日月 天空渐变 大气散射相关的知识,希望对你有一定的参考价值。

1 日月 SunAndMoon

昼夜的话肯定少不了太阳和月亮,太阳和月亮实现的道理是一样的,只不过是月亮比太阳多了一个需要控制月牙程度(or添加贴图)的细节~

1.1 Sun

太阳的话很简单,直接在shader里实现一个太阳跟随平行光旋转而旋转的样子就行。实现这个效果需要用到Unity内置变量_WorldSpaceLightPos0获取当前平行光的方向,不要被这个参数名字“lightPos”迷惑了,它实际上就是一个归一化的vector(w=0)。接着用Unity内置的distance函数计算当前uv坐标(i.uv.xyz)到上面那个的距离。

如何理解这个“距离”呢?——我们再来复习一遍图形学基础吧:学习齐次坐标时讲到“点与向量的区别”时,例如vector(a, b, c, 0)和point(a, b, c, 1),我们可以把vector看作这个point挪向无穷远处位置。行,那么我们再来看看这个distance计算出来的结果的几何意义是不是就十分简单了——uv坐标越靠近,代表离这个无穷远处的点越近,于是结果越小。

  • 我们需要的是一个实心圆(模拟“日月”),如果只将distance的结果与_SunColor相乘并作为片元着色器的结果输出,主要代码及效果如下:
float sun = distance(i.uv.xyz, _WorldSpaceLightPos0);
...
fixed4 col = sun * _SunColor;

  • 场景中越靠近远处的“太阳”位置越黑,想要实现一个实心圆就很简单了,补充后代码及效果如下:
float sun = distance(i.uv.xyz, _WorldSpaceLightPos0);
float sunDis = 1 - sun / _sunRadius;
...
fixed4 col = sunDis * _SunColor;

 为了实现控制最大化,加入了_sunRadius参数以控制“太阳”的大小,选择除法的原因也很简单——太阳越大意味着白色部分越大,意味着disatance()函数计算结果的权重更加分散,所以是除法。

  • 到了这一步你会发现,上图还是缺点什么!

【第一】边界有些模糊,我们想实现的效果是轮廓明显的“日月”,这是因为边界的sunDis数值不够大,导致_sunColor相乘混合的颜色“淡”。如何处理?——简单,直接乘上个合适的倍数就行!

【第二】原有的网格不见了,给人一种黑色“笼罩”整个天空盒的错觉。看到这里你似乎会觉得疑惑:“网格不见了”啥意思?我来举个例子,下面是我将sunDis值分别设置为1,0,-1的效果:

2和3看似都是黑色,其实还是有差别的,他会掩盖网格。而且这个sunDis系数最后是跟类型为Color的_SunColor变量相乘,参考光照计算模型,这里理应将该系数也限制在(0, 1),这里用saturate()就行!

考虑了以上两点后最终的代码:

float sun = distance(i.uv.xyz, _WorldSpaceLightPos0);
float sunDis = saturate((1 - sun / _sunRadius) * 50);
...
fixed4 col = sunDis * _SunColor;

最终效果:

1.2 Moon 

太阳做完了,到了月亮部分,它俩一个在光的正方向(太阳)一个在反方向(月亮),这个不难理解吧!所以,关于跟随平行光方向的部分,设置跟Sun一样,只是多了一个负号。

这里我计划实现两种月亮的方法,一种是贴上一个真实的月亮图片,另一个是做一个简单的可以控制的月牙形状。

第一种:月牙

另外,由于月亮是有月牙的~就用两个圆相减的形式做出月牙的效果,相减效果采用给uv.x一个偏移值_CrescentOffset的方法实现的。同样,这里也需要注意只要涉及相减的需要给数值规范到(0, 1)才能确保效果的正确。

下面是月亮部分的代码:

//2.Add Moon
float moon = distance(i.uv.xyz, -_WorldSpaceLightPos0);
float moonDis = saturate((1 - moon / _MoonRadius) * 50);
float crescent = distance(float3(i.uv.x + _CrescentOffset, i.uv.yz), -_WorldSpaceLightPos0);
float crescentDis = saturate((1 - crescent / _MoonRadius) * 50);
moonDis = saturate(moonDis - crescentDis);
fixed4 moonN = moonDis * _MoonColor;

最终效果:

第二种:月亮贴图

贴图主要问题是解决UV坐标系变换问题,因为如果继续用原始的改变光照方向后月亮贴图会变形的问题,我们希望每个角度贴图形状都是圆圆的(参考日常生活中每个角度的月亮都是圆圆的),那么我们就需要一个建立一个4x4的坐标系变换矩阵去实现。

问题又来了:Unity自带的变换矩阵unity_WorldToLight对于平行光是行不通的,只适用于point/spot/烘焙光,所以这里我们需要自己脚本实现变换矩阵。这里我们不难想到shadowmap里也需要求得光源空间的变换矩阵,可以参考我记录的GAMES202作业1CalcLightMVP()的实现过程去写出这个变换矩阵,还可以跟着这一篇用Unity实现Shadow Map的博客实现这个变换矩阵,道理都是相通的。

更方便的还可以用Unity自带的transform的worldToLocalMatrix获得当前对象的世界空间到local空间的变换矩阵,给场景中的平行光一个子Camera再获取一下这个变换矩阵就行(后面会补充完整脚本内容和shader部分的内容,这里就先放一个效果)。

效果如下,加上了一点后面会做的渐变天空效果:

2 天空主体色 Gradient Sky

1.0版本

完成了日月交替的部分只能说才实现了一小部分,而且天空很单调,为了实现更加酷炫的效果,加点渐变天空颜色。说起渐变,要用上lerp()了!分别给白天天空和晚上天空赋予颜色——基于UV坐标的y 轴值来做天空颜色的渐变,记住还是需要saturate()!最后根据uv坐标(i.uv.y)判断何时昼夜交替。

// 3.gradient sky
fixed3 gradientDay = lerp(_DayBottomColor, _DayTopColor, saturate(i.uv.y));
fixed3 gradientNight = lerp(_NightBottomColor, _NightTopColor, saturate(i.uv.y));
fixed3 gradientSky = lerp(gradientNight, gradientDay, saturate(_WorldSpaceLightPos0.y));

2.0版本

上面的渐变天空简单的lerp做的,很枯燥。参考程序化天空盒实现昼夜变换,我们也来这位大佬的构思思路,做一次分析(毕竟是作品集,所有过程自己实现一遍最好啦!),也天空变化归纳了出来:

参考他提出的思路,地平线渐变用worldPos.y控制,但天空的主体颜色用光方的z和y共同控制,关于天空主体色我的理解:

(纠正一下,感觉早晨不是supper blue而是一种偏绿色的蓝色?颜色可以自行调整的)

即从早晨开始,早晨(非常蓝)-->中午(正常蓝)-->傍晚(紫色)-->深夜(深蓝色),其中,y控制着白天和晚上,z控制白天三种颜色变换、晚上三种颜色交替

白天:

上述思路对应的理解代码为,

// 早午过渡
col = lerp(morningCol, noonCol, smoothstep(0,0.5,z));
// 再把上面的当作午的,进行午傍晚过渡
lerp(col, nightfallCol, smoothstep(0.5,1,z));

同理晚上:

// 早晚过渡
col = lerp(morningCol, nightCol, smoothstep(0,0.5,z));
// 再把上面的当作晚的,进行晚和傍晚的过渡
lerp(col, nightfallCol, smoothstep(0.5,1,z));

再优化一下:我想要清晨和傍晚的颜色持续的少一点,白天的天蓝色维持的久一点,这样就需要更改参数,索性直接多给两个参数,来调节一天内清晨和傍晚的持续时间,最终关于天空颜色早晚变化的参数如下:

3 地平线渐变

跟参考文章不太一样的是,我把渐变天空分为两个部分:半天空渐变色+范围变化的Bloom,虽然这样比较麻烦(叠加了三层orz),但原神有些画面它就是三种颜色叠加的效果,例如下图傍晚的天空:

原神的傍晚天空

索性直接拆成三块做。 

3.1 半天空渐变

半天空渐变色有4种颜色,4个颜色变化的时间刚好跟天空主题渐变色对应,

1 晚上:深蓝色+淡色地平线

早晨:浅蓝色+淡色地平线

中午:天蓝色+浅蓝地平线

傍晚:紫色+淡黄色地平线

由于这个是跟主色叠加,是不是应该在天空主体渐变的基础上修改,达到天空主色渐变的感觉:

好,说做就做,以早晨为例,还需要能控制渐变程度等,最后的参数如下:

整个天空颜色很接近了(斗胆),现在就差地平线附近的Bloom。

3.2 范围变化的Bloom

(做了一下午了,起来活动一下继续,orz,,)

再继续观察:

早->午:红色(小)-->黄色(大)-->白色(小)

傍晚->晚上:黄色(大)-->橙黄色(超大)-->黄色(小)

(补充:既然节点4Mie散射开始,那我们把大部分白色的Bloom都交给Mie散射,假设整个发光到节点4就宣告结束。) 

【空缺了一部分思路,这里做得比较着急没有记录,放个对比图吧之后补充】

日出日落配上天空颜色变化:

emmm配色什么的感觉挺脏的,然后bloom那里需要给参数调整的,做的时候随便选的,先继续进行下面的步骤,最后再优化。 

接下来是加上云、和星空银河,放在下一篇文章吧。

我上面实现渐变天空选择的颜色还是照抄参考里的配色。但是!我认为一个优秀的天空盒,配色一定是要有讲究的!配色的实现方法打算参考这篇文章里提到的将配色数据做成数组形式,像动画关键帧那样呈现出来的方法。

3 大气散射

做的时候没有记录过程,作品搞完后会回来补上。

参考

这里是我搜刮遍了各种网站找到的实现动态天空盒的文章,自己在做的过程中也参考了很多,这里罗列出来希望对看到这篇文章的你有所帮助:

风格化的动态天空球 – WalkingFat

Unity日夜循环天空球(Procedural Skybox) - 知乎

Making a Stylized Skybox Shader

Unity 卡通渲染 程序化天空盒 - 知乎

【unity URP】昼夜循环天空球 - 知乎

程序化天空盒实现昼夜变换 - 知乎 (zhihu.com)

Unity天空盒卡通渲染中如何实现云的消散效果

写在前面

完成大气渲染之后,接下来就是考虑云渲染了。因为我想做的天空盒本身是想跟着这位大佬Unity 卡通渲染 程序化天空盒 - 知乎里叙述的进程来的,里面云实现的是原神里的云,原神又是在崩3的基础上加上了消散效果。但现在能找到的一些教程or展示的视频里,很多天空盒的云都是通过贴图+noise map实现的,如何实现类似原神那种云伴随着太阳光的消散效果少有涉及,因此打算写一篇文章简单的记录一下学习过程,然后亲自实现一下~

1 原神的消散云

首先是原神里的消散的云效果,在玩的时候蛮好看的,既然原神是在崩坏3的基础上加上了消散效果,那先来看看崩坏3是如何实现云的:

1.1 崩坏3如何实现云

指路崩坏3的技术分享From mobile to high-end PC: Achieving high quality anime style rendering on Unity

跳过前面的(有时间可以看完!收获很大!!1),直接重点看这一部分:

重点:多层着色

为了让玩家感受到纵深、具有各种丰富形态以及动态光照变化的云渲染系统,游戏中实现了24小时动态变化的云,但并没有直接储存庞大数量的贴图,而是选择多层着色实现这个目的。

使用了4个通道来表示云的光照及阴影,如下图所示,从左到右依次为:基础照明、阴影1、阴影2和边缘光层。

1.2 原神的云贴图

上面提到的文章的做法:

以及评论区有人说:

那么我们拿到原神的云贴图,看看它每个通道是不是这样的。

RGBA

R

嗯,阴影,Shadow Layer。

G

G是边缘光,对应上面崩坏3里的Rim Layer。

B

B通道就是上面评论区提到的SDF!

可喜可贺,我们知道了云贴图每个通道对应的内容是什么,那接下来就是如何拿着这个帖图去实现效果了。

补充:SD中拆分

2 别人是怎么做的

2.1 两个案例

搜刮遍了只能找到两位大佬做出来我想要的效果:

Unity NPR 原神Cloud,Sky,Shader

Unity 卡通渲染 程序化天空盒 昼夜变化

2.2 简单分析

看看会发现,两位大佬实现的效果都有一种伴随着太阳光消散的感觉。第一位大佬没有具体说明该怎么做这个云,但是这个时候又要善于看评论区了!

!解决了,这个生长数据(灰度图),就是上面展示的贴图里通道B的“灰度图”。  

再看看第二位大佬的方法,很开心,知乎文章Unity 卡通渲染 程序化天空盒里最后一部分介绍了云如何实现的,思路跟第一位大佬几乎一致!

接下来就是跟着文章里面介绍的实现思路来复刻一遍。

3 学习安排

3.1 获得灰度图

好的,现在已知方法,就差如何实践了,手上已经有了现成的云贴图,那么制作SDF灰度图的过程可以跳过:

与之对应的就是这张图:

PS查看阈值

那么我们模仿教程里的进度,也查看查看SDF图。

3.2 学习SDF【已】

虽然我们的SDF图不需要自己去做了,但是!SDF与卡通渲染一定是紧密相关的,要掌握!

图形学基础|基于SDF的卡通阴影图_桑来93的博客-CSDN博客

卡通渲染之基于SDF生成面部阴影贴图的效果实现(URP) - 知乎 (zhihu.com)

 学习记录【Unity云消散】理论基础:实现SDF的8SSEDT算法_九九345的博客-CSDN博客

3.3 Blender做云面片模型

对应教程中的这一步:

打算趁机再巩固一下UV映射(本身是美术苦手TAT刚好学习一波!)

Blender 2.8 UV 映射 Blender 2.8 UV Mapping

3.4 卡通渲染中的边缘光

在之前的《入门精要》学习中,14.1就已经介绍过了NPR,由于这次想实现的天空盒其实就是卡通渲染的天空盒,所以这里再趁机进行巩固!结合下面这篇不错的文章进行边缘光的实现的学习。

【Cel-Shading】边缘光的实现 | Invictus maneo (x-wflo.github.io)

3.5 跟教程写shader

有了上面Blender获得的cloudTex和理论知识储备后,就可以开始参考教程中的代码进行实现了!


总结一下,通过这次云的实现我可以:复习Blender展UV、学习NPR边缘光等、学习SDF等,接下来就是学习+实现! 

以上是关于程序化天空盒过程记录01:日月 天空渐变 大气散射的主要内容,如果未能解决你的问题,请参考以下文章

Skybox 渲染的 VkRenderPass 加载操作问题

大气散射 GPU Gems2 Chapter 16. Accurate Atmospheric Scattering

unity怎么不让天空盒子的太阳反射到地面太难看

DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现

OpenGL:天空盒放大太多

Unity Shader用Cubemap实现天空盒和环境映射