Unity Shader光照研究(一)

最近3D项目需要调模型在游戏里面的效果,之前提供的一个只能调漫反射的强度的简单Shader不太够,需要支持更多的功能,比如高光,环境光,阴影等。

这一块的Shader之前也都是到处乱翻的资料,没有系统学习过,趁此机会翻了下《Unity Shader入门精要》关于光照的知识并做个总结。

知识点

这里讲到的一些光照模型都是经验模型,不完全符合真实世界中的光学现象

漫反射

用来表示有多少光线会被折射、吸收和散射出表面,漫反射符合兰伯特定律:反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比,计算公式

cdiffuse=(clight · mdiffuse) max(0, n · l)

  • n: 表面法线
  • l: 指向光源的单位矢量,可以用Unity Shader中的函数UnityWorldSpaceLightDir, WorldSpaceLightDir获取,这些函数返回的向量没有归一化,需要用normalize进行归一化处理
  • mdiffuse: 材质漫反射的颜色
  • clight: 光源颜色

对应Unity Shader的代码

1
2
3
4
5
6
7
8
// 灯光方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));

// 法线归一化
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射颜色
fixed3 diffuseColor = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

为了防止法线和光源方向点乘为负值,需要用取最大值进行截取

兰伯特定律的被光面因为光照无法到达,所以表现为全黑的效果,没有任何明暗变化。为了解决这个问题,有一个改善的技术半兰伯特模型,计算公式

cdiffuse=(clight · mdiffuse) (a(n · l) + b)

取消了max防止为负值,但对结果进行了缩放然后再便宜,绝大多数情况下a和b都为0.5

对应的Unity Shader代码

1
2
3
4
5
6
7
8
// 灯光方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));

// 法线归一化
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射颜色
fixed3 diffuseColor = _LightColor0.rgb * _Diffuse.rgb * (0.5 * dot(worldNormal, worldLightDir)) + 0.5);

高光反射

用来表示物体表面是如何反射光的,高光反射也有两个常用经验模型

  1. Phong模型
    需要知道几个信息,表面法线、视角方向、光源方向、反射方向,如图:

其中反射方向r可以通过其他信息计算得到:

r = 2(n · l)n - l (可以使用Unity Shader中的函数reflect获得反射向量,因为参数的向量需要从反射点射出,所欲传入的灯光向量需要取负值,另外该函数返回的向量没有归一化)

得到r之后,我们可以带入Phone模型公式:

cspecular=(clight · mspecular) max(0, v · r)mgloss

  • r: 反射方向
  • v:点到摄像机方向的单位矢量,可以用Unity Shader中的函数UnityWorldSpaceViewDirWorldSpaceViewDir获取,这些函数返回没有进行归一化
  • mspecular: 高光反射的颜色
  • clight: 光源颜色
  • mgloss: 材质的光泽度,用来控制高光区域的亮点有多宽,值越大,两点越小

对应的Unity Shader代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 灯光方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));

// 法线归一化
fixed3 worldNormal = normalize(i.worldNormal);

// 摄像机方向
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPosition));

// 反射方向
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

// 高光颜色
fixed3 specularColor = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, worldViewDir)), _Gloss);
  1. Blinn模型
    跟Phong模型相比,它避免计算反射方向r,但引入的另外一个矢量h

h = (v + l) / |v + l| 对应Unity代码normalize(worldViewDir + worldLightDir)

模型公式:
cspecular=(clight · mspecular) max(0, n · h)mgloss

对应的Unity代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 灯光方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPosition));

// 法线归一化
fixed3 worldNormal = normalize(i.worldNormal);

// 摄像机方向
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPosition));

// 向量h
fixed3 halfDir = normalize(worldLightDir + worldNormal);

// 高光颜色
fixed3 specularColor = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldViewDir)), _Gloss);

在某些情况下(比如摄像机和光源距离模型足够远),Blinn模型会快于Phone模型

这两种模型都是经验模型,不能说谁对谁错。只是Blinn模型更符合实验结果

环境光

模拟来自于其他物体的间接光照,环境光公式比较简单就是一个全局变量:

cambient = gambient

对应的Unity代码

1
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;

逐顶点光照和逐像素光照

取决于光照计算在顶点着色其还是片元着色其中

一般情况下光照计算都会在片元着色器中,因为这样效果会更好点。但是一般情况下像素点会远远超过顶点,所以计算量会比较大,当灯光比较多的时候,一些灯光会降级到顶点着色器处理。

完整的例子

增加了几个参数:

  • 环境光、漫反射、高光强度设置
  • 不受光照影响的比例(使用原贴图显示的比例)
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
Shader "Light"
{
Properties
{
_MainTex("主贴图", 2D) = "white" {}
_MainColor("主颜色", Color) = (1, 1, 1, 1)
_MainFactor("不受光照影响的比例", Range(0, 1)) = 1

_AmbientFactor("环境光强度", float) = 1
_DiffuseFactor("漫反射强度", float) = 1
_SpecularFactor("高光强度", float) = 1
_SpecularColor("高光颜色", Color) = (1, 1, 1, 1)
_Gloss("光泽度", Range(8, 255)) = 20
}

SubShader
{
Tags { "RenderType" = "Qpaque" }

CGINCLUDE

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

sampler2D _MainTex;
fixed4 _MainTex_ST;
fixed4 _MainColor;
fixed _MainFactor;

float _AmbientFactor;
fixed _DiffuseFactor;
fixed _SpecularFactor;
fixed4 _SpecularColor;
float _Gloss;
ENDCG

Pass
{
Tags {"LightMode" = "ForwardBase"}

CGPROGRAM
#pragma multi_compile_fwdbase

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

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : NORMAL;
float3 worldVertex : TEXCOORD1;
};

#pragma vertex vert
#pragma fragment frag

v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldVertex = mul(unity_ObjectToWorld, v.vertex);
return o;
}

fixed4 frag(v2f i) : SV_TARGET0
{
// 物体本身颜色
fixed3 mainColor = tex2D(_MainTex, i.uv) * _MainColor;

// 受环境光之后影响之后的颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * _AmbientFactor * mainColor;

fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldVertex));
fixed3 worldNormal = normalize(i.worldNormal);

// 漫反射光颜色
fixed3 diffuseColor = max(0, dot(worldNormal, worldLightDir)) * mainColor.xyz * _LightColor0.rgb * _DiffuseFactor;

// 高光颜色
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldVertex));
fixed3 hDir = normalize(worldViewDir + worldLightDir);
fixed3 specularColor = _SpecularColor.rgb * _LightColor0.rgb * pow(max(0, dot(hDir, worldNormal)), _Gloss) * _SpecularFactor;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldVertex);

// 受灯光影响之后的颜色
fixed3 lightColor = ambient + (diffuseColor + specularColor) * atten;

// 最终颜色
fixed4 color = fixed4(mainColor * _MainFactor + lightColor * (1 - _MainFactor), 1);
return color;
}
ENDCG
}
}
FallBack "Diffuse"
}

疑问

为什么漫反射光照结果计算时,要使用归一化的法线以及单位光源方向?

涉及到一个概念就是辐照度辐照度用来量化光,可以理解为辐照度越大,光越亮

辐照度在平行光中指的是单位面积上单位时间穿过的能量,如图:

在左图中,光是垂直照射到物体表面,因此光线之间的垂直距离保持不变;而在右图中,光是斜着照射到物体表面,在物体表面光线之间的距离是d/cosθ,因此单位面积上接收到的光线数目要少于左图

可以看到d/cosθ越大,光射到平面的间距就越大,辐照度就越小,也就是辐照度跟cosθ成正比。

而n · l = |n| * |l| * cosθ,为了计算cosθ,所以需要归一化法线和光源方向向量

Tags {"LightMode" = "ForwardBase"}#pragma multi_compile_fwdbase作用

Tags {"LightMode" = "ForwardBase"}表示告诉Unity这个Pass是使用前向渲染中的ForwardBase路径,另外一种叫ForwardAdd路径。

#pragma multi_compile_fwdbase是一个编译指令,可以为相应类型的Pass生成所有需要的Shader变种,这些变种会处理不同条件下的渲染逻辑,例如是否使用光照贴图、当前处理哪种光源类型、是否开启阴影等,同时Unity也会在背后将一些内置变量传递到Shader中,例如环境光变量UNITY_LIGHTMODEL_AMBIENT

这一块后续第二讲会继续介绍相关内容

为什么要max约束点成的结果

防止算出来的系数是一个负值,从而导致算出来的光照颜色是一个负值,这样与原始颜色相加之后,颜色就会变暗了