日夜循环天空球(Procedural Skybox)

本节实现Unity日夜循环天空球♥(´∀` )人

概况

Unity中可实现两类skybox,一类是贴图类(6 sided, cubemap, panoramic),一类是procedural类,本文就是procedural天空盒(其实就是100%纯手写= - =)

本文中的天空盒主要参考Minionsart的分享→Making a Stylized Skybox Shader进行制作,根据需求有魔改,并关联时间系统(」・ω・)」→魔改后repo

本文中的天空盒包含内容:

天空盒设置

确保摄像机中设定Clear Flagsskybox模式,然后在随便哪个物体下挂上skybox组件,添加使用了skybox shader的材质。

日月绘制

Unity内置变量_WorldSpaceLightPos0存储了directional light的方向。这样就可以通过改变directional light的旋转使天空球旋转,形成日夜交替效果。

首先,光指向的正反方向就是我们绘制日月的地方!

计算uv坐标上天空球上的坐标与_WorldSpaceLightPos0间的距离,根据距离返这个数值绘制,得到的是一个中心到边缘亮度递减的圆形效果(距离方向坐标越近数值越小),可以用saturate对球形区域内的颜色再处理一次,将sphere乘上一个大点的数,返回数都为1,即可获得清晰边界。

// sun
float sun = distance(i.uv.xyz, _WorldSpaceLightPos0);
float sunDisc = 1 - (sun / _SunRadius);
sunDisc = saturate(sunDisc * 50);

月亮的绘制同理,月食/日食的效果可以通过两个球实现——

// moon
float moon = distance(i.uv.xyz, -_WorldSpaceLightPos0); //日月方向相反
float moonDisc = 1 - (moon / _MoonRadius);
moonDisc = saturate(moonDisc * 50);

float crescentMoon = distance(float3(i.uv.x + _MoonOffset, i.uv.yz), -_WorldSpaceLightPos0);
float crescentMoonDisc = 1 - (crescentMoon / _MoonRadius);
crescentMoonDisc = saturate(crescentMoonDisc * 50);
				
moonDisc = saturate(moonDisc - crescentMoonDisc);

渐变天色

一共需要黑夜和白天两种天色。

使用lerp函数形成颜色渐变,根据uv坐标判定非日夜交替时的天色,根据saturate函数和世界坐标y值判定日夜交替。

// gradient day sky
float3 gradientDay = lerp(_DayBottomColor, _DayTopColor, saturate(i.uv.y));
float3 gradientNight = lerp(_NightBottomColor, _NightTopColor, saturate(i.uv.y));
float3 skyGradients = lerp(gradientNight, gradientDay,saturate(_WorldSpaceLightPos0.y));

星空/云朵

绘制

星空和云朵使用noise贴图+cutoff制作。

在“天空”贴贴图不能直接使用uv坐标,uv坐标下就是emmm感觉贴在一个巨大的球形上,这里星空和云彩只会出现在“天上”,到画面上的表现即只会出现在屏幕的上半部分。所以这里使用世界坐标实现,将贴图显示在xz平面,并用y轴坐标控制显示范围在上半部分

对天空贴图使用的坐标处理如下。使用clamp控制世界坐标y值在0~最大值间(不知道咋获得y轴最大值但是大于10000的时候没差别了所以就用这个数了…然后是熟悉的cutoff操作,使用step设置一个cutoff阈值,凸显亮色部分。关键代码如下:

float2 skyuv = i.worldPos.xz / clamp(i.worldPos.y, 0, 500);
float3 stars = tex2D(_Stars, skyuv + float2(_StarsSpeed, _StarsSpeed) * _Time.x);
stars = step(_StarsCutoff, stars);

同理加上云朵(跟星星的一模一样只是换了张贴图啦),效果如下:

云朵优化

添加噪音贴图

现在的云朵只是一张单层贴图,显得很没层次感而且可以看出明显贴图图案。接下来通过添加噪音贴图丰富云朵形状。

这里添加了两张贴图,一张用来和原贴图叠加丰富云朵的图案,一张用来制造更明显的扭曲效果。

float distort = tex2D(_DistortTex, (skyuv + (_Time.x * _DistortionSpeed)) * _DistortScale);
float noise = tex2D(_CloudNoise, ((skyuv + distort ) - (_Time.x * _CloudSpeed)) * _CloudNoiseScale);
float finalNoise = saturate(noise) * 3 * saturate(i.worldPos.y);
cloud = saturate(step(_CloudCutoff, cloud * finalNoise));

Fluffy!

现在的云朵边缘太过清晰,接下来让云显得更加蓬松,添加一个参数,在一个cutoff区间内平滑取值,使云在边缘处模糊一点。

cloud = saturate(smoothstep(_CloudCutoff * cloud, _CloudCutoff * cloud + _Fuzziness, finalNoise));

这样显得更接近真实一点了。

云朵的层次

把之前的那层cloud复制粘贴再加一层,赋一个不一样的cutoff范围效果就很好了,浓密或稀疏的云层都可以做出来,多层也方便之后添加不同色彩。

cloud = saturate(smoothstep(_CloudCutoff * cloud, _CloudCutoff * cloud + _Fuzziness, finalNoise));
float cloudSec = saturate(smoothstep(_CloudCutoff * cloud, _CloudCutoff * cloud + _FuzzinessSec, finalNoise));			
色彩plus

给上下两层云赋予不同的颜色,这里根据白天黑夜加了两套颜色,然后根据日夜交替替换云朵颜色。

星空优化

夜晚出现

星空不同于云的地方在于它只在夜晚出现。用saturate和世界坐标y轴返回1或者0作为乘数即可。

stars = step(_StarsCutoff, stars) * saturate(-_WorldSpaceLightPos0.y);

位于云后

目前的叠加星星和云的效果如下,星星看上去位于云前。

判定一下只在没有云的地方画星星即可。

stars *= (1 - cloud);

地平线

最后一步,添加地平线。根据uv.y可以绘制出水平线。然后依然是根据日夜判定不同的地平线颜色。地平线还有个功能就是模糊上下边界让云和地的边界看起来不那么明显,这里多画一道线赋上亮色其遮挡作用。

float3 horizon = abs((i.uv.y * _HorizonIntensity) - _HorizonHeight);
horizon = saturate((1 - horizon)) * (_HorizonColorDay * saturate(-_WorldSpaceLightPos0.y) + _HorizonColorNight * saturate(_WorldSpaceLightPos0.y));	

时间脚本

功能如下:

结束了!

本期素材

  1. Minionsart的梦幻天空球

  2. Catlike Coding · Clock

ps: 感觉step,smoothstep,saturate,clamp和lerp这五个函数承包了shader里的画面绘制呢…