Volume 框架

Volume 框架是HDRP最重要的组成部分之一,其作用如下:

  • 为场景设置来自天空盒的环境光照

  • 设置各种阴影效果,包括场景中的Contact Shadow 和 Micro Shadow

  • 设置场景中的雾效

  • 设置基于屏幕空间的反射(SSRelection)和折射(SSRefraction)、环境光遮蔽(Ambient Occlusion)

  • 设置后处理效果

  • 设置实时光线追踪

在同一个场景中可以添加多个带Volume组件的GameObject。每个Volume的模式可以按照需要设置成Global 或是 Local

Global意味着这个Volumn针对整个场景生效。不管相机朝向哪里,Global模式的Volumn都会被计算在内。

Local模式的Volumn则只能在它的碰撞体作用区域内生效。一般Box Collider即可满足碰撞体需要。

Exposure

曝光指的是相机感光元器件接受的光线强度。曝光的数值越高,相机接受的光线强度越大。

参数说明

Exposure重载包括四种模式:Fixed、Automatic、Curve Mapping 和 Use Physical Camera

Fixed 固定曝光

用于曝光变化不大的场景,也就是场景中的各个区域有类似的曝光强度。

可以使用多个被设置为Local模式的Volume,通过Blend Distance(混合距离)来融合切换不同的Volume的曝光。

由于HDRP的光照系统是完全基于物理的,所以可以参考物理世界真实的曝光值。

5cfd281ebd4bad3543fc5a737f12e351.png

Automatic 自动曝光

根据当前相机的位置信息和相关场景信息来自动调节曝光。非常适用于曝光随位置不同而发生变化的场景,例如从光照强烈的户外进入阴暗的室内或者从山洞中移动到山洞外

可以模拟现实中人眼适应不同曝光的情况。

Limit Min & Limit Max

分别用于控制场景自动曝光的最小值和最大值。可以用这两个值分别调节当相机处于场景的暗部和亮部时整体的曝光。

99b37196985c18f0e7285b3578469b4e.png

当画面靠近场景中的亮部,周围的环境会变暗,但是环境也许不应该“那么”暗。此时可以将Limit Max 调低,以提升整个画面的曝光。f688c683062bbe0c79c0bb2e09b5814d.png

当画面处于场景的暗部,比如在黑暗的角落朝外看,那么外部环境会变亮,可能导致画面亮部曝光过度(月光即使很亮也不应该像白天)此时可以将LImit Min 调高,降低整个画面的曝光。

Curve Mapping 曲线控制曝光

X轴代表目前场景的曝光

Y轴代表我们想要的曝光

专家使用。

Use Physical Camera 物理相机控制曝光

通过物理相机的Iso、Shutter Speed 和 Aperture 参数来控制曝光。

Volume 界面中只剩下Compensation(补偿)这一项。

Metering Mode

用于控制如何采样用于曝光的计算场景中的灯光。

  1. Average 针对整个画面进行采样,结果稳定

  2. Post 针对画面中央区域采样。如果画面中央的采样区域较暗,整个画面可能曝光过度。

  3. Center Weighted

Visual Environment

Intro to HDRP & DXR

从2018版本开始,Unity Editor中共有三套渲染管线。

HD Render Pipeline Asset

HDRP配置文件(HD Render Pipeline Asset)管理HDRP项目的所有渲染功能。HDRP会用这个配置文件生成一个HDRP渲染管线的实例,而这个渲染管线的实例包含用于渲染的中间资源。

为了满足项目开发中不同的需求,HDRP还提供了另外两种渲染方式。

针对不同平台使用不同的HDRP配置文件

在同一个HDRP项目中可以创建多个HDRP配置文件,针对不同的计算平台应用不同的HDRP配置文件。

要针对不同的平台使用不同的设置,需要将对应的HDRP配置文件关联到不同的质量等级上。

质量设置里面的HDRP配置文件会覆盖默认的配置文件。

Frame Settings

帧设置针对的是场景中的Camera、Baked or Custom Reflection 和 Realtime Reflection 的相关设置。后面两个反射相关的设置应用在Reflection Probe上

帧设置的优先级低于HDRP配置文件,也就是说,如果在HDRP配置文件中没有打开某项功能,那么帧设置中对应的功能就会自动被禁用。

帧设置可以让我们为不同的相机和反射探针启用/禁用不同的HDRP功能组合。

Volume 框架

Volume 的作用是通过调整各项HDRP功能的参数,影响相机所看到画面的最终渲染效果。

dd003dc04f47c381deb98503587eb575.png

  • 每个HDRP项目中可以有多个HDRP配置文件

    每个配置文件对应不同的画质或目标平台。但是HDRP项目每次只能使用一个HDRP配置文件,也无法在运行时切换HDRP配置文件。

  • HDRP会为Camera(相机),Backed or Custom Reflection(烘培或自定义反射)和Realtime Reflection(实时反射)提供一套默认的帧设置。如果在HDRP配置中没有被启用,就会在帧设置里被禁用。

  • 注意,在HDRP配置文件中已经启用的功能,也要确保默认帧设置中启用了相关功能。即在HDRP配置文件中启用了某个功能,但是没有在帧设置中启用它,那么在项目中也是无法使用它。

  • 可以为场景中一个(或者多个)相机和反射探针自定义帧设置。 如果在这些自定义帧设置中启用某个功能(前提是在HDRP配置文件中已经启用),那么自定义帧设置中的配置信息会覆盖(Override)默认帧设置中的配置信息。

  • 可以为同一个场景这创建多个Volume。这些Volume的模式可能是全局(Global)的或者是本地(Local)的。如果在当前活跃相机的帧设置(如果没有启用自定义帧设置,就使用默认帧设置)中没有启用某个功能,比如Fog,那么在与此相机相关的Volume中调整Fog参数值就没有意义。

HD Render Pipeline Asset中的7类参数

Rendering

  1. Color Buffer Format

    出于对性能的考虑,HDRP默认使用R11G11B10格式(不包含Alpha通道)

    如果要把HDRP渲染的画面合成到另外的图片上,就需要包含Alpha通道,这时就要选择R16G16B16A16格式。带有Alpha通道的格式会对性能造成一定的影响。

    如果需要将R16G16B16A16格式作为最终渲染输出的格式,那么在Post Processing的Buffer Format中也要选择相同的格式。

  2. Lit Shader Mode

    Lit Shader 是HDRP材质使用的默认着色器。可以选择以下三种模式:

    1. Forward:Lit Shader仅使用前向渲染

    2. Deferred:Lit Shader会使用延迟渲染,一些高级材质还会使用前向渲染。

    3. Both:延迟和前向渲染都可用

      选择Both模式可以通过自定义帧设置为相机选择Deffered或者Forward渲染。不过此模式会让HDRP为两种渲染方式都编译相关的着色器变体。

      如果选择前向渲染或Both模式,则可以选择MSAA

  3. Motion Vector

    HDRP可以在屏幕空间反射和运动模糊中使用运动矢量。通过Camera组件启用的TAA必须使用运动矢量才能工作。如果禁用此选项,则运动模糊和TAA功能将不会正常工作,屏幕空间反射则会用低质量渲染模式。

  4. Runtime Debug Display

    启用该选项后可以在运行时显示灯光和材质的属性信息。正式出包时建议禁用。

  5. Dithering Cross-fade 平滑转换

    这是与Game Object的LOD转换相关的功能。启用该选项后可以让HDRP在做LOD转换时进行平滑的转换。

  6. Terrain Hole 地形洞

    启用该选项后可以显示地形上的凹陷孔洞。如果禁用此选项,则地形上的孔洞不会显示。

  7. 如果你的场景中没有使用透明材质或者没有在Lit材质中使用相关选项,则可以禁用以下选项以减少构建时间。

    1. Transparent Backface 透明背面
    2. Transparent Depth Prepass 透明深度预处理
    3. Transparent Depth Postpass 透明深度后处理

    4e7db5f02099684052b94ad28a7b96ec.png

  8. Custom Pass 自定义通道

    如果没有使用Custom Pass 功能,禁用此功能可以节约内存。

  9. Realtime Raytracing 实时光线追踪

    如果要在HDRP项目中使用实时光线追踪功能,则需要先启用此选项。

  10. LOD Bias LOD 偏差

    场景中的相机会使用此数值来计算LOD偏差。

  11. Maximum LOD Level 最大LOD级别

    用于设置相机支持的最大LOD级别。

  12. Decals 贴花

    1. Draw Distance 用于定义相机离开物体多远以后不再渲染贴花。

    2. Atlas Width和Atlas Height 用于设置纹理图集的宽度和高度。这个纹理图集用于保存场景中所有投射在透明表面上的贴花。

    3. Metal and Ambient Occlusion Properties 启用该选项后,贴花能够影响材质上的金属高光和环境光遮蔽。

    4. Maximum Clustered Decals on Screen 屏幕上能够同时显示的贴花数量(这些贴花影响的是透明表面)。
      e9609527dd29619d4590b419e98c8923.png

  13. Dynamic Resolution 动态分辨率

  14. Low res Transparency 低分辨率透明

    启用该选项后使用低分辨率的透明效果。

Lighting

  1. SSAO(Screen Space Ambient Occlusion)启用此选项后可以为场景添加基于屏幕空间计算的环境光遮蔽效果。可以在Volume中的Ambient Occlusion Override中对效果进行调整。

  2. Volumetrics 体积光

    为场景中的灯光和雾效增加体积光效果。

  3. Light Layers 光照层可以

    在这里启用/禁用光照层(Light Layers)功能。此功能可以让场景中的光源只照亮指定的物体,忽略无关的物体。在HDRP Global Settings -> Layers Names 中可以见到对应层

  4. Cookies 光线遮罩

  5. Reflections 反射
    9494534745278471e7e3a3217ea91803.png

  6. Sky 天空
    2524c9d6c97cc60d8bb2ac333a2fe060.png

    1. Reflection 当场景中没有如何反射探针可以用于计算物体表面的反射信息时,HDRP会使用天空盒Cubemap来计算反射信息。Reflection Size可以控制用于计算反射信息的天空盒Cubemap的分辨率。此分辨率斌不会影响天空盒本身的质量。

    2. Lighting Override Mask 可以让环境光照和天空背景进行分离。如果在此指定了一个Layer而不是使用默认的Nothing,那么HDRP会在场景中寻找与此Layer相关联的Game Object,如果找到的GameObject中包含Volume组件而且可以对当前相机产生影响,那么HDRP就会使用这些Volumn中的信息来计算环境光照。

  7. Shadow 阴影
    d1b2413c2a758ae18a76e6a00215c0a2.png

    1. Shadowmask

      控制Shadowmask光照模式(Shadowmask Lighting Mode)的启用/禁用。

    2. Maximum Shadows on Screen 同屏显示最大阴影数量

      超过这里设定的阴影数量之外的阴影将不被渲染。

    3. Filtering Quality 过滤质量

      选择高质量可以提升阴影质量,减少阴影边缘的锯齿。在Forward模式和Both模式下,可以选择Low,Medium和High三档质量。在Deferred模式下,只能使用Medium质量。

    4. Screen Space Shadows

      启用该选项后,HDRP会在一个单独的通道中计算基于屏幕空间的阴影。

    5. Use Contact Shadows 使用接触阴影

      此处可以选择Low、Medium、High质量。然后在Light组件中可以选择可用的接触阴影质量。

      如果在HDRP配置文件中不勾选上述任何一项,那么只能在Light组件中选择Custom选项。

      要使用接触阴影,需要在Default Frame Settings中启用Contact Shadows选项。

Lighting Quality Settings

光照质量的相关设置,配合Lighting中的参数使用,调整相应数值以达到质量与性能的平衡。

Material

  1. Available Material Quality Levels 可用材质质量等级

    默认所有材质质量都可以使用

  2. Default Material Quality Level 默认材质质量等级

  3. Distortion 变形

  4. Subsurface Scattering 次表面散射

    可以很好的表示光在材质内多次反弹折射等等效果,玉石、翡翠等应用此效果明显。

  5. Fabric BSDF Convolution                             

    若启用此选项,则在使用Fabric材质时,HDRP会单独为织物着色器(Fabric Shader)计算一份反射探针数据,用于生成更准确的光照效果。不过这样做会导致项目中存在两份光照数据,也会导致目前可见的反射探针数量减少一半。

  6. Diffusion Profile List 漫射配置文件列表

    在此保存用于控制次表面散射效果和半透明效果的Diffusion Profile。一个HDRP配置文件最多可以保存15个Diffusion Profile。

Post-processing

  1. Grading LUT Size & Grading LUT Format

    Size用于控制颜色分级时所用的LUT(Lookup Texture)的大小。默认数值32提供了比较平衡的速度和质量表现。

    Format用于设置LUT的编码格式。可以选择R11G11B10R16G16B16A16或者R32G32B32A32格式。精度越高的格式颜色越精准。

Post-processing Quality Settings 后处理质量设置

与光照质量设置类似,可以为后处理效果设置采样值。

e21c555d3500b554ee4bec37f8590f04.png

目前可以设置Depth of Field、Motion Blur、Bloom、Chromatic Aberration(色差)

XR设置

HDRP在特定的平台上输出VR应用

PC需要DX11支持

Ray Tracing

Unity 支持的光追是基于HDRP的,一个光追的Unity项目,首先是一个HDRP项目

利用await/async 与 Task 关键词替换协程功能

Intro

参考视频:# Unity async / await: Coroutine’s Hot Sister [C# & Unity]

协程是Unity提供的异步解决方法,但是实际应用时有诸多不便与不优雅之处。本文将用几个例子对比参照协程与await/async关键字。当然,Unity指出,在Unity中写多线程代码应当参考Unity - Manual: C# Job System

旧例

利用协程转动三个物体,并使得这三个物体逐个停止。

给出下列协程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using UnityEngine;

public class Shape: MonoBehaviour()
{
public IEnumerator RotateForSeconds(float duration)
    {
    var end = Time.time + duration;
    while(Time.time < end)
    {
    transform.Rotate(new Vector3(1,1)*Time.deltaTime*150;
    yield return null;
    }
    }
}

给出下列调用协程的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using UnityEngine;

public class ShapeManager: MonoBehaviour()
{
[SerializeField] private Shape[] _shapes;

public void BeginTest()
{
for(var i = 0; i<_shapes.Length;i++)
{
StartCoroutine(_shapes[i].RotateForSeconds(1+1*i);
}
}
}

Shader中的光照模型

Lambert光照模型
在现实生活中,一个物体的颜色是由射进眼睛里的光线决定的。
当光线射到物体表面时,一部分被物体表面吸收,另一部分被反射。
对于透明物体而言,还有一部分光会穿过透明体,产生透射光。
被物体吸收的光会产生热量,只有反射光和透射光才能进入眼睛。
从而产生视觉效果(物体呈现出亮度和颜色)。
所以,物体表面的光照颜色是由入射光、物体材质、以及材质和光的交互规律共同决定。

最常用的是Lambert光照模型
因为它能比较真实地还原粗糙物体表面与光地交互行为,并且计算效率非常高,因此即便是现在也依然被广泛应用与游戏渲染。

Lambert 光照渲染模型
当光线照射到表面粗糙的物体,例如:石灰墙壁、纸张、布料等,光线会向各个方向等强度的反射,这种现象称为光的漫反射现象。

漫反射满足 Lambert 定律(Lambert’s Law):反射光线强度与表面法线和光源方向之间的夹角成正比。

产生漫反射的物体被称为理想漫反射物体,这种光照模型称为Lambert光照模型。(**基本光照模型系列——1.Lambert光照模型:https://zhuanlan.zhihu.com/p/144681176**)

Cdiffuse = (Clight · Mdiffuse)saturate(n·l)

其中,Cdiffuse是物体的漫反射颜色;Clight为入射光的颜色;Mdiffuse为物体的漫反射颜色;n为物体表面的法线;l为物体的灯光方向

为了避免负值的出现,因此使用CG数学函数 saturate( )将点积结果截取到[0,1]的区间范围内

在Shader中获取灯光变量
灯光参数如何传递给Shader取决于当前Unity项目使用的渲染路径(Rendering Path),以及Shader中用于声明灯光模式的Pass Tag。

查看当前项目的渲染路径:
Edit>Project Settings>Graphics

在Low,Medium,High 这三个等级中,可以发现Unity已经默认将Rendering Path 设置成了 Forward(向前渲染)。

变量 类型 说明
_LightColor0 fixed4 灯光的颜色乘上亮度,在UnityLightingCommon.cginc中被声明
_WorldSpaceLightPos0 float4 平行光属性:float4(世界空间灯光方向,0)
其他灯光属性:float4(世界空间灯光位置,1)
_LightMatrix0 float4x4 世界到灯光的变换矩阵,用于采样灯光 cookie 和衰减贴图,在AutoLight.cginc中被声明
unity_4LightPosX0,
unity_4LightPosY0,
unity_4LightPosZ0
float4 前四个非重要带点光在世界空间的位置,只能用于ForwardBass pass
unity_4LightAtten0 float4 前四个非重要点光的衰减系数,只能用于ForwardBass pass
untiy_LightColor half4[4] 前四个非重要点光的颜色,只能用于ForwardBass pass
unity_WorldToShadow float4x4[4] 世界到阴影的变换矩阵,一个用于聚光灯矩阵,最多4个用于串联平行光的矩阵。

基于 Lambert 光照模型的 Shader
使用一个平行光进行照射,此处代码运行在URP渲染管线下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Shader "Custom/Lambert"
{
    Properties
    {
        _MainColor("Main Color",Color) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            # pragma vertex vert
            # pragma fragment frag
            # include "UnityCG.cginc"
            //声明包含灯光变量的文件
            # include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed4 dif : COLOR0;
            };
             fixed4 _MainColor;
             fixed3 l;
             v2f o;
             v2f vert(appdata_base v)
             {
                 o.pos = UnityObjectToClipPos(v.vertex);
                 //法线向量
                 float3 n = UnityObjectToWorldNormal(v.normal);
                 n = normalize(n);
                 //灯光方向向量
                 l = normalize(_WorldSpaceLightPos0.xyz);
                 //按照公式计算漫反射
                 fixed ndotl = dot(n, l);
                 o.dif = _LightColor0 * _MainColor * saturate(ndotl);
                 return o;
             }
             fixed4 frag(v2f i) : SV_Target
             {
                 return i.dif;
             }
             ENDCG
        }

    }

}

注意,标红部分与《Unity ShaderLab 新手宝典》上的代码有出入,原书代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Shader "Custom/Lambert"
{
    Properties
    {
        _MainColor("Main Color",Color) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            # pragma vertex vert
            # pragma fragment frag
            # include "UnityCG.cginc"
            //声明包含灯光变量的文件
            # include "UnityLightingCommon.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed4 dif : COLOR0;
            };

             fixed4 _MainColor;

             v2f vert(appdata_base v)
             {
                 v2f o;
                 o.pos = UnityObjectToClipPos(v.vertex);

                 //法线向量
                 float3 n = UnityObjectToWorldNormal(v.normal);
                 n = normalize(n);

                 //灯光方向向量

                 fixed3 l = normalize(_WorldSpaceLightPos0.xyz);

                 //按照公式计算漫反射
                 fixed ndotl = dot(n, l);
                 o.dif = _LightColor0 * _MainColor * saturate(ndotl);
                 return o;
             }
             fixed4 frag(v2f i) : SV_Target
             {
                 return i.dif;
             }
             ENDCG
        }

    }

}

fe5bd2e763c71fc77830474b208a8ba0.png

CG标准库函数
一些现成的数学函数

函数 说明
abs(x) 返回x的绝对值
ceil(x) 返回大于x的最小整数
clamp(x,a,b) 将x限制在[min,max]范围内
cos(x) 返回x的余弦值
cross(A,B) 返回A和B的叉积,A,B必须为三维向量
degrees(x) 将弧度转变为角度
dot(A,B) 返回A和B的点积
floor(x) 返回小于x的最大整数
fmod(x,y) 返回x/y的浮点型余数
frac(x) 返回x的小数部分
lerp(a,b,f) 线性插值,返回(1-f) * a + b*f
max(a,b) 返回 a、b中的最大值
min(a,b) 返回a、b中的最小值
mul(M,V) 使用矩阵M对向量V进行变换
pow(x,y) 返回x的y次幂
radians(x) 将度数转变为弧度
round(x) 返回最接近x的整数,相当于对x的四舍五入
saturate(x) 将x范围截取到[0,1]
sign(x) 如果 x >0,返回1;如果 x <0,返回-1;否则返回0
sin(x) 返回x的正弦值
step(a,x) 如果 x < a,返回0;如果 x ≥ a,返回1
sqrt(x) 返回x的平方根
distance(pt1,pt2) 返回pt1与pt2之间的距离
length(v) 返回向量v的长度
normalize(x) 返回方向相同,长度为1的向量
reflect(I,N) 输入入射方向 I 和表面法线 N,返回反射向量,只能用于三维向量。
reflect(I,N,eta) 输入入射方向 I 和表面法线 和折射率eta,返回折射向量,只能用于三维向量。

CG标准库中的函数是针对于硬件设备底层实现的算法,因此可以非常快速地执行。
虽然对于一些简单的算法也可以自己输入公式计算,但是使用CG标准库里的函数更节省性能,
因此建议大家尽量使用CG标准库中的函数进行计算。

如果想要了解更多CG标准库的函数,可以阅读Nvidia的CG用户手册。

Half-Lambert 光照模型
使用Lambert光照模型有一个明显的缺点,那就是物体背光完全是黑的,看不到表面的任何细节,以至于只能再添加一盏辅助光照亮物体表面,这非常不利于性能的优化。

计算公式为:

Cdiffuse = (Clight · Mdiffuse)[0.5(n·l)+ 0.5]

其中,Cdiffuse是物体的漫反射颜色;Clight为入射光的颜色;Mdiffuse为物体的漫反射颜色;n为物体表面的法线;l为物体的灯光方向

与Lambert光照模型不同的是,表面法线与光照方向点积之后并不是直接截取到区间[0,1],而是先乘以0.5将数值区间缩小到[-0.5,0.5],
然后加上0.5,将区间移动到[0.1]。
如此一来,物体的光照强度会从最亮的迎光面逐渐过度到最暗的被光面。

将Lamber光照Shader改写为Hafl-Lambert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
v2f vert(appdata_base v)
             {
                 v2f o;
                 o.pos = UnityObjectToClipPos(v.vertex);

                 //法线向量
                 float3 n = UnityObjectToWorldNormal(v.normal);
                 n = normalize(n),

                 //灯光方向向量
                 fixed3 l = normalize(_WorldSpaceLightPos0.xyz);

                 //按照公式计算漫反射
                 fixed ndotl = dot(n, l);
                 o.dif = _LightColor0 * _MainColor * (0.5 * ndotl + 0.5);
                 return o;
             }

完整Shader代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Shader "Custom/TestShader"
{

        Properties
        {
            _MainColor("Main Color",Color) = (1,1,1,1)
        }
            SubShader
        {
            Pass
            {
                CGPROGRAM
                # pragma vertex vert
                # pragma fragment frag
                # include "UnityCG.cginc"
                //声明包含灯光变量的文件
                # include "UnityLightingCommon.cginc"
                struct v2f
                {
                    float4 pos : SV_POSITION;
                    fixed4 dif : COLOR0;
                };
                 fixed4 _MainColor;
                 fixed3 l;
                 v2f o;
                 v2f vert(appdata_base v)
                 {

                     o.pos = UnityObjectToClipPos(v.vertex);
                     //法线向量
                     float3 n = UnityObjectToWorldNormal(v.normal);
                     n = normalize(n),
                         //灯光方向向量
                          l = normalize(_WorldSpaceLightPos0.xyz);
                     //按照公式计算漫反射
                     fixed ndotl = dot(n, l);
                     o.dif = _LightColor0 * _MainColor * (0.5 * ndotl + 0.5);
                     return o;
                 }
                 fixed4 frag(v2f i) : SV_Target
                 {
                     return i.dif;
                 }
                 ENDCG
            }
        }
}

问题同上,效果:
球体的背光部分多了很多细节,不再是全黑的了
0d4f61b2e7c02aa9d97717fc58bce28f.png

Phong 光照模型

Lambert模型能够较好地模拟出粗糙物体表面的光照效果,但是在真实环境中还存在很多表面光滑的物体,例如金属、陶瓷、塑料等,而Lambert光照模型却无法对此进行很好的表现,因此引入表面镜面反射的光照模型 —— Phong 光照模型

Phong 光照模型理论
Bui Tuong Phong提出一种局部光照的经验模型,他认为物体表面反射光线由三部分组成:

SurfaceColor = CAmbient +  CDiffuse + CSpecular

其中,CAmbient为环境光,CDiffuse为漫反射,CSpecular为镜面反射。
物体不仅会受到灯光的直接照射,还会受周围环境光(Ambient)的影响。
274c7a52e0d094238b732891330f705d.png
如图所示,当光线照射到一个表面光滑的物体时,除了有漫反射光光(Diffuse)之外, 从某个角度还可以看到很强的反射光。
这是因为在接近镜面反射角的一个固定区域内,大部分入射光会被反射,这种现象称为镜面反射(Specular)。

镜面反射的公式为:

CSpecular = (Clight · Mspecular)saturate(v·r)Mshininess

其中,Clight为灯光强度,Mspecular为物体材质的镜面反射颜色,v为视角方向(由顶点指向摄像机),r为光线的反射方向, Mshininess为物体材质的反光度。

由于这种光照模型在当时影响力相当广泛,即便是现在很多游戏引擎依然还在使用。
为了表现 Bui Tuong Phong 对图形渲染领域做出的突出贡献,后人就以他的名字命名这种光照模型——Phong模型

在Shader中获取环境光变量

Phong光照模型中会用到环境变量,一些Unity提供的可以直接使用的环境光变量如下表给出,表中的变量在UnityShaderVariables.cginc中给出。

变量 类型 说明
unity_AmbientSky fixed4 Gradient类型环境中的Sky Color
unity_AmbientEuator fixed4 Gradient类型环境中的Equator Color
unity_AmbientGround fixed4 Gradient类型环境中的Ground Color
UNITY_LIGHTMODEL_AMBIENT fixed4 Gradient类型环境中的Sky Color,将被 unity_AmbientSky 取代

查看当前场景中的环境光:
依次点击菜单
Window > Rendering > Lighting
a31464ec3e81577883472f2f277456c0.png

需要注意的是,要把默认的Skybox类型改为Gradient类型,才能使用上述的变量。

基于 Phong 光照的 Shader

1
2
3
4
5
6
Properties
{
    _MainColor("Main Color",Color) = (1,1,1,1)
    _SpecularColor("Specular Color",Color) = (0,0,0,0)
    _Shininess("Shininess",Range(1,100)) = 1
}

在Properties代码块增加了控制高光颜色的 _SpecularColor 属性和控制光泽度的 _Shininess 属性。

1
2
3
4
float3 n = UnityObjectToWorldNormal(v.normal);
n = normalize(n);
fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
fixed3 view = normalize(WorldSpaceViewDir(v.vertex));

在顶点着色器中,计算出标准化的世界空间法线向量n、世界空间灯光方向l和时间空间视角方向 view,为后续计算光照效果做好准备。
漫反射光照部分还是继续沿用 Lambert 光照模型计算方法。

1
2
3
4
float3 ref = reflect(-1,n);
ref = normalize(ref);
fixed rdotv = saturate(dot(ref,view));
fixed4 spec = _LightColor0 * _SpecularColor * pos(rdotv,_Shininess);

在计算镜面反射的过程中,通过使用 CG 函数 reflect() 得到光线的反射向量ref,由于函数需要传入的是灯光指向顶点的方向,而使用WorldSpaceViewDir()函数得到的是顶点指向灯光的方向,所以需要将向量l乘以-1进行反向。

将标准化之后的ref与view点乘得到 rdotv,然后按照镜面反射的计算公式得到镜面反射部分光照。

o.color = unity_AmbientSky + dif + spec
最后把环境光、漫反射和镜面反射相加,得到最终的光照。
片段着色器的计算跟之前一样保持不变。

完整的 Shader 代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader "Custom/TestShader"
{

    Properties
    {
        _MainColor("Main Color",Color) = (1,1,1,1)
        _SpecularColor("Specular Color",Color) = (0,0,0,0)
        _Shininess("Shininess",Range(1,100)) = 1
    }
            SubShader
        {
            Pass
            {
                CGPROGRAM
                # pragma vertex vert
                # pragma fragment frag
                # include "UnityCG.cginc"
                //声明包含灯光变量的文件
                # include "Lighting.cginc"
                struct v2f
                {
                    float4 pos : SV_POSITION;
                    fixed4 color : COLOR0;
                };
                 fixed4 _MainColor;
                 fixed4 _SpecularColor;
                 half _Shininess;
                 v2f vert(appdata_base v)
                 {
                     v2f o;
                     o.pos = UnityObjectToClipPos(v.vertex);
                     //计算公式中各个变量
                     float3 n = UnityObjectToWorldNormal(v.normal);
                     n = normalize(n);
                     fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                     fixed3 view = normalize(WorldSpaceViewDir(v.vertex));
                     //漫反射部分
                     fixed ndotl = saturate(dot(n, l));
                     fixed dif = _LightColor0 * _MainColor * ndotl;
                     //镜面反射部分
                     float3 ref = reflect(-1, n);
                     ref = normalize(ref);
                     fixed rdotv = saturate(dot(ref, view));

                     fixed4 spec = _LightColor0 * _SpecularColor * pow(rdotv,  _Shininess);

                     //环境光+满发射+镜面反射
                     o.color = unity_AmbientSky + dif + spec;
                     return o;
                 }
                 fixed4 frag(v2f i) : SV_Target
                 {
                     return i.color;
                 }
                 ENDCG
            }
        }
}

ac99e53530b50df237209d7e731034d8.png

将改写好的Shader指定给之前的材质,然后将环境光设置为 Gradien 类型。
如图所示,可以看到不同部分的灯光效果以及最终完整的光照效果。
其中环境光效果与方向无关,物体任意部位的强度都一样;漫反射效果与Lambert一样,强度随着物体表面方向的不同而改变;
镜面反射效果比较聚集吗,但强度很大。

环境光 + 漫反射 + 镜面反射 = 完整光照效果。

逐像素光照
使用上述Phong光照Shader之后,不难发现这样一个现象:
高光部分与平时看到的不太一样。
印象中的高光点应该是边缘很圆滑的,而现在却不清晰。

出现这样的现象是因为光照模型的计算一直是逐顶点的(Per-Vertex)光照,而不是逐像素(Per-Pixel)光照。

逐顶点光照是在顶点着色器中计算光照颜色。
计算过程中,GPU将为多边形的每个顶点执行一遍光照计算,得到顶点颜色,然后顶点在多边形上所占的范围对像素颜色进行线性插值。
对于细分较高的模型,由于每个多边形所占范围很小,因此插值之后每个像素的误差很小,所以逐顶点光照基本上已经可以满足需求了。

逐像素光照其实就是在像素着色器(也就是Unity中的片段着色器)中计算光照颜色。
在像素着色器中,颜色的计算就不再是基于顶点了,而是基于像素的计算,所以最终屏幕的分辨率越高计算量越大。

如果想要解决高光不清晰的问题,可以使用一个细分程度更高的多边形,或者将逐顶点光照改逐像素光照。

在Phong Shader的基础上,使用改为逐像素光照的方法:

1
2
3
4
5
6
struct v2f
{
    float4 pos : SV_POSITION;
    float3 normal : TEXCOORD0;
    float4 vertex : TEXCOORD1;
};

由于不是在顶点着色器中计算光照颜色,而是将所有需要的数据都传到片段着色器中进行计算,因此在v2f结构体中不再需要保存顶点色的变量,而是添加了normal 和 vertex 两个变量,分别用来保存世界空间法线向量和顶点坐标。

1
2
3
4
5
6
7
8
9
v2f vert(appdata_base v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.normal = v.normal;
    o.vertex = v.vertex;

    return o;
}

在顶点着色器中,只将世界空间法线向量和顶点坐标传递给片段着色器,除此之外不做其他计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fixed4 frag(v2f i) : SV_Target
{
    //计算公式中的所有变量
    float3 n = UnityObjectToWorldNormal(i.normal)
    n = normalize(n);
    fixed3 l = normalize(_WorldSpaceLightPos0,xyz);
    fixed3 view = normalize(WorldSpaceViewDir(i.vertex));

    //漫反射部分
    fixed ndotl = saturate(dot(n,l));
    fixed4 dif = _LightColor0 * _MainColor * ndotl;

    //镜面反射部分
    float3 ref = reflect(-1.n);
    ref = normalize(ref);
    fixed rdotv = saturate(dot(ref,view));
    fixed4 spec = _LightColor0 * _SpecularColor * pow(rdotv,_Shininess);

    //环境光 + 漫反射 + 镜面反射
    return unity_AmbientSky + dif  + spec;
}

片段着色器的计算方法和在顶点着色器中的计算是一样的。

完整的Shader代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Shader "Custom/per-pixel Phong"
{

    Properties
    {
        _MainColor("Main Color",Color) = (1,1,1,1)
        _SpecularColor("Specular Color",Color) = (0,0,0,0)
        _Shininess("Shininess",Range(1,100)) = 1
    }
            SubShader
        {
            Pass
            {
                CGPROGRAM
                # pragma vertex vert
                # pragma fragment frag
                # include "UnityCG.cginc"
                //声明包含灯光变量的文件
                # include "Lighting.cginc"
                struct v2f
                {
                   float4 pos : SV_POSITION;
                   float3 normal : TEXCOORD0;
                   float4 vertex : TEXCOORD1;
                };
                 fixed4 _MainColor;
                 fixed4 _SpecularColor;
                 half _Shininess;
                 v2f vert(appdata_base v)
                 {
                     v2f o;
                     o.pos = UnityObjectToClipPos(v.vertex);
                     o.normal = v.normal;
                     o.vertex = v.vertex;
                     return o;
                 }
                 fixed4 frag(v2f i) : SV_Target
                 {
                     //计算公式中的所有变量
                     float3 n = UnityObjectToWorldNormal(i.normal);
                     n = normalize(n);
                     fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                     fixed3 view = normalize(WorldSpaceViewDir(i.vertex));
                     //漫反射部分
                     fixed ndotl = saturate(dot(n,l));
                     fixed4 dif = _LightColor0 * _MainColor * ndotl;
                     //镜面反射部分
                     float3 ref = reflect(-1,n);
                     ref = normalize(ref);
                     fixed rdotv = saturate(dot(ref,view));

                     fixed4 spec = _LightColor0 * _SpecularColor *  pow(rdotv,_Shininess);

                     //环境光 + 漫反射 + 镜面反射
                     return unity_AmbientSky + dif + spec;
                 }
                 ENDCG
            }
        }
}

fc4b997a99bfbd780845f1b7a902c15a.png
逐像素光照(上图)比逐顶点光照更加细腻。

使用逐像素计算不仅可以提升光照效果的精确度,还可以在渲染时添加多边形原本并不存在的表面细节。
例如使用 Normal Map 可以在像素级别上使原本平坦的表面表现出凹凸效果。

逐像素提供了更好的表现效果。
但是由于逐像素的计算量比逐顶点要大,性能要求更高。
在具体实际使用中究竟做作何选择,需要开发者考虑最终运行的终端设备。

Blinn-Phong 光照模型

Blinn-Phong 光照模型不再使用反射向量r计算镜面反射,而是使用半角向量 h 代替 rh为表视角方向 v 和灯光方向 l 的角平分线。

4da25ae9f824a8a0bb6e1cca38ba940b.png

Blinn-Phong shading using WebGL:https://www.geertarien.com/blog/2017/08/30/blinn-phong-shading-using-webgl

半角向量的计算公式为:

h = normalize( v + l )

Blinn-Phong 镜面反射的计算公式为:

CSpecular = (Clight · Mspecular)saturate(*n *· h)Mshininess

Blinn-Phong光照模型的Shader
改写的片段着色器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fixed4 frag(v2f i) : SV_Target
                 {
                     //计算公式中的各个变量
                     float3 n = UnityObjectToWorldNormal(i.normal);
                     n = normalize(n);
                     fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                     fixed3 view = normalize(WorldSpaceViewDir(i.vertex));

                     //漫反射部分
                     fixed ndotl = saturate(dot(n,l));
                     fixed4 dif = _LightColor0 * _MainColor * ndotl;

                     //镜面反射部分
                     float3 h = normalize(l + view);
                     fixed rdoth = saturate(dot(n,h));

                     fixed4 spec = _LightColor0 * _SpecularColor *  pow(rdoth,_Shininess);

                     //环境光 + 漫反射 + 镜面反射
                     return unity_AmbientSky + dif + spec;
                 }

完整的Shader代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
hader "Custom/TestShader"
{
    Properties
    {
        _MainColor("Main Color",Color) = (1,1,1,1)
        _SpecularColor("Specular Color",Color) = (0,0,0,0)
        _Shininess("Shininess",Range(1,100)) = 1
    }
        SubShader
    {
        Pass
        {
            CGPROGRAM
            # pragma vertex vert
            # pragma fragment frag
            # include "UnityCG.cginc"
            //声明包含灯光变量的文件
            # include "Lighting.cginc"
            struct v2f
            {
               float4 pos : SV_POSITION;
               float3 normal : TEXCOORD0;
               float4 vertex : TEXCOORD1;
            };
             fixed4 _MainColor;
             fixed4 _SpecularColor;
             half _Shininess;
             v2f vert(appdata_base v)
             {
                 v2f o;
                 o.pos = UnityObjectToClipPos(v.vertex);
                 o.normal = v.normal;
                 o.vertex = v.vertex;
                 return o;
             }
             fixed4 frag(v2f i) : SV_Target
             {
                 //计算公式中的各个变量
                 float3 n = UnityObjectToWorldNormal(i.normal);
                 n = normalize(n);
                 fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                 fixed3 view = normalize(WorldSpaceViewDir(i.vertex));
                 //漫反射部分
                 fixed ndotl = saturate(dot(n,l));
                 fixed4 dif = _LightColor0 * _MainColor * ndotl;
                 //镜面反射部分
                 float3 h = normalize(l + view);
                 fixed rdoth = saturate(dot(n,h));

                 fixed4 spec = _LightColor0 * _SpecularColor *  pow(rdoth,_Shininess);

                 //环境光 + 漫反射 + 镜面反射
                 return unity_AmbientSky + dif + spec;
             }
             ENDCG
        }
    }
}

d43e34d7781cf4942de3a85e5ccb97f4.png

Blinn-Phong 效果与 Phong 效果在视觉上的差距并不是很大。

在性能方面,当观察者和灯光离被照射物体非常远时,Blinn-Phong 的计算效率要高于Phong。
因为 h 是取决于视角方向以及灯光方向的,两者都很远时 h 可以被认为是常量,跟位置以及表面曲率没有关系,因此可以大大减少计算量。
而 Phong 却要根据表面曲率去逐顶点或逐像素计算反射向量 r ,相对而言计算量比较大。

灯光阴影
“平行光和环境光对物体照明”的效果在上述Shader中已实现。
但是目前的Shader在实际游戏中应用是不太完美的。
例如:开启灯光的投射阴影选项,物体上没有任何阴影,也不会接受其他物体投射的阴影;
当灯光类型切换为聚光灯或者点光源的时候,光照效果依然和平行光一样,只会受到方向的影响,更改灯光位置不会有任何光照变化。

渲染路径
Unity支持不同的渲染路径(Rendering Path),不同的渲染路径在光照和阴影方面会有不同的功能特点。
当显卡不能使用选定的渲染路径进行渲染时,Unity会自动选择一个较低精度的渲染路径。
例如,当设置的延迟着色(Deferred Shading)不能被执行,前向渲染(Forward Rendering)将会被采用。

1.延迟着色渲染路径

延迟着色顾名思义是将着色步骤延迟处理的渲染方式,是实现最逼真光影效果的渲染路径,即使场景中有成千上万个实时灯光,依然可以保持比较流畅的渲染帧率,但是它需要硬件达到一定的级别才能使用。

当使用延迟着色时,灯光 Pass 基于G-Buffer(Geometry Buffer,屏幕空间缓存)和深度信息计算光照,光照在屏幕空间进行计算,因此计算量与场景的复杂程度无关,如此一来就可以避免计算因未通过深度测试而被丢弃的片段,从而减少性能浪费。并且所有灯光都可以逐像素渲染,这意味着所有灯光都可以与法线贴图等产生正确的交互,并且所有的灯光都可以使用阴影和 cookies。

遗憾的是,延迟着色不支持真正的抗锯齿,也不能处理半透明物体,这些会自动使用前向渲染处理。
使用延迟着色的模型不支持接受阴影选项,Culling Mask 只能在限定条件下使用,且最多只能使用4个。

在性能方面,光照对于性能的损耗不再受灯光数量或受光物体数量影响,而是与受灯光影响的像素数量或者灯光的照射范围有关,所以物体受灯光影响的数量不会再有限制。
投射阴影的灯光依然比无阴影的灯光更耗费性能,投射阴影的物体任然需要为阴影投射灯光渲染多次。但是可以通过减少照射范围来降低性能消耗。

延迟着色只能在有多重渲染目标(Multiple Render Targets, MRT)、Shader Model3.0或以上,并且支持深度渲染贴图的显卡上运行。

移动端上,可以在OpenGL ES 3.0 及以上的设备上运行。

延迟着色不支持正交投影,当摄像机使用正交投影模式的时候,摄像机会自动向前渲染。

2.前向渲染路径
前向渲染路径是传统的渲染路径,它支持所有的Unity图形功能,例如法线贴图、逐像素光照、阴影等。
前向渲染路径使用一个或多个Pass渲染每一个物体,这取决于影响到物体的灯光数量。
并且灯光也会因为自身设置和强度不同而被区别对待。

在前向渲染中,一部分最亮的灯光以完全逐像素照明的方式渲染,然后4个点光源以逐顶点的方式渲染,其余灯光以更快速的SH(Spherical Harmonics,球谐:球谐光照——球谐函数https://zhuanlan.zhihu.com/p/153352797 ;球谐光照——简单理解和实现 https://blog.csdn.net/qq_33999892/article/details/83862583**),**解决环境光对物体的影响光照渲染。

SH 光照可以被非常快速地渲染,他只能消耗很少地CPU性能,几乎不消耗GPU性能。
并且增加SH灯光的数量不会影响性能的消耗。

一个灯光是逐像素光照还是其他方式渲染取决于以下几点:

  1. 渲染模式设置为 Not Important 的灯光总是以逐顶点或者SH的方式渲染。
  2. 渲染模式设置为 Important 的灯光总是逐像素渲染。
  3. 最亮的平行光总是逐像素渲染。
  4. 如果逐像素光照的灯光数量少于项目质量设置中 Pixel Light 的数量,那么其余比较亮的灯光将会被逐像素渲染。

灯光的渲染模式(Render Mode)可以在每个灯光的属性面板里进行设置,默认为 Auto,Unity 会根据灯光的亮度以及与物体的距离自动判断该灯光是否重要。

如果想要设置当前 Unity 项目中逐像素灯光的限制数量,依次点击
Edit > Project Settings > Quality > Pixel Light Count
Unity 默认限制为4个逐像素灯光。

943a17129b71d94cd3833b40a1eaf423.png531851a2f79612c865d397b14852efab.png

基础Pass 包含一个逐像素的平行光和所有逐顶点或 SH 的灯光,并且也会包含所有来自于 Shader 的光照贴图、环境光和自发光。
平行光能够投射阴影,但是灯光贴图不能接受 SH 灯光的照明。

其他逐像素的灯光会在额外的 Pass 中渲染,每一个灯光会产生一个额外的 Pass 。
在额外 Pass 中的灯光默认不会投射阴影。
这意味着在默认情况下,前向渲染只支持一个投射阴影的平行光。

如果希望更多的灯光能够产生投影,就需要添加内置的 multi-compile_fwdadd_fullshadows 编译指令编译出不同的 Shader 变体(Variant)。

3.两种渲染路径的特性对比

特性 前向渲染 延迟着色
功能 逐像素光照(法线贴图、灯光 cookie) 支持 支持
实时阴影 需要满足某些条件 支持
反射探针 支持 支持
深度和法线缓存 需要添加额外的Pass 支持
软粒子 不支持 支持
半透明物体 支持 不支持
抗锯齿 支持 不支持
灯光剔除蒙版 支持 部分共嗯
光照的细节程度 部分灯光逐像素渲染 所有灯光逐像素渲染
单个逐像素照明所耗性能 取决于像素数量乘以灯光照亮的物体数量 取决于照亮的像素数量
物体被正常渲染需要的次数 取决于逐像素光照的灯光数量 1次
对于简单场景的开销
PC(Windows/Mac) 支持 Shader Model3.0 以上
移动(iOS/Android) 支持 OpenGL,ES3.0,MRT
Metal(A8以上设备)
Consoles 支持 XB1,PS4

前向渲染相对于延迟着色的兼容性更高,并且前向渲染也是Unity默认的渲染路径。

Pass 标签
SubShader 中有 Tag(标签) 的概念,其实 Pass 中也有标签功能,弱国想要实现正常的光照效果,需要在 Pass 中使用对应的标签。
Pass 标签的使用语法和 SubShader 是一样的,也是键值对的形式,并且没有数量的限制,语法结构如下所示:
Tags{"TagName1" = "Value1" "TagName2" = "Value2"}

1.LightMode 标签
LightMode 可以设置的标签值

标签值 作用
Always 除了主要平行光,其他灯光不会产生任何光照
ForwardBase 用于计算主要平行光、逐顶点或者SH灯光、环境光和光照贴图,只能在前向渲染中使用
ForwardAdd 为每一个逐像素灯光生成一个Pass进行光照计算,只能在前向渲染中使用。
Deferred 用于渲染 G-Buffer, 只能在延迟着色中使用
ShadowCaster 将物体的深度渲染到阴影贴图或者深度贴图中

例如,代码中可能会这样使用:
Tags {"LightMode" = "ForwardBase"}

2.PassFlags 标签
PassFlags 标签用于更改渲染流水线传递数据给 Pass 的方式。
目前仅可使用的值为 OnlyDirectional。
当使用前向渲染的时候,这个标签使得只有主要平行光、环境光或灯光探针、光照贴图的数据才能传递到 Shader,SH和逐顶点灯光不能传递数据。

内置的 multi_compile
前向渲染只支持一个投射阴影的平行光,如果想要修改默认状态,就需要添加多重编译指令。
Unity 提供了一系列多重编译指令以编译出不同的 Shader 变体,这些编译指令主要用于处理不同类型的灯光、阴影和灯光贴图,可以使用的编译指令如下:

(1) multi_compile_fwdbase:编译 ForwardBase Pass 中的所有变体,用于处理不同类型的光照贴图,并为主要平行光开启或者关闭阴影。

(2) multi_compile_fwdadd:编译 ForwardAdd Pass 中的所有变体,用于处理平行光、聚光灯和点光源,以及它们的 cookie 纹理。

(3) multi_compile_fwdadd_fullshadow:与 multi_compile_fwdadd 类似,但是增加了灯光投射实时阴影的效果。

实现阴影效果
渲染路径、设置 Pass 标签、添加多重编译指令等是和灯光进行完美的交互所需要的前提准备工作。

1.编写 Shader
案例采用 Lambert 光照模型,不在计算镜面反射效果。
(1) 第一个部分为基础 Pass,用于渲染主要平行光和逐顶点或 SH 的灯光,并为主要平行光产生阴影投射。
(2) 第二个部分为额外 Pass,用于渲染其他逐像素灯光,并且在这个 Pass 中也为其他逐像素的灯光产生了阴影投射。

代码详解:

1
2
3
4
5
6
7
8
9
Tags{"LightMode" = "ForwardBase"}

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

在第一个 Pass 中,首先添加标签 “LightMode” = “ForwardBase” 将 Pass 的光照模式设置为前向渲染基础 Pass 。然后调用 multi_compile_fwdbase 多重编译指令为在当前 Pass 中渲染每个灯光编译出不同的 Shader 变体。

由于 Shader 中会用到很多内置变量和预定义函数,所以把相关的文件都包含进来。

1
2
3
4
5
6
7
struct v2f
{
    float4 pos : SV_POSITION;
    float3 normal :TEXCOORD0;
    float4 vertex : TEXCOORD1;
    SHADOW_COORDS(2) //使用预定义宏保存阴影坐标
};

在v2f结构体中,除了要保存裁切空间顶点坐标,模型空间法线向量和模型空间顶点坐标之外,还引进了一个新的变量,使用宏 UNITY_SHADOW_COORDS(idxl)保存阴影贴图的坐标。

括号内的数字表示 TEXCOORD 语义后的序号,由于0和1已经被法线向量和顶点坐标占用,所以此处使用序号2。
这个宏在包含文件 AutoLight.cginc 中被定义。

1
2
3
4
5
6
7
8
9
10
v2f vert (appdata_base v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.normal = v.normal;
    o.vertex = v.vertex;
    TRANSFER_SHADOW(O)    //使用预定义宏变换阴影坐标

    return o;
}

将内置的 appdata_base 结构体传入顶点着色器之后,在顶点着色器中计算出裁切空间顶点坐标,并直接输出传入的法线向量和顶点坐标。

在这里需要引进一个新的宏定义”RANSFER_SHADOW(a)”,这个宏在包含文件 AutoLight.cginc 中被定义,它的作用是变换阴影贴图的纹理坐标并存入结构体中。

宏括号内的 a 表示要存入的结构体名称,由于顶点函数中声明的结构体名称为 o,因此括号内需要填写 o 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fixed4 frag(v2f i) : SV_Target
{
    //准备变量
    float3 n = UnityObjectToWorldNormal(i.normal);
    n = normalize(n);
    float3 l = WorldSpaceLightDir(i.vertex);
    l = normalize(l);
    float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

    // Lambert 光照
    fixed ndotl = saturate(dot(n,l));
    fixed4 color = _LightColor0 * _MainColor * ndotl;

    // 加上4个点光源光照
    color.rgb  += Shade4PointLights(
    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
    unity_LightColor[0].rgb, unity_LightColor[1].rgb,
    unity_LightColor[2].rgb, unity_LightColor[3].rgb,
    unity_4LightAtten0, worldPos.rgb, n) * _MainColor;

    //加上环境光照
    color += unity_AmbientSky;

    //使用预定义宏计算阴影系数
    UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)

     //阴影合成
     color.rgb *= shadowmask;

    return color;
    }

在片段着色器中,通过计算得到标准化的世界空间法线和世界空间灯光方向,由于使用了 WorldSpaceLightPos( ) 变量,导致聚光灯和点光源的位置对于光照效果是起不了任何作用。

为了解决这个问题,在本Shader中使用WorldSpaceLightDir()函数计算灯光方向,它会根据不同类型的灯光分别计算灯光方向,该函数在包含文件 UnityCG.cginc中被定义。

等到光照效果之后,还需要计算阴影结果。

首先使用宏 UNITY_LIGHT_ATTENUATION(destName,input,worldPos)计算出阴影遮罩,这个宏文件在包含文件 AutoLight.cginc 中被定义,

需要用到的变量如下:
(1)destName:表示阴影系数名称,后续可以直接使用这个名称调用
(2)input:表示传入片段着色器的结构体名称
(3)worldPos:表示世界空间顶点坐标

计算出投影遮罩之后,将其与光照效果相乘,就可以得到有投影的光照效果了。

编写完第一部分的 Shader 之后,还需要为除主光外的其他逐像素灯光产生投影:

1
2
3
4
5
6
7
8
9
10
11
12
13
Tags{"LightMode" = "ForwardAdd"}

//使用相加混合,使绘制的图像与上一个 Pass 完全吻合=
Blend One One

CGPROGRAM

# pragma vertex vert

# pragma fragment frag

# pragma multi_compile_fwdadd_fullshadows

在第二个 Pass 中,首先使用标签”LightMode” = “ForwarAdd”将光照模式设置为ForwardAdd,然后添加多重编译指令 multi_compile_fwdadd_fullshadows 为当前 Pass 中渲染的灯光编译出不同的 Shader 变体,并产生阴影投射。

为了避免当前 Pass 绘制出的图像完全覆盖掉上一个 Pass,需要使用混合指令 Blend 将两个Pass绘制出的图像进行混合。
本Shader中使用 “Blend One One” 混合指令使两个Pass 绘制的图像完全相加。

因为在第一个