光照计算

光照计算由三个重要的组成决定:

  1. 光源数据:光源数据受光源类型影响,平行光、点光、聚光灯是不一样的。灯光数据的传递方式由RenderPath来决定。
    1. 光源方向LightDir
    2. 衰减范围Attenuation
    3. 光源颜色LightColor
  2. 模型表面材质结构:
    1. 顶点法线
    2. 法线贴图(像素级法线)
    3. 光滑度
    4. PBR理论框架——描述自然界大多数物体的表面材质结构
  3. 观察方向

渲染路径

前向渲染

Unity的Built-in渲染管线和URP渲染管线都是前向渲染。但是URP使用的是forward+渲染,能够使用多盏灯光。

  1. 在前向渲染中,每个模型根据自己受光的灯光数量执行一个Pass,如果一个模型除了主光源(平行光)外还受4个其他光源的影响,那么这个模型就要多重复渲染4次,多产生4个DrawCall

  2. Unity对于精细的逐像素光照是有限制的,在Project Setting——Quality里面,Pixel Light Count指定了逐像素光照的光源数量。其中

    超出这个数量的灯光会使用逐顶点的光照,并且逐顶点的光照会在ForwardBase这个Pass里面绘制。在Unity中,会根据灯光的Intensity来排序需要逐像素光照的灯光,当然也可以在灯光的Render Mode中设置Important。

  3. 在前向渲染中,主光源(平行光)和超出了pixel Light Count的其他光源都会在ForwardBase这个Pass里面绘制,在pixel限制内的逐个像素光源会在ForwardAdd这个Pass里面绘制

  4. 综上,在前向渲染的Shader中,需要两个Pass

    1. ForwardBase
      1. 主光源(平行光),以及超出pixel数量的灯光都作为逐顶点灯光传入
      2. SH(球谐光照)、Light Map(预烘焙的光照贴图)、Reflection Probe等计算均在这个Pass里面完成。
    2. ForwardAdd
      1. Pixel Light Count数量范围内的点光、平行光,每个灯光的计算,均会调用这个Pass进行,计算的结果通过Blend One One叠加起来

    这里的灯光都是Realtime模式,先不讨论Mixed和Baked模式的灯光。

延迟渲染

UE4和Unity的HDRP渲染管线都是延迟渲染,这种渲染方式无视场景中的灯光个数。

想要在Unity中开启延迟渲染,选择场景中的main Camera,然后再在Render Path中选择Deffered。

我们可以打开FrameDebugger来观察延迟渲染管线的流程:

延迟渲染流程

延迟渲染有两个重要的阶段,生成GBuffer和Lighting

在延迟渲染管线中,使用了MRT(Multi Render Target)技术,场景中的每个模型对应的Shader都可以输出多个信息到不同的Layer当中。在上图中:

  1. RT0代表的是当前选中的模型的Diffuse + Occlusion信息,ARGB32
  2. RT1代表的是当前选中的模型的Metallic信息(Specular color,smoothness),ARGB32
  3. RT2代表的是当前选中的模型的World Space Normal信息
  4. RT3代表的是当前选中的模型的Emission + lighting + lightmaps + reflection probes信息,根据摄像机是否是HDR,占用的内存大小不同
  5. RT4代表的是前选中的模型的Light occlusion信息,这个RT只有在开启了Shadow Mask或者Distance Shadow Mask模式才会看到。
  6. Depth代表的是当前选中的模型的Depth + Stencil信息

在前向渲染中,每个Shader只输出最终的颜色信息,没有延迟渲染中这么丰富。

在GBuffer中拿到足够的模型信息之后,在Lighting阶段只需要让场景中的每个灯光直接计算就好了,每个灯光计算一次然后叠加起来即可。

Phong光照

ForwardBase Pass

现在实现一下前向渲染下的Phong光照,前面说过,前向渲染需要两个Pass,在第一个ForwardBasePass中需要先添加一些东西:

1
2
3
4
5
6
7
8
9
10
11
Pass
{
Tags {"LightMode" = "ForwardBase"} //在Pass内再添加一个Tag,说明这个是ForwardBasePass
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragme multi_compile_fwdbase//启用多线程
#include "UnityCG.cginc"
#include "AutoLight.cginc"//引用内置的函数
//...
}

添加的这三行是Unity引入光照必备的,记住即可

Phong光照

上图的N是法线方向,L是从当前顶点到光源的方向。R是当前灯光反射的方向(Unity提供了reflect函数可以快速计算)V是从当前顶点看向摄像机的方向。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
Shader "KerryTA/Code/201PhongLighting"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_AOMap ("AOMap", 2D) = "white" {}//AO贴图
_SpecMask ("SpecMask", 2D) = "white" {}//高光遮罩贴图
_Shininess("Shininess", Range(0.01, 100)) = 1.0 //控制高光的范围
_SpecIntensity("SpecIntensity", Range(0.1,5)) = 1.0 //控制高光的强度
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
Tags {"LightMode" = "ForwardBase"} //在Pass内再添加一个Tag,说明这个是ForwardBasePass
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "AutoLight.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_dir : TEXCOORD1;//世界空间法线
float3 pos_world : TEXCOORD2;//世界空间顶点,用来在片元Shader中计算视线方向
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _AOMap;
sampler2D _SpecMask;//高光遮罩贴图

float4 _LightColor0;//定义后才能获取到主光源的颜色
float _Shininess;//控制高光的范围
float _SpecIntensity;//控制高光的强度

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

half4 frag (v2f i) : SV_Target
{
half4 base_color = tex2D(_MainTex, i.uv);
half4 ao_color = tex2D(_AOMap, i.uv);
half4 spec_mask = tex2D(_SpecMask, i.uv);

half3 normal_dir = normalize(i.normal_dir);
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
//获取世界主光源的方向,这里正好是指向光源的
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
half NdotL = dot(normal_dir, light_dir);
half3 diffuse_color = max(0.0, NdotL) * _LightColor0.xyz * base_color.xyz;//_LightColor0表示主光源的颜色
//使用内部提供的reflect函数求反射光的方向,用于计算高光,记住这里使用负的light_dir
half3 reflect_dir = reflect(-light_dir, normal_dir);
half RDotV = dot(view_dir, reflect_dir);
half3 spec_color = pow(max(0.0, RDotV),_Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;

//使用Unity提供的参数获取环境光
half3 ambient_color = UNITY_LIGHTMODEL_AMBIENT.rgb * base_color.rgb;
half3 final_color = (diffuse_color + spec_color + ambient_color) * ao_color;//AO贴图和最终结果乘算
return half4(final_color, 1.0);
}
ENDCG
}
//接下来是ForwardAdd Pass
}
}

高光遮罩贴图可以从PBR贴图中的Roughness贴图反相得到

ForwardAdd Pass

在ForwardAdd Pass中需要先添加一些东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
Pass
{
Tags {"LightMode" = "ForwardAdd"} //在Pass内再添加一个Tag,说明这个是ForwardAddPass
Blend One One //设置和其他灯光的混合公式
CGPROGRAM
#pragma vertex vert
#pragma fragment frag、
//这里也改为fwdadd
#pragma multi_compile_fwdadd
#include "UnityCG.cginc"
#include "AutoLight.cginc"
//...
}

点光源或聚光灯需要自己的衰减系数attenuation

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Pass
{
Tags {"LightMode" = "ForwardAdd"} //在Pass内再添加一个Tag,说明这个是ForwardAddPass
Blend One One //设置和其他灯光的混合公式
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#include "UnityCG.cginc"
#include "AutoLight.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_dir : TEXCOORD1;//世界空间法线
float3 pos_world : TEXCOORD2;//世界空间顶点,用来在片元Shader中计算视线方向
};

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _AOMap;
sampler2D _SpecMask;//高光遮罩贴图

float4 _LightColor0;//定义后才能获取到主光源的颜色
float _Shininess;//控制高光的范围
float _SpecIntensity;//控制高光的强度

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

half4 frag (v2f i) : SV_Target
{
half4 base_color = tex2D(_MainTex, i.uv);
half4 ao_color = tex2D(_AOMap, i.uv);
half4 spec_mask = tex2D(_SpecMask, i.uv);

half3 normal_dir = normalize(i.normal_dir);
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
#if defined (DIRECTIONAL)
//平行光直接获取光源的方向
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
//平行光不会有衰减
half attenuation = 1.0;
#elif defined (POINT)
//如果是点光源的话,_WorldSpaceLightPos0指的就是当前的点光源,需要单独计算此片元到点光源的方向
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz - i.pos_world);
half distance = length(_WorldSpaceLightPos0.xyz - i.pos_world);
//unity中的WorldToLight矩阵提供了当前灯光的范围值,我们取矩阵第一个数的倒数
half range = 1.0 / unity_WorldToLight[0][0];
half attenuation = saturate((range - distance) / range);
#endif
half NdotL = dot(normal_dir, light_dir);
half3 diffuse_color = max(0.0, NdotL) * _LightColor0.xyz * base_color.xyz * attenuation;//_LightColor0表示主光源的颜色
//使用内部提供的reflect函数求反射光的方向,用于计算高光,记住这里使用负的light_dir
half3 reflect_dir = reflect(-light_dir, normal_dir);
half RDotV = dot(view_dir, reflect_dir);
half3 spec_color = pow(max(0.0, RDotV),_Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb * attenuation;

half3 final_color = (diffuse_color + spec_color) * ao_color;//AO贴图和最终结果乘算
return half4(final_color, 1.0);
}
ENDCG
}

此Shader中点光源的衰减计算方法已过时,Unity使用的是采样渐变图来计算灯光衰减,这里依然使用旧方法便于理解。

在ForwardAddPass中不需要加算Ambient光。

Blin-Phong高光

Blin-Phong是对Phong光照模型的高光部分的优化,它不再使用reflect这个比较消耗性能的函数。而是将当前像素到灯光的向量当前像素到摄像机的向量相加并归一化,得到一个“半角向量”,然后让这个半角向量和当前像素的法向量点乘,用来做高光。

1
2
3
4
5
//求Blin-Phong的半角向量
half3 half_dir = normalize(light_dir + view_dir);
half NdotH = dot(normal_dir, half_dir);

half3 spec_color = pow(max(0.0, NdotH),_Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;