《shader入门精要》笔记-第6章-Unity中的基础光照

光照基础

我们要模拟真实的光照环境来生成一张图像,需要考虑三种物理现象:

  • 首先,光线从光源中被发射出来
  • 然后,光线和场景中的一些物体相交: 一些光线被物体吸收了,而另一些光线被散射到其他方向
  • 最后,摄像机吸收了一些光,产生了一张图像

光源

在光学里,我们用辐照度(irradiance)来量化光.

对于平行光来说,它的辐照度可通过计算在垂直于l的单位面积上单位时间内穿过的量来得到.

吸收和散射

光线由光源发射出来后,就会与一些物体相交,通常,相交的结果有两个:散射(scattering)和吸收(absorption)

散射只改变光线的方向,但不改变光的密度和颜色.而吸收只改变光线的密度和颜色,而不改变光线的方向.

光线在物体表面经过散射后,有两种方向:
一种将会散射到物体内部,这种现象被称为折射(refraction)或透射(transmission);
另一种将会散射到外部,这种现象被称为反射(reflection).

对于不透明物体,折射进入物体内部的光线还会继续与内部颗粒进行相交,其中一部分光线最后会重新发射出物体表面.而另一些则会被物体吸收.那些从物体表面重新发射出的光线将具有和入射光线不同的分布和颜色.
20190319115203.png

为了区分这两种不同的散射方向,我们在光照模型中使用了不同的部分来计算它们:
高光反射(specular)部分表示物体表面是如何反射光线的
漫反射(diffuse)部分则表示有多少光线会被折射,吸收和散射出表面.

根据入射光线的数量和方向,我们可以计算出射光线的数量和方向,我们常用出射度(exitance)来描述它.
辐照度和出射度之间只满足先行关系的,而他们之间的比值就是材质的漫反射和高光反射属性

着色

着色(shading)是指,根据材质属性(如漫反射属性等),光源信息(如光源方向,辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程.我们也把这个等式称为光照模型(Lighting Model).
不同光照模型有不同的目的.例如一些用于描述粗糙的物体表面,一些用于描述金属表面等.

BRDF光照模型

我们已经了解了光线在和物体表面相交时会发生那些现象.
当已知光源位置和方向,视角方向时,我们就需要知道一个表面是如何和光照进行交互的.
BRDF(Bidirectional Reflection Distribution Function)就是用来回答这些问题的.

当给定模型的一个点时,BRDF包含了对该点外观的完整的描述.在图形学中,BRDF大多使用同一个数学公式来表示,并且提供了一些参数来调整材质属性.
通俗来讲,当给定入射光线和辐照度后,BRDF可以给出在某个方向上的光照能量分布.
本章设计BRDF都是对真实场景进行理想化和简化后的模型.它们并不能真实地反映物体和光照之间的交互,这些光照模型被称为是经验模型.

标准光照模型

标准光照模型只关心直接光照(direct light),也就是那些从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机光线.
它的基本方法是,把进入摄像机内的光线分为4部分,每个部分使用一种方法来计算它的贡献度.

  • 自发光(emissive)
    这个部分用于描述当给定一个方向时,一个表面本身会向该方向发射多少辐射量.如果没有全局光照(global illuminiation)技术,这些自发光的表面并不会真的照亮物体,只是它本身看起来更亮了而已.
  • 高光反射(specular)
    这个部分用于描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射出多少辐射量.
  • 漫反射(diffuse)
    这个部分用于描述,当光线从光源照射到物体表面时,该表面会向每个方向散射多少辐射量.
  • 环境光(ambient)
    用于描述其他所有间接光照

环境光

虽然标准光照模型的重点在于直接光照,但在真实世界中,物体也可以被间接光照(indirect light)所照亮.
间接光照是指,光线通常会在多个物体之间反射,最后进入摄像机.

在标准光照模型中,我们使用环境光来近似模拟间接光照.
环境光的计算非常简单,它通常是一个全局变量,场景中的所有物体都使用这个环境光

自发光

光线可以直接由光源发射进入摄像机,而不需要经过其他物体的反射.标准光照模型使用自发光来计算这个部分的贡献度.
它的计算也很简单,就是直接使用了材质的自发光颜色.

通常在实时渲染中,自发光的表面往往并不会照亮周围的表面,也就是说,这个物体并不会被当做是一个光源.
Unity 5引入的全局光照则可以模拟这类自发光物体对周围物体的影响(详见18章)

漫反射

漫反射光照是用于对那些被物体表面完全随机散射到各个方向的辐射度进行建模的.
在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的.
但是入射光线的角度很重要.

漫反射光照符合兰伯特定律(Lambert’s law):

反射光线的强度与表面法线和光源方向之间的夹角的余弦值成正比

高光反射

这里的高光反射是一种经验模型,也就是说,它并不完全符合真实世界中的高光反射现象.他可以用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看上去是有光泽的,例如金属材质

计算高光反射需要知道的信息比较多,如表面法线,视角方向,光源方向,反射方向等.

高光反射的数学表达式太多了,还是看书吧,书上p124

逐像素还是逐顶点

在片元着色器中计算光照模型,被称作逐像素光照(per-pixel lighting)
在顶点着色器中计算光照模型,被称作逐顶点光照(per-vertex lighting)

逐像素光照

在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以通过对顶点法线的插值得到,也可以从法线纹理中采样得到),然后进行光照模拟的计算.
这种在面片之间对顶点法线进行插值的技术被称为Phone 着色(Phone Shader),也被称为Phone插值或法线插值着色技术,这不同于之前的Phone模型.

逐顶点光照

也被称为高洛德着色(Gouraud Shading).
在逐顶点光照中,我们在每个顶点上计算光照,然后在渲染图元内部进行线性插值,最后输出成颜色.
因为顶点数目往往远小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照.
但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性计算(入计算高光反射)时,逐顶点光照就会出问题.
而且,由于逐顶点光照会在渲染图元内部对顶点颜色进行插值,这会导致渲染图元内部的颜色总是暗于顶点处的最高颜色值.这在某些情况下会产生明显的棱角,

总结

标准光照模型并不完全符合真实世界中的光照现象,但由于它的易用性,计算速度和得到的效果都比较好,因此仍在被广泛使用.
标准光照模型也被称为Phone光照模型Blinn-Phong光照模型.

但这种模型也有很多局限性.
首先,很多重要的物理现象无法用该光照模型表现出来,例如菲涅尔反射(Fresenel reflection).
其次,该模型是各项同性(isotropic)的,也就是说,当我们固定视角和光源方向,旋转这个表面时,反射不会发生任何改变.但有些表面是各向异性(anisotropic)的,如拉丝金属,毛发等.

Unity中的环境光和自发光

在Shader中,我们只需通过UNITY_LIGHTMODEL_AMBIENT就可以得到环境光的颜色和强度信息.

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

漫反射光照模型

实践: 逐顶点漫反射光照

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 "Custom/6.4"
{
Properties
{
_Diffuse("Diffuse",Color) = (1, 1, 1, 1)
}

Subshader{
Pass{

// 使用Tags指明该Pass的光照模式
// 只有定义了正确的LightMode,我们才能得到一些Unity的内置光照变量,如后面的_LightColor0
Tags{"LightMode" = "ForwardBase"}

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

// 需要使用UNity中的内置光照变量,如后面的_lightColor0,还需包含Unity的内置文件Lighting.cginc
#include "Lighting.cginc"

fixed4 _Diffuse;

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

struct v2f{
float4 pos : POSITION;
float3 color : COLOR;
};

v2f vert(a2v v){
v2f o;

// 将顶点坐标从模型空间变换到剪裁空间中
o.pos = UnityObjectToClipPos(v.vertex);

// 通过UNITY_LIGHTMODEL_AMBIENT获得环境光部分
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

// 将法线变换至世界坐标空间下,并进行归一化
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));

// 光源方向由_WorldSpaceLightPos0得到,并进行归一化
float3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

// 通过_LightColor0访问光源的颜色和强度信息
// saturate是Cg提供的一种函数,它的作用是把参数截取到[0, 1]的范围内.
// 将 法线 与 光源方向 的 点积 与 光源的颜色 和 强度 以及 材质的漫反射颜色 相乘 得到 最终的漫反射光照部分
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"
}

顶点着色器部分解释:
在第一行,我们首先定义了返回值o.我们已经重复过很多次,顶点着色器最基本的任务就是把顶点位置从模型空间转换到剪裁空间中.
书上比较过时的方法是 使用Unity内置的 模型世界投影矩阵 UNITY_MATRIX_MVP 来完成这样的坐标变换.实际打代码的时候,编辑器顶部出现了一行

1
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

,然后对应部分的代码也被改了.

接下来是真正计算漫反射光照的部分.回忆一下,为了计算漫反射我们需要知道4个参数.
在前面的步骤中,我们已经知道了材质的漫反射颜色_Diffuse以及顶点法线v.normal.还需要知道光源的颜色和强度信息以及光源方向.

  • 通过Unity提供的内置变量 _LightColor0 来访问该Pass处理光源的颜色和强度信息
  • 通过Unity提供的内置变量 _WorldSpaceLightPos0 得到光源方向

需要注意的是,这里对光源方向的计算不具有通用性.当前场景下,我们假设只有一个光源且其类型是平行光.
如果场景下有多个光源并且类型是点光源等其他类型,直接使用_WorldSpaceLightPos0就不能得到正确的结果.

计算光源方向与法线的点积时,只有两者处于统一坐标空间下,他们的点积才有意义.
于是在这里我们选用世界空间.

在得到世界空间下的法线和光源方向后,我们需要对它们进行归一化操作;
在得到它们的点积的结果后,我们需要防止这个结果为负值.为此,我们选用saturate函数.
saturate函数是Cg提供的一种函数,它的作用是可以把参数截取到[0, 1]范围内.
再将 法线 与 光源方向 的 点积 与 光源的颜色 和 强度 以及 材质的漫反射颜色 相乘 得到 最终的漫反射光照部分

最后,将环境光与漫反射光部分相加,得到最终的光照结果

思考:法线 与 光源方向 的 点积 与 光源的颜色 和 强度 以及 材质的漫反射颜色 相乘 的意义
我的理解: 法线 与 光源方向 的 点积 的结果,应该就是光源方向在法线方向上的投影值.
这个投影值的意义是漫反射的强度.顶点法线正对入射光线,漫反射效果最强.顶点法线与入射光线的夹角大于90度,则完全没有漫反射效果.之后再以强度与光源颜色和材质颜色相乘,得到漫反射颜色.

对于细分度较高的模型,逐顶点光照已经可以得到比较好的光照效果了.但对于一些细分程度较低的模型,逐顶点光照就会出现一些视觉问题.
20190319185255.png
例如这里就有点锯齿

实践: 逐像素漫反射光照

相较于上个人逐顶点光照,把对光照的计算从vert函数转到了frag函数.

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
Shader "Custom/6.4"
{
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){
// vert函数中仅传递光照方向及法线方向
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
return o;
}

fixed4 frag(v2f i) : SV_TARGET{
// 计算在frag函数中进行
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);
}

ENDCG
}
}
FallBack "Diffuse"
}

逐像素光照更佳平滑
20190319192441.png
但是,即使用了逐像素漫反射光照,有一个问题仍然存在:在光照无法到达的区域,模型外观通常是黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去模型细节表现.
实际上我们可以通过添加环境光来得到非全黑的效果,但即便这样也无法解决背光面明暗一样的缺点.为此,有一种改善技术被提出来,这就是半兰伯特(Half Lambert)光照模型

半兰伯特光照模型

在上面的光照实例中使用的光照模型也被称为兰伯特光照模型,因为它符合兰伯特定于----在平面某点漫反射光强与该反射点的法向量和入射角的余弦值成正比.
半兰伯特光照模型是在原兰伯特光照模型上修改的.
与原兰伯特模型相比,版兰伯特光照模型没有使用saturate函数截取法向量和光照方向的点积,而是使点积结果乘以一个值再加上一个值.通常情况下.这两个值都为0.5,通过这种方式就能把法向量和光照方向的点积的结果范围从[-1, 1]映射到[0, 1]范围内.

对于模型的背光面,原兰伯特模型中点积结果将映射到同一个值,即0处;而半兰伯特模型中,背光面也会有明暗变化.

需要注意的是,半兰伯特光照模型是没有任何物理依据的,仅仅是一个视觉加强.

球的背光面在两种光照模型下的表现:

原兰伯特光照模型 半兰伯特光照模型
20190319202410.png 20190319202346.png

高光反射光照模型

基本光照模型中高光反射部分的计算公式:
20190319203246.png
计算高光反射需要四个参数:入射光线的颜色和强度,材质的高光反射系数,视角方向以及反射方向.其中,反射方向可以由表面法线和光源方向计算而得.

上述计算很简单,更幸运的是,Cg提供了计算反射方向的函数:reflect.

函数: reflect(i,n)
参数: i: 入射方向;n:法线.可以是float,float2,float3等.

实践: 逐顶点高光反射光照

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 "Custom/6.4"
{
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;

// 漫反射部分
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);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));

// 入射光线关于法线的反射方向.
// 由于Cg的reflect函数的入射方向要求是由光源指向交点处的,因此我们要把worldLightDir取反后再传给reflect函数
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));

// 世界空间下的 摄像机坐标与顶点坐标相减,得到视角方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld,v.vertex).xyz);

// 根据公式和所有的四个参数,带入公式得到高光反射的光照部分.
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
FallBack "Specular"
}

使用逐顶点的方法得到的高光效果,高光部分及其不平滑:因为高光部分的计算是非线性的,而在顶点着色器中在进行插值的过程是线性的,破坏了原计算的非线性关系.
20190319214927.png

实践: 逐片元高光反射光照

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 "Custom/6.4"
{
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;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)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 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss);

return fixed4((ambient + diffuse + specular),1.0);
};

ENDCG
}
}

// fallback调成Specular
FallBack "Specular"
}

将计算转移到了frag函数中.
20190319225259.png

Blinn-Phong光照模型

上面是Phong光照模型的实现.
而Blinn模型没有使用反射方向,而是引入了一个新的矢量
20190319223342.png
而Blinn模型计算高光反射的公式是
20190319223412.png
理解:理解个尖儿,貌似也是个没有物理根据的经验模型

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
Shader "Custom/6.4"
{
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;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(v.normal, (float3x3)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 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);

// 新增
fixed3 halfDir = normalize(worldLightDir + viewDir);

// 公式
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(worldNormal,halfDir)),_Gloss);

return fixed4((ambient + diffuse + specular),1.0);
};

ENDCG
}
}
FallBack "Specular"
}

效果:20190319225327.png
谔谔,总感觉区别不大…高光范围比上面大了点…