RedZero9 发表于 2022-2-11 12:47

《Unity Shader入门精要》笔记(十一)

本文为《Unity Shader入门精要》第九章内容《更复杂的光照》的上半部分笔记,主要涉及渲染路径和光源类型。
本文相关代码,详见:
原书代码,详见原作者github:

<hr/>
1. Unity的渲染路径

渲染路径决定了光照是如何应用到Unity Shader中的,它告诉Unity光源和处理后的光照信息从哪些数据中去取。

Unity 5.0之前,有3种渲染路径:前向渲染路径(Forward Rendering Path)、延迟渲染路径(Deferred Rendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。
Unity5.0之后,顶点照明渲染路径已不建议使用(但Unity依然兼容);且新的延迟渲染路径代替了原来的延迟渲染路径(老版本的延迟渲染路径在Unity中依然兼容)。

大多数情况下一个项目只会使用一种渲染路径。修改项目渲染路径的操作:
菜单-Edit-Project Settings-Graphics-Tier Settings,可在对应的Tier下面去掉Use Defaults的勾选,选择想要的Rendering Path。





但有时会需要使用多个渲染路径来渲染不同的物体,可以使用多个相机,分别在Camera组件中设置不同的渲染路径,以覆盖Project Settings中的设置:



如果当前显卡不支持所选择的渲染路径,Unity会自动使用更低一级的渲染路径。比如:GPU不支持延迟渲染,则会使用前向渲染。
完成设置后,就可以在Unity Shader的Pass中设置标签LightMode:
Pass
{
    Tags { "LightMode" = "ForwardBase" }
}

前向渲染除了ForwardBase,还有ForwardAdd。
LightMode标签支持的渲染路径设置选项:



Unity会根据Pass中LightMode标签的设置,对一些内置光照变量进行赋值。如果没有设置标签或设置了不正确的标签,当我们调用光照变量时,会得到不正确的结果。

1.1 前向渲染路径

前向渲染路径是传统的渲染方式,也是最常用的一种渲染路径。
1.1.1 原理

每进行一次完整的前向渲染,我们需要渲染该对象的图元,并计算两个缓冲区的信息:颜色缓冲区、深度缓冲区。
先根据深度缓冲来决定一个片元是否可见,如果可见再更新颜色缓冲区中的颜色值。
伪代码:
Pass
{
    for (each primitive in this model)
    {
      for (each fragment covered by this primitive)
      {
            if (failed in depth test)
            {
                // 如果没有通过深度测试,说明该片元不可见
                discard;
            }
            else
            {
                // 如果该片元可见,则进行光照计算
                float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
                // 更新帧缓冲
                writeFrameBuffer(fragment, color);
            }
      }
    }
}
每个逐像素光源都需要进行上述渲染流程。如果一个物体存在多个逐像素光源的影响,则需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合,得到最终的颜色值。
Pass执行次数 = N个物体 * M个光源
渲染引擎会限制每个物体的逐像素光照数目,以此来限制Pass执行的次数。

1.1.2 Unity中的前向渲染

Unity中,前向渲染有3种处理光照的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。

决定光源使用哪种处理方式,取决于它的类型和渲染模式。

[*]类型指该光源是平行光还是其他类型的光源;
[*]渲染模式指该光源是否是重要的(Important)。

如果一个光照被设置成Important,Unity就会以逐像素的方式处理该光源。
设置光照类型和渲染模式:



前向渲染中,渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(距离远近、光照强度等)对这些光源进行重要度排序。
其中:

[*]一定数目的光源会按逐像素的方式处理;
[*]最多4个光源按逐顶点的方式处理;
[*]剩下的光源按球谐(SH)方式处理。
Unity使用的判断规则:

[*]场景中最亮的平行光总是按逐像素处理;
[*]渲染模式被设置成Not Important的光源,会按逐顶点或球谐(SH)处理;
[*]渲染模式被设置成Important的光源,会按逐像素处理;
[*]如果以上规则得到的逐像素光源数量小于Quality设置中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。



光照会在Unity Shader的Pass中进行计算。前向渲染有两种Pass:

[*]Base Pass
[*]Additional Pass


上图说明:

[*]使用#pragmamulti_compile_fwdbase和#pragma multi_compile_fwdadd,为了在Pass中得到一些正确的光照变量,如:光照衰减;
[*]Base Pass中渲染的平行光默认支持阴影(需要开启光源的阴影功能),而Additional Pass中渲染的光源默认没有阴影效果,即便设置了Shadow Type也不生效,但我们依然可以使用#pragma multi_compile_fwdadd_fullshadows编译指令代替#pragma multi_compile_fwdadd,为点光源和聚光灯开启阴影效果,但这需要Unity在内部使用更多的Shader变种;
[*]环境光和自发光对于一个物体来说,只希望计算一次,如果在Additoinal Pass中计算它们,会造成叠加计算多次,得不到我们想要的效果,所以会放在Base Pass中计算;
[*]Additional Pass中会计算不同的光源,不同的光源的光照效果需要混合,所以需要开启并设置混合模式,通常会使用Blend One One等混合指令;
[*]通常一个Unity Shader会定义一个Base Pass(双面渲染等情况会定义多次)和一个Additional Pass,但是一个Base Pass只会执行一次,而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被调用多次,即:每个逐像素光源都会执行一次Additional Pass。

1.1.3 内置的光照变量和函数

前向渲染使用的内置光照变量:



前向渲染可以使用的内置光照函数:



上面列出的内置变量和函数只是一部分,后面学习中会用到其他一些变量和函数。

1.2 顶点照明渲染路径

顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时得到效果最差的一种类型。它仅支持逐顶点的光源计算,不支持逐像素,所以无法得到:阴影、法线映射、高精度的高光反射等效果,它是前向渲染的一个子集。
1.2.1 Unity中的顶点照明渲染

顶点照明渲染路径通常在一个Pass中就可以完成对物体的渲染,在这个Pass中,我们会计算关心的光源对物体的照明,且是按照逐顶点处理的。
它是Unity中最快速的渲染路径,且具有最广泛的硬件支持(游戏机上不支持这种路径)。Unity 5以后被作为一个遗留的渲染路径,未来的版本中可能会被移除。

1.2.2 可访问的内置变量和函数

在Unity中一个顶点照明的Pass中最多可以访问8个逐顶点光源。
顶点照明渲染路径中可以使用的内置变量:


如果影响该物体的光源数目小于8,则数组中剩余的光源颜色会被设置成黑色。有些变量我们同样可以在前向渲染路径中使用,如:unity_LightColor,但这些变量的数组的维度和数值在不同渲染路径下的值是不同的。

顶点照明渲染路径中可以使用的内置函数:



1.3 延迟渲染路径

当场景中包含大量实时光源,使用前向渲染会造成性能急剧下降。每个光源都会执行一个Pass,重复地渲染一个物体会存在很多相同的重复计算。
在这样的环境下,本是一种更古老的渲染方法的延迟渲染,又流行了起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还使用了额外的缓冲区——G缓冲(Geometry Buffer,G-buffer)。G缓冲区存储了我们关心的表面(通常指离相机最近的表面)的其他信息,如:表面的法线、位置、用于光照计算的材质属性等。

1.3.1 原理

延迟渲染主要包含2个Pass。第一个Pass不进行任何光照计算,仅仅计算哪些片元可见,将可见的片元信息存储到G缓冲区。
伪代码:
Pass 1
{
    // 第一个Pass不进行真正的光照计算
    // 仅仅把光照计算需要的信息保存到G缓冲中
    for (each primitive in this model)
    {
      for (each fragment covered by this primitive)
      {
            if (failed in depth test)
            {
                // 如果没有通过深度测试,则该片元不可见
                discard;
            }
            else
            {
                // 如果该片元可见,把需要的信息存储到G缓冲中
                writeGBuffer(materialInfo, pos, normal, lightDir, viewDir);
            }
      }
    }
}

Pass 2
{
    // 使用G缓冲中的信息进行真正的光照计算
    for (each pixel in the screen)
    {
      if (the pixel is valid)
      {
            // 如果该像素是有效的,读取对应G缓冲中的信息
            readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir);
            // 根据读取到的信息进行光照计算
            float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
            // 更新帧缓冲
            writeFrameBuffer(pixel, color);
      }
    }
}
延迟渲染使用的Pass数目通常是2个,与场景中包含的光源数目没有关系,和屏幕的大小有关。

1.3.2 Unity中的延迟渲染

目前Unity有2种延迟渲染路径:

[*]Unity 5之前使用的,遗留的延迟渲染路径;
[*]Unity 5及以后使用的延迟渲染路径。

延迟渲染路径需要一定的硬件支持。新旧延迟渲染路径之间的差别很小,只是使用了不同的技术来权衡不同的需求,比如:旧版的延迟渲染路径不支持Unity 5以后版本的基于物理的Standard Shader。
遗留的延迟渲染路径可参考官方文档:
http://docs.unity3d.com/Manual/RenderTech-DeferredLighting.html
下面仅讨论Unity 5以后的延迟渲染路径。

延迟渲染适合在光源数目众多、使用前向渲染造成性能瓶颈的情况下使用,每个光源都可以按逐像素处理。但它也有缺点:

[*]不支持真正的抗锯齿功能;
[*]不能处理半透明物体;
[*]对显卡有要求,必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

延迟渲染需要两个Pass:

[*]第一个Pass用于渲染G缓冲
把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中对每个物体来说,这个Pass仅执行一次。

[*]第二个Pass用于计算真正的光照模型
使用上一个Pass中渲染的数据来计算最终的颜色光照颜色,再存储到帧缓冲中。

默认的G缓冲区包含了以下几个渲染纹理(Render Texture,RT):

[*]RT0
ARGB32格式,RBG通道存储漫反射颜色,A通道未被使用。

[*]RT1
ARGB32格式,RGB通道存储高光反射颜色,A通道存储高光反射的指数部分。

[*]RT2
ARGB2101010格式,RGB通道存储法线,A通道未被使用。

[*]RT3
ARGB32格式(非HDR)或ARGBHalf(HDR),用于存储自发光 + lightmap + 反射探针(reflection probes)。

[*]深度缓冲和模板缓冲

第二个Pass计算光照,默认情况下仅可以使用Unity内置的Standard光照模型。若想使用其他模型,需要替换掉原来的Internal-DeferredShading.shader文件,详见:http://docs.unity3d.com/Manual/RenderTech-DeferredShading.html。

1.3.3 可访问的内置变量和函数

延迟渲染路径中可以使用的内置变量:


这些变量可以在UnityDeferredLibrary.cginc文件中找到声明。

1.4 选择哪种渲染路径

Unity官方给出了4种渲染路径:

[*]前向渲染路径
[*]延迟渲染路径
[*]遗留的延迟渲染路径
[*]顶点照明渲染路径
的详细比较,包括特性比较(是否支持逐像素光照、半透明物体、实时阴影等)、性能比较以及平台支持,详见:http://docs.unity3d.com/Manual/RenderingPaths.html。

一般会根据游戏发布的平台来选择渲染路径,若当前显卡不支持所选渲染路径,则会自动使用更低一级的渲染路径。
本笔记中后续的学习主要使用Unity的前向渲染路径。

2. Unity的光源类型

Unity一共支持4种光源类型:平行光、点光源、聚光灯和面光源(area light)。面光源仅在烘焙时才有用,不在我们讨论的范围。
2.1 光源类型有什么影响

最常使用的光源属性有:光源的位置、方向(到某点的方向)、颜色、强度以及衰减(到某点的衰减,与点到光源的距离有关)。
2.1.1 平行光





平行光可以照亮的范围是没有限制的,它通常作为太阳这样的角色在场景中出现,它没有位置的概念,将它放到场景中任意位置都不影响光照的效果,它的几何属性只有方向,通过调整Transform组建中的Rotation属性来改变它的光源方向。
平行光没有衰减的概念,光照强度不会随距离发生改变。

2.1.2 点光源





点光源表示由一个点发出的、向所有方向延伸的光,它的照亮空间是有限的,由空间中的一个球体定义。
在Scene视图中开启光照才能预览光源是如何影响场景中的物体的:



点光源的属性有:位置(Position)、光照范围(Range)、方向、颜色强度等。对于方向属性,可以用点光源的位置减去某点的位置得到它到该点的方向,其他属性可以在面板中调整。
点光源会随距离增大而衰减,球心处的光照强度最强,边界处最弱(值为0),中间的衰减值可由一个函数定义。

2.1.3 聚光灯

聚光灯是这3种光源类型中最复杂的一种,照亮范围是有限的,相比点光源,不再是简单的球体,而是有空间中的一个锥形区域定义的。
聚光灯可用于表示由一个特定位置出发、向特定方向延伸的光,类比生活中的例子,比较像手电筒。




聚光灯有位置(Position)、光照范围(Range)、张开角度(Spot Angle)、方向、颜色强度等。对于方向属性,可以用聚光灯的位置减去某点的位置得到它到该点的方向。
聚光灯的衰减随物体的远离逐渐减小,锥形的顶点处光照强度最强,锥形的边界处强度为0,中间的衰减值可由一个函数定义,这个函数相比点光源衰减的计算公式更加复杂,因为需要判断一个点是否在椎体的范围内。

2.2 在前向渲染中处理不同的光源类型

接下来实现在前向渲染路径的基础上,在Unity Shader中访问它们的5个属性:位置、方向、颜色、强度以及衰减。
2.2.1 准备工作

完成如下准备工作:

[*]新建名为Scene_9_2_2_1的场景,并去掉天空盒子;
[*]新建名为ForwardRenderingMat的材质;
[*]新建名为Chapter9-ForwardRendering的Unity Shader,并赋给上一步的材质;
[*]在场景中创建一个胶囊体,并将第二步创建的材质赋给它;
[*]为了让胶囊体受到多个光源的影响,在场景中新建一个点光源,并将颜色设为绿色(和平行光区分开);
[*]保存场景。
详细操作详见《<Unity Shader入门精要>笔记(四)》里的案例常用操作说明。

2.2.2 编写Shader代码

将之前笔记中写过的Chapter6-BlinnPhong代码复制覆盖到Chapter9-ForwardRendering文件中,做部分修改。关于光源属性的访问代码已用注释说明,代码如下:
// 修改Shader命名
Shader "Unity Shaders Book/Chapter 9/Forward Rendering"
{
    Properties
    {
      _Diffuse("Diffuse", Color) = (1, 1, 1, 1)
      _Specular("Specular", Color) = (1, 1, 1, 1)
      _Gloss("Gloss", Range(8.0, 256)) = 20
    }

    SubShader
    {
      Tags { "RenderType" = "Opaque" }

      // Base pass
      Pass
      {
            // 设置为ForwardBase,处理环境光和第一个逐像素光照(平行光)
            Tags { "LightMode" = "ForwardBase" }

            CGPROGRAM

            // 执行该指令,让前向渲染路径的光照衰减等变量可以被正确赋值
            #pragma multi_compile_fwdbase

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 worldNormal = normalize(i.worldNormal);
                // Unity会选择最亮的平行光传递给Base Pass进行逐像素处理
                // 其他平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理
                // 对于Base Pass来说,处理逐像素光源类型一定是平行光
                // 使用_WorldSpaceLightPos0得到这个平行光的方向
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                // 环境光只需要计算在Base Pass中计算一次即可
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

                // 使用_LightColor0得到平行光的颜色和强度
                // (_LightColor0已经是颜色和强度相乘后的结果)
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 因为平行光没有衰减,所以衰减值为1.0
                fixed atten = 1.0;

                return fixed4(ambient + (diffuse + specular) * atten, 1.0);
            }
            ENDCG
      }

      // Additional pass
      Pass
      {
            // 设置为ForwardAdd
            Tags { "LightMode" = "ForwardAdd" }

            // 开启混合模式,将帧缓冲中的颜色值和不同光照结果进行叠加
            // (Blend One One并不是唯一,也可以使用其他Blend指令,比如:Blend SrcAlpha One)
            Blend One One

            // Additional pass中的顶点、片元着色器代码是根据Base Pass中的代码复制修改得到的
            // 这些修改一般包括:去掉Base Pass中的环境光、自发光、逐顶点光照、SH光照的部分
            CGPROGRAM

            // 执行该指令,保证Additional Pass中访问到正确的光照变量
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;

                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 去掉Base Pass中环境光

                fixed3 worldNormal = normalize(i.worldNormal);

                // 计算不同光源的方向
                #ifdef USING_DIRECTIONAL_LIGHT
                  // 平行光方向可以直接通过_WorldSpaceLightPos0.xyz得到
                  fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                  // 点光源或聚光灯,_WorldSpaceLightPos0表示世界空间下的光源位置
                  // 需要减去世界空间下的顶点位置才能得到光源方向
                  fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif

                // 使用_LightColor0得到光源(可能是平行光、点光源或聚光灯)的颜色和强度
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                // 计算不同光源的衰减
                #ifdef USING_DIRECTIONAL_LIGHT
                  // 平行光不衰减
                  fixed atten = 1.0;
                #else
                  // 将片元的坐标由世界空间转到光源空间
                  float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
                  // Unity选择使用一张纹理作为查找表(Lookup Table, LUT)
                  // 对衰减纹理进行采样得到衰减值
                  fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                #endif

                return fixed4((diffuse + specular) * atten, 1.0);
            }

            ENDCG
      }
    }

    Fallback "Specular"
}

保存代码,得到如下效果:



注意:上述代码仅供学习了解,并不会用于真正的项目中。

2.3 实验:Base Pass和Additional Pass的调用

上一节给出了前向渲染中Unity是如何决定哪些光源是逐像素光,而哪些是逐顶点或球谐(SH)光。为了更直观地理解,接下来通过实验学习了解。
2.3.1 准备工作

完成如下准备工作:

[*]新建名为Scene_9_2_2_2的场景,并去掉天空盒子;
[*]将平行光的颜色调为绿色;
[*]在场景中创建一个胶囊体,将上一节的ForwardRenderingMat材质赋给该胶囊体;
[*]新建4个点光源,颜色调整成相同的红色;
[*]保存场景。

得到如下效果:



2.3.2 原理分析

当我们创建一个光源时,它的默认Render Mode是Auto,意味着Unity会自动为我们判断哪些光源会逐像素处理,哪些会逐顶点或球谐(SH)的方式处理。
在菜单-Edit-Project Settings-Quality-Pixel Light Count中,我们可以看到默认情况下,一个物体可以接收出除最亮的平行光外的4个逐像素光照:



而除了平行光,场景里刚好有4个点光源,它们的Render Mode为Auto,因此它们会在Additional Pass中以逐像素的方式被处理,每个光源调用一次Additional Pass。

2.3.3 使用FrameDebugger分析绘制过程

在菜单-Window-Analysis-Frame Debugger打开帧调试器,点击Enable按钮可以看到场景的渲染事件:





可以看到渲染胶囊体一共有6个关键事件,它们分别为:



[*]清除颜色、深度和模板缓冲;
[*]使用Base Pass将平行光的光照渲染到胶囊体;
[*]使用Additional Pass将第1个点光源的光照渲染到胶囊体;
[*]使用Additional Pass将第2个点光源的光照渲染到胶囊体;
[*]使用Additional Pass将第3个点光源的光照渲染到胶囊体;
[*]使用Additional Pass将第4个点光源的光照渲染到胶囊体;

Unity处理这些点光源是按照它们的重要度排序的,在我们的实验中,点光源的颜色和强度相同,所以距离就成了重要度的唯一衡量标准,离胶囊体最近的点光源会先被渲染。
如果将其中一个点光源移动到胶囊体不受其影响的位置:



将帧渲染器Disable再Enable,会看到关键的渲染事件变成了5个:



因为不对物体产生影响的光源,Unity不会调用相关的渲染事件。
如果我们将点光源的Render Mode设置为Not Important,这些点光源就不会以逐顶点和球谐的方式被处理,而因为我们的Base Pass中没有计算光照,所以这4个点光源不会对物体产生任何光照效果:



以上是关于Unity渲染路径和光源类型的学习笔记,下一个笔记我们将学习Unity的光照衰减和阴影。

写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页: [1]
查看完整版本: 《Unity Shader入门精要》笔记(十一)