文章目錄
  1. 1. 光与材质
  2. 2. 标准光照模型(数学基础
    1. 2.1. 环境光
    2. 2.2. 自发光
    3. 2.3. 漫反射
    4. 2.4. 高光反射
  3. 3. 实现(Unity Shader
    1. 3.1. 漫反射光照模型
      1. 3.1.1. 逐顶点光照
      2. 3.1.2. 逐像素光照
      3. 3.1.3. 半兰伯特模型
    2. 3.2. 高光反射模型
      1. 3.2.1. 逐顶点光照
      2. 3.2.2. 逐像素光照
      3. 3.2.3. Blinn-Phong光照模型
    3. 3.3. 使用Unity内置函数
      1. 3.3.1. UnityCG.cginc的常用函数

n个月后重读《Unity Shader入门精要》(第六章

光与材质

首先要明白一件事情,物体的呈现的颜色不是物体的性质。物体的颜色取决于物体的材质和光。

而光有分为好几种,一是最简单的平行光,它没有所谓的光源,光强均等,是一种理想模型(例如狭义上的太阳光),二是点光源,例如基本上所有的灯光都是点光源,它的光强与它有限的距离成反比,还有其他衍生下去的光类型,不外乎这几种性质:照射距离,光的强度(颜色)。

物体的材质包括对各个颜色分量的处理程度,因此对某个颜色处理得更宽容,物体就偏向呈现那个颜色。还包括反射率,漫反射率,和环境光率,这些都直接影响到物体呈现的效果。

物体除了被光直接照亮,反射到摄像机中,还有其他的光线。例如密室中一个物体被聚光灯照亮,背部也不是完全的黑暗。这就是部分散射光和环境光的作用。总体来说,「从摄像机看到某位置的」光=反射光+散射光+环境光,这有一个具体的公式,有兴趣的可以查阅相关资料

标准光照模型(数学基础

裴祥风(Bui Tuong Phong)提1975年出的的标准光照模型,基本方法是把进入摄像机的光线分为四个部分,每个部分用各自的方法计算贡献度。由简单至复杂陈列如下:

环境光

环境光用于描述所有其他的间接光照,但在标准光照模型中,通常使用一个全局变量环境光来近似模拟间接光照:

自发光

光线可以由光源直接进入摄像机而不进行反射。自发光的表面并不会照亮其他物体,只是使自身看起来更亮而已。计算时直接使用材质的自发光颜色:

漫反射

漫反射光照符合兰伯特定律(Lambert’s law):反射光线强度与表面法线和光源方向间夹角的余弦成正比。因此漫反射部分计算如下:

其中$c_{light}$光源颜色,$m_{diffuse}$为材质的漫反射颜色,$n$为表面法线,$l$为指向光源的单位矢量

高光反射

这里的高光反射都是经验模型,不符合真实世界情况。它用于计算那些镜面反射的光线,使物体看起来比较有光泽。

已知表面法线$n$,光源方向$l$,视角方向$v$这三个矢量,可以推出第四个矢量反射方向$r$:

这样就可以利用Phone模型,计算高光反射部分如下

其中$c_{light}$为光源颜色和强度, $m_{specular}$为材质的高光反射颜色。$m_{gloss}$为材料的光泽度(gloss),也成为反光度(shininess),$m_{gloss}$越小,亮点就越大。

Blinn使用了一个更简单的修改方法来得到类似的效果,它的基本思想是避免计算反射方向$r$。因此引入一个新的矢量$h$,它是通过$v$和$l$的取平均后再归一得到的,即

然后使用$n$和$h$之间的夹角进行计算

在硬件实现时如果摄像机和光源足够近($v$和$l$都是定值,因此$h$是常量),Blinn模型会更快。如果$v$和$l$不是定值时Phong模型可能更快一点。Blinn方法由于简化计算从而在某些情况可能更快,而且在一些情况下更符合实验结果。但是注意,这两种光照模型都是经验模型,不应该认为Blinn是『正确的』Phong模型的近似。

标准光照模型是一个经验模型,因为裴祥风先提出、Blinn的方法简化计算,因此这种模型称为Blinn-Phone模型。

实现(Unity Shader

漫反射光照模型

上一节我们给出漫反射部分的计算公式:

  • saturate(x)函数把x截取在[0,1]内

逐顶点光照

DiffuseVertexLevel.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
Shader "Unity Shaders Book/Chapter 6/Diffuse Vertex-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
// Get the light direction in world space
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
o.color = ambient + diffuse;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return fixed4(i.color, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}

逐像素光照

DiffusePixelLevel.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 "Unity Shaders Book/Chapter 6/Diffuse Pixel-Level" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Diffuse;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Get the normal in world space
fixed3 worldNormal = normalize(i.worldNormal);
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}

半兰伯特模型

Valve在开发HF的时候在兰伯特模型(即漫反射光照模型)上进行修改,因此称为半兰伯特光照模型,广义的半兰伯特光照模型的公式如下

使用了一次函数来代替max函数防止负值,绝大多数情况下,$\alpha$和$\beta$的值为0.5,即

修改DiffusePixelLevel.shader中的片源着色器:

HalfLambert.shader

1
2
3
4
5
6
7
8
9
10
11
fixed4 frag(v2f i) : SV_Target {
......
// Compute diffuse term
fixed halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}

以下是三个光照的对比效果:(半兰伯特很明显能够使背光面照亮

diff.gif

高光反射模型

上一节我们给出高光反射部分的计算公式:

  • reflect(i,n)函数用于计算反射方向(i:入射方向,n:法线方向)

逐顶点光照

SpecularVertexLevel.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
61
62
63
64
65
66
67
68
69
Shader "Unity Shaders Book/Chapter 6/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
#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;
fixed3 color : COLOR;
};
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Transform the normal from object space to world space
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)_World2Object));
// Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(_Object2World, v.vertex).xyz);
// Compute specular term
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"
}

逐像素光照

SpecularPixelLevel.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
61
62
63
64
65
66
67
68
69
70
Shader "Unity Shaders Book/Chapter 6/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
#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;
// Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
// Transform the vertex from object spacet to world space
o.worldPos = mul(_Object2World, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// Get the reflect direction in world space
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}

Blinn-Phong光照模型

上一节中提到了Phong光照模型的实习,但是我们提出来另一种Blinn光照模型,即引入一个矢量$h$来避免计算反射方向$r$:

然后使用$n$和$h$之间的夹角进行计算(Blinn模型):

修改SpecularPixelLevel.shader中的片源着色器:

BlinnPhong.shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fixed4 frag(v2f i) : SV_Target {
...
// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}

以下是三个高光的对比效果:(Blinn-Phong模型看起来更大更亮一些。

spe.gif

使用Unity内置函数

与BlinnPhong.shader中代码几乎一致,修改部分代码如下

BlinnPhongUseBuildInFunctions.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
...
v2f vert(a2v v) {
v2f o;
...
// Use the build-in funtion to compute the normal in world space
o.worldNormal = UnityObjectToWorldNormal(v.normal);
...
return o;
}
fixed4 frag(v2f i) : SV_Target {
...
fixed3 worldNormal = normalize(i.worldNormal);
// Use the build-in funtion to compute the light direction in world space
// Remember to normalize the result
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
...
// Use the build-in funtion to compute the view direction in world space
// Remember to normalize the result
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
...
}
...

UnityCG.cginc的常用函数

函数名 描述
float3 WorldSpaceViewDir(float4 v) 模型空间的顶点位置->世界空间中该点到摄像机的观察方向(内部使用UnityWorldSpaceViewDir)
float3 UnityWorldSpaceViewDir(float4 v) 世界空间的顶点位置->世界空间中该点到摄像机的观察方向
float3 ObjSapceViewDir(float4 v) 模型空间的顶点位置->模型空间中该点到摄像机的观察方向
float3 WorldSpaceLightDir(float4 v) 前向渲染中,模型空间的顶点位置->世界空间中该点到摄像机的光照方向。没有归一化(内部使用UnityWorldSpaceLightDir)
float3 UnityWorldSpaceLightDir(float4 v) 前向渲染中,世界空间的顶点位置->模型空间中该点到摄像机的光照方向。没有归一化
float3 ObjSpaceLightDir(float4 v) 前向渲染中,模型空间的顶点位置->模型空间中该点到摄像机的光照方向。没有归一化
float3 UnityObjectToWorldNormal(float3 norm) 法线方向,模型空间->世界空间
float3 UnityObjectToWorldDir(float3 dir) 方向矢量,模型空间->世界空间
float3 UnityWorldToObjectDir(float3 dir) 方向矢量,世界空间->模型空间
文章目錄
  1. 1. 光与材质
  2. 2. 标准光照模型(数学基础
    1. 2.1. 环境光
    2. 2.2. 自发光
    3. 2.3. 漫反射
    4. 2.4. 高光反射
  3. 3. 实现(Unity Shader
    1. 3.1. 漫反射光照模型
      1. 3.1.1. 逐顶点光照
      2. 3.1.2. 逐像素光照
      3. 3.1.3. 半兰伯特模型
    2. 3.2. 高光反射模型
      1. 3.2.1. 逐顶点光照
      2. 3.2.2. 逐像素光照
      3. 3.2.3. Blinn-Phong光照模型
    3. 3.3. 使用Unity内置函数
      1. 3.3.1. UnityCG.cginc的常用函数