UnityShader-基础光照

UnityShader-基础光照

从宏观上说,渲染包含了两大部分:决定一个像素的可见性,决定这个像素上的光照计算

标准光照模型

环境光

用来模拟间接光照,指光线会在多个物体之间反射,最后进入摄像机。计算方法如下:
$$
c_{ambient} = g_{ambient}
$$

在Unity中,场景中的环境光通过Window->Render->Lighting中设置,在Shader中通过内置变量UNITY_LIGHTMODEL_AMBIENT获取

自发光

光线可以直接由光源发射进入摄像机,而不需要经过任何物体反射,计算方法:
$$
c_{emissive} = m_{emissive}
$$
在实时渲染中,该物体不会照亮周围的表面

如果需要计算自发光,只需要在片元着色器输出最后的颜色之前,把材质的自发光添加到输出颜色上即可

漫反射

漫反射用于对那些物体表面随机散射到各个方向的辐射度进行建模的,视角的位置不重要,即任何反射方向上的分布都是一样的。但是受到入射角影响很大

兰伯特定律(Lambert’s Law)

漫反射光照遵循兰伯特定律(Lambert’s law):反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比,计算方式如下:
$$
c_{diffuse} = (c_{light} \cdot m_{diffuse})max(0, n \cdot I)
$$
其中,$n$是表面法线,$I$是
指向光源
的单位矢量,$m_{diffuse}$是材质的漫反射颜色,$c_{light}$是光源颜色


Unity中逐顶点漫反射光照效果实现如下:

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 "Diffuse Vertex Level"{
Properties {
_Diffuse ("Diffuse", Color) = (1.0, 1.0, 1.0, 1.0)
}

SubShader {
Pass {
// 光照模式为前向渲染,得到Unity内置光照变量
Tags {"LightMode"="ForwardBase"}

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
// 包含计算光照的头文件
#include "Lighting.cginc"

fixed4 _Diffuse;

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

struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

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

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

// 使用法向量左乘世界空间变换到模型空间的矩阵 等价于 使用法向量右乘模型空间变换到世界空间的逆转置矩阵
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

// 只能适用于只有一个光源而且是平行光的情况
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

// diffuse = c_light * m_diffuse * max(0, dot(Vec_n, Vec_light))
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));

o.color = ambient + diffuse;
// o.color = _Diffuse.rgb;
return o;
}

fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}

ENDCG
}
}
Fallback "Diffuse"
}

Unity中逐像素漫反射光照效果实现如下:

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
Shader "AABB/Diffuse Pixel Level"{
Properties {
_Diffuse ("Diffuse", Color) = (1.0, 1.0, 1.0, 1.0)
}

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

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Diffuse;

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

struct v2f {
float4 pos : SV_POSITION;
fixed3 worldNormal : TEXCOORD0;
};

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

o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 worldNormal = normalize(i.worldNormal);

fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

fixed3 color = ambient + diffuse;

return fixed4(color, 1.0);
}

ENDCG
}
}
Fallback "Diffuse"
}

对于当前模型,在光线无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这使得模型失去细节表现。


半兰伯特模型(Half Lambert)

在任何区域的光照强度都将大于等于0,使得被光面也能够有明暗变化,计算公式为:
$$
c_{diffuse} = (c_{light} \cdot m_{diffuse})(\alpha(\hat n \cdot I) + \beta)
$$
大多数情况下,$\alpha$ 和 $\beta$ 的值均为0.5,这样把点乘的结果范围从[-1, 1]映射到[0, 1],保证模型暗面的细节也能够被显示出来。

半兰伯特模型实现如下:

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
Shader "Half Lambert"{
Properties {
_Diffuse ("Diffuse", Color) = (1.0, 1.0, 1.0, 1.0)
}

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

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

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

fixed4 _Diffuse;

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

struct v2f {
float4 pos : SV_POSITION;
fixed3 worldNormal : TEXCOORD0;
};

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

o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 worldNormal = normalize(i.worldNormal);

fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (0.5 * dot(worldNormal, worldLightDir) + 0.5);

fixed3 color = ambient + diffuse;

return fixed4(color, 1.0);
}

ENDCG
}
}
Fallback "Diffuse"
}

高光反射

用于计算那些沿着完全镜面反射方向被反射的光线,可以让物体看起来有光泽

Phong模型

反射方向计算及图案示例:
$$
r = 2(\hat n \cdot I)\hat n - I
$$
image-20210819233731955

然后利用Phong模型来计算高光反射部分:
$$
c_{specular} = (c_{light} \cdot m_{specular})max(0, \hat v \cdot r)^{m_{gloss}}
$$
$m_{gloss}$是**光泽度(gloss)**,也被称为反光度(shininess)。用于控制高光区域的亮点有多宽,两者的增长趋势相反

$m_{specular}$ 是材质的高光反射颜色,控制该材质对于高光反射的强度和颜色

$c_{light}$是光源的颜色和强度

$\hat v$是视角方向

Phong高光反射模型图如下:

specular.png-31.2kB


Blinn模型

另一种高光反射模型——Blinn模型如下图:

Blinn.png-32.1kB

该模型避免计算反射方向$\hat r$,为此引入新的矢量$\hat h$,计算方法:
$$
\hat h = \frac{\hat v + I}{\abs {\hat v + I}}
$$
Blinn模型公式如下:
$$
c_{specular} = (c_{light} \cdot m_{specular})max(0, \hat n \cdot \hat h)^{m_{gloss}}
$$
两种模型对比:

性能:在硬件实现时,如果摄像机和光源距离足够远,Blinn模型会快于Phong模型,因为这时可以认为 $\hat v$ 和 $\hat I$ 都是定值,因此 $\hat h$ 是一个常量。否则,Phong模型可能更快。

效果:两者都是经验模型,需要根据实际情况判断

实现

逐顶点Phong模型实现,由于高光反射部分的计算是非线性的,而在顶点着色器中计算光照再进行插值的过程是线性的,破环了原计算的非线性关系,会出现较大的视觉问题

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
Shader "Specular Vertex Level" {
Properties {
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_Specular("Specular", Color) = (1, 1, 1, 1)
// 控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM

#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;

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

struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};

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

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

// diffuse = C_light * m_diffuse * max(0, dot(Vec_n, Vec_light))
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

// 使用内置函数reflect计算反射光方向,参数1为光源到反射点的向量,因此取反
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));

fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
// specular = C_light * m_specular * (max(0, dot(Vec_r, Vec_v)))**m_gloss
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

o.color = ambient + diffuse + specular;
return o;
}

fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

逐像素Phong模型实现:

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
Shader "Specular Pixel Level" {
Properties {
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_Specular("Specular", Color) = (1, 1, 1, 1)
// 控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM

#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

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 = mul(v.normal, (fixed3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);

return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

Blinn-Phong模型实现:

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
Shader "AABB/Blinn Phong" {
Properties {
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_Specular("Specular", Color) = (1, 1, 1, 1)
// 控制高光区域的大小
_Gloss("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM

#include "Lighting.cginc"

#pragma vertex vert
#pragma fragment frag

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 = mul(v.normal, (fixed3x3)unity_WorldToObject);
// 使用Unity 内置函数
o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);



// fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 输入为世界空间中的顶点位置
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));

// fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 输入为世界空间中的顶点位置
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));

// Vec_h = (Vec3_view + Vec3_light) / (length(Vec3_view + Vec3_light))
fixed3 halfDir = normalize(worldLightDir + viewDir);

// C_specular = (C_light * m_specular) * (max(0, dot(Vec3_normal * Vec3_h)))**m_gloss
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}

逐像素光照 vs 逐顶点光照

在逐像素光照中,以每个像素为基础,得到法线(来自顶点法线插值或者法线纹理采样)后进行光照模型计算,又被称为Phong着色

在逐顶点光照中,以每个顶点为基础计算光照,然后在渲染图元内进行线性插值,最后输出像素颜色

性能:逐像素光照计算量大于逐顶点光照

效果:由于逐顶点光照依赖线性插值得到像素光照,因此当光照模型中有非线性计算时(如高光反射),逐顶点光照会有明显瑕疵。同时,顶点处的颜色总是亮于图元内部的颜色,某些情况下会出现明显的棱角

Unity内置函数

image-20210820013839185

注意:这些函数都没有保证得到的额方向矢量是单位矢量,因此,需要在使用前把它们归一化

实际使用过程中,推荐在UnityCG.cginc和Lighting.cginc文件中直接查找关键词,同时浏览实现方法,避免踩坑


UnityShader-基础光照
http://example.com/2021/07/13/UnityShader-基础光照/
作者
Chen Shuwen
发布于
2021年7月13日
许可协议