RhinoFreak 发表于 2023-2-6 17:03

程序化天空盒实现昼夜变换

一、资料收集与分析

1.昼夜变换

Unity Shader 基于光照图的简易昼夜变化
Unity日夜循环天空球(Procedural Skybox)
【unity URP】昼夜循环天空球
Unity 卡通渲染 程序化天空盒
Unity 卡通渲染 程序化天空盒 昼夜变化
Making a Stylized Skybox Shader
Skybox tutorial part 1
【原神】好美,原神一天中天空的变化
【原神·延时摄影】孤云阁
【原神】3.0之前记得来海岛看银河|星空延时摄影
2.原神







银河会动,颜色在变,星星在闪,
白天云移动很快,不断消失出现,晚上云移速慢
流星,可以考虑
云边缘的蓝光


天渐红



很强的bloom


渐变白,太阳很亮




高处有较透明的云在反向流动




没那么亮了


变灰变红




最红的时候


蓝下来


月亮




3.天气系统

Unity手游开发札记——移动平台的天气系统实现
通用天气系统调研及手游方案推荐
游戏中雨天效果开发:如何打造“最美下雨天”
二、实现

0.前期准备

一如既往地创建一个普通shader,但是千千万万不要加这句lightmode=前向渲染。


subshader tag设置。previewType可以让材质在预览时显示为天空盒。




我不喜欢在摄像机下加天空盒组件,直接加到lighting设置里的。也方便shader里直接调用天空盒变量。是不该叫常量?我只知道uniform是传进传出过程中不变的,unity_SpecCube0算不算。


测试:
return uv.y


return uv.x


return uv.z


1.太阳

为什么神奇的操作后会出现跟随光源的小圆片呢,把他们看成世界空间中心出发的两个单位向量,当他们重合时,距离为0,显示黑色。越远越白。
half sunDist = distance(v.uv.xyz, light.direction);

half sunArea = 1 - (sunDist * _SunRadius);上图可见渐变到白的部分还是很大的,因此乘以一个缩放系数,再取反。这里是椭圆因为我开了正交视角。。透视下就是个圆。


half sunArea = 1 - smoothstep(0.7, 1, sunDist * _SunRadius);哎,我这个控制狂魔都懒得加边界参数了,再说吧后面。


2.月亮

我也遇到天空盒uv不行,导致月亮贴图offset改变一下就很诡异的情况。
half2 moonUV = TRANSFORM_TEX(v.uv.xz, _MoonTex);
half3 moon = SAMPLE_TEXTURE2D(_MoonTex, sampler_MoonTex, moonUV) * _DiffColor;

开启google。
参考:Skybox tutorial part 1
此文章中,月亮贴图使用nasa发布的全景图作为cube map。我看到人都傻了,但是又觉得合理,毕竟我们的天空盒uv有三个坐标,寻常的两个坐标满足不了对月亮的采样了。
下载链接中的moon cube map,更改贴图为cube map。


抄一个三手函数。
// From Inigo Quilez, https://www.iquilezles.org/www/articles/intersectors/intersectors.htm
float sphIntersect(float3 rayDir, float3 spherePos, float radius)
{
    float3 oc = -spherePos;
    float b = dot(oc, rayDir);
    float c = dot(oc, oc) - radius * radius;
    float h = b * b - c;
    if(h < 0.0) return -1.0;
    h = sqrt(h);
    return -b - h;
}参数:射线方向(视角方向) ,球体的位置(月球方向),半径(月球半径)。
输出:从光线原点(我们的摄像机)到球面交点的距离,如果没有交点,它返回 -1。然后检查交集是否大于 -1来创建一个mask。
在properties定义:

      //_MoonTex("Moon Texture", 2D) = "white"{}
       _MoonCubeMap ("Moon cube map", Cube) = "black" {}
      _MoonRadius ("Moon Radius", Range(0, 1)) = 0.05
      _MoonMaskRadius("Moon Mask Radius", range(1, 25)) = 10
      _MoonSmooth("Moon Smooth", Range(0, 1)) = 0.7
      _MoonExposure("Moon Exposure", range(0, 1)) = 1文中viewDir就是WorldPos,即天空盒顶点的世界空间坐标。一开始很不理解,后来想想有道理。所以我名字也不改了,就叫posWS。提醒,一定要归一化,否则死得很惨。
接下来文中通过脚本传递月亮这个空物体的位置,这边我月亮的位置直接设定为太阳的反方向,即灯光方向取反。
调用上面的函数,用这个函数返回值计算法线。这边我是看不懂,但是好用!
直接用注释的那一句作为moonMask,月亮边界会很清楚,怎么smoothstep都没用(所以那函数返回的到底是什么玩意啊!!)


于是我用回了太阳的算法,添加一堆控制参数。
用法线采样cubeMap,moonTex是这样的:


所以需要mask把月亮圈出来。经过调参,他变成了这样:


完美符合心目中的月亮!
float3 posWS = normalize(v.posWS);
float3 lDir = normalize(-light.direction);
half moonIntersect = sphIntersect(posWS, lDir, _MoonRadius);
// half moonMask = moonIntersect > 0 ? 1 : 0;
half moonDist = distance(v.uv.xyz, -light.direction);
half moonMask = 1 - smoothstep(_MoonSmooth, 1, moonDist * _MoonMaskRadius);

half3 moonNormal = normalize(lDir - posWS * moonIntersect);
half3 moonTex = SAMPLE_TEXTURECUBE(_MoonCubeMap, sampler_MoonCubeMap, moonNormal).rgb;
half3 moonColor = moonMask * exp2(_MoonExposure) * moonTex;但是需要一些修正,因为随着灯光旋转,它自己转的太快了。。




我猜啊,我猜这个物体空间的位置是moon gameObj的位置,具体不太懂,我用TransformWorldToObjDir函数对法线进行变换,没效果。那算了吧,转就转。。


我的水好像是透明的。。。所以下面的太阳月球都会透出来,但这其实不是问题,谁家海洋下面没地形??哦我家。


3.天空渐变色

来喽,我真的对着这几个玩意思考半天。
别的lerp、ramp图做法,好像太阳升起落下,周围一个颜色。观察了前面的原神天空,发现可以分成两部分,地平线附近由worldPos.y控制,整体天空颜色为了有傍晚紫色、清晨“super blue”色,用LDir.z与y控制。
测试:
将lightDir.y从-1到1映射到0到1,然后白天输出1,晚上输出0.


效果很好,就是费眼。
对lDir的y与z分析,通过这两个值控制早晨与傍晚的不同颜色。




测试:
分别输出上半天空与下半天空:


下半:


上半:


相加,改成step吧。


lerp里可以调的细节很多。就不放了。
接下来做地平线的颜色。用worldPos.y插值天空颜色与地平线颜色,再整点函数控制worldPos.y的高度。
观察原神天空,见分析图。


可见上半球(白天)渐变高度较为恒定,在晚霞,橙色升起时显著变高。
同样使用z分量区分白天傍晚,设定这样一条函数:


写成代码是这样的


白天天空颜色代码:


下半球(夜晚)复杂了一点,因为朝霞刚开始高度很低,所以要做出这个凹进去的曲线。


具体参数自己调,因为要配合颜色做升降。调至0到0.202间是一条完整的下降上升曲线,将左右归零,加到原有函数上。
在desmos制作曲线:


夜晚天空颜色代码:


夜晚地平线渐变效果展示:


修改了亿点点后,skyGradient加起来展示:


没有处理地平线以下的部分。
4.太阳月亮Bloom

去看了一下pbr的大气散射,人傻了。我还没有进pbr的思想准备,下,下次一定。
使用那篇文章里的方法,用lDir点乘posWS,实现在太阳周围的很大一个圈。
half sunBloomMask = pow(saturate(dot(light.direction, posWS)),4);
half3 sunBloomColor = lerp(_sunBloomC1, skyColor, lDir.y) * sunBloomMask;

酱紫,坠出超的,的sunBloom就做好辣!【心虚
<hr/>实地观察了原神的日出日落天空颜色,发现太阳近地平线的时候,另一半天空是别。。的。。颜色。。
又观察一次,发现整体天色不变,但是太阳附近的bloom很强很强,显得一半的天变色了。跟上面分析图里的一九年测试版本有区别。最明显的就是傍晚地平线一圈橙色不见了,直接是蓝的,但太阳周围又红又黄,很漂亮。【凌华真好看,嘿嘿嘿,凌华






那我就不改了,后面脚本里调整不同太阳高度时不同光源颜色进行插值,同时作为sunBloom的颜色。
当bloomColor为红色


月亮辉光的话,在月亮那块下面加一句
half moonBloomMask = (1 - smoothstep(0, 1, moonDist * 2)) * smoothstep(0.4, 1, lDir.y);后面加的话要注意lDir是月亮的方向,不然就在下半截出不来了。
在有了skyColor后,乘以这个颜色。再乘个系数加亮。
此外月亮颜色额外加点,实现这种感觉:


moonColor *= skyColor * lerp(1, 15, smoothstep(0.1, 1, posWS.y));//0.5,0.7,1; 0.7,1,15
half3 moonBloomColor = moonBloomMask * skyColor * 3.5;单独输出:
合并:【gif太大,删了一半。】
![](https://z4a.net/images/2023/02/03/shi.gif)
5.镜头光晕(Lens Flares)





URP12以前与内置管线,有自己的lens flares,URP12后的后面跟个括号SRP。我还在用7.7哪


太阳周围的shimmer会随视角旋转。
Flare


巧了我搞到这么张图


在我的2019.4.38的urp和build in都试了,完全没看到效果,不就是加三件套吗,不就是这个流程吗,为什么我!没!有!
虽然看到了两个大佬在低版本手动实现lens flares,但是不想在这上面花脑子,所以,没错,我换unity版本去了。。


真的奇怪,人家2019还能看一看flare,我怎么调都没有


那就彻底抛弃2019吧,虽然转高版本有点报错和警告,但是没有大问题。




具体去看别的教程,我负责调参数。。
URP系列教程-手把手教你在URP中实现镜头光晕效果
在ps中将图集裁成五份


这pa结束,谢谢【鞠躬】,寄了寄了。


原神整体bloom都特别糊。我不会改,诶,不会。
6.高天旋转云彩

实现这个:




是这张帖图。


一通调整uv,贴了上去。图需要调整v方向,确保留两条黑边
half2 highCloudUV = TRANSFORM_TEX(v.uv.xy, _HighCloudTex);
//highCloudUV.x = v.uv.z > 0 ? highCloudUV.x : 1;
highCloudUV.y = smoothstep(0.4, 0.8, highCloudUV.y);
half highCloudTex = SAMPLE_TEXTURE2D(_HighCloudTex, sampler_HighCloudTex, highCloudUV).a;
highCloudTex = smoothstep(0.004, 1, highCloudTex) * abs(v.uv.z);0.004是因为云里的黑不是0,而是1,1/255约0.004.这么大点值采出来发灰,就离谱er
然后由于uv x轴1和-1贴得太近,那地方拉老长一条


用另一方向:uv.z做遮罩。当然范围有点大,本来很白的变灰了,但本来也不需要他多亮


加入uv.x动画
half2 highCloudUV = TRANSFORM_TEX(v.uv.xy, _HighCloudTex);
highCloudUV.x = v.uv.z < 0 ? 1-highCloudUV.x : highCloudUV.x;
highCloudUV.x += _Time / 2;
highCloudUV.y = smoothstep(0.4, 0.8, highCloudUV.y);
half highCloudTex = SAMPLE_TEXTURE2D(_HighCloudTex, sampler_HighCloudTex, highCloudUV).a;
highCloudTex = smoothstep(0.004, 1, highCloudTex) * abs(v.uv.z);首先改了一下1和-1那个边界,让四个象限(原谅我这么称呼)都从0到1,就是首尾相接,这样z近0的地方就会产生断层,而我们上面遮过了丑所以没问题。
为什么要这么改,因为x直接加时间就能顺利的团团转而不是在z为0的地方被吞进去,那样很奇怪。
效果展示:
shiyi.gif
![](https://z4a.net/images/2023/02/03/shiyi.gif)
接缝处拉伸有点多,但是没事。
7.银河与星星

终于知道这频繁出现的一张图是干啥的了……
shier.gif


在阈值图里选择这样一张:


然后是无限smoothstep和采样贴图,修改贴图,先有鸡才有蛋,顺序该咋办,银河不会画,不够透,涂抹笔刷救了狗命勉强能看,写得又臭又长,累了不讲解。
//////////////////////////////////
                  ////      Galaxy Star       ////
                //////////////////////////////////
                half2 noiseUV = TRANSFORM_TEX(v.uv.xz, _NoiseTex);
                half2 galaxyNoiseUV = noiseUV + _Time.y / 18;
                half2 starNoiseUV = noiseUV + _Time.y / 40;
                half galaxyNoiseTex = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, galaxyNoiseUV).r;
                half starNoiseTex = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, starNoiseUV * 5).r;

                half2 galaxyUV = TRANSFORM_TEX(v.uv.xz, _GalaxyTex);
                galaxyUV.x = (galaxyUV.x + 1) / 2; //map to 0-1
                galaxyUV.x = smoothstep(0, 1, galaxyUV.x) ;
                galaxyUV.y = (galaxyUV.y + 1) / 2;
                galaxyUV.y = smoothstep(0, 1, galaxyUV.y);
                half4 galaxyTex = SAMPLE_TEXTURE2D(_GalaxyTex, sampler_GalaxyTex, galaxyUV + galaxyNoiseTex / _NoiseIntens);
                lDir = (-light.direction + 1) * 0.5;
                galaxyTex *= smoothstep(0.6, 1, lDir.y);
                skyColor += galaxyTex.rgb;

                half2 starUV = TRANSFORM_TEX(v.uv.xz, _StarTex);
                starUV.x = (starUV.x + 1) / 2; //map to 0-1
                starUV.x = smoothstep(0, 1, starUV.x);
                starUV.y = (starUV.y + 1) / 2;
                starUV.y = smoothstep(0, 1, starUV.y);
                half starTex = SAMPLE_TEXTURE2D(_StarTex, sampler_StarTex, starUV * 4).r;
                half starTex2 = SAMPLE_TEXTURE2D(_StarTex, sampler_StarTex, half2(starUV.y, starUV.x) * 3).r;
                half starTex3 = SAMPLE_TEXTURE2D(_StarTex, sampler_StarTex, (starUV+6) * 5).r;
                starTex = starTex > 0.78? 1 * starNoiseTex * 3: 0;
                starTex2 = starTex2 > 0.9? 1 : 0;
                starTex3 = starTex3 > 0.95? 1 : 0;
                starTex += starTex2 + starTex3;
                starTex *= galaxyTex.a;
                half3 starColor = lerp(starTex.xxx, skyColor, 0.5) * smoothstep(0.3, 1, posWS.y) * smoothstep(0.6, 1, lDir.y);
                skyColor += starColor;效果:
![](https://z4a.net/images/2023/02/03/shisan.gif)

8.面片云

首先手上有三张云图


观察发现地平线附近的云用一种,地平线附近第二层在后面逆着转的云用一种,高处的云再用一种。因此合成三个fbx,用三个材质控制。因为很多属性相同,可以去脚本传递属性。
blender中制作:


做完感觉一片太大了,而且合并后才改uv,后期要是重做那就真的重做了。做完才想起来可以一片片先分好uv再组合。blender里是这样的。【我甚至不会把a通道设为透明度。连连看是一窍不通啊】


我们的云贴图的rgb通道分别是自阴影,边缘光,SDF消散图。a通道做透明度。


浅看一下SDF效果:


闲话休说,直接开始shader。
首先设置队列为transparent,然后设置blend,深度写入关闭。
边缘光与漫反射色:


传入亮区与暗区的颜色,根据自阴影图插值。这样做出来其实太暗了,之后看情况调整。
边缘光据观察,发现在太阳周围的云才有。因此使用NdotL,添加参数进行控制。


增加noise扰动




在添加底部颜色部分又卡了一夜。。半夜四点偷窥群友聊天,是这样的。就不信今晚改不了这个bug,然后一夜过去了。
据观察,底层云朵底部有与天际线相近的颜色,因此我们把底部的云设置第三套uv,从零到一,根据这个uv进行渐变。前面,因为颜色相同,所以决定去脚本里设置,就是setGlobalColor,结果传是传进去了,我们水面反射出问题了,天空盒颜色不能一开始就渲染好放进unity什么cube0里,而要运行脚本后才行。所以反射色不会变。所以拉倒,再定义一次。
而复制了天空盒的渐变色到云里,可能因为透明度混合啥的问题,它。。闪烁起来了。。【现知可能是抗锯齿的问题】
![](https://z4a.net/images/2023/02/03/shiwu.gif)
就是这样。改了无数地方该闪还是闪,下午打开shader看了看,它娘不会是ifelse的问题吧,而且一想,这也用不上分支啊,我写的时候脑子长泡了??


把上面的ifelse去掉。就正常了。可是这是为什么??
上色,以及bloom,与边缘光相同的算法。




有个问题,如果面片是这样的,那么法线数量不够,着色会一片一片的。




因此在blender里把他彻底整成面片。添加精简修改器就好了,吓死我还以为又重做。【不学建模,处处是魔法】


边缘光颜色更改,着重希望月亮刚出来的时候边缘光不要太亮。


然后修改点别的,就完成了
16m gif:
![](https://z4a.net/images/2023/02/03/finalf19acf6312c7b4a6.gif)
持续修改中,不代表最终品质。
后后后期会上传b站。
三、感想,然后求实习求实习

两个星期吧,花了。
急死我了提前放假一堆期末大作业没做,又想先把天空盒做完。
好不容易上个知乎发了存货,这篇先存着慢慢来。【已知估计拖了一个月,目前在搭场景学GI。中场休息。真有我的】
在线求技术美术实习,base杭州,别的地方可考虑,时间今年暑期,没有三个月也得凑出三个月来。

ainatipen 发表于 2023-2-6 17:10

这么厉害还是实习水平吗?应该是没看上之前差点的吧[捂脸]

LiteralliJeff 发表于 2023-2-6 17:19

[赞同]

IT圈老男孩1 发表于 2023-2-6 17:21

吔?我很菜很菜的,甚至简历都还没做
页: [1]
查看完整版本: 程序化天空盒实现昼夜变换