大气散射(五)大气球体的shader

106 阅读6分钟

一、介绍

1.1 写shader

有无数种方法可以开始编写此效果的着色器代码。由于我们想要在行星上渲染大气散射,因此假设它将用于球体是合理的。

如果您正在为游戏使用此教程,那么很可能会将其用于现有的行星。在球体上添加大气散射的计算是可能的,但通常会产生较差的结果。原因是大气层比行星半径大,因此需要在一个透明的、略大一些的球体上渲染。下面的图片(由NASA提供)显示了大气层延伸到行星表面之上,与其背后的空旷空间融为一体。

image.png 将散射材质应用于单独的球体是可能的,但是是多余的。在本教程中,我建议扩展Unity标准表面着色器,添加一个着色器通道,用于在稍大的球体上渲染大气层。我们将其称为大气层球体

1.2 双通道shader

如果您之前在Unity中使用过表面着色器,您可能已经注意到它不支持 Pass 块,而 Pass 块通常用于在顶点和片段着色器中定义多个通道。

创建一个双通道表面着色器是可能的,只需在同一个 SubShader 块中添加两个独立的CG代码部分即可:

Shader "Custom/NewSurfaceShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader {
        // --- 第一个通道 ---
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        // 这里放置 Cg 代码
        ENDCG
        // ------------------
        // --- 第二个通道 ---
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        // 这里放置 Cg 代码
        ENDCG
        // -------------------
    }
    FallBack "Diffuse"
}

您可以编辑第一个通道以渲染行星。从现在开始,我们将专注于第二个通道,用于大气散射。

1.3 法线挤出

大气层球体略大于行星。这意味着第二个通道需要将球体拉伸出来。如果您使用的模型具有平滑法线,我们可以通过一种称为法线挤出的技术来实现这种效果。

法线挤出是最古老的着色器技巧之一,通常也是最先教授的。本博客有很多相关参考资料;一个很好的起点是系列文章《渲染器入门》中的《表面着色器》。

如果您对法线挤出的工作原理不熟悉,那么所有顶点都将通过着色器的顶点函数进行处理。我们可以利用该函数修改每个顶点的位置,使球体变大。

第一步是更改编译指示,以包含 vertex:vert;这会强制 Unity 在每个顶点上运行一个名为 vert 的函数。

#pragma surface surf StandardScattering vertex:vert
void vert (inout appdata_full v, out Input o)
{
    UNITY_INITIALIZE_OUTPUT(Input,o);
    v.vertex.xyz += v.normal * (_AtmosphereRadius - _PlanetRadius);
}

上面的代码片段显示了一个顶点函数,它沿着法线将球体挤出。球体被挤出的量取决于大气层和行星的大小。这两个数量都需要作为属性提供给着色器,并且可以从材质检视器中访问。

我们的着色器还需要知道行星的中心位置。我们也可以在顶点函数中包含此计算。在世界空间中找到对象的中心位置是我们在文章《顶点和片段着色器》中讨论过的内容。

struct Input
{
    float2 uv_MainTex;
    float3 worldPos; // Unity 自动初始化
    float3 centre;   // 在顶点函数中初始化
};
void vert (inout appdata_full v, out Input o)
{
    UNITY_INITIALIZE_OUTPUT(Input,o);
    v.vertex.xyz += v.normal * (_AtmosphereRadius - _PlanetRadius);
    o.centre = mul(unity_ObjectToWorld, half4(0,0,0,1));
}

1.4 加法混合

我们需要解决的另一个重要特性是透明度。通常,透明材质允许看到其后面的内容。但是,这种解决方案在这里不太适用,因为大气层不仅仅是一张透明的塑料薄片。它携带着光,因此我们应该使用加法混合模式来确保增加行星的亮度。

Unity提供的标准表面着色器默认情况下没有任何混合模式激活。为了改变这一点,我们可以将第二个通道中的标签替换为以下新的标签:

Tags { "RenderType"="Transparent"
    "Queue"="Transparent"}
LOD 200
Cull Back
Blend One One

表达式 Blend One One 被着色器用来指代加法混合模式。

1.6、自定义光线函数

大多数情况下,程序员必须编写表面着色器时,会修改其 surf 函数,该函数用于提供“物理”属性,例如反照率、平滑度、金属性等等。然后,着色器会利用这些属性来计算逼真的着色。

在这种特殊情况下,我们不需要进行任何这些计算。为此,我们需要替换着色器正在使用的光照模型。我们已经广泛涵盖了这个主题;如果您想更好地了解如何做到这一点,可以参考以下文章:

  • 3D 打印机着色效果
  • CD-ROM 着色器:衍射光栅
  • Unity 中的快速次表面散射

新的光照模型将被称为 StandardScattering;我们需要分别为实时光照和全局光照提供 LightingStandardScattering 和 LightingStandardScattering_GI 函数。

我们需要编写的代码还依赖于诸如光照方向和视图方向之类的属性。它们在以下代码片段中被检索。

#pragma surface surf StandardScattering vertex:vert
#include "UnityPBSLighting.cginc"
inline fixed4 LightingStandardScattering(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
{
    float3 L = gi.light.dir;  // 光线方向
    float3 V = viewDir;       // 视图方向
    float3 N = s.Normal;      // 表面法线
    float3 S = L;             // 太阳光的方向
    float3 D = -V;            // 穿过大气层的视线方向
    ...
}
void LightingStandardScattering_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
{
    LightingStandard_GI(s, data, gi);        
}

其中的 ... 将包含我们需要实现此效果的实际着色器代码。

1.6、浮点数精度

为了本教程的目的,我们假设所有计算都是以米为单位的。这意味着如果你想模拟地球,你需要一个半径为 6371000 米的球体。实际上,在 Unity 中这是不可能的,因为在处理非常大和非常小的数字时会出现浮点错误。

如果你想克服这些限制,你可以重新调整散射系数以相应地进行补偿。例如,如果你的行星半径只有 6.371 米,那么散射系数 β(λ)\beta\left(\lambda\right) 应该增加 1000000 倍,尺度高度 H 减小 1000000 倍。

在实际的 Unity 项目中,可以下载,所有的属性和计算都以米为单位表达。这使我们能够使用真实的物理值来计算散射系数和尺度高度。然而,着色器还接收球体的大小(以米为单位),以便它可以执行从 Unity 单位到真实尺度米的比例转换。