在很多太空科幻类的电影、游戏中、我们常常看到在太空中的星球的场景,在这些场景中我们可以看到真实的行星地表光影效果和云层、以及非常炫酷的大气层效果。在unity中我们也可以创建类似的效果。本文我将介绍如何在unity shader中编写地球特效渲染。

上图是最终的效果。实现这样的效果可以使用基于物理的大气渲染或体素渲染(详见【Unity/大气渲染】单次散射的原理和简单实现 – Relolihentai – 博客园 (cnblogs.com))。在本文我主要介绍通过特效的形式实现地球渲染,并非基于物理的大气渲染。以下是我的思路。

主要的思路是:对效果进行分层。我们使用三个同心球来渲染地球,从里到外分别是地表、云层、大气层。以下将分别描述。

1、 地表

地表的渲染其实没有太多特别之处。主体部分直接用标准冯氏光照模型,如果能够获取到高清的地表金属度和粗糙度贴图的话也也可以用PBR来达到更真实的效果(不过地球哪来的PBR贴图啊),在一些影视级别的制作中,为了追求真实性,甚至可以使用高度贴图而非法线贴图来真实模拟出地表的凹凸效果,并且将海水与大陆分离来获取更加真实的水体渲染,不过这都不是本文的重点。

我们直接将三张贴图:表面颜色贴图、表面法线贴图和陆地-海洋贴图传递给shader,其中陆地-海洋图主要用于区分不同地区的高光强度的区别。

现在的效果如下:

接下来,我们可以加入一个地球的标志性的夜晚灯光的特效。在冯氏光照漫反射项的计算的过程中,我们计算了光源方向与法线方向的点积。夜晚地球暗面的灯光的强度可以视为反向平行光的光照强度。因此直接用1减去点积,即可以得到初步的一个值。将这叠加上去,效果如下:

然后我们开始制作云层。

2、 云层

云层我们使用球体+透明度剔除的方案来制作。给一张云层贴图即可,将灰度直接映射到剔除检测值上,就可以完成一个最基础的云层,同时也使用最基础的冯氏光照。

补充说明:为了增强真实性,可以在地球表面上通过贴图的方式显示云层阴影(因为云层和地表是同心球的方式,所以无法正常投影)。在地表shader中新建一个外部变量,将云层的旋转量传入shader,通过云层的旋转量来偏移uv坐标,采样云层贴图,通过云层密度来控制阴影浓度。以上思路所得的结果如下图

3、 大气层

大气层是三者之中最复杂的。我们虽然不使用完全基于物理的大气渲染方法,但是我们也可以通过简化的运算来模仿大气投射的效果。

首先假设地面是一个平面,当摄像机在太空中俯瞰地表一点时,光所在大气层中穿过的距离等于大气层厚度除以观察角的正弦值,即

\(l=\frac{h}{\sinα}\)

示意图如下:

由此图容易知道,当我们假设大气不存在散射,并且消光效应对于各个波长的光线是均匀分布的话,那么可以知道:假设垂直向下看时的“浓度”为f,那么对于观察角度为α时,“大气浓度”为

\(\frac{f}{\sinα}\)

那么我们就将这个式子拿到球形的地球-大气层模型中套用即可。我们可以通过地球球心坐标和大气与地球半径来计算某一片元处的光在大气中穿过的距离。示意图如下:

最终计算得到大气的浓度是f*r/l*sinalpha. 其中f是单位距离上大气的浓度值,r是地球半径,l是地球到球心的距离,α是大气表面该点到摄像机的向量和地球球心到摄像机的向量的夹角。

注意,我们不仅需要考虑穿过大气看到地表的情况,也应该考虑穿过大气看到外太空的情况,所以此处应该分情况讨论。

这一部分的代码如下所示:

                    // 大气                    //                     // 当透过大气能看到地面时                    // α是球心到摄像机向量与片元到摄像机向量的夹角                    // l是摄像机到球心的距离                    // r是地球半径                    // F是大气雾系数(垂直看向地面时雾的强度/单位积分值)                    // 计算公式是 (F*r)/(l*sinα)                    //                    // 当透过大气无法看到地面时                    // 类似                    float3 center = mul(unity_ObjectToWorld , float4(0,0,0,1)).xyz;// 球心                    float3 centerDir = normalize(center.xyz - _WorldSpaceCameraPos.xyz);// 摄像机指向球心的方向                    float3 fragDir = -normalize(UnityWorldSpaceViewDir(i.worldPos)); // 摄像机指向片元的方向                    float cosalpha = abs(dot(centerDir, fragDir));                    float sinalpha = abs(sin(acos(cosalpha)));                                        float3 cV = (center.xyz - _WorldSpaceCameraPos.xyz);                    float length = pow((cV.x * cV.x + cV.y * cV.y + cV.z * cV.z), 0.5);                    float tangentAngle = asin(EarthRadius / length);                                        // 最终的大气雾强度                    float fogStrength;                    if (sinalpha <= sin(tangentAngle) + 0.0001) { // 透过大气能看到地面                        float sintheta = abs(sin(acos(length * sinalpha / (EarthRadius + 0.0005))));                        fogStrength = InnerFog_Strength * AtoFog / sintheta;                    }                    else {                              // 透过大气看到宇宙                        float3 AtoRadiusVec = center.xyz - i.worldPos.xyz;                        float AtoRadius = pow((AtoRadiusVec.x * AtoRadiusVec.x + AtoRadiusVec.y * AtoRadiusVec.y + AtoRadiusVec.z * AtoRadiusVec.z), 0.5) + 0.0005;                        float lightLength = 2 * pow((AtoRadius * AtoRadius - (length * sinalpha) * (length * sinalpha)), 0.5);                        fogStrength = AtoFog * lightLength / (AtoRadius - EarthRadius);                        fogStrength *= 1 - pow((((length * sinalpha) - EarthRadius) / (AtoRadius - EarthRadius)), 0.45);                    }

但是由于我们没有使用基于物理的大气散射计算,所以电影中常见的从很低的角度观察大气层时的边缘辉光是不存在的,所以我们通过手工的方式添加这部分边缘辉光。边缘辉光既然存在于边缘,那么应该是越到边缘越强,在中心则极弱。于是我们可以采用类似于菲涅尔的做法来计算边缘辉光的强度,这一部分的具体代码如下:

                    fixed Alpha = dot(worldLightDir, worldNormal) * 0.5 + 0.5;                    Alpha = pow(Alpha, 2.75);                                        // 边缘辉光                    float TheCos = dot(worldLightDir, fragDir);                    float glowStrength_1 = 2 * pow(E, 50 * TheCos - 50);                    float glowStrength_2 = 5 * pow(E, 200 * TheCos - 200);                    float3 glow_1;                    float3 glow_2;                    if (sinalpha >= sin(tangentAngle) - 0.01) {    // 透过大气看到宇宙                        glow_1 = glowStrength_1 * SunGlowColor.xyz;                        glow_2 = glowStrength_2 * SunGlowColor.xyz;                    }                    else {                         glow_1 = float3(0, 0, 0);                        glow_2 = float3(0, 0, 0);                    }                    fixed4 ans_atomosphere_base = fixed4(AtoCol.xyz, fogStrength * Alpha);                    fixed4 ans_atomosphere_glow = fixed4(glow_1.xyz + glow_2.xyz, pow(fogStrength, 2) * (glow_1.x + glow_1.y + glow_1.z + glow_2.x + glow_2.y + glow_2.z) / 6);

这样的大气层已经非常好看了,但是当我们把摄像机运动到晨昏线附近时,晨昏线附近的大气层太蓝了,缺乏更多的太阳光的暖色部分的层次感,因此我们考虑再增加一层太阳光暖色光波长部分的散射光,尤其是在摄像机从背向对准晨昏线是,需要更多的层次来营造更好看的轮廓。我们通过计算光源(太阳)方向和大气表面片元到摄像机的向量的夹角大小来控制这部分暖色光的强度,这部分的实现如下:

                    // 背向辉光                    float DawnGlowStrength_1 = 1 * pow(E, 8 * TheCos - 8);                    fixed Strength = dot(worldNormal, worldLightDir);                    fixed DawnGlowStrength = max((- 27 / 6 * Strength * pow(E, -4 * pow(Strength, 2))), 0);                    float3 DawnGlow;                    if (sinalpha >= sin(tangentAngle) - 0.01) {    // 透过大气看到宇宙                        DawnGlow = DawnGlowStrength_1 * DawnGlowColor.xyz * DawnGlowStrength;                    }                    else {                        DawnGlow = float3(0, 0, 0);                    }

以上的每一层效果单独的效果和混合的效果如下图所示:

最终我们添加一些小的参数的调整,就得到了最终的结果,可以看到还是比较漂亮的,不过不够真实,如果需要真实的话还是要用大气散射模拟的方式来制作。

以上就是本文的全部内容了,因为距离我实现这个效果已经有一段时间了,有些细节我也无法说得很清楚了,还请见谅。